204 lines
8.2 KiB
Python
Executable File
204 lines
8.2 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
# Copyright (C) 2026 David (Corey) Herrera
|
|
"""
|
|
mutate_program_blob_header.py
|
|
Damage a generated ft_tt_programs.h in one specific, named way, so that the gate aimed
|
|
at that damage can be watched failing.
|
|
|
|
A gate nobody has seen fail is not evidence (coding-best-practices.md 9), and four
|
|
vacuous gates have already been caught in this project. Every check in
|
|
tests/test_program_blob.c has a mutation here whose only job is to make that check red.
|
|
|
|
This is a TEXT transform over the emitted header, in Python. The detectors are in C and
|
|
were written from the format, not from this file — so a mutation and its detector never
|
|
share an implementation.
|
|
|
|
Usage: mutate_program_blob_header.py <mode> <input.h> <output.h>
|
|
|
|
Modes:
|
|
flip-byte one payload byte of prog_alpha's DM0 kernel changes; the recorded
|
|
digest does not -> the digest check must fire
|
|
truncate-image the last twelve bytes of that array are deleted
|
|
-> the digest check must fire, and the size stops matching disk
|
|
break-elf-magic that array's ELF magic is destroyed AND its digest is recomputed
|
|
to match -> the digest is happy and ft_tt_elf_parse must refuse
|
|
swap-kernel-images prog_alpha's DM0 and DM1 rows exchange payload AND digest, so
|
|
every image is internally consistent and two of them are in the
|
|
wrong slot -> only the comparison against the files on disk can
|
|
see it. This is the mutation that proves that comparison is doing
|
|
work.
|
|
merge-firmware prog_gamma is pointed at prog_alpha's firmware set -> the dedup
|
|
check must refuse to call two different sets the same
|
|
drop-program prog_beta is removed from the lookup table -> the lookup must
|
|
notice, rather than the count being whatever it happens to be
|
|
|
|
Status: Runs on the Mac; used by tests/run_program_blob_gates.sh.
|
|
Issues: None
|
|
Todo: None
|
|
"""
|
|
|
|
import re
|
|
import sys
|
|
|
|
FNV64_OFFSET = 0xCBF29CE484222325
|
|
FNV64_PRIME = 0x100000001B3
|
|
FNV64_MASK = 0xFFFFFFFFFFFFFFFF
|
|
|
|
ARRAY_PATTERN = re.compile(
|
|
r"static const unsigned char (\w+)\[\] = \{\n(.*?)\n\};\n", re.S
|
|
)
|
|
|
|
|
|
def fnv1a_64(data):
|
|
digest = FNV64_OFFSET
|
|
for byte in data:
|
|
digest ^= byte
|
|
digest = (digest * FNV64_PRIME) & FNV64_MASK
|
|
return digest
|
|
|
|
|
|
def array_body(text, symbol):
|
|
for match in ARRAY_PATTERN.finditer(text):
|
|
if match.group(1) == symbol:
|
|
return match
|
|
raise SystemExit("mutate: no array named %r in the header" % symbol)
|
|
|
|
|
|
def array_bytes(body):
|
|
return bytes(int(value, 16) for value in re.findall(r"0x([0-9a-fA-F]{2})", body))
|
|
|
|
|
|
def render_body(payload):
|
|
lines = []
|
|
for start in range(0, len(payload), 12):
|
|
chunk = payload[start:start + 12]
|
|
lines.append(" " + " ".join("0x%02x," % byte for byte in chunk))
|
|
return "\n".join(lines)
|
|
|
|
|
|
def replace_array(text, symbol, payload):
|
|
match = array_body(text, symbol)
|
|
return text[:match.start(2)] + render_body(payload) + text[match.end(2):]
|
|
|
|
|
|
def kernel_row(text, program, processor):
|
|
"""The one line of `ft_tt_program_<program>_kernels[]` for one processor."""
|
|
table = re.search(
|
|
r"static const ft_tt_program_blob_image ft_tt_program_%s_kernels\[\] = \{\n(.*?)\n\};"
|
|
% program, text, re.S)
|
|
if not table:
|
|
raise SystemExit("mutate: no kernel table for %r" % program)
|
|
for line in table.group(1).split("\n"):
|
|
if ("FT_TT_PROC_%s" % processor) in line:
|
|
return line
|
|
raise SystemExit("mutate: %r has no %s row" % (program, processor))
|
|
|
|
|
|
def row_symbol(row):
|
|
match = re.search(r"FT_TT_PROC_\w+, (\w+), sizeof", row)
|
|
if not match:
|
|
raise SystemExit("mutate: cannot read the payload symbol out of %r" % row)
|
|
return match.group(1)
|
|
|
|
|
|
def set_row_digest(text, symbol, digest):
|
|
"""Every row that references `symbol` gets the new digest — a payload is shared across
|
|
programs when its bytes are identical, and leaving one row behind would make the result
|
|
a mutation of something other than what was asked for."""
|
|
pattern = re.compile(r"(%s, sizeof\(%s\), )0x[0-9a-f]{16}ull" % (symbol, symbol))
|
|
(text, count) = pattern.subn(r"\g<1>0x%016xull" % digest, text)
|
|
if count == 0:
|
|
raise SystemExit("mutate: no row references %r" % symbol)
|
|
return text
|
|
|
|
|
|
def mutate_flip_byte(text):
|
|
symbol = row_symbol(kernel_row(text, "prog_alpha", "DM0"))
|
|
payload = bytearray(array_bytes(array_body(text, symbol).group(2)))
|
|
index = len(payload) // 2
|
|
payload[index] ^= 0xFF
|
|
return replace_array(text, symbol, bytes(payload))
|
|
|
|
|
|
def mutate_truncate_image(text):
|
|
symbol = row_symbol(kernel_row(text, "prog_alpha", "DM0"))
|
|
payload = array_bytes(array_body(text, symbol).group(2))
|
|
return replace_array(text, symbol, payload[:-12])
|
|
|
|
|
|
def mutate_break_elf_magic(text):
|
|
symbol = row_symbol(kernel_row(text, "prog_alpha", "DM0"))
|
|
payload = bytearray(array_bytes(array_body(text, symbol).group(2)))
|
|
payload[0] = 0x00
|
|
text = replace_array(text, symbol, bytes(payload))
|
|
return set_row_digest(text, symbol, fnv1a_64(bytes(payload)))
|
|
|
|
|
|
def mutate_swap_kernel_images(text):
|
|
left = kernel_row(text, "prog_alpha", "DM0")
|
|
right = kernel_row(text, "prog_alpha", "DM1")
|
|
tail = re.compile(r"(FT_TT_PROC_\w+, )(\w+, sizeof\(\w+\), 0x[0-9a-f]{16}ull)")
|
|
left_tail = tail.search(left).group(2)
|
|
right_tail = tail.search(right).group(2)
|
|
text = text.replace(left, tail.sub(r"\g<1>" + right_tail, left, count=1), 1)
|
|
return text.replace(right, tail.sub(r"\g<1>" + left_tail, right, count=1), 1)
|
|
|
|
|
|
def mutate_merge_firmware(text):
|
|
sets = re.findall(r"static const ft_tt_program_blob_firmware (\w+) =", text)
|
|
if len(sets) < 2:
|
|
raise SystemExit("mutate: the fixture blob must hold two firmware sets to merge")
|
|
alpha = re.search(r"ft_tt_program_prog_alpha = \{.*?&(\w+),", text, re.S).group(1)
|
|
gamma_entry = re.search(r"(ft_tt_program_prog_gamma = \{.*?&)(\w+)(,)", text, re.S)
|
|
if gamma_entry.group(2) == alpha:
|
|
raise SystemExit("mutate: alpha and gamma already share a firmware set")
|
|
return text[:gamma_entry.start(2)] + alpha + text[gamma_entry.end(2):]
|
|
|
|
|
|
def mutate_drop_program(text):
|
|
(text, count) = re.subn(r"\n &ft_tt_program_prog_beta,", "", text)
|
|
if count != 1:
|
|
raise SystemExit("mutate: prog_beta is not in the table exactly once")
|
|
# The entry itself stays defined and merely unreferenced, which -Wunused would reject
|
|
# under -Werror; a build failure is not the red this mutation is aiming for. Referencing
|
|
# it from a comment is not enough, so it is kept alive by a second table nothing reads.
|
|
return text.replace(
|
|
"static const ft_tt_program_blob_entry *const *const ft_tt_program_blob_table =",
|
|
"static const ft_tt_program_blob_entry *const ft_tt_program_dropped[] = {\n"
|
|
" &ft_tt_program_prog_beta,\n};\n"
|
|
"static const ft_tt_program_blob_entry *const *const ft_tt_program_dropped_ref =\n"
|
|
" ft_tt_program_dropped;\n"
|
|
"static const ft_tt_program_blob_entry *const *const ft_tt_program_blob_table =",
|
|
1)
|
|
|
|
|
|
MODES = {
|
|
"flip-byte": mutate_flip_byte,
|
|
"truncate-image": mutate_truncate_image,
|
|
"break-elf-magic": mutate_break_elf_magic,
|
|
"swap-kernel-images": mutate_swap_kernel_images,
|
|
"merge-firmware": mutate_merge_firmware,
|
|
"drop-program": mutate_drop_program,
|
|
}
|
|
|
|
|
|
def main(argv):
|
|
if len(argv) != 3 or argv[0] not in MODES:
|
|
raise SystemExit("usage: mutate_program_blob_header.py <%s> <in.h> <out.h>"
|
|
% "|".join(sorted(MODES)))
|
|
with open(argv[1]) as handle:
|
|
text = handle.read()
|
|
mutated = MODES[argv[0]](text)
|
|
if mutated == text:
|
|
raise SystemExit("mutate: %r changed nothing -- the mutation itself is vacuous"
|
|
% argv[0])
|
|
with open(argv[2], "w") as handle:
|
|
handle.write(mutated)
|
|
print("mutated %s -> %s (%s)" % (argv[1], argv[2], argv[0]))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main(sys.argv[1:]))
|