Foundry/tt/tools/explode_tensix_program_blob.c

277 lines
11 KiB
C

/* SPDX-License-Identifier: AGPL-3.0-or-later */
/* Copyright (C) 2026 David (Corey) Herrera */
/**
* @file explode_tensix_program_blob.c
* @brief Write the EMBEDDED program blob back out as program DIRECTORIES — the exact
* shape `emit_tensix_program_header.py` consumes — so a machine with no
* `tt/programs/` can still regenerate the blob after one kernel changes.
*
* ---------------------------------------------------------------------------
* WHY THIS EXISTS
* ---------------------------------------------------------------------------
* `ft_tt_programs.h` is generated FROM directories of ELFs, and those directories live on
* whichever machine last ran the builders. `tt/programs/` is not in the tree. So on any
* other machine — including the second motherboard the card now lives in — changing one
* kernel meant rebuilding all nineteen programs from scratch, or not changing it.
*
* The blob already holds everything a directory holds: five kernel ELFs, the five firmware
* ELFs, each image's `.src` source name, the toolchain version and the build recipe. This
* writes them back out. The round trip is then:
*
* explode -> rebuild the ONE image that changed -> emit_tensix_program_header.py
*
* ---------------------------------------------------------------------------
* THE CHECK THAT MAKES IT TRUSTWORTHY, AND IT IS NOT THIS PROGRAM'S OWN
* ---------------------------------------------------------------------------
* Explode, re-emit with NOTHING changed, and diff against the header you started from. A
* faithful round trip is byte-identical. That is the discriminator, and it is worth having
* because the failure this tool could produce — a program silently emitted from a
* mis-assembled directory — is exactly the "stale artifact baked into a binary" the
* emitter's own refusals exist to prevent, one level further back.
*
* `--verify` additionally runs `ft_tt_program_blob_verify()` on every entry before writing
* anything, so a blob whose hex emission lost a byte is refused rather than exploded.
*
* Usage:
* explode_tensix_program_blob <output-directory> [--verify] [--recipes <file>]
*
* `--recipes` writes one `<name>\t<toolchain>\t<recipe>` line per program, because those
* two strings are arguments to the emitter and are not recoverable from the ELFs.
*
* @status Written for the resident cooperative matmul round, where one kernel image
* changed on a machine with no program directories.
* @issues None
* @todo None
*/
#define _POSIX_C_SOURCE 200809L
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include "ft_tt_device.h"
#include "ft_tt_memmap.h"
#include "ft_tt_program_blob.h"
/** Indexed by FT_TT_PROC_*, and it must stay in the emitter's order — the emitter reads
* these file names back and infers the processor from the position. */
static const char *const kKernelFileNames[FT_TT_PROC_COUNT] = { "brisc.elf", "ncrisc.elf",
"trisc0.elf", "trisc1.elf",
"trisc2.elf" };
static const char *const kFirmwareFileNames[FT_TT_PROC_COUNT] = {
"fw_brisc.elf", "fw_ncrisc.elf", "fw_trisc0.elf", "fw_trisc1.elf", "fw_trisc2.elf"
};
static int write_whole_file(const char *path, const void *data, size_t bytes)
{
FILE *handle = fopen(path, "wb");
size_t written;
if (handle == NULL) {
fprintf(stderr, "explode: could not create %s: %s\n", path, strerror(errno));
return FT_TT_ERROR;
}
written = fwrite(data, 1u, bytes, handle);
if (fclose(handle) != 0 || written != bytes) {
fprintf(stderr, "explode: short write to %s (%zu of %zu bytes)\n", path, written,
bytes);
return FT_TT_ERROR;
}
return FT_TT_OK;
}
/** The `.src` sidecar the emitter reads to name an image's kernel source. Firmware has
* none — it is not built from a kernel source — and the emitter's own reader falls back
* to the file name for exactly that case, so writing one here would be a lie. */
static int write_sidecar(const char *directory, const char *file_name, const char *source_name)
{
char path[1024];
if (source_name == NULL || source_name[0] == '\0')
return FT_TT_OK;
/* An image whose recorded source is just its own file name had no sidecar when it was
* embedded, and inventing one now would make a later staleness check compare an ELF
* against a source that never built it. */
if (strcmp(source_name, file_name) == 0)
return FT_TT_OK;
if (snprintf(path, sizeof(path), "%s/%s.src", directory, file_name) >= (int)sizeof(path))
return FT_TT_ERROR;
return write_whole_file(path, source_name, strlen(source_name));
}
/** The `<image>.prov` content record beside the ELF.
*
* WITHOUT THIS, AN EXPLODE LAUNDERS THE ONE CHECK THAT SURVIVES IT. The `.src` sidecar's
* mtime is reset by definition here — every file is written fresh — which is how 21
* directories reported `current` with two of them provably stale. The provenance record
* is content, so writing it back out unchanged carries the truth across the round trip
* rather than erasing it. Firmware carries NULL and gets no file. */
static int write_provenance(const char *directory, const char *file_name,
const char *provenance)
{
char path[1024];
if (provenance == NULL || provenance[0] == '\0')
return FT_TT_OK;
if (snprintf(path, sizeof(path), "%s/%s.prov", directory, file_name) >= (int)sizeof(path))
return FT_TT_ERROR;
return write_whole_file(path, provenance, strlen(provenance));
}
static int write_image(const char *directory, const char *file_name,
const ft_tt_program_blob_image *image, bool with_sidecar)
{
char path[1024];
int rc;
if (image == NULL || image->elf_data == NULL || image->elf_size == 0u)
return FT_TT_OK;
if (snprintf(path, sizeof(path), "%s/%s", directory, file_name) >= (int)sizeof(path))
return FT_TT_ERROR;
rc = write_whole_file(path, image->elf_data, image->elf_size);
if (rc != FT_TT_OK || !with_sidecar)
return rc;
rc = write_sidecar(directory, file_name, image->source_name);
if (rc != FT_TT_OK)
return rc;
return write_provenance(directory, file_name, image->provenance);
}
static int explode_one(const char *root, const ft_tt_program_blob_entry *entry)
{
char directory[1024];
unsigned index;
int rc;
if (snprintf(directory, sizeof(directory), "%s/%s", root, entry->name) >=
(int)sizeof(directory))
return FT_TT_ERROR;
if (mkdir(directory, 0755) != 0 && errno != EEXIST) {
fprintf(stderr, "explode: could not create %s: %s\n", directory, strerror(errno));
return FT_TT_ERROR;
}
for (index = 0u; index < entry->image_count; index++) {
const ft_tt_program_blob_image *image = &entry->images[index];
if (image->processor >= FT_TT_PROC_COUNT) {
fprintf(stderr, "explode: %s image %u names processor %u\n", entry->name, index,
image->processor);
return FT_TT_ERROR;
}
rc = write_image(directory, kKernelFileNames[image->processor], image, true);
if (rc != FT_TT_OK)
return rc;
}
if (entry->firmware == NULL) {
/* Firmware is all five images or none: a program whose set is incomplete boots a
* core into whatever L1 held, and the emitter refuses such a directory. Refuse to
* WRITE one rather than leave it for the emitter to find. */
fprintf(stderr, "explode: %s carries no firmware set\n", entry->name);
return FT_TT_ERROR;
}
for (index = 0u; index < FT_TT_PROC_COUNT; index++) {
rc = write_image(directory, kFirmwareFileNames[index], &entry->firmware->images[index],
false);
if (rc != FT_TT_OK)
return rc;
}
printf(" %-20s %u kernel image(s), firmware set %s, toolchain %s\n", entry->name,
entry->image_count, entry->firmware->set_id, entry->toolchain);
return FT_TT_OK;
}
int main(int argc, char **argv)
{
const char *root = NULL;
const char *recipes_path = NULL;
bool verify = false;
unsigned count;
unsigned index;
int argument;
for (argument = 1; argument < argc; argument++) {
if (strcmp(argv[argument], "--verify") == 0) {
verify = true;
} else if (strcmp(argv[argument], "--recipes") == 0 && argument + 1 < argc) {
recipes_path = argv[++argument];
} else if (argv[argument][0] == '-') {
fprintf(stderr, "usage: %s <output-directory> [--verify] [--recipes <file>]\n",
argv[0]);
return 2;
} else if (root == NULL) {
root = argv[argument];
} else {
fprintf(stderr, "usage: %s <output-directory> [--verify] [--recipes <file>]\n",
argv[0]);
return 2;
}
}
if (root == NULL) {
fprintf(stderr, "usage: %s <output-directory> [--verify] [--recipes <file>]\n",
argv[0]);
return 2;
}
count = ft_tt_program_blob_count();
/* ZERO IS A MEANINGFUL ANSWER and is not "nothing to do": it means this binary was
* built against a header that was never regenerated, and exploding it would produce an
* empty tree that a later emit would happily turn into an empty blob. */
if (count == 0u) {
fprintf(stderr, "explode: this binary's program blob is EMPTY (%s) — there is "
"nothing to write, and an empty tree is how an empty blob gets "
"regenerated by accident\n",
ft_tt_program_blob_generated_header());
return 1;
}
if (mkdir(root, 0755) != 0 && errno != EEXIST) {
fprintf(stderr, "explode: could not create %s: %s\n", root, strerror(errno));
return 1;
}
printf("exploding %u programs from %s into %s\n", count,
ft_tt_program_blob_generated_header(), root);
if (verify)
for (index = 0u; index < count; index++) {
const ft_tt_program_blob_entry *entry = ft_tt_program_blob_entry_at(index);
if (entry == NULL || ft_tt_program_blob_verify(entry) != FT_TT_OK) {
fprintf(stderr, "explode: entry %u failed its integrity check; NOTHING was "
"written\n", index);
return 1;
}
}
for (index = 0u; index < count; index++) {
const ft_tt_program_blob_entry *entry = ft_tt_program_blob_entry_at(index);
if (entry == NULL || explode_one(root, entry) != FT_TT_OK)
return 1;
}
if (recipes_path != NULL) {
FILE *handle = fopen(recipes_path, "w");
if (handle == NULL) {
fprintf(stderr, "explode: could not create %s: %s\n", recipes_path,
strerror(errno));
return 1;
}
for (index = 0u; index < count; index++) {
const ft_tt_program_blob_entry *entry = ft_tt_program_blob_entry_at(index);
fprintf(handle, "%s\t%s\t%s\n", entry->name, entry->toolchain,
entry->build_recipe);
}
if (fclose(handle) != 0) {
fprintf(stderr, "explode: could not finish %s\n", recipes_path);
return 1;
}
printf("recipes written to %s\n", recipes_path);
}
return 0;
}