Files
LithosAnanake/tools/mkcapsule.c
T
Robert Allan JamesandClaude Sonnet 5 cd2fda4351
Build / build-amd64-iso (push) Waiting to run
Build / build-aarch64-iso (push) Waiting to run
Build / build-riscv64-img (push) Waiting to run
Add mkcapsule --resolve: build-time claim registry for capsule block collisions (FABRIC-3.md §XXIV)
Traced what a "Block NNNN" collision actually means before designing
a fix for it: capsule_loader.c's block-write path routes through the
generic block-subsystem API, which kernel_main.c registers as two
devices in a fixed order -- the volatile ramdrive first (LBN
2048-3071), then Artemis's real virtio-blk device immediately after
(LBN 3072+, backed by disk/artemis.img). Every capsule this project
has lands in Artemis's persistent range, not the ramdrive, and
blk_update()'s dirty-marking + repl.c's idle-loop flush write that
content through to the real disk file on every boot. A block-number
collision is therefore a silent, persistent overwrite of real disk
content surviving reboots, not a transient RAM mixup.

The existing collision gate (check_block_conflicts(), already a hard
non-interactive build failure) already catches capsule-vs-capsule
collisions across the whole flat range. The real gap: zero visibility
into blocks something other than a capsule owns (Artemis's own
non-capsule persistent data), and no device-boundary/capacity
awareness at all.

Added, scoped step by step before writing any code:

- tools/capsule-claims.txt -- derived, auto-created/regenerated,
  git-ignored. Lets --resolve tell "this capsule's own content
  changed" apart from "genuinely new collision with something else."
- tools/capsule-reserved.txt -- human-authored, git-tracked, seeded
  with nothing yet rather than guessed at. Checked by both the plain
  build gate (new check_reserved_conflicts()) and --resolve.
- tools/patches/ -- git-tracked, one file per accepted interactive
  renumber; a structured old->new block list, not a generic diff,
  since that's the only thing a renumber ever changes.
- mkcapsule --resolve <dir> -- the only interactive mkcapsule mode,
  a deliberate separate invocation from the plain build path (which
  stays non-interactive so CI never blocks on a prompt). Suggests a
  renumbering that preserves a capsule's own existing block spacing,
  prompts y/N, rewrites the .4th source in place on acceptance.

Found and fixed a real bug during verification: the registry's
empty-block-list case (workload-5.4th, zero Block headers) serialized
with a stray trailing space that the reader parsed back as a phantom
block 0, causing spurious re-registration every run -- caught by
testing idempotency directly, not assuming a clean first run meant
it worked.

Verified: isolated collision tests confirm both accept and reject
paths, confirm a resolved collision doesn't re-prompt the other side,
confirm reserved-range collisions are caught by both --resolve and
the plain build gate. Full 3-architecture rebuild via the real
Makefile.starkernel succeeded clean; amd64 boots with an unchanged
dict_hash/capsule_hash from every prior boot this session.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EXieurDfDSsDFdnSyusuWo
2026-09-12 21:56:37 -04:00

1735 lines
65 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
mkcapsule --resolve <capsules_dir> interactive block-conflict
resolver (FABRIC-3.md SXXIV)
--resolve is the only interactive mode -- never part of the plain build
path above, which stays non-interactive so CI/unattended builds never
block on a prompt. It maintains tools/capsule-claims.txt (auto-created,
derived, git-ignored) and checks against tools/capsule-reserved.txt
(human-authored, git-tracked, blocks something other than a capsule
owns -- e.g. Artemis's own non-capsule persistent data). On a genuine
conflict it suggests a renumbering that preserves the capsule's own
existing block spacing, prompts y/N, and on acceptance rewrites the
.4th source in place and records the decision in tools/patches/.
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 <unistd.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-2.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-2.md §I.4, FABRIC-1.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-2.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; workload-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;
}
/*===========================================================================
* Reserved Ranges (FABRIC-3.md SXXIV, 2026-09-12)
*
* capsule-reserved.txt is human-authored and git-tracked -- mkcapsule can
* discover collisions between capsules on its own (they're all .4th files
* it scans), but it has zero visibility into blocks something OTHER than a
* capsule owns (Artemis's own non-capsule identity/home-blocks data, e.g.)
* unless a human tells it. Never auto-created, never auto-modified: a
* missing file just means "nothing reserved yet," not an error.
*===========================================================================*/
#define RESERVED_FILE_PATH "tools/capsule-reserved.txt"
#define CLAIMS_FILE_PATH "tools/capsule-claims.txt"
#define PATCHES_DIR_PATH "tools/patches"
#define MAX_RESERVED_RANGES 256
typedef struct {
int start;
int end; /* inclusive */
char desc[256];
} ReservedRange;
static ReservedRange reserved_ranges[MAX_RESERVED_RANGES];
static int reserved_count = 0;
/* load_reserved_ranges: parse "START-END description" lines, '#'-comments
* and blank lines ignored. Missing file -> reserved_count stays 0, silently
* (this file is optional until a human seeds it). */
static void load_reserved_ranges(const char *path) {
reserved_count = 0;
FILE *f = fopen(path, "r");
if (!f) return;
char line[512];
while (fgets(line, sizeof(line), f)) {
char *p = line;
while (*p == ' ' || *p == '\t') p++;
if (*p == '#' || *p == '\n' || *p == '\0') continue;
int start = 0, end = 0;
int consumed = 0;
if (sscanf(p, "%d-%d%n", &start, &end, &consumed) != 2) continue;
if (reserved_count >= MAX_RESERVED_RANGES) break;
ReservedRange *r = &reserved_ranges[reserved_count];
r->start = start;
r->end = end;
p += consumed;
while (*p == ' ' || *p == '\t') p++;
size_t dlen = strcspn(p, "\r\n");
if (dlen >= sizeof(r->desc)) dlen = sizeof(r->desc) - 1;
memcpy(r->desc, p, dlen);
r->desc[dlen] = '\0';
reserved_count++;
}
fclose(f);
}
/* reserved_owner_of: returns the reserved range covering lbn, or NULL. */
static const ReservedRange *reserved_owner_of(int lbn) {
for (int i = 0; i < reserved_count; i++) {
if (lbn >= reserved_ranges[i].start && lbn <= reserved_ranges[i].end)
return &reserved_ranges[i];
}
return NULL;
}
/* check_reserved_conflicts: hard build-gate companion to
* check_block_conflicts() -- same manifest_entries scan, checked against
* capsule-reserved.txt instead of against each other. Call AFTER
* check_block_conflicts() has already populated manifest_entries via its
* own nftw scan (avoids scanning the directory twice). */
static int check_reserved_conflicts(void) {
load_reserved_ranges(RESERVED_FILE_PATH);
if (reserved_count == 0) return 0;
int conflicts = 0;
for (int i = 0; i < manifest_count; i++) {
for (int j = 0; j < manifest_entries[i].block_count; j++) {
int lbn = manifest_entries[i].blocks[j];
const ReservedRange *r = reserved_owner_of(lbn);
if (r) {
fprintf(stderr,
"mkcapsule: ERROR: block %d (%s) falls inside reserved "
"range %d-%d: %s\n",
lbn, manifest_entries[i].name, r->start, r->end, r->desc);
conflicts++;
}
}
}
if (conflicts > 0) {
fprintf(stderr,
"mkcapsule: ERROR: %d capsule block(s) collide with reserved "
"ranges in %s\n", conflicts, RESERVED_FILE_PATH);
}
return conflicts > 0 ? 1 : 0;
}
/*===========================================================================
* Claim Registry + Interactive Resolve (mkcapsule --resolve)
*
* capsule-claims.txt is auto-created/regenerated -- pure derived state,
* never hand-edited, git-ignored. Unlike the plain build gate (which just
* re-scans and hard-fails every time, no memory needed), --resolve needs
* to tell "this capsule's own content changed" apart from "a genuinely new
* collision with something else," which needs the previous run's state.
*===========================================================================*/
typedef struct {
char name[CAPSULE_NAME_MAX];
uint64_t hash;
int blocks[MAX_BLOCKS_PER_CAPSULE];
int block_count;
} ClaimEntry;
static ClaimEntry claim_registry[MAX_CAPSULES];
static int claim_count = 0;
static ClaimEntry *find_claim(const char *name) {
for (int i = 0; i < claim_count; i++) {
if (strcmp(claim_registry[i].name, name) == 0) return &claim_registry[i];
}
return NULL;
}
/* load_claims_registry: parse "<name> <hash-hex> <b1>,<b2>,..." lines.
* Missing file -> claim_count stays 0 (fresh baseline, not an error --
* this is the auto-create case). */
static void load_claims_registry(const char *path) {
claim_count = 0;
FILE *f = fopen(path, "r");
if (!f) return;
char line[2048];
while (fgets(line, sizeof(line), f)) {
if (line[0] == '#' || line[0] == '\n' || line[0] == '\0') continue;
if (claim_count >= MAX_CAPSULES) break;
char name[CAPSULE_NAME_MAX];
char hashbuf[32];
char blocklist[1600] = "";
int nf = sscanf(line, "%511s %31s %1599[^\n]", name, hashbuf, blocklist);
if (nf < 2) continue; /* nf==3 has a block list; nf==2 is a valid
* empty-block-list entry (raw code capsule,
* no "Block N" headers at all) */
ClaimEntry *e = &claim_registry[claim_count];
snprintf(e->name, CAPSULE_NAME_MAX, "%s", name);
e->hash = strtoull(hashbuf, NULL, 16);
e->block_count = 0;
char *tok = strtok(blocklist, ",");
while (tok && e->block_count < MAX_BLOCKS_PER_CAPSULE) {
while (*tok == ' ') tok++; /* defensive: tolerate stray
* whitespace from older/
* hand-written registry files */
if (*tok == '\0') { tok = strtok(NULL, ","); continue; }
e->blocks[e->block_count++] = (int)strtol(tok, NULL, 10);
tok = strtok(NULL, ",");
}
claim_count++;
}
fclose(f);
}
static void save_claims_registry(const char *path) {
FILE *f = fopen(path, "w");
if (!f) {
fprintf(stderr, "mkcapsule: WARNING: could not write %s\n", path);
return;
}
fprintf(f, "# DO NOT EDIT — re-run mkcapsule --resolve to refresh.\n"
"# This file is derived state, not a source of truth (the\n"
"# .4th files themselves are) -- git-ignored on purpose.\n");
for (int i = 0; i < claim_count; i++) {
ClaimEntry *e = &claim_registry[i];
fprintf(f, "%s %016" PRIx64, e->name, e->hash);
for (int j = 0; j < e->block_count; j++) {
fprintf(f, "%s%d", j == 0 ? " " : ",", e->blocks[j]);
}
fprintf(f, "\n");
}
fclose(f);
}
/* blocks_free_at_delta: true if shifting every block in `blocks` by `delta`
* lands on numbers that are simultaneously free -- not claimed by any OTHER
* current capsule (manifest_entries, ground truth for this run, excluding
* `skip_idx` itself) and not inside a reserved range. */
static int blocks_free_at_delta(const int *blocks, int block_count, int delta,
int skip_idx) {
for (int j = 0; j < block_count; j++) {
int candidate = blocks[j] + delta;
if (candidate < 2048) return 0; /* system-reserved floor */
if (reserved_owner_of(candidate)) return 0;
for (int i = 0; i < manifest_count; i++) {
if (i == skip_idx) continue;
for (int k = 0; k < manifest_entries[i].block_count; k++) {
if (manifest_entries[i].blocks[k] == candidate) return 0;
}
}
/* also check other candidates within the SAME shifted set don't
* collide with each other (only possible if the caller passes a
* pathological blocks[] with duplicate relative offsets) */
for (int j2 = 0; j2 < j; j2++) {
if (blocks[j2] + delta == candidate) return 0;
}
}
return 1;
}
/* suggest_renumbering: preserves the capsule's own existing relative
* spacing (workload-3.4th's 4606,4615,4625,... convention, e.g.) by
* searching for a single shift delta that lands every block on free
* ground at once, rather than reassigning each block independently. */
#define MAX_DELTA_SEARCH 20000
static int suggest_renumbering(const int *blocks, int block_count,
int skip_idx, int *out_delta) {
for (int delta = 0; delta <= MAX_DELTA_SEARCH; delta += 1) {
if (blocks_free_at_delta(blocks, block_count, delta, skip_idx)) {
*out_delta = delta;
return 1;
}
}
return 0;
}
/* rewrite_block_headers: in-place rewrite of a capsule's own "Block N"
* header lines to "Block N+delta", preserving everything else in the file
* byte-for-byte (content, comments, formatting). Returns 1 on success. */
static int rewrite_block_headers(const char *fpath, int delta) {
FILE *f = fopen(fpath, "rb");
if (!f) return 0;
fseek(f, 0, SEEK_END);
long sz = ftell(f);
rewind(f);
uint8_t *data = malloc((size_t)sz + 1);
if (!data || fread(data, 1, (size_t)sz, f) != (size_t)sz) {
free(data); fclose(f); return 0;
}
fclose(f);
FILE *out = fopen(fpath, "wb");
if (!out) { free(data); return 0; }
size_t pos = 0;
while (pos < (size_t)sz) {
size_t start = pos;
while (pos < (size_t)sz && data[pos] != '\n') pos++;
size_t line_len = pos - start;
int has_nl = (pos < (size_t)sz);
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);
fprintf(out, "Block %d", bn + delta);
} else {
fwrite(data + start, 1, line_len, out);
}
if (has_nl) { fputc('\n', out); pos++; }
}
fclose(out);
free(data);
return 1;
}
/* write_renumber_patch: permanent, git-tracked decision record -- not a
* generic diff (the only thing a renumber ever changes is Block header
* integers, so a structured before/after list is more directly useful
* than a line diff would be). */
static void write_renumber_patch(const char *capsule_name, const int *old_blocks,
int block_count, int delta,
const char *reason) {
mkdir(PATCHES_DIR_PATH, 0755); /* ignore EEXIST */
time_t now = time(NULL);
struct tm *tm_ptr = gmtime(&now);
char stamp[32];
strftime(stamp, sizeof(stamp), "%Y%m%dT%H%M%SZ", tm_ptr);
char safe_name[CAPSULE_NAME_MAX];
size_t k;
for (k = 0; capsule_name[k] && k < sizeof(safe_name) - 1; k++) {
char c = capsule_name[k];
safe_name[k] = (c == '/' || c == ':') ? '_' : c;
}
safe_name[k] = '\0';
char path[MAX_PATH_LEN];
snprintf(path, sizeof(path), "%s/%s-%s.patch", PATCHES_DIR_PATH, stamp, safe_name);
FILE *f = fopen(path, "w");
if (!f) {
fprintf(stderr, "mkcapsule: WARNING: could not write patch record %s\n", path);
return;
}
fprintf(f, "# mkcapsule --resolve renumber patch\n");
fprintf(f, "# capsule: %s\n", capsule_name);
fprintf(f, "# timestamp: %s\n", stamp);
fprintf(f, "# reason: %s\n", reason);
for (int j = 0; j < block_count; j++) {
fprintf(f, "%d -> %d\n", old_blocks[j], old_blocks[j] + delta);
}
fclose(f);
fprintf(stderr, "mkcapsule: wrote patch record %s\n", path);
}
/* prompt_yes_no: reads one line from stdin, true only on 'y'/'Y'. */
static int prompt_yes_no(const char *question) {
fprintf(stderr, "%s [y/N] ", question);
char reply[16];
if (!fgets(reply, sizeof(reply), stdin)) return 0;
return (reply[0] == 'y' || reply[0] == 'Y');
}
/* cmd_resolve: mkcapsule --resolve <dir> -- see main()'s usage text and
* FABRIC-3.md SXXIV for the full design. Deliberately the ONLY interactive
* mkcapsule mode; the plain build path (check_block_conflicts/
* check_reserved_conflicts) stays non-interactive so CI/unattended builds
* never block on a prompt. */
static int cmd_resolve(const char *dir) {
base_dir = dir;
base_dir_len = strlen(base_dir);
while (base_dir_len > 0 && base_dir[base_dir_len - 1] == '/') base_dir_len--;
manifest_count = 0;
if (nftw(base_dir, manifest_file, 20, FTW_PHYS) != 0) {
fprintf(stderr, "mkcapsule: ERROR: directory scan failed\n");
return 1;
}
load_reserved_ranges(RESERVED_FILE_PATH);
int fresh_registry = (access(CLAIMS_FILE_PATH, F_OK) != 0);
load_claims_registry(CLAIMS_FILE_PATH);
if (fresh_registry) {
fprintf(stderr, "mkcapsule --resolve: %s not found, creating fresh "
"baseline from current state\n", CLAIMS_FILE_PATH);
}
int registered = 0, updated = 0, resolved = 0, rejected = 0;
for (int i = 0; i < manifest_count; i++) {
ManifestEntry *m = &manifest_entries[i];
ClaimEntry *prior = find_claim(m->name);
if (prior && prior->hash == m->hash &&
prior->block_count == m->block_count &&
memcmp(prior->blocks, m->blocks,
(size_t)m->block_count * sizeof(int)) == 0) {
continue; /* unchanged, nothing to do */
}
/* Does this capsule's CURRENT block set collide with anything else
* live right now (ground truth = this run's fresh scan), or with
* a reserved range? Fresh-registered-or-updated-in-place capsules
* whose own new content simply doesn't collide need no prompt --
* only an actual collision needs a human decision. */
int collides = 0;
for (int j = 0; j < m->block_count && !collides; j++) {
int lbn = m->blocks[j];
if (reserved_owner_of(lbn)) { collides = 1; break; }
for (int k = 0; k < manifest_count; k++) {
if (k == i) continue;
for (int b = 0; b < manifest_entries[k].block_count; b++) {
if (manifest_entries[k].blocks[b] == lbn) { collides = 1; break; }
}
if (collides) break;
}
}
if (!collides) {
ClaimEntry *dest = prior ? prior : &claim_registry[claim_count++];
snprintf(dest->name, CAPSULE_NAME_MAX, "%s", m->name);
dest->hash = m->hash;
dest->block_count = m->block_count;
memcpy(dest->blocks, m->blocks, (size_t)m->block_count * sizeof(int));
if (prior) { updated++; fprintf(stderr, "mkcapsule --resolve: %s updated (no conflict)\n", m->name); }
else { registered++; fprintf(stderr, "mkcapsule --resolve: %s registered\n", m->name); }
continue;
}
/* Real conflict -- try to suggest a renumbering. */
fprintf(stderr, "\nmkcapsule --resolve: CONFLICT for %s:\n", m->name);
for (int j = 0; j < m->block_count; j++) {
int lbn = m->blocks[j];
const ReservedRange *r = reserved_owner_of(lbn);
if (r) {
fprintf(stderr, " block %d is reserved: %s\n", lbn, r->desc);
continue;
}
for (int k = 0; k < manifest_count; k++) {
if (k == i) continue;
for (int b = 0; b < manifest_entries[k].block_count; b++) {
if (manifest_entries[k].blocks[b] == lbn) {
fprintf(stderr, " block %d also claimed by %s\n",
lbn, manifest_entries[k].name);
}
}
}
}
int delta;
if (suggest_renumbering(m->blocks, m->block_count, i, &delta)) {
fprintf(stderr, " suggested renumbering (shift by %+d):\n", delta);
for (int j = 0; j < m->block_count; j++) {
fprintf(stderr, " %d -> %d\n", m->blocks[j], m->blocks[j] + delta);
}
char fpath[MAX_PATH_LEN];
snprintf(fpath, sizeof(fpath), "%s/%s", base_dir, m->name);
/* capsule name uses ':' for subdirectories; the real path uses '/' */
for (char *p = fpath; *p; p++) if (*p == ':') *p = '/';
if (prompt_yes_no(" Accept this renumbering?")) {
if (rewrite_block_headers(fpath, delta)) {
char reason[256];
snprintf(reason, sizeof(reason),
"block conflict, resolved via --resolve");
write_renumber_patch(m->name, m->blocks, m->block_count,
delta, reason);
for (int j = 0; j < m->block_count; j++) m->blocks[j] += delta;
ClaimEntry *dest = prior ? prior : &claim_registry[claim_count++];
snprintf(dest->name, CAPSULE_NAME_MAX, "%s", m->name);
dest->hash = m->hash;
dest->block_count = m->block_count;
memcpy(dest->blocks, m->blocks, (size_t)m->block_count * sizeof(int));
resolved++;
} else {
fprintf(stderr, " ERROR: could not rewrite %s\n", fpath);
rejected++;
}
} else {
fprintf(stderr, " rejected -- %s left unchanged\n", m->name);
rejected++;
}
} else {
fprintf(stderr, " no free renumbering found within +%d of any "
"existing block -- resolve %s by hand\n",
MAX_DELTA_SEARCH, m->name);
rejected++;
}
}
save_claims_registry(CLAIMS_FILE_PATH);
fprintf(stderr, "\nmkcapsule --resolve: %d registered, %d updated, "
"%d resolved, %d rejected/unresolved\n",
registered, updated, resolved, rejected);
return rejected > 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;
}
/* --resolve mode: interactive block-conflict resolver (FABRIC-3.md
* SXXIV). The ONLY interactive mkcapsule mode -- deliberately a
* separate, deliberate invocation, never part of the plain `make`
* build path (that stays non-interactive so CI/unattended builds
* never block on a prompt). */
if (argc == 3 && strcmp(argv[1], "--resolve") == 0) {
return cmd_resolve(argv[2]);
}
/* --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"
" %s --resolve <capsules_dir> interactive block-conflict\n"
" resolver (see file header)\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], 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;
}
if (check_reserved_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;
}