Phase D: RUNCAP -- runtime capsule construction from thumbdrive content

capsule_runcap_birth() (new capsule_runcap.h/.c): builds a heap-only,
single-entry CapsuleDirHeader + CapsuleDesc + CapsuleNameEntry + arena
from a home-blocks drive's identity_src region (skipping the first
devblock, reserved for MINT's user_identity_seed_t record) and hands it
to the existing, unmodified capsule_birth_baby() -- no new birth
mechanism, matching FABRIC-3.md §F.6's own trace.

Found and closed a real gap in that trace along the way:
capsule_birth_baby()'s signature check calls capsule_get_signatures(),
which unconditionally returns the compile-time-baked global array --
meaningless for a heap-built directory, where index 0 would compare
RUNCAP's own content against whatever real capsule happens to occupy
that slot in the baked array (guaranteed-wrong, not a security check).
Added an explicit skip_pki_sig flag (0 for all 4 existing call sites,
1 for RUNCAP): that content's trust comes from CERTVERIFY, a separate
root, not the capsule-PKI chain.

Also found live: capsule_birth_baby() never sets the registry entry's
own .name (every existing caller does this itself afterward via
capsule_vm_registry_set_name() -- RUNCAP now does too), and
capsule_exec_payload() requires a "Block NNNN" header per chunk of
content or it's silently skipped, never executed -- not a bug, but
necessary context for whoever authors MINT's default personality
content next.

Added a small accessor pair (repl.h/.c) exposing the currently attached
home-blocks device/sig -- the same gap F.9's own BINDSTEP scoping had
already flagged, needed by both.

Verified end-to-end live in QEMU: synthetic identity-source content
written directly to a thumbdrive image's raw devblocks (no capsule
build, no mkcapsule) was read, compiled, and executed by a genuinely
new VM via a diagnostic RUNCAP-TEST word -- confirmed via VM-EXEC
invoking a word defined only in that source. Clean 3-architecture
regression boot (no RUNCAP drive attached) confirms no side effects.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZGkimpfyh63EZyRkNbkPD
This commit is contained in:
Robert Allan James
2026-08-28 14:29:42 -04:00
co-authored by Claude Sonnet 5
parent 75311967a7
commit e1e839258d
13 changed files with 37736 additions and 3 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
# Capsule Block Manifest — Auto-generated
<!-- Generated by mkcapsule --manifest 2026-08-28T17:01:08Z -->
<!-- Generated by mkcapsule --manifest 2026-08-28T18:28:25Z -->
<!-- DO NOT EDIT — re-run mkcapsule --manifest to refresh. -->
<!-- Hand-written justifications and immutability notes live -->
<!-- in MANIFEST.md alongside this auto-generated index. -->
BIN
View File
Binary file not shown.
+18
View File
@@ -127,6 +127,23 @@ CapsuleRunResult capsule_birth_mama(
* @param descs Capsule descriptor array
* @param names Capsule name entry array (parallel to descs)
* @param arena Capsule payload arena
* @param skip_pki_sig 0 for every build-time capsule (the normal case --
* checked against the compile-time-baked signature
* array via capsule_get_signatures()). Non-zero only
* for RUNCAP (FABRIC-3.md §F.6/F.18): a heap-built,
* single-entry directory sourced from a user's own
* thumbdrive has no entry in that array at all --
* index 0 would silently compare against whatever
* real capsule happens to occupy slot 0, which is
* not a security check, just a guaranteed-wrong one.
* Trust for that content comes from CERTVERIFY (a
* separate root, the user's own Zuse-signed cert)
* already having run before RUNCAP is ever called,
* not from this flag -- this only skips a check that
* was never meaningful for that content in the first
* place. Deliberately a plain flag, not a new entry
* point, so the policy is one call-site decision,
* trivially reversible.
* @param out_vm_id Output: assigned VM ID
* @param out_vm_ctx Output: new VM context
* @return CAPSULE_RUN_OK on success, error code otherwise
@@ -137,6 +154,7 @@ CapsuleRunResult capsule_birth_baby(
const CapsuleDesc *descs,
const CapsuleNameEntry *names,
const uint8_t *arena,
int skip_pki_sig,
VMUuid *out_vm_id,
void **out_vm_ctx
);
+75
View File
@@ -0,0 +1,75 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 20232025 Robert A. James
All rights reserved.
Licensed under the StarForth License, Version 1.0
*/
/**
* capsule_runcap.h - RUNCAP: runtime capsule construction from thumbdrive
* content (FABRIC-3.md §F.6/§F.18).
*
* A user's identity source (raw FORTH init/personality text, minted by
* MINT into a home-blocks drive's identity_src region) never exists at
* build time, so it can never appear in the compile-time-baked capsule
* directory. This builds a heap-only, single-entry CapsuleDirHeader +
* CapsuleDesc + CapsuleNameEntry + arena from that region and hands it to
* the existing, unmodified capsule_birth_baby() -- no new birth mechanism,
* per §F.6's own trace ("capsule_birth_baby() is already generic").
*
* Does not verify the caller has already run CERTVERIFY -- that's the
* caller's responsibility (WIREBIND, not yet built). This function's own
* job is narrow: read the region, construct the directory, birth it.
*/
#ifndef STARKERNEL_CAPSULE_RUNCAP_H
#define STARKERNEL_CAPSULE_RUNCAP_H
#ifdef __STARKERNEL__
#include <stdint.h>
#include "starkernel/capsule_run.h" /* CapsuleRunResult */
#include "starkernel/vm_uuid.h" /* VMUuid */
#include "starkernel/homeblocks_sig.h" /* homeblocks_sig_t */
struct blkio_dev;
/**
* capsule_runcap_birth - Birth a VM from a home-blocks drive's own
* identity_src region.
*
* Reads sig->identity_src_devblocks devblocks starting at
* sig->identity_src_offset. The first devblock is the identity's own
* user_identity_seed_t record (MINT, §F.8) and is skipped here -- RUNCAP
* only cares about the FORTH source that follows it. Refuses cleanly
* (CAPSULE_RUN_ERR_INVALID) if identity_src_offset is 0 (never minted) or
* identity_src_devblocks < 2 (no source content beyond the seed record).
*
* The heap-allocated directory/descriptor/name/arena are never freed --
* deliberate, matching kernel_main.c's own compile-time-directory-to-heap
* copy at Mama's own birth (also never freed): a VM's IDENTITY exec reads
* directly from this arena, and nothing in this codebase frees capsule
* arenas after a successful birth today.
*
* @param dev Already-open block device for the attached drive.
* @param sig Already-verified homeblocks_sig_t read from it.
* @param vm_name Symbolic name for the new VM (becomes both the
* capsule's own single directory entry name and the
* VM registry name).
* @param out_vm_id Output: assigned VM ID.
* @param out_vm_ctx Output: new VM context (may be NULL if not needed).
* @return CAPSULE_RUN_OK on success, error code otherwise.
*/
CapsuleRunResult capsule_runcap_birth(
struct blkio_dev *dev,
const homeblocks_sig_t *sig,
const char *vm_name,
VMUuid *out_vm_id,
void **out_vm_ctx
);
#endif /* __STARKERNEL__ */
#endif /* STARKERNEL_CAPSULE_RUNCAP_H */
+13
View File
@@ -15,6 +15,9 @@
#define STARKERNEL_REPL_H
#include "vm.h"
#include "starkernel/homeblocks_sig.h"
struct blkio_dev;
#ifdef __cplusplus
extern "C" {
@@ -68,6 +71,16 @@ void sk_repl_set_active_vm(VM *vm);
*/
VM *sk_repl_get_active_vm(void);
/**
* sk_repl_get_homeblocks_dev / sk_repl_get_homeblocks_sig - The currently
* attached home-blocks USB drive, or NULL if none is attached / the
* attached drive didn't check out as HOMEBLOCKS_SIG_OK (FABRIC-3.md
* §F.6/§F.9/§F.18). Both return NULL together; never one without the
* other.
*/
struct blkio_dev *sk_repl_get_homeblocks_dev(void);
const homeblocks_sig_t *sk_repl_get_homeblocks_sig(void);
#ifdef __cplusplus
}
#endif
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+9 -2
View File
@@ -488,6 +488,7 @@ CapsuleRunResult capsule_birth_baby(
const CapsuleDesc *descs,
const CapsuleNameEntry *names,
const uint8_t *arena,
int skip_pki_sig,
VMUuid *out_vm_id,
void **out_vm_ctx)
{
@@ -507,8 +508,14 @@ CapsuleRunResult capsule_birth_baby(
/* Milestone 6 (Phase 8): enforced only on INVALID -- see the fuller
* comment in capsule_birth_mama() above for why MISSING/NO_ROOT_KEY
* stay WARN-only. */
{
* stay WARN-only. Skipped entirely when skip_pki_sig is set (RUNCAP,
* FABRIC-3.md §F.6/F.18): capsule_get_signatures() is the compile-
* time-baked array, indexed against the build-time capsule_descriptors[]
* -- meaningless for a heap-built directory sourced from a thumbdrive,
* where idx 0 would just compare against whatever real capsule happens
* to occupy that slot. That content's trust already comes from a
* separate root (CERTVERIFY, run by the caller before this). */
if (!skip_pki_sig) {
int idx = (int)(cap - descs);
CapsuleSigResult sr = capsule_verify_signature(
descs, names, capsule_get_signatures(), arena, dir->desc_count, idx);
+115
View File
@@ -0,0 +1,115 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 20232025 Robert A. James
All rights reserved.
Licensed under the StarForth License, Version 1.0
*/
#ifndef __STARKERNEL__
#error "capsule_runcap.c is kernel-only"
#endif
#include "starkernel/capsule_runcap.h"
#include "starkernel/capsule.h"
#include "starkernel/capsule_birth.h"
#include "starkernel/xxhash64.h"
#include "starkernel/kmalloc.h"
#include "blkio.h"
#include <string.h>
CapsuleRunResult capsule_runcap_birth(
struct blkio_dev *dev,
const homeblocks_sig_t *sig,
const char *vm_name,
VMUuid *out_vm_id,
void **out_vm_ctx)
{
if (!dev || !sig || !vm_name || !out_vm_id) return CAPSULE_RUN_ERR_INVALID;
/* 0 = never minted (homeblocks_sig.h's own field doc). */
if (sig->identity_src_offset == 0) return CAPSULE_RUN_ERR_INVALID;
/* First devblock is the user_identity_seed_t record (MINT, §F.8);
* everything after it is raw FORTH source. Need at least one devblock
* of actual source beyond the seed. */
if (sig->identity_src_devblocks < 2) return CAPSULE_RUN_ERR_INVALID;
uint32_t source_devblocks = sig->identity_src_devblocks - 1u;
uint64_t source_len = (uint64_t)source_devblocks * 4096u;
uint8_t *arena = (uint8_t *)kmalloc((size_t)source_len);
if (!arena) return CAPSULE_RUN_ERR_STILLBORN;
/* homeblocks_sig_t's own offset/devblocks fields are in 4KiB
* devblocks; blkio_read() works in 1KiB forth-blocks (same *4
* conversion homeblocks_sig_check() already uses for its own
* sig_start_fblock). Skip the seed devblock (+1 devblock = +4
* forth-blocks) before reading source content. */
uint32_t base_fblock = (sig->identity_src_offset + 1u) * 4u;
uint32_t source_fblocks = source_devblocks * 4u;
uint32_t i;
for (i = 0; i < source_fblocks; i++) {
if (blkio_read((blkio_dev_t *)dev, base_fblock + i,
arena + (size_t)i * BLKIO_FORTH_BLOCK_SIZE) != BLKIO_OK) {
return CAPSULE_RUN_ERR_INVALID;
}
}
/* Heap-built single-entry directory -- exact shape §F.6 traced
* against capsule_birth_baby()'s own parameters, not a new mechanism.
* Never freed: matches kernel_main.c's own compile-time-directory
* heap copy at Mama's birth, which is also never freed -- the VM's
* IDENTITY exec reads directly from this arena. */
CapsuleNameEntry name_entry;
memset(name_entry.name, 0, sizeof(name_entry.name));
{
size_t n = strlen(vm_name);
if (n >= CAPSULE_NAME_MAX) n = CAPSULE_NAME_MAX - 1u;
memcpy(name_entry.name, vm_name, n);
}
CapsuleDesc desc;
memset(&desc, 0, sizeof(desc));
desc.magic = CAPSULE_MAGIC_PACK(CAPSULE_VERSION_0, CAPSULE_HASH_XXHASH64);
desc.content_hash = xxhash64_capsule(arena, (size_t)source_len);
desc.capsule_id = desc.content_hash; /* content-addressed invariant */
desc.offset = 0;
desc.length = source_len;
desc.flags = CAPSULE_FLAG_ACTIVE | CAPSULE_FLAG_PRODUCTION;
desc.owner_vm = 0;
desc.birth_count = 0;
desc.created_ns = 0; /* no monotonic-ns source exists anywhere in this
* codebase yet, §F.8's own open item -- matches
* CapsuleDesc.created_ns's existing hardcoded-0
* precedent at mkcapsule generation time. */
CapsuleDirHeader dir;
memset(&dir, 0, sizeof(dir));
dir.magic = CAPSULE_DIR_MAGIC;
dir.arena_base = (uint64_t)(uintptr_t)arena;
dir.arena_size = source_len;
dir.desc_count = 1;
dir.desc_capacity = 1;
dir.name_count = 1;
dir.dir_hash = 0; /* not verified anywhere in the birth path today */
CapsuleRunResult r = capsule_birth_baby(
vm_name, &dir, &desc, &name_entry, arena,
1 /* skip_pki_sig -- trust comes from CERTVERIFY, a separate root */,
out_vm_id, out_vm_ctx);
/* capsule_birth_baby() itself never sets the registry entry's own
* .name -- every existing caller (mama_word_birth, CONNECT-HERMES/
* -ARTEMIS) does this as a separate step after a successful birth,
* via capsule_vm_registry_set_name(). Found live: skipping this left
* a freshly RUNCAP-born VM's registry name empty, which the idle-loop
* pump (FABRIC-3.md Phase C) then read as a zero-length name and
* refused ("VM name too long or empty") every tick. */
if (r == CAPSULE_RUN_OK && out_vm_id) {
capsule_vm_registry_set_name(*out_vm_id, vm_name);
}
return r;
}
+52
View File
@@ -39,6 +39,7 @@
#include "platform_alloc.h"
#include "starkernel/capsule.h"
#include "starkernel/capsule_birth.h"
#include "starkernel/capsule_runcap.h"
#include "starkernel/capsule_loader.h"
#include "starkernel/capsule_run.h"
#include "starkernel/capsule_loader.h"
@@ -294,6 +295,7 @@ void mama_word_birth(VM *vm)
capsule_get_descriptors(),
capsule_get_names(),
capsule_get_arena(),
0, /* skip_pki_sig: normal build-time capsule */
&new_vm_id,
(void **)0
);
@@ -768,6 +770,51 @@ static void mama_word_vm_call(VM *vm)
}
}
/**
* @brief RUNCAP-TEST ( caddr u -- ok? rc )
* Diagnostic-only word (FABRIC-3.md §F.6/§F.18): calls
* capsule_runcap_birth() against whatever drive sk_repl_get_homeblocks_
* dev()/sig() currently report, naming the new VM from the given string.
* Not the real RUNCAP call site -- that's WIREBIND (still unbuilt); this
* exists to exercise capsule_runcap_birth() live before WIREBIND exists.
* ok? is 1/0; rc is the raw CapsuleRunResult for diagnosis either way.
*/
static void mama_word_runcap_test(VM *vm)
{
char vm_name[VM_NAME_MAX];
cell_t u, caddr;
uint32_t i;
if (vm->dsp < 1) { vm->error = 1; return; }
u = vm_pop(vm);
caddr = vm_pop(vm);
if (u <= 0 || (uint32_t)u >= VM_NAME_MAX) {
console_println("RUNCAP-TEST: name too long or empty");
vm_push(vm, 0); vm_push(vm, (cell_t)CAPSULE_RUN_ERR_INVALID);
return;
}
{
const uint8_t *p = vm_ptr(vm, (vaddr_t)caddr);
if (!p) { vm->error = 1; return; }
for (i = 0; i < (uint32_t)u; i++) vm_name[i] = (char)p[i];
}
vm_name[u] = '\0';
struct blkio_dev *dev = sk_repl_get_homeblocks_dev();
const homeblocks_sig_t *sig = sk_repl_get_homeblocks_sig();
if (!dev || !sig) {
console_println("RUNCAP-TEST: no home-blocks drive attached");
vm_push(vm, 0); vm_push(vm, (cell_t)CAPSULE_RUN_ERR_INVALID);
return;
}
VMUuid new_vm_id;
CapsuleRunResult r = capsule_runcap_birth(dev, sig, vm_name, &new_vm_id, (void **)0);
vm_push(vm, r == CAPSULE_RUN_OK ? 1 : 0);
vm_push(vm, (cell_t)r);
}
/**
* @brief CAPSULE-BIRTH ( capsule-id -- vm-id-hi vm-id-lo )
* Birth a baby VM from a production (p) capsule.
@@ -798,6 +845,7 @@ void mama_word_capsule_birth(VM *vm)
capsule_get_descriptors(),
capsule_get_names(),
capsule_get_arena(),
0, /* skip_pki_sig: normal build-time capsule */
&new_vm_id,
(void **)0 /* Don't need VM context back */
);
@@ -991,6 +1039,7 @@ static void mama_word_connect_artemis(VM *vm __attribute__((unused)))
capsule_get_descriptors(),
capsule_get_names(),
capsule_get_arena(),
0, /* skip_pki_sig: normal build-time capsule */
&new_vm_id, (void **)0);
console_set_vm_name(saved);
@@ -1062,6 +1111,7 @@ static void mama_word_connect_hermes(VM *vm __attribute__((unused)))
capsule_get_descriptors(),
capsule_get_names(),
capsule_get_arena(),
0, /* skip_pki_sig: normal build-time capsule */
&new_vm_id, (void **)0);
console_set_vm_name(saved);
@@ -1118,6 +1168,7 @@ void register_mama_forth_words(VM *vm)
register_word(vm, "CAPSULE-LEN@", mama_word_capsule_len_fetch);
register_word(vm, "CAPSULE-BIRTH", mama_word_capsule_birth);
register_word(vm, "CAPSULE-RUN", mama_word_capsule_run);
register_word(vm, "RUNCAP-TEST", mama_word_runcap_test);
register_word(vm, "MAMA-VM-ID", mama_word_mama_vm_id);
register_word(vm, "VM-COUNT", mama_word_vm_count);
register_word(vm, "VM-CONSERVED?", mama_word_vm_conserved);
@@ -1147,6 +1198,7 @@ void register_mama_forth_words(VM *vm)
register_word(vm, "CAPSULE-LEN@", mama_word_capsule_len_fetch);
register_word(vm, "CAPSULE-BIRTH", mama_word_capsule_birth);
register_word(vm, "CAPSULE-RUN", mama_word_capsule_run);
register_word(vm, "RUNCAP-TEST", mama_word_runcap_test);
register_word(vm, "MAMA-VM-ID", mama_word_mama_vm_id);
register_word(vm, "VM-COUNT", mama_word_vm_count);
register_word(vm, "VM-CONSERVED?", mama_word_vm_conserved);
+28
View File
@@ -67,6 +67,27 @@ static VM *g_repl_active_vm = (void *)0;
void sk_repl_set_active_vm(VM *vm) { g_repl_active_vm = vm; }
VM *sk_repl_get_active_vm(void) { return g_repl_active_vm; }
/*===========================================================================
* Currently attached home-blocks device: mirrors g_repl_active_vm's own
* shape (FABRIC-3.md §F.9's own precedent for this exact accessor). Set
* once sk_repl_idle()'s own attach handling confirms HOMEBLOCKS_SIG_OK
* below; cleared on detach. RUNCAP (§F.6/§F.18) and, later, BINDSTEP's
* re-verify-live check (§F.9) both need this -- neither lives in this
* file, and usb_blk_dev/xdev below are function-static, invisible outside
* sk_repl_idle() without an accessor like this one.
*===========================================================================*/
static blkio_dev_t *g_homeblocks_dev = (void *)0;
static homeblocks_sig_t g_homeblocks_sig;
static int g_homeblocks_sig_valid = 0;
blkio_dev_t *sk_repl_get_homeblocks_dev(void) {
return g_homeblocks_sig_valid ? g_homeblocks_dev : (void *)0;
}
const homeblocks_sig_t *sk_repl_get_homeblocks_sig(void) {
return g_homeblocks_sig_valid ? &g_homeblocks_sig : (void *)0;
}
/*===========================================================================
* Idle heartbeat service
*
@@ -133,6 +154,9 @@ static void sk_repl_idle(VM *active_vm)
switch (sig_rc) {
case HOMEBLOCKS_SIG_OK:
console_println("xhci: USB drive recognized as a home-blocks drive");
g_homeblocks_dev = &usb_blk_dev;
g_homeblocks_sig = sig;
g_homeblocks_sig_valid = 1;
break;
case HOMEBLOCKS_SIG_BLANK:
console_println("xhci: USB drive not recognized (blank or foreign media) -- read-only general use only");
@@ -165,6 +189,10 @@ static void sk_repl_idle(VM *active_vm)
if (xdev && xdev->bot_msc_detach_pending) {
xdev->bot_msc_detach_pending = 0;
blk_subsys_detach_device(&usb_blk_dev);
if (g_homeblocks_dev == &usb_blk_dev) {
g_homeblocks_dev = (void *)0;
g_homeblocks_sig_valid = 0;
}
}
/* FABRIC.md/FABRIC-2.md Section V item 6: "a cheap 'anything dirty?