#!/usr/bin/env python3 # SPDX-License-Identifier: AGPL-3.0-or-later # Copyright (C) 2026 David (Corey) Herrera """ check_tensix_program_provenance.py Refuse a Tensix PROGRAM -- on disk or already embedded in the blob -- whose recorded build inputs are not the files in this tree. This is the CONTENT check that replaces the mtime one. `check_tensix_program_sources.sh` asks "is the named source newer than the ELF"; an `explode` rewrites every file and every answer becomes `current`. This asks "does the content this image was built from still hash to what it hashed then", which nothing about file timestamps can change. tensix_image_provenance.py's header carries the full argument and the two shipped artifacts it was written against. Usage: check_tensix_program_provenance.py [--tt-root DIR] [more dirs...] check_tensix_program_provenance.py [--tt-root DIR] --header ../ft_tt_programs.h --tt-root the foundry/tt directory the recorded input paths are relative to. Defaults to the parent of this script's directory. --header check the GENERATED BLOB instead of directories. This is the one that matters, and it needs no program directories, no toolchain and no card: it is what the binary will actually run, checked against the sources beside it. It runs on the Mac. --quiet print only the failures and the summary. Exit: 0 every image is current 1 at least one image is stale, unverifiable, or carries no provenance 2 usage A MISSING PROVENANCE RECORD IS A FAILURE, not a skip. An image nobody can vouch for is exactly the state that shipped a reader with no cooperative code for a day, and a checker that passes what it cannot check is worth less than no checker -- it converts "unknown" into "green". Status: added 2026-08-19. Verified to fail before it was believed: run against the pre-regeneration `programs/gdn` it named the five GDN sources by content; run against `programs/rope`, whose sources had not moved, it passed. Issues: None Todo: None """ import os import re import sys HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, HERE) from tensix_image_provenance import ( # noqa: E402 FORMAT_NAME, FORMAT_VERSION, ProvenanceError, check_record, parse_record, ) KERNEL_FILE_NAMES = ["brisc.elf", "ncrisc.elf", "trisc0.elf", "trisc1.elf", "trisc2.elf"] RECORD_OPENER = '"%s %u\\n"' % (FORMAT_NAME, FORMAT_VERSION) STRING_LITERAL_RE = re.compile(r'"((?:[^"\\]|\\.)*)"') PROGRAM_TABLE_RE = re.compile(r"ft_tt_program_(\w+)_kernels\[\] = \{") def unescape(text): return text.replace("\\n", "\n").replace('\\"', '"').replace("\\\\", "\\") def records_from_header(path): """ Every provenance record embedded in a generated ft_tt_programs.h, as [(program_name, record_text)]. The blob emits each record as consecutive C string literals inside its image row, so this walks from each opener to the literal carrying `build_id` and joins them. It is the emitter's own format, read back by the same tree that writes it. """ with open(path) as handle: text = handle.read() program_at = [(match.start(), match.group(1)) for match in PROGRAM_TABLE_RE.finditer(text)] def program_for(position): name = "?" for (start, candidate) in program_at: if start < position: name = candidate else: break return name found = [] position = text.find(RECORD_OPENER) while position != -1: pieces = [] cursor = position while True: match = STRING_LITERAL_RE.search(text, cursor) if match is None: raise ProvenanceError("%s: a provenance record at offset %u never ends" % (path, position)) pieces.append(unescape(match.group(1))) cursor = match.end() if pieces[-1].startswith("build_id"): break found.append((program_for(position), "".join(pieces))) position = text.find(RECORD_OPENER, cursor) return found def check_program_directory(directory, tt_root, quiet): """Findings for one build_tensix_program.sh output directory.""" problems = [] checked = 0 if not os.path.isdir(directory): return [" %s: MISSING" % directory] for file_name in KERNEL_FILE_NAMES: image = os.path.join(directory, file_name) if not os.path.isfile(image): continue sidecar = image + ".prov" if not os.path.isfile(sidecar): problems.append(" %s: NO PROVENANCE for %s -- built by something that does not " "record what it was built from, so it cannot be vouched for" % (directory, file_name)) continue try: with open(sidecar) as handle: record = parse_record(handle.read(), sidecar) except ProvenanceError as error: problems.append(" %s" % error) continue checked += 1 problems.extend(" " + line for line in check_record(record, tt_root, directory)) if checked == 0 and not problems: problems.append(" %s: holds no kernel images" % directory) if not problems and not quiet: print(" %s: current (%u image(s), content-checked)" % (directory, checked)) return problems def check_header(path, tt_root, quiet): """ Findings for a generated ft_tt_programs.h. THE BLOB IS THE ONE THAT MATTERS AND IT IS THE ONE THAT WENT STALE. `tt/programs/` is gitignored, so the committed header is the ONLY thing that carries a Tensix kernel across a checkout: a commit that changes a kernel source without regenerating the blob ships a binary running yesterday's kernel on every machine that clones the repo. Measured 2026-08-26 on foundry main: `gdnc` carries the PRE-`first_row` writer (b014330) and the four `rtriad_*` plus `probe` carry the pre-row-block matmul (71c0b7a) -- both landed with the blob untouched, and the C = 16 chunkwise prefill that ships by default therefore runs a writer that cannot honour its own sub-chunk offset. Build intermediates a checkout does not carry are COUNTED rather than reported, because 66 of the 94 findings on that same blob were only that -- and a check that is red on a fresh clone for a reason that is not a defect is a check nobody gates on. """ problems = [] by_program = {} unverifiable = [] for (program, text) in records_from_header(path): try: record = parse_record(text, "%s [%s]" % (path, program)) except ProvenanceError as error: problems.append(" %s" % error) continue found = check_record(record, tt_root, "%s [%s]" % (path, program), unverifiable) by_program.setdefault(program, [0, []]) by_program[program][0] += 1 by_program[program][1].extend(found) if not by_program: return [" %s: NO PROVENANCE anywhere in the blob -- this header predates the " "provenance format and nothing in it can be vouched for" % path] for program in sorted(by_program): (count, found) = by_program[program] if found: problems.extend(" " + line for line in found) elif not quiet: print(" %s: current (%u image(s), content-checked)" % (program, count)) if unverifiable and not quiet: print(" %u recorded input(s) are build intermediates under programs/ that this " "checkout does not carry (gitignored); every TRACKED input was hashed" % len(unverifiable)) return problems def main(argv): tt_root = os.path.abspath(os.path.join(HERE, "..")) quiet = False header = None directories = [] index = 0 while index < len(argv): argument = argv[index] if argument == "--tt-root": index += 1 tt_root = argv[index] elif argument == "--header": index += 1 header = argv[index] elif argument == "--quiet": quiet = True elif argument in ("-h", "--help"): print(__doc__) return 0 else: directories.append(argument) index += 1 if not header and not directories: print(__doc__) return 2 if not os.path.isdir(tt_root): print("REFUSED: no tree at --tt-root %s -- nothing can be checked against it" % tt_root, file=sys.stderr) return 1 problems = [] try: if header: problems.extend(check_header(header, tt_root, quiet)) for directory in directories: problems.extend(check_program_directory(directory, tt_root, quiet)) except ProvenanceError as error: print("REFUSED: %s" % error, file=sys.stderr) return 1 if problems: print("\n".join(problems)) print("\nPROVENANCE CHECK FAILED: %u finding(s). Rebuild before believing any result " "from these images -- a stale directory on disk costs a re-run, a stale " "directory baked into the binary survives one." % len(problems)) return 1 print("\nprovenance: every image is current against %s" % tt_root) return 0 if __name__ == "__main__": sys.exit(main(sys.argv[1:]))