247 lines
9.4 KiB
Python
Executable File
247 lines
9.4 KiB
Python
Executable File
#!/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] <program-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:]))
|