Files
LithosAnanake/tools/mkcapsule.c
T
Robert Allan JamesandClaude Sonnet 5 eeceec21a5 FABRIC-3.md §I.5: Milestone 7 trust tiers (QEMU-vs-real-hardware), closing it
Closes the contributor-capsule/trust-tier punch-list item. Decided
direction: QEMU-vs-real-hardware conditional enforcement.

Found before building on that decision: the obvious mechanism (expose
TimerInfo.vm_mode) only works on amd64 -- aarch64 and riscv64 both had
vm_mode hardcoded to 1 unconditionally, meaning they'd always report
"running under QEMU" even on real hardware. Built real detection for
both instead of shipping that: aarch64 checks the ACPI RSDP's OEM ID
for QEMU's "BOCHS " SeaBIOS-heritage signature; riscv64 checks the
devicetree root compatible property for "qemu". Confirmed vm_mode was
otherwise unread anywhere else in either file first -- zero risk to
existing timing behavior.

CAPSULE_FLAG_CONTRIB (mkcapsule.c: FLAG_CONTRIB) path-matches on
capsules/contrib/, mirroring FLAG_MAMA_INIT's exact-match pattern.
contrib_capsule_refused() (capsule_birth.c) enforces: no additional
check under QEMU (same WARN-only as everything else); on real hardware,
a contrib capsule additionally requires CAPSULE_SIG_OK, since it has no
other provenance to fall back on. Wired into capsule_birth_baby() and
capsule_run_experiment().

Also updates §I.7 (Milestone 9): its stated precondition (Milestone 7
closing) is now met, flagged as stale rather than treated as a green
light to design networking from nothing.

Verified 3-arch boot to ok> (amd64/aarch64/riscv64, each in the
foreground) -- compile/boot verification only; the real-hardware
enforcement branch is unverifiable from this environment, same as all
of §I.6. logs and DoE CSVs from this session's verification runs
included per this repo's own audit-artifact convention.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019YcT3H2PQeyujrzjqS3Var
2026-09-04 11:04:25 -04:00

1258 lines
46 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 20232025 Robert A. James
All rights reserved.
mkcapsule - Generate capsule directory from a capsule tree
Usage:
mkcapsule <capsules_dir> <output.c> build capsule C source
mkcapsule --lint <capsules_dir> lint all .4th files
mkcapsule --manifest <capsules_dir> print block manifest to stdout
mkcapsule --manifest <capsules_dir> <out> write block manifest to file
Build mode generates a C source file containing:
- capsule_arena[] (all payload bytes concatenated)
- capsule_descriptors[] (CapsuleDesc, one per file)
- capsule_names[] (CapsuleNameEntry, parallel to descriptors)
- capsule_directory (CapsuleDirHeader)
Manifest mode generates a markdown block ownership index:
- Capsule summary (name, blocks claimed, xxHash64)
- Block map sorted by LBN with conflict detection
- Conflict register
The xxHash64 column is the anchor for future Ed25519 fingerprints (Phase 8).
Name encoding: relative path from capsules_dir root, '/' replaced by ':'.
Files whose encoded name would exceed 511 bytes are skipped with a warning.
The file extension is preserved verbatim and is meaningful only at load time.
*/
#define _DEFAULT_SOURCE /* For nftw, strdup */
#define _XOPEN_SOURCE 500
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <inttypes.h>
#include <ftw.h>
#include <time.h>
#include <sys/stat.h>
#include "pkcs8_ed25519.h"
#include "starkernel/ed25519.h"
/*===========================================================================
* xxHash64 (embedded for build tool — no external dependency)
*===========================================================================*/
#define XXHASH64_PRIME1 0x9E3779B185EBCA87ULL
#define XXHASH64_PRIME2 0xC2B2AE3D27D4EB4FULL
#define XXHASH64_PRIME3 0x165667B19E3779F9ULL
#define XXHASH64_PRIME4 0x85EBCA77C2B2AE63ULL
#define XXHASH64_PRIME5 0x27D4EB2F165667C5ULL
static inline uint64_t xxh64_rotl(uint64_t x, int r) {
return (x << r) | (x >> (64 - r));
}
static inline uint64_t xxh64_read64(const uint8_t *p) {
return ((uint64_t)p[0]) | ((uint64_t)p[1] << 8) |
((uint64_t)p[2] << 16) | ((uint64_t)p[3] << 24) |
((uint64_t)p[4] << 32) | ((uint64_t)p[5] << 40) |
((uint64_t)p[6] << 48) | ((uint64_t)p[7] << 56);
}
static inline uint32_t xxh64_read32(const uint8_t *p) {
return ((uint32_t)p[0]) | ((uint32_t)p[1] << 8) |
((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
}
static inline uint64_t xxh64_round(uint64_t acc, uint64_t input) {
acc += input * XXHASH64_PRIME2;
acc = xxh64_rotl(acc, 31);
acc *= XXHASH64_PRIME1;
return acc;
}
static inline uint64_t xxh64_merge_round(uint64_t acc, uint64_t val) {
val = xxh64_round(0, val);
acc ^= val;
acc = acc * XXHASH64_PRIME1 + XXHASH64_PRIME4;
return acc;
}
static inline uint64_t xxh64_avalanche(uint64_t h) {
h ^= h >> 33;
h *= XXHASH64_PRIME2;
h ^= h >> 29;
h *= XXHASH64_PRIME3;
h ^= h >> 32;
return h;
}
static uint64_t xxhash64(const void *data, size_t len, uint64_t seed) {
const uint8_t *p = (const uint8_t *)data;
const uint8_t *end = p + len;
uint64_t h64;
if (len >= 32) {
const uint8_t *limit = end - 32;
uint64_t v1 = seed + XXHASH64_PRIME1 + XXHASH64_PRIME2;
uint64_t v2 = seed + XXHASH64_PRIME2;
uint64_t v3 = seed + 0;
uint64_t v4 = seed - XXHASH64_PRIME1;
do {
v1 = xxh64_round(v1, xxh64_read64(p)); p += 8;
v2 = xxh64_round(v2, xxh64_read64(p)); p += 8;
v3 = xxh64_round(v3, xxh64_read64(p)); p += 8;
v4 = xxh64_round(v4, xxh64_read64(p)); p += 8;
} while (p <= limit);
h64 = xxh64_rotl(v1, 1) + xxh64_rotl(v2, 7) +
xxh64_rotl(v3, 12) + xxh64_rotl(v4, 18);
h64 = xxh64_merge_round(h64, v1);
h64 = xxh64_merge_round(h64, v2);
h64 = xxh64_merge_round(h64, v3);
h64 = xxh64_merge_round(h64, v4);
} else {
h64 = seed + XXHASH64_PRIME5;
}
h64 += (uint64_t)len;
while (p + 8 <= end) {
uint64_t k1 = xxh64_round(0, xxh64_read64(p));
h64 ^= k1;
h64 = xxh64_rotl(h64, 27) * XXHASH64_PRIME1 + XXHASH64_PRIME4;
p += 8;
}
if (p + 4 <= end) {
h64 ^= (uint64_t)xxh64_read32(p) * XXHASH64_PRIME1;
h64 = xxh64_rotl(h64, 23) * XXHASH64_PRIME2 + XXHASH64_PRIME3;
p += 4;
}
while (p < end) {
h64 ^= (uint64_t)(*p) * XXHASH64_PRIME5;
h64 = xxh64_rotl(h64, 11) * XXHASH64_PRIME1;
p++;
}
return xxh64_avalanche(h64);
}
/*===========================================================================
* Capsule Collection
*===========================================================================*/
#define MAX_CAPSULES 256
#define MAX_PATH_LEN 4096
#define CAPSULE_NAME_MAX 512
#define MAX_BLOCKS_PER_CAPSULE 64
typedef struct {
char path[MAX_PATH_LEN]; /* Absolute source file path */
char name[CAPSULE_NAME_MAX]; /* Colon-separated capsule name */
uint8_t *data; /* File contents */
size_t length; /* File size in bytes */
uint64_t hash; /* xxHash64 of contents */
uint32_t flags; /* Capsule flags */
uint8_t sig[64]; /* Ed25519 R||S, valid iff has_sig */
int has_sig; /* 1 if sig[] was produced by --sign-key */
} CapsuleEntry;
static CapsuleEntry capsules[MAX_CAPSULES];
static int capsule_count = 0;
static const char *base_dir = NULL;
static size_t base_dir_len = 0;
typedef struct {
char name[CAPSULE_NAME_MAX];
uint64_t hash;
int blocks[MAX_BLOCKS_PER_CAPSULE];
int block_count;
int has_sig; /* 1 if signed this run (--manifest --sign-key <path>) */
} ManifestEntry;
static ManifestEntry manifest_entries[MAX_CAPSULES];
static int manifest_count = 0;
/*===========================================================================
* Capsule signing (Milestone 6, Phase 8) -- host-side only
*===========================================================================
* Signs using this project's own ed25519_sign() (src/starkernel/crypto/
* ed25519.c), linked directly into this tool, and pkcs8_ed25519.c to
* extract the raw seed from the intermediate's PKCS#8 private key file --
* NOT by shelling out to the external `openssl` binary. Corrected
* 2026-08-26: an earlier version of this function forked+exec'd
* `openssl pkeyutl -sign`, reasoning that private-key handling was
* sensitive enough to prefer an already-audited external tool; the
* project's own standing preference is the opposite -- no new host
* binary dependency when the repo's own code (already independently
* verified against OpenSSL in Phase B) can do the job. Verified
* end-to-end (2026-08-26): the seed this extracts, run through this
* project's own ed25519_keygen(), reproduces the exact pubkey embedded
* in the cert, and a full self-contained sign+verify round-trip (no
* openssl involved at all) passes.
*
* The seed is read once at startup (see main()'s --sign-key handling),
* not per-capsule -- this function just signs.
*
* Signs the exact same bytes content_hash already covers (the raw
* capsule payload), not the hash value -- the natural reading of
* "a signature alongside each capsule's existing xxHash64."
*/
static uint8_t sign_seed[32]; /* set once by main() if --sign-key given */
static int have_sign_seed = 0;
static void sign_capsule_bytes(const uint8_t *data, size_t len,
uint8_t sig_out[64]) {
ed25519_sign(sign_seed, data, len, sig_out);
}
/* Flag constants — must match capsule.h */
#define FLAG_ACTIVE 0x00000001
#define FLAG_PRODUCTION 0x00000010
#define FLAG_EXPERIMENT 0x00000020
#define FLAG_MAMA_INIT 0x00000040
#define FLAG_CONTRIB 0x00000080
/*
* Determine flags from the colon-separated capsule name.
*
* init.4th (bare) is Mama's canonical init — gets FLAG_MAMA_INIT only.
* Anything under capsules/contrib/ (name starts with "contrib:") gets
* FLAG_CONTRIB in addition to the usual PRODUCTION|EXPERIMENT pair
* (FABRIC-3.md §I.5, 2026-09-04) — path-match, mirrors FLAG_MAMA_INIT's
* own exact-match pattern one line up, just prefix instead of exact.
* All other capsules carry both FLAG_PRODUCTION and FLAG_EXPERIMENT so
* that birth eligibility is not gated on mode type (D2).
*/
static uint32_t flags_from_name(const char *name) {
uint32_t flags = FLAG_ACTIVE;
if (strcmp(name, "init.4th") == 0) {
flags |= FLAG_MAMA_INIT;
} else {
flags |= FLAG_PRODUCTION | FLAG_EXPERIMENT;
if (strncmp(name, "contrib:", 8) == 0) {
flags |= FLAG_CONTRIB;
}
}
return flags;
}
/*
* Magic-number content-type detection (FABRIC-3.md §I.4, FABRIC-2.md
* Section U item 14). mkcapsule's own header comment has long said "the
* file extension is preserved verbatim and is meaningful only at load
* time" -- nothing has ever checked that a file's actual bytes match
* what its extension claims. This is a cross-check, not a new content
* pipeline: process_file() below calls it once per file and WARNS (never
* refuses -- same "land WARN-only first" rollout this project already
* used for capsule signing) on a mismatch. Deliberately narrow: this
* project's own capsule universe is exactly four content shapes today
* (FORTH source, markdown, DER binary, TTF/OpenType font), not a general
* multimedia magic-number library.
*/
typedef enum {
CONTENT_TYPE_UNKNOWN = 0, /* no expectation, or couldn't classify -- not an error */
CONTENT_TYPE_TEXT, /* .4th (FORTH source), .md (markdown) */
CONTENT_TYPE_DER, /* .der (X.509 cert / PKCS8 key, both ASN.1 DER) */
CONTENT_TYPE_TTF /* .ttf (TrueType) or OpenType sharing the same extension */
} content_type_t;
static const char *content_type_name(content_type_t t) {
switch (t) {
case CONTENT_TYPE_TEXT: return "text";
case CONTENT_TYPE_DER: return "DER binary";
case CONTENT_TYPE_TTF: return "TTF/OpenType font";
default: return "unknown";
}
}
/* Detects by leading bytes, not extension. TTF/OpenType: the four
* well-known sfnt version tags. DER: ASN.1 SEQUENCE tag (0x30) -- every
* DER-encoded cert or PKCS8 key in this codebase's own universe (X.509
* certs, PKCS8 private keys) opens with one, per the existing
* x509_ed25519.c/pkcs8_ed25519.c parsers this mirrors. Text has no fixed
* magic -- heuristic: every byte is printable ASCII or TAB/CR/LF; a
* capsule embeds source/markdown, never arbitrary binary under those two
* extensions, so this heuristic doesn't need to be general-purpose. */
static content_type_t detect_content_type(const uint8_t *data, size_t len) {
if (len >= 4 &&
((data[0] == 0x00 && data[1] == 0x01 && data[2] == 0x00 && data[3] == 0x00) ||
memcmp(data, "OTTO", 4) == 0 || memcmp(data, "true", 4) == 0 ||
memcmp(data, "ttcf", 4) == 0)) {
return CONTENT_TYPE_TTF;
}
if (len >= 1 && data[0] == 0x30) {
return CONTENT_TYPE_DER;
}
for (size_t i = 0; i < len; i++) {
uint8_t c = data[i];
if (c == '\t' || c == '\r' || c == '\n') continue;
if (c < 0x20 || c > 0x7E) return CONTENT_TYPE_UNKNOWN;
}
return CONTENT_TYPE_TEXT;
}
/* What a file's extension implies its content should be. UNKNOWN means
* "no expectation" (e.g. fonts:README.md's sibling files, or anything
* outside this project's four known shapes) -- process_file() skips the
* cross-check entirely in that case, never warns on an extension it has
* no opinion about. */
static content_type_t expected_type_from_ext(const char *fpath) {
size_t n = strlen(fpath);
if (n >= 4 && strcmp(fpath + n - 4, ".der") == 0) return CONTENT_TYPE_DER;
if (n >= 4 && strcmp(fpath + n - 4, ".ttf") == 0) return CONTENT_TYPE_TTF;
if (n >= 4 && strcmp(fpath + n - 4, ".4th") == 0) return CONTENT_TYPE_TEXT;
if (n >= 3 && strcmp(fpath + n - 3, ".md") == 0) return CONTENT_TYPE_TEXT;
return CONTENT_TYPE_UNKNOWN;
}
/*
* Build the colon-separated capsule name from a relative path.
* Returns 1 on success, 0 if the name would exceed CAPSULE_NAME_MAX-1 bytes.
*/
static int build_capsule_name(const char *relpath, char *out) {
size_t len = strlen(relpath);
if (len >= CAPSULE_NAME_MAX) {
return 0; /* too long */
}
size_t i;
for (i = 0; i < len; i++) {
out[i] = (relpath[i] == '/') ? ':' : relpath[i];
}
out[len] = '\0';
return 1;
}
/*
* validate_forth_blocks: enforce 64-char × 16-line block format on .4th files.
* Checks: block header "Block N", line length ≤ 64, ≤ 16 content lines/block,
* and that every line (including the last) is terminated with '\n'.
* Returns 0 if clean, >0 if violations were found.
*/
static int validate_forth_blocks(const char *fpath,
const uint8_t *data, size_t size)
{
int errors = 0;
int block_num = -1;
int line_in_block = 0;
int file_line = 0;
size_t pos = 0;
while (pos < size) {
size_t start = pos;
while (pos < size && data[pos] != '\n') pos++;
int has_newline = (pos < size); /* stopped at '\n', not EOF */
size_t line_len = pos - start;
if (line_len > 0 && data[start + line_len - 1] == '\r') line_len--;
file_line++;
/* Unterminated: non-empty content with no trailing '\n' */
if (!has_newline && line_len > 0) {
fprintf(stderr,
"mkcapsule: ERROR: %s:%d: unterminated line (missing newline)\n",
fpath, file_line);
errors++;
}
/* Build a printable snippet for diagnostics (first 40 chars) */
char snippet[44];
{
size_t slen = line_len < 40 ? line_len : 40;
size_t i;
for (i = 0; i < slen; i++) {
unsigned char c = data[start + i];
snippet[i] = (c >= 0x20 && c < 0x7F) ? (char)c : '?';
}
if (line_len > 40) {
snippet[40] = '.'; snippet[41] = '.';
snippet[42] = '.'; snippet[43] = '\0';
} else {
snippet[slen] = '\0';
}
}
/* Block header: "Block NNN" */
if (line_len >= 7 &&
data[start+0]=='B' && data[start+1]=='l' && data[start+2]=='o' &&
data[start+3]=='c' && data[start+4]=='k' && data[start+5]==' ') {
if (line_len > 64) {
fprintf(stderr,
"mkcapsule: ERROR: %s:%d: block header %zu chars (max 64)"
": \"%s\"\n",
fpath, file_line, line_len, snippet);
errors++;
}
block_num = (int)strtol((const char *)(data + start + 6), NULL, 10);
line_in_block = 0;
/* Block number must be in user space: [2048, 5120) */
if (block_num < 2048 || block_num >= 5120) {
fprintf(stderr,
"mkcapsule: ERROR: %s:%d: block %d out of user range "
"[2048, 5120)\n",
fpath, file_line, block_num);
errors++;
}
} else if (block_num >= 0) {
if (line_len > 64) {
fprintf(stderr,
"mkcapsule: ERROR: %s:%d: block %d line %d: "
"%zu chars (max 64): \"%s\"\n",
fpath, file_line, block_num, line_in_block + 1,
line_len, snippet);
errors++;
}
line_in_block++;
if (line_in_block == 17) {
fprintf(stderr,
"mkcapsule: ERROR: %s:%d: block %d: "
"more than 16 content lines\n",
fpath, file_line, block_num);
errors++;
}
}
if (has_newline) pos++; /* advance past '\n' */
}
return errors;
}
/*
* nftw callback — called once per filesystem entry.
* Incorporates every regular file found; skips names that are too long.
*/
static int process_file(const char *fpath, const struct stat *sb,
int typeflag, struct FTW *ftwbuf) {
(void)sb;
if (typeflag != FTW_F) {
return 0; /* directories and symlinks: skip */
}
/* Skip hidden files (basename begins with '.') */
if (fpath[ftwbuf->base] == '.') {
return 0;
}
/* Derive relative path from base_dir */
const char *relpath;
if (strlen(fpath) > base_dir_len && fpath[base_dir_len] == '/') {
relpath = fpath + base_dir_len + 1;
} else {
relpath = fpath;
}
/* Build colon-separated name; skip with warning if too long */
char name[CAPSULE_NAME_MAX];
if (!build_capsule_name(relpath, name)) {
fprintf(stderr,
"mkcapsule: WARNING: skipping '%s' — name would exceed %d bytes\n",
relpath, CAPSULE_NAME_MAX - 1);
return 0;
}
if (capsule_count >= MAX_CAPSULES) {
fprintf(stderr, "mkcapsule: ERROR: too many capsules (max %d)\n",
MAX_CAPSULES);
return -1;
}
/* Read file contents */
FILE *f = fopen(fpath, "rb");
if (!f) {
fprintf(stderr, "mkcapsule: ERROR: cannot open '%s'\n", fpath);
return -1;
}
fseek(f, 0, SEEK_END);
long size = ftell(f);
fseek(f, 0, SEEK_SET);
if (size <= 0) {
fclose(f);
fprintf(stderr, "mkcapsule: WARNING: skipping empty file '%s'\n", fpath);
return 0;
}
uint8_t *data = malloc((size_t)size);
if (!data) {
fclose(f);
fprintf(stderr, "mkcapsule: ERROR: out of memory reading '%s'\n", fpath);
return -1;
}
if (fread(data, 1, (size_t)size, f) != (size_t)size) {
free(data);
fclose(f);
fprintf(stderr, "mkcapsule: ERROR: failed to read '%s'\n", fpath);
return -1;
}
fclose(f);
/* Validate 64x16 block format for .4th capsule files */
{
size_t nlen = strlen(fpath);
if (nlen >= 4 && strcmp(fpath + nlen - 4, ".4th") == 0) {
int bv = validate_forth_blocks(fpath, data, (size_t)size);
if (bv > 0) {
free(data);
fprintf(stderr,
"mkcapsule: ERROR: %s: %d block format violation(s) "
"— fix before building\n", fpath, bv);
return -1;
}
}
}
/* Magic-number content-type cross-check (FABRIC-3.md §I.4) --
* WARN-only, never refuses; UNKNOWN on either side means "no
* expectation," not a mismatch. */
{
content_type_t want = expected_type_from_ext(fpath);
content_type_t got = detect_content_type(data, (size_t)size);
if (want != CONTENT_TYPE_UNKNOWN && got != CONTENT_TYPE_UNKNOWN && want != got) {
fprintf(stderr,
"mkcapsule: WARNING: %s: content doesn't match its extension "
"(expected %s, detected %s)\n",
fpath, content_type_name(want), content_type_name(got));
}
}
/* Fill entry */
CapsuleEntry *e = &capsules[capsule_count];
snprintf(e->path, MAX_PATH_LEN, "%s", fpath);
snprintf(e->name, CAPSULE_NAME_MAX, "%s", name);
e->data = data;
e->length = (size_t)size;
e->hash = xxhash64(data, (size_t)size, 0);
e->flags = flags_from_name(name);
e->has_sig = 0;
if (have_sign_seed) {
sign_capsule_bytes(data, (size_t)size, e->sig);
e->has_sig = 1;
}
capsule_count++;
char mode = 'e';
if (e->flags & FLAG_MAMA_INIT) mode = 'm';
else if (e->flags & FLAG_PRODUCTION) mode = 'p';
fprintf(stderr, " [%c] %s (%" PRIu64 " bytes, hash=0x%016" PRIx64 "%s)\n",
mode, e->name, (uint64_t)e->length, e->hash,
e->has_sig ? ", signed" : "");
return 0;
}
/*===========================================================================
* Standalone Lint Mode
*===========================================================================*/
static int lint_errors = 0;
static int lint_files = 0;
static int lint_failing = 0;
static int lint_file(const char *fpath, const struct stat *sb,
int typeflag, struct FTW *ftwbuf) {
(void)sb;
if (typeflag != FTW_F) return 0;
if (fpath[ftwbuf->base] == '.') return 0;
size_t nlen = strlen(fpath);
if (nlen < 4 || strcmp(fpath + nlen - 4, ".4th") != 0) return 0;
FILE *f = fopen(fpath, "rb");
if (!f) {
fprintf(stderr, " ERROR: cannot open '%s'\n", fpath);
lint_errors++;
return 0;
}
fseek(f, 0, SEEK_END);
long sz = ftell(f);
rewind(f);
lint_files++;
if (sz <= 0) {
fclose(f);
fprintf(stderr, " PASS: %s (empty)\n", fpath);
return 0;
}
uint8_t *data = malloc((size_t)sz);
if (!data || fread(data, 1, (size_t)sz, f) != (size_t)sz) {
free(data);
fclose(f);
fprintf(stderr, " ERROR: read failed '%s'\n", fpath);
lint_errors++;
return 0;
}
fclose(f);
int errs = validate_forth_blocks(fpath, data, (size_t)sz);
free(data);
if (errs == 0) {
fprintf(stderr, " PASS: %s\n", fpath);
} else {
fprintf(stderr, " FAIL: %s (%d violation%s)\n",
fpath, errs, errs == 1 ? "" : "s");
lint_errors += errs;
lint_failing++;
}
return 0;
}
/*===========================================================================
* Manifest Mode
*===========================================================================*/
/*
* collect_block_numbers: scan file data for "Block N" header lines and
* accumulate unique, sorted LBNs. Returns the count stored.
*/
static int collect_block_numbers(const uint8_t *data, size_t size,
int *out, int max)
{
int count = 0;
size_t pos = 0;
while (pos < size) {
size_t start = pos;
while (pos < size && data[pos] != '\n') pos++;
size_t line_len = pos - start;
if (line_len > 0 && data[start + line_len - 1] == '\r') line_len--;
if (line_len >= 7 &&
data[start+0]=='B' && data[start+1]=='l' && data[start+2]=='o' &&
data[start+3]=='c' && data[start+4]=='k' && data[start+5]==' ') {
int bn = (int)strtol((const char *)(data + start + 6), NULL, 10);
int dup = 0;
for (int i = 0; i < count; i++) {
if (out[i] == bn) { dup = 1; break; }
}
if (!dup && count < max)
out[count++] = bn;
}
if (pos < size) pos++;
}
return count;
}
static int cmp_int(const void *a, const void *b) {
return (*(const int *)a) - (*(const int *)b);
}
static int cmp_manifest_name(const void *a, const void *b) {
return strcmp(((const ManifestEntry *)a)->name,
((const ManifestEntry *)b)->name);
}
/* nftw callback: collect .4th capsules for manifest */
static int manifest_file(const char *fpath, const struct stat *sb,
int typeflag, struct FTW *ftwbuf) {
(void)sb;
if (typeflag != FTW_F) return 0;
if (fpath[ftwbuf->base] == '.') return 0;
size_t nlen = strlen(fpath);
if (nlen < 4 || strcmp(fpath + nlen - 4, ".4th") != 0) return 0;
const char *relpath;
if (strlen(fpath) > base_dir_len && fpath[base_dir_len] == '/')
relpath = fpath + base_dir_len + 1;
else
relpath = fpath;
char name[CAPSULE_NAME_MAX];
if (!build_capsule_name(relpath, name)) return 0;
FILE *f = fopen(fpath, "rb");
if (!f) return 0;
fseek(f, 0, SEEK_END);
long sz = ftell(f);
rewind(f);
if (sz <= 0) { fclose(f); return 0; }
uint8_t *data = malloc((size_t)sz);
if (!data || fread(data, 1, (size_t)sz, f) != (size_t)sz) {
free(data); fclose(f); return 0;
}
fclose(f);
if (manifest_count >= MAX_CAPSULES) { free(data); return 0; }
ManifestEntry *e = &manifest_entries[manifest_count++];
snprintf(e->name, CAPSULE_NAME_MAX, "%s", name);
e->hash = xxhash64(data, (size_t)sz, 0);
e->block_count = collect_block_numbers(data, (size_t)sz,
e->blocks, MAX_BLOCKS_PER_CAPSULE);
qsort(e->blocks, (size_t)e->block_count, sizeof(int), cmp_int);
e->has_sig = 0;
if (have_sign_seed) {
uint8_t sig[64];
sign_capsule_bytes(data, (size_t)sz, sig);
e->has_sig = 1;
}
free(data);
return 0;
}
/* Flat (LBN, entry-index) pair used for the sorted block map */
typedef struct { int lbn; int idx; } BlockRef;
static int cmp_blockref(const void *a, const void *b) {
const BlockRef *x = (const BlockRef *)a;
const BlockRef *y = (const BlockRef *)b;
if (x->lbn != y->lbn) return x->lbn - y->lbn;
return strcmp(manifest_entries[x->idx].name,
manifest_entries[y->idx].name);
}
static void generate_manifest(FILE *out) {
time_t now = time(NULL);
struct tm *tm_ptr = gmtime(&now);
char timestamp[64];
strftime(timestamp, sizeof(timestamp), "%Y-%m-%dT%H:%M:%SZ", tm_ptr);
qsort(manifest_entries, (size_t)manifest_count,
sizeof(ManifestEntry), cmp_manifest_name);
/* Build flat block reference list */
static BlockRef refs[MAX_CAPSULES * MAX_BLOCKS_PER_CAPSULE];
int ref_count = 0;
for (int i = 0; i < manifest_count; i++) {
for (int j = 0; j < manifest_entries[i].block_count; j++) {
refs[ref_count].lbn = manifest_entries[i].blocks[j];
refs[ref_count].idx = i;
ref_count++;
}
}
qsort(refs, (size_t)ref_count, sizeof(BlockRef), cmp_blockref);
fprintf(out,
"# Capsule Block Manifest — Auto-generated\n"
"<!-- Generated by mkcapsule --manifest %s -->\n"
"<!-- DO NOT EDIT — re-run mkcapsule --manifest to refresh. -->\n"
"<!-- Hand-written justifications and immutability notes live -->\n"
"<!-- in MANIFEST.md alongside this auto-generated index. -->\n"
"\n", timestamp);
/* Capsule summary */
fprintf(out, "## Capsule Summary\n\n");
fprintf(out, "| Capsule | Blocks claimed | xxHash64 | Signed |\n");
fprintf(out, "|---------|----------------|----------|--------|\n");
for (int i = 0; i < manifest_count; i++) {
ManifestEntry *e = &manifest_entries[i];
fprintf(out, "| `%s` | ", e->name);
if (e->block_count == 0) {
fprintf(out, "*(none — raw code capsule)*");
} else {
for (int j = 0; j < e->block_count; j++) {
if (j > 0) fprintf(out, ", ");
fprintf(out, "%d", e->blocks[j]);
}
}
fprintf(out, " | `0x%016" PRIx64 "` | %s |\n", e->hash,
have_sign_seed ? (e->has_sig ? "yes" : "no") : "n/a");
}
fprintf(out, "\n");
if (!have_sign_seed) {
fprintf(out,
"*Signed column is `n/a`: this manifest run had no `--sign-key`. "
"Re-run with `--sign-key <path>` to check signing status "
"(does not modify or require rebuilding capsule_generated.c).*\n\n");
}
/* Block map sorted by LBN */
fprintf(out, "## Block Map (sorted by LBN)\n\n");
fprintf(out, "| LBN | Capsule | xxHash64 | Status |\n");
fprintf(out, "|-----|---------|----------|--------|\n");
for (int i = 0; i < ref_count; i++) {
int lbn = refs[i].lbn;
ManifestEntry *e = &manifest_entries[refs[i].idx];
int conflict = (i > 0 && refs[i-1].lbn == lbn) ||
(i < ref_count - 1 && refs[i+1].lbn == lbn);
fprintf(out, "| %d | `%s` | `0x%016" PRIx64 "` | %s |\n",
lbn, e->name, e->hash,
conflict ? "CONFLICT" : "ok");
}
fprintf(out, "\n");
/* Conflict register */
int any = 0;
for (int i = 0; i + 1 < ref_count; i++)
if (refs[i].lbn == refs[i+1].lbn) { any = 1; break; }
fprintf(out, "## Conflicts\n\n");
if (!any) {
fprintf(out, "None.\n\n");
} else {
fprintf(out, "| LBN | Capsules |\n");
fprintf(out, "|-----|----------|\n");
int i = 0;
while (i < ref_count) {
int lbn = refs[i].lbn;
int j = i;
while (j < ref_count && refs[j].lbn == lbn) j++;
if (j - i > 1) {
fprintf(out, "| %d | ", lbn);
for (int k = i; k < j; k++) {
if (k > i) fprintf(out, ", ");
fprintf(out, "`%s`", manifest_entries[refs[k].idx].name);
}
fprintf(out, " |\n");
}
i = j;
}
fprintf(out, "\n");
}
fprintf(out, "---\n");
fprintf(out,
"*%d capsule(s) scanned. "
"Re-run `mkcapsule --manifest <dir>` to refresh.*\n",
manifest_count);
}
/*===========================================================================
* Block Conflict Gate
*
* Runs the same block-collision scan as --manifest, but as a hard build
* gate: any two capsules claiming the same LBN fail the build outright.
* This exists because the ordinary build path (process_file / nftw below)
* bakes capsules with zero collision checking, which is exactly how two
* real bugs (Hermes vs Artemis; init-6.4th/init-l8-omni.4th vs init.4th)
* went undetected for days until someone happened to run --manifest by
* hand. See capsules/MANIFEST.md Conflict Register C1/C2/C5.
*===========================================================================*/
static int check_block_conflicts(void) {
manifest_count = 0;
if (nftw(base_dir, manifest_file, 20, FTW_PHYS) != 0) {
fprintf(stderr, "mkcapsule: ERROR: directory scan failed during conflict check\n");
return 1;
}
static BlockRef refs[MAX_CAPSULES * MAX_BLOCKS_PER_CAPSULE];
int ref_count = 0;
for (int i = 0; i < manifest_count; i++) {
for (int j = 0; j < manifest_entries[i].block_count; j++) {
refs[ref_count].lbn = manifest_entries[i].blocks[j];
refs[ref_count].idx = i;
ref_count++;
}
}
qsort(refs, (size_t)ref_count, sizeof(BlockRef), cmp_blockref);
int conflicts = 0;
int i = 0;
while (i < ref_count) {
int lbn = refs[i].lbn;
int j = i;
while (j < ref_count && refs[j].lbn == lbn) j++;
if (j - i > 1) {
fprintf(stderr, "mkcapsule: ERROR: block %d claimed by %d capsules: ",
lbn, j - i);
for (int k = i; k < j; k++) {
if (k > i) fprintf(stderr, ", ");
fprintf(stderr, "%s", manifest_entries[refs[k].idx].name);
}
fprintf(stderr, "\n");
conflicts++;
}
i = j;
}
if (conflicts > 0) {
fprintf(stderr,
"mkcapsule: ERROR: %d block conflict(s) found — resolve in the\n"
" .4th source (renumber one side) and in capsules/MANIFEST.md\n"
" before building. Run `mkcapsule --manifest %s` for the full\n"
" report.\n",
conflicts, base_dir);
}
return conflicts > 0 ? 1 : 0;
}
/*===========================================================================
* C Source Generation
*===========================================================================*/
static void emit_escaped_name(FILE *out, const char *name) {
/* Emit name as a C string literal with all non-printable bytes escaped */
fputc('"', out);
for (const char *p = name; *p; p++) {
unsigned char c = (unsigned char)*p;
if (c == '"' || c == '\\') {
fputc('\\', out);
fputc(c, out);
} else if (c < 0x20 || c >= 0x7F) {
fprintf(out, "\\x%02x", c);
} else {
fputc(c, out);
}
}
fputc('"', out);
}
static void generate_output(FILE *out) {
time_t now = time(NULL);
struct tm *tm_ptr = gmtime(&now);
char timestamp[64];
strftime(timestamp, sizeof(timestamp), "%Y-%m-%dT%H:%M:%SZ", tm_ptr);
fprintf(out,
"/*\n"
" * capsule_generated.c — Generated Capsule Directory\n"
" *\n"
" * Generated by mkcapsule at %s\n"
" * DO NOT EDIT — this file is auto-generated by the build system.\n"
" *\n"
" * Capsule count: %d\n"
" */\n\n",
timestamp, capsule_count);
fprintf(out, "#include \"starkernel/capsule.h\"\n\n");
/*------------------------------------------------------------------
* Payload arena
*------------------------------------------------------------------*/
fprintf(out,
"/*===========================================================================\n"
" * Payload Arena\n"
" *===========================================================================*/\n\n"
"const uint8_t capsule_arena[] = {\n");
size_t arena_offset = 0;
for (int i = 0; i < capsule_count; i++) {
CapsuleEntry *e = &capsules[i];
fprintf(out,
" /* [%d] %s offset=%zu length=%zu hash=0x%016" PRIx64 " */\n",
i, e->name, arena_offset, e->length, e->hash);
for (size_t j = 0; j < e->length; j++) {
if (j % 16 == 0) fprintf(out, " ");
fprintf(out, "0x%02X,", e->data[j]);
if (j % 16 == 15 || j == e->length - 1) fputc('\n', out);
else fputc(' ', out);
}
fputc('\n', out);
arena_offset += e->length;
}
fprintf(out, "};\n\n");
/*------------------------------------------------------------------
* Descriptor array
*------------------------------------------------------------------*/
fprintf(out,
"/*===========================================================================\n"
" * Capsule Descriptors\n"
" *===========================================================================*/\n\n"
"const CapsuleDesc capsule_descriptors[%d] = {\n", capsule_count);
size_t offset = 0;
for (int i = 0; i < capsule_count; i++) {
CapsuleEntry *e = &capsules[i];
const char *flags_comment =
(e->flags & FLAG_MAMA_INIT) ? "MAMA_INIT | ACTIVE" :
"PRODUCTION | EXPERIMENT | ACTIVE";
fprintf(out,
" /* [%d] %s */\n"
" {\n"
" .magic = CAPSULE_MAGIC_PACK(CAPSULE_VERSION_0, CAPSULE_HASH_XXHASH64),\n"
" .capsule_id = 0x%016" PRIx64 "ULL,\n"
" .content_hash = 0x%016" PRIx64 "ULL,\n"
" .offset = %zuULL,\n"
" .length = %zuULL,\n"
" .flags = 0x%08X, /* %s */\n"
" .owner_vm = 0,\n"
" .birth_count = 0,\n"
" .created_ns = 0,\n"
" },\n",
i, e->name,
e->hash, e->hash,
offset, e->length,
e->flags, flags_comment);
offset += e->length;
}
fprintf(out, "};\n\n");
/*------------------------------------------------------------------
* Name array (parallel to descriptors)
*------------------------------------------------------------------*/
fprintf(out,
"/*===========================================================================\n"
" * Capsule Names (colon-separated paths, 512 bytes each)\n"
" *===========================================================================*/\n\n"
"const CapsuleNameEntry capsule_names[%d] = {\n", capsule_count);
for (int i = 0; i < capsule_count; i++) {
CapsuleEntry *e = &capsules[i];
fprintf(out, " /* [%d] */ { .name = ", i);
emit_escaped_name(out, e->name);
fprintf(out, " },\n");
}
fprintf(out, "};\n\n");
/*------------------------------------------------------------------
* Signature array (parallel to descriptors, Milestone 6 / Phase 8)
*------------------------------------------------------------------*/
fprintf(out,
"/*===========================================================================\n"
" * Capsule Signatures (Ed25519 R||S, parallel to descriptors)\n"
" *===========================================================================*/\n\n"
"const CapsuleSigEntry capsule_signatures[%d] = {\n", capsule_count);
for (int i = 0; i < capsule_count; i++) {
CapsuleEntry *e = &capsules[i];
fprintf(out, " /* [%d] %s */\n { .sig = {", i, e->name);
for (int j = 0; j < 64; j++) {
fprintf(out, "0x%02X,", e->sig[j]);
if (j % 16 == 15) fprintf(out, "\n ");
}
fprintf(out, "}, .has_sig = %d },\n", e->has_sig);
}
fprintf(out, "};\n\n");
/*------------------------------------------------------------------
* Directory header
*------------------------------------------------------------------*/
uint64_t dir_hash = 0;
for (int i = 0; i < capsule_count; i++) {
dir_hash = xxhash64(&capsules[i].hash, sizeof(uint64_t), dir_hash);
}
fprintf(out,
"/*===========================================================================\n"
" * Directory Header\n"
" *===========================================================================*/\n\n"
"const CapsuleDirHeader capsule_directory = {\n"
" .magic = 0x%016" PRIx64 "ULL, /* 'CAPD' */\n"
" .arena_base = (uint64_t)(uintptr_t)capsule_arena,\n"
" .arena_size = sizeof(capsule_arena),\n"
" .desc_count = %d,\n"
" .desc_capacity = %d,\n"
" .name_count = %d,\n"
" .reserved = 0,\n"
" .dir_hash = 0x%016" PRIx64 "ULL,\n"
"};\n\n"
"/*===========================================================================\n"
" * PE/COFF GOT-indirection fix: accessor functions\n"
" *\n"
" * In -fPIC PE builds, cross-TU data references go through GOT and the PE\n"
" * linker does NOT convert GOTPCREL->LEA as the ELF linker does. Defining\n"
" * these accessors in the same TU as the symbols lets the compiler emit\n"
" * direct RIP-relative addressing; callers reach them via a direct CALL.\n"
" *===========================================================================*/\n\n"
"__attribute__((visibility(\"hidden\")))\n"
"uint32_t capsule_get_desc_count(void) { return capsule_directory.desc_count; }\n\n"
"__attribute__((visibility(\"hidden\")))\n"
"const CapsuleDirHeader *capsule_get_directory(void) { return &capsule_directory; }\n\n"
"__attribute__((visibility(\"hidden\")))\n"
"const CapsuleDesc *capsule_get_descriptors(void) { return capsule_descriptors; }\n\n"
"__attribute__((visibility(\"hidden\")))\n"
"const CapsuleNameEntry *capsule_get_names(void) { return capsule_names; }\n\n"
"__attribute__((visibility(\"hidden\")))\n"
"const CapsuleSigEntry *capsule_get_signatures(void) { return capsule_signatures; }\n\n"
"__attribute__((visibility(\"hidden\")))\n"
"const uint8_t *capsule_get_arena(void) { return capsule_arena; }\n",
(uint64_t)0x44504143ULL,
capsule_count, MAX_CAPSULES, capsule_count,
dir_hash);
}
/*===========================================================================
* Main
*===========================================================================*/
/* Loads key_path, extracts its Ed25519 seed into sign_seed, sets
* have_sign_seed=1 on success. Shared by build mode and --manifest mode
* (both accept an optional --sign-key <path>). Returns 0 on success,
* writes an ERROR to stderr and returns -1 on failure. */
static int load_sign_key(const char *key_path) {
FILE *kf = fopen(key_path, "rb");
if (!kf) {
fprintf(stderr, "mkcapsule: ERROR: cannot open key '%s'\n", key_path);
return -1;
}
fseek(kf, 0, SEEK_END);
long klen = ftell(kf);
fseek(kf, 0, SEEK_SET);
uint8_t *kbuf = malloc((size_t)klen);
if (!kbuf || fread(kbuf, 1, (size_t)klen, kf) != (size_t)klen) {
fprintf(stderr, "mkcapsule: ERROR: cannot read key '%s'\n", key_path);
fclose(kf);
free(kbuf);
return -1;
}
fclose(kf);
if (pkcs8_extract_ed25519_seed(kbuf, (size_t)klen, sign_seed) != 0) {
fprintf(stderr, "mkcapsule: ERROR: '%s' is not a valid PKCS#8 "
"Ed25519 private key\n", key_path);
free(kbuf);
return -1;
}
free(kbuf);
have_sign_seed = 1;
fprintf(stderr, "mkcapsule: signing capsules with key '%s'\n", key_path);
return 0;
}
int main(int argc, char **argv) {
/* --manifest mode: generate block ownership markdown. Optional
* --sign-key <keyfile> prefix: mkcapsule --manifest [--sign-key
* <keyfile>] <capsules_dir> [<out.md>] -- lets the manifest report
* real signing status without touching capsule_generated.c. */
if (argc >= 3 && strcmp(argv[1], "--manifest") == 0) {
int a = 2;
if (argc >= 4 && strcmp(argv[2], "--sign-key") == 0) {
if (argc < 5) {
fprintf(stderr, "mkcapsule: ERROR: --manifest --sign-key "
"requires <keyfile> <capsules_dir>\n");
return 1;
}
if (load_sign_key(argv[3]) != 0) return 1;
a = 4;
}
base_dir = argv[a];
base_dir_len = strlen(base_dir);
while (base_dir_len > 0 && base_dir[base_dir_len - 1] == '/') base_dir_len--;
if (nftw(base_dir, manifest_file, 20, FTW_PHYS) != 0) {
fprintf(stderr, "mkcapsule: ERROR: directory scan failed\n");
return 1;
}
FILE *mout = stdout;
int have_out = (argc == a + 2);
if (have_out) {
mout = fopen(argv[a + 1], "w");
if (!mout) {
fprintf(stderr, "mkcapsule: ERROR: cannot create '%s'\n", argv[a + 1]);
return 1;
}
}
generate_manifest(mout);
if (have_out) {
fclose(mout);
fprintf(stderr, "mkcapsule: manifest written to %s (%d capsule(s))\n",
argv[a + 1], manifest_count);
}
return 0;
}
/* --lint mode: validate all .4th files without generating C output */
if (argc == 3 && strcmp(argv[1], "--lint") == 0) {
base_dir = argv[2];
base_dir_len = strlen(base_dir);
while (base_dir_len > 0 && base_dir[base_dir_len - 1] == '/') base_dir_len--;
fprintf(stderr, "mkcapsule lint: scanning %s\n", base_dir);
nftw(base_dir, lint_file, 20, FTW_PHYS);
fprintf(stderr,
"mkcapsule lint: %d file(s), %d failing, %d violation(s) [%s]\n",
lint_files, lint_failing, lint_errors,
lint_errors == 0 ? "ALL PASS" : "FAIL");
return lint_errors > 0 ? 1 : 0;
}
/* Optional --sign-key <keyfile> prefix on build mode: mkcapsule
* [--sign-key <keyfile>] <capsules_dir> <output.c>. Read once here,
* not per-capsule -- process_file() just checks have_sign_seed. */
int arg0 = 1;
if (argc >= 2 && strcmp(argv[1], "--sign-key") == 0) {
if (argc != 5) {
fprintf(stderr, "mkcapsule: ERROR: --sign-key requires "
"<keyfile> <capsules_dir> <output.c>\n");
return 1;
}
if (load_sign_key(argv[2]) != 0) return 1;
arg0 = 3;
}
if (argc != arg0 + 2) {
fprintf(stderr,
"Usage:\n"
" %s [--sign-key <keyfile>] <capsules_dir> <output.c>\n"
" build capsule C source\n"
" %s --lint <capsules_dir> lint all .4th files\n"
" %s --manifest [--sign-key <keyfile>] <capsules_dir> [<out.md>]\n"
" print/write block manifest\n"
"\n"
"Lint checks (per .4th file):\n"
" - each block opens with 'Block N'\n"
" - every line <= 64 chars\n"
" - no more than 16 content lines per block\n"
" - every line terminated with newline\n"
"\n"
"Manifest output:\n"
" - capsule summary (name, blocks claimed, xxHash64)\n"
" - block map sorted by LBN with conflict detection\n"
" - conflict register\n"
" xxHash64 column is the anchor for Ed25519 fingerprints (Phase 8 PKI).\n"
"\n"
"Names are colon-separated relative paths (e.g. core:init.4th).\n"
"Files whose encoded name exceeds %d bytes are skipped with a warning.\n",
argv[0], argv[0], argv[0], CAPSULE_NAME_MAX - 1);
return 1;
}
base_dir = argv[arg0];
base_dir_len = strlen(base_dir);
/* Strip trailing slash from base_dir length so relpath strips cleanly */
while (base_dir_len > 0 && base_dir[base_dir_len - 1] == '/') {
base_dir_len--;
}
const char *output_path = argv[arg0 + 1];
fprintf(stderr, "mkcapsule: checking for block conflicts...\n");
if (check_block_conflicts() != 0) {
return 1;
}
fprintf(stderr, "mkcapsule: scanning %s\n", base_dir);
if (nftw(base_dir, process_file, 20, FTW_PHYS) != 0) {
fprintf(stderr, "mkcapsule: ERROR: directory scan failed\n");
return 1;
}
if (capsule_count == 0) {
fprintf(stderr, "mkcapsule: WARNING: no files found in %s\n", base_dir);
}
fprintf(stderr, "mkcapsule: %d capsule(s) collected\n", capsule_count);
FILE *out = fopen(output_path, "w");
if (!out) {
fprintf(stderr, "mkcapsule: ERROR: cannot create '%s'\n", output_path);
return 1;
}
generate_output(out);
fclose(out);
fprintf(stderr, "mkcapsule: wrote %s\n", output_path);
for (int i = 0; i < capsule_count; i++) {
free(capsules[i].data);
}
return 0;
}