352 lines
14 KiB
C
352 lines
14 KiB
C
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
|
/* Copyright (C) 2026 David (Corey) Herrera */
|
|
/**
|
|
* @file ft_tt_program_resolve.c
|
|
* @brief Implementation of name-based program resolution (see ft_tt_program_resolve.h).
|
|
*
|
|
* Its own translation unit, and not part of `ft_tt_runtime.c`, for a reason that is
|
|
* about the build and not about tidiness: this is the only file that pulls
|
|
* `ft_tt_program_blob.c` (and through it the whole embedded blob) into a link. A caller
|
|
* that has not adopted name-based resolution yet links exactly what it linked before.
|
|
*
|
|
* @status Implemented; gated off-device by tests/test_program_blob.c.
|
|
* @issues None
|
|
* @todo None
|
|
*/
|
|
|
|
#include "ft_tt_program_resolve.h"
|
|
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
#include "ft_tt_device.h"
|
|
#include "ft_tt_program_blob.h"
|
|
|
|
/** Longest `<root>/<name>` this builds. The same bound ft_tt_runtime.c uses. */
|
|
#define FT_TT_PROGRAM_RESOLVE_PATH_BYTES 512u
|
|
|
|
const char *ft_tt_program_source_string(ft_tt_program_source source)
|
|
{
|
|
switch (source) {
|
|
case FT_TT_PROGRAM_SOURCE_EMBEDDED: return "embedded";
|
|
case FT_TT_PROGRAM_SOURCE_DIRECTORY: return "directory";
|
|
case FT_TT_PROGRAM_SOURCE_NONE: return "none";
|
|
default: return "none";
|
|
}
|
|
}
|
|
|
|
/* ================================================================
|
|
* The directory arm
|
|
* ================================================================ */
|
|
|
|
/** Build `<root>/<name>`, refusing a truncation rather than reading a shortened path. */
|
|
static int join_program_path(const char *root, const char *name, char *out, size_t out_bytes)
|
|
{
|
|
int written = snprintf(out, out_bytes, "%s/%s", root, name);
|
|
|
|
if (written < 0 || (size_t)written >= out_bytes) {
|
|
ft_tt_log_error("tt program: '%s/%s' does not fit in %lu bytes", root, name,
|
|
(unsigned long)out_bytes);
|
|
return FT_TT_EINVAL;
|
|
}
|
|
return FT_TT_OK;
|
|
}
|
|
|
|
/**
|
|
* The explicit-directory arm of both entry points.
|
|
*
|
|
* NOTHING here falls back to the embedded blob. The caller named a directory; if that
|
|
* directory cannot serve the request, the answer is the reason and not a quiet substitute
|
|
* that makes the next measurement a measurement of something else.
|
|
*/
|
|
static int open_from_directory(const char *program_name, const char *override_root,
|
|
uint32_t processor_mask, int want_firmware,
|
|
ft_tt_runtime_images *out_images)
|
|
{
|
|
char path[FT_TT_PROGRAM_RESOLVE_PATH_BYTES];
|
|
int rc = join_program_path(override_root, program_name, path, sizeof(path));
|
|
|
|
if (rc != FT_TT_OK)
|
|
return rc;
|
|
ft_tt_log_error("tt program: '%s' %s OVERRIDDEN to %s (embedded blob not used)",
|
|
program_name, want_firmware ? "firmware" : "kernels", path);
|
|
|
|
rc = want_firmware ? ft_tt_runtime_firmware_load(path, out_images)
|
|
: ft_tt_runtime_program_load(path, processor_mask, out_images);
|
|
if (rc != FT_TT_OK) {
|
|
ft_tt_log_error("tt program: REFUSED — %s was named as an override and cannot be "
|
|
"read (%s). This does NOT fall back to the embedded program: a run "
|
|
"that silently used images the operator did not choose is the "
|
|
"defect this refusal replaces.", path, ft_tt_error_string(rc));
|
|
return rc;
|
|
}
|
|
(void)snprintf(out_images->name, sizeof(out_images->name), "%s", program_name);
|
|
out_images->program.name = out_images->name;
|
|
return FT_TT_OK;
|
|
}
|
|
|
|
/* ================================================================
|
|
* The embedded arm
|
|
* ================================================================ */
|
|
|
|
/** Find the entry and prove its bytes before any of them are handed out. */
|
|
static int find_verified_entry(const char *program_name,
|
|
const ft_tt_program_blob_entry **out_entry)
|
|
{
|
|
const ft_tt_program_blob_entry *entry = NULL;
|
|
int rc = ft_tt_program_blob_find(program_name, &entry);
|
|
|
|
if (rc != FT_TT_OK)
|
|
return rc;
|
|
rc = ft_tt_program_blob_verify(entry);
|
|
if (rc != FT_TT_OK) {
|
|
ft_tt_log_error("tt program: REFUSED — embedded program '%s' failed its integrity "
|
|
"check. The binary itself is damaged; rebuilding it is the fix.",
|
|
program_name);
|
|
return rc;
|
|
}
|
|
*out_entry = entry;
|
|
return FT_TT_OK;
|
|
}
|
|
|
|
/** Point one slot of `out_images` at an embedded image. Nothing is copied and nothing is
|
|
* owned, which is why `owned_data` stays NULL. */
|
|
static void adopt_embedded_image(ft_tt_runtime_images *out_images, unsigned processor,
|
|
const ft_tt_program_blob_image *image)
|
|
{
|
|
out_images->elf_data[processor] = image->elf_data;
|
|
out_images->elf_size[processor] = image->elf_size;
|
|
out_images->owned_data[processor] = NULL;
|
|
(void)snprintf(out_images->source_names[processor],
|
|
sizeof(out_images->source_names[processor]), "%s",
|
|
image->source_name ? image->source_name : "embedded");
|
|
}
|
|
|
|
static void finish_embedded(ft_tt_runtime_images *out_images,
|
|
const ft_tt_program_blob_entry *entry)
|
|
{
|
|
(void)snprintf(out_images->name, sizeof(out_images->name), "%s", entry->name);
|
|
out_images->source = FT_TT_PROGRAM_SOURCE_EMBEDDED;
|
|
out_images->program.name = out_images->name;
|
|
out_images->program.toolchain = entry->toolchain;
|
|
out_images->program.compile_time_args = entry->build_recipe;
|
|
}
|
|
|
|
static int open_embedded_firmware(const char *program_name, ft_tt_runtime_images *out_images)
|
|
{
|
|
const ft_tt_program_blob_entry *entry = NULL;
|
|
unsigned processor;
|
|
int rc = find_verified_entry(program_name, &entry);
|
|
|
|
if (rc != FT_TT_OK)
|
|
return rc;
|
|
memset(out_images, 0, sizeof(*out_images));
|
|
for (processor = 0; processor < FT_TT_PROC_COUNT; processor++)
|
|
adopt_embedded_image(out_images, processor, &entry->firmware->images[processor]);
|
|
finish_embedded(out_images, entry);
|
|
return FT_TT_OK;
|
|
}
|
|
|
|
static int open_embedded_kernels(const char *program_name, uint32_t processor_mask,
|
|
ft_tt_runtime_images *out_images)
|
|
{
|
|
const ft_tt_program_blob_entry *entry = NULL;
|
|
unsigned processor;
|
|
unsigned count = 0;
|
|
int rc = find_verified_entry(program_name, &entry);
|
|
|
|
if (rc != FT_TT_OK)
|
|
return rc;
|
|
memset(out_images, 0, sizeof(*out_images));
|
|
|
|
for (processor = 0; processor < FT_TT_PROC_COUNT; processor++) {
|
|
const ft_tt_program_blob_image *image = NULL;
|
|
|
|
if ((processor_mask & (1u << processor)) == 0u)
|
|
continue;
|
|
rc = ft_tt_program_blob_kernel_image(entry, processor, &image);
|
|
if (rc != FT_TT_OK) {
|
|
ft_tt_log_error("tt program: REFUSED — '%s' was asked to enable processor %u and "
|
|
"carries no kernel for it. A dropped image is a core the launch "
|
|
"message starts with nothing to run.", program_name, processor);
|
|
memset(out_images, 0, sizeof(*out_images));
|
|
return rc;
|
|
}
|
|
adopt_embedded_image(out_images, processor, image);
|
|
out_images->images[count].source_name = out_images->source_names[processor];
|
|
out_images->images[count].processor = processor;
|
|
out_images->images[count].elf_data = image->elf_data;
|
|
out_images->images[count].elf_size = image->elf_size;
|
|
count++;
|
|
}
|
|
finish_embedded(out_images, entry);
|
|
out_images->program.images = out_images->images;
|
|
out_images->program.image_count = count;
|
|
return FT_TT_OK;
|
|
}
|
|
|
|
/* ================================================================
|
|
* Entry points
|
|
* ================================================================ */
|
|
|
|
int ft_tt_program_open_firmware(const char *program_name, const char *override_root,
|
|
ft_tt_runtime_images *out_images)
|
|
{
|
|
if (!program_name || !*program_name || !out_images)
|
|
return FT_TT_EINVAL;
|
|
if (override_root && *override_root)
|
|
return open_from_directory(program_name, override_root, 0u, 1, out_images);
|
|
return open_embedded_firmware(program_name, out_images);
|
|
}
|
|
|
|
int ft_tt_program_open_kernels(const char *program_name, const char *override_root,
|
|
uint32_t processor_mask, ft_tt_runtime_images *out_images)
|
|
{
|
|
if (!program_name || !*program_name || !out_images || processor_mask == 0u ||
|
|
processor_mask >= (1u << FT_TT_PROC_COUNT))
|
|
return FT_TT_EINVAL;
|
|
if (override_root && *override_root)
|
|
return open_from_directory(program_name, override_root, processor_mask, 0, out_images);
|
|
return open_embedded_kernels(program_name, processor_mask, out_images);
|
|
}
|
|
|
|
/* ================================================================
|
|
* The recorded build recipe — what the ARMED image says about itself
|
|
* ================================================================
|
|
*
|
|
* `ft_tt_program_blob.h` says of the `provenance` field that "NOTHING AT RUN TIME READS
|
|
* THIS", and the reason it gave is still right about the thing it was about: a binary has no
|
|
* path to a source tree, so a STALENESS check here could only ever pass. This is a different
|
|
* question. It does not ask whether the image matches sources that are not present; it asks
|
|
* what the image was BUILT WITH, which the record carries and which nothing else in the
|
|
* binary can say.
|
|
*
|
|
* IT EXISTS BECAUSE ONE HOST ASSUMPTION HAS NO OTHER EVIDENCE. The matmul's DST section is a
|
|
* compile-time property of the armed kernel — four tiles at `DstSync::SyncHalf`, eight at
|
|
* SyncFull — and being wrong in the permissive direction is SILENT: a pass writes over the
|
|
* half the packer is reading and every kernel retires clean. The interlock that guarded it
|
|
* keyed on "an override directory is set", which was sound only while the embedded blob was
|
|
* SyncHalf. A SyncFull blob REVERSES that argument, and there is no version of "which
|
|
* directory did you name" that survives the reversal. The recorded `defines=` does, because
|
|
* it moves with the image instead of with the operator.
|
|
*/
|
|
|
|
/** The `recipe` line of a `.prov` record, or NULL. Records are small and line-oriented. */
|
|
static const char *provenance_recipe_line(const char *record)
|
|
{
|
|
const char *line = record;
|
|
|
|
while (line && *line) {
|
|
if (strncmp(line, "recipe ", 7) == 0)
|
|
return line + 7;
|
|
line = strchr(line, '\n');
|
|
if (line)
|
|
line++;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/**
|
|
* Copy the `defines=` field out of a recipe line.
|
|
*
|
|
* `defines` is the LAST key `format_recipe()` writes and the only one whose value may
|
|
* contain spaces, so it is read to end-of-line rather than as a whitespace token. An absent
|
|
* key is not the same as an empty one: `format_recipe()` writes every key even when unset,
|
|
* so a record with no `defines=` at all predates the field and cannot answer the question —
|
|
* which is a REFUSAL here, not an empty string that would read as "not SyncFull".
|
|
*/
|
|
static int recipe_defines(const char *recipe, char *out, size_t out_len)
|
|
{
|
|
const char *field = strstr(recipe, "defines=");
|
|
const char *end;
|
|
size_t length;
|
|
|
|
if (!field)
|
|
return FT_TT_ENOTFOUND;
|
|
field += 8;
|
|
end = strchr(field, '\n');
|
|
length = end ? (size_t)(end - field) : strlen(field);
|
|
if (length + 1u > out_len)
|
|
return FT_TT_EINVAL;
|
|
memcpy(out, field, length);
|
|
out[length] = '\0';
|
|
return FT_TT_OK;
|
|
}
|
|
|
|
int ft_tt_program_recorded_defines(const char *program_name, const char *override_root,
|
|
unsigned processor, char *out, size_t out_len)
|
|
{
|
|
const ft_tt_program_blob_entry *entry = NULL;
|
|
const ft_tt_program_blob_image *image = NULL;
|
|
const char *recipe;
|
|
int rc;
|
|
|
|
if (!program_name || !*program_name || !out || out_len == 0u ||
|
|
processor >= FT_TT_PROC_COUNT)
|
|
return FT_TT_EINVAL;
|
|
out[0] = '\0';
|
|
if (override_root && *override_root) {
|
|
char path[FT_TT_PROGRAM_RESOLVE_PATH_BYTES];
|
|
char record[4096];
|
|
size_t read_bytes;
|
|
FILE *file;
|
|
|
|
const char *image_name = ft_tt_runtime_kernel_image_name(processor);
|
|
|
|
if (!image_name)
|
|
return FT_TT_EINVAL;
|
|
if (snprintf(path, sizeof(path), "%s/%s/%s.prov", override_root, program_name,
|
|
image_name) >= (int)sizeof(path))
|
|
return FT_TT_EINVAL;
|
|
file = fopen(path, "rb");
|
|
if (!file) {
|
|
ft_tt_log_error("tt program: '%s' has no provenance record at %s — a directory "
|
|
"that does not record how it was built cannot answer what it was "
|
|
"built WITH.", program_name, path);
|
|
return FT_TT_ENOTFOUND;
|
|
}
|
|
read_bytes = fread(record, 1u, sizeof(record) - 1u, file);
|
|
fclose(file);
|
|
record[read_bytes] = '\0';
|
|
recipe = provenance_recipe_line(record);
|
|
if (!recipe)
|
|
return FT_TT_ENOTFOUND;
|
|
return recipe_defines(recipe, out, out_len);
|
|
}
|
|
|
|
rc = ft_tt_program_blob_find(program_name, &entry);
|
|
if (rc != FT_TT_OK)
|
|
return rc;
|
|
rc = ft_tt_program_blob_kernel_image(entry, processor, &image);
|
|
if (rc != FT_TT_OK)
|
|
return rc;
|
|
if (!image->provenance)
|
|
return FT_TT_ENOTFOUND;
|
|
recipe = provenance_recipe_line(image->provenance);
|
|
if (!recipe)
|
|
return FT_TT_ENOTFOUND;
|
|
return recipe_defines(recipe, out, out_len);
|
|
}
|
|
|
|
int ft_tt_program_defines_contain(const char *defines, const char *wanted)
|
|
{
|
|
const char *at;
|
|
size_t length;
|
|
|
|
if (!defines || !wanted || !*wanted)
|
|
return 0;
|
|
length = strlen(wanted);
|
|
for (at = strstr(defines, wanted); at; at = strstr(at + 1, wanted)) {
|
|
const char after = at[length];
|
|
const char before = (at == defines) ? ' ' : at[-1];
|
|
|
|
/* WHOLE TOKEN ONLY. `-DFT_TT_MATMUL_DST_SYNC=1` must not be found inside
|
|
* `-DFT_TT_MATMUL_DST_SYNC=11`, and a substring match is exactly how a check like
|
|
* this reads green on something it does not mean. */
|
|
if ((before == ' ' || before == '\t') &&
|
|
(after == '\0' || after == ' ' || after == '\t'))
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|