Files
LithosAnanake/src/starkernel/capsule/capsule_birth.c
T
Robert Allan JamesandClaude Sonnet 5 d9da82b065 Stage 4 increments 2+3: WIREBIND VMs as switch-signal participants + mark-and-defer tombstone reap (FABRIC-3.md §XXVIII Stage 4)
Increment 2: WIREBIND user VMs (the ones that actually run FORTH work;
console VMs are pure REPL proxies and never participate) register as
Stage 3 switch-signal participants at attach, unregister at teardown.
Slot table bumped 8 -> 16, matching messaging.4th's own VM-MAX -- a real,
already-agreed ceiling, not an invented number. Added
sk_vm_switch_signal_unregister() (compaction-based; Tripod VMs never
needed removal, WIREBIND VMs cycle constantly and would otherwise
exhaust the bounded table).

Increment 3: implements the plan's own ratified option (A) for the
async-detach UAF risk -- mark-and-defer via a new pending_reap flag on
VMRegistryEntry, deliberately not a new VMState (capsule_vm_kill()
already treats VM_STATE_DEAD as idempotent success, which would silently
swallow a reap attempt; SWITCHED_OUT still accurately describes a
tombstoned VM until the moment it's actually freed). unclean_detach()
sets it when capsule_vm_kill() refuses a SWITCHED_OUT target; the Stage 3
checkpoint (vm_core.c) checks it before ever attempting to resume a
pending switch target, and calls the new capsule_vm_force_reap() instead
-- the one caller allowed to bypass capsule_vm_kill()'s own refusal,
because it runs at the exact safe cooperative point the switcher itself
controls. A new idle-tick sweep cleans up the WIREBIND live-table entry
once the reap has actually happened.

Verified clean on all 3 architectures (baseline regression -- no
WIREBIND attach happens in a plain boot). The reap mechanism's own
correctness under a genuinely parked context is verified separately,
next, via a temporary deterministic probe.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BWpNjdwPtFLuVLaAq44L9K
2026-09-15 00:58:34 -04:00

943 lines
37 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.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
*/
/**
* capsule_birth.c - VM Birth Protocol Implementation (M7.1)
*
* Mama init, baby birth, and experiment execution.
* Freestanding - no libc dependency.
*
* Birth sequence for a baby VM:
* 1. Find capsule by name (capsule_find_by_name)
* 2. Assert CAPSULE_BIRTH_ELIGIBLE
* 3. Validate content hash
* 4. vm_alloc_hook() — fresh VM
* 5. vm_exec_hook(payload) — IDENTITY (init capsule from Hera's store)
* 6. vm_exec_hook("1 LOAD") — PERSONALITY (baby's personal init.4th from block 1, if present)
* 7. Log parity record
*/
#include "starkernel/capsule_birth.h"
#include "starkernel/capsule.h"
#include "starkernel/capsule_run.h"
#include "starkernel/capsule_generated.h" /* capsule_get_signatures() */
#include "starkernel/capsule_sig.h"
#include "starkernel/kmalloc.h"
#include "starkernel/console.h"
#include "starkernel/vm/stadium.h" /* item 4.1a -- stadium_grant_quota() */
#include "starkernel/vm/arena.h" /* FABRIC-3.md §XXVIII Stage 1 -- sk_vm_native_stack_alloc() */
#include "starkernel/session.h" /* session_register()/session_set_pinned() -- FABRIC-2.md §H.12 step 5 */
#include "starkernel/timer.h" /* timer_calibration_record()->vm_mode -- FABRIC-2.md §I.5 CONTRIB trust tier */
#include "starkernel/capsule_vm_switch_signal.h" /* FABRIC-3.md §XXVIII Stage 4 -- capsule_vm_force_reap() */
#include "vm.h"
#include "platform_alloc.h"
/* No LOG_LINE_MAX include-order constraint anymore: vm.h's own
* LOG_LINE_MAX (persistent block-log, 64) and log.h's in-memory line
* length (renamed LOG_MSG_LINE_MAX, 256) are distinct names, so include
* order no longer redefines anything (the -Werror collision found
* 2026-08-26 wiring capsule signature logging is structurally gone). */
#include "log.h"
/*===========================================================================
* VM Execution Hooks
*===========================================================================*/
static CapsuleExecFn vm_exec_fn = 0;
static CapsuleDictHashFn vm_dict_hash_fn = 0;
static CapsuleVMAllocFn vm_alloc_fn = 0;
void capsule_birth_set_hooks(
CapsuleExecFn exec_fn,
CapsuleDictHashFn dict_hash_fn,
CapsuleVMAllocFn vm_alloc_fn_arg)
{
vm_exec_fn = exec_fn;
vm_dict_hash_fn = dict_hash_fn;
vm_alloc_fn = vm_alloc_fn_arg;
}
/*===========================================================================
* VM Registry — dynamic linked list, heap-allocated via kmalloc
*===========================================================================*/
typedef struct vm_node {
VMRegistryEntry entry;
struct vm_node *next;
} vm_node_t;
static vm_node_t *vm_registry_head = (void *)0;
static uint32_t vm_registry_count = 0;
/* item 3.8: vm_id generation moved to vm_uuid_next()'s deterministic pool;
* the monotonic next_vm_id counter this replaced is gone. Hera's id is
* vm_uuid_hera() (fixed, reserved), not drawn from the pool. */
/* Copy at most VM_NAME_MAX-1 chars, always null-terminate */
static void vm_name_copy(char *dst, const char *src) {
uint32_t i;
for (i = 0; i < (VM_NAME_MAX - 1u) && src[i]; i++)
dst[i] = src[i];
dst[i] = '\0';
}
/* Case-sensitive equality test (no libc) */
static int vm_name_eq(const char *a, const char *b) {
while (*a && *b) {
if (*a != *b) return 0;
a++; b++;
}
return *a == *b;
}
/* Internal: return mutable pointer into registry node for vm_id */
static VMRegistryEntry *vm_find_entry_ptr(VMUuid vm_id) {
vm_node_t *node = vm_registry_head;
while (node) {
if (vm_uuid_equal(node->entry.vm_id, vm_id)) return &node->entry;
node = node->next;
}
return (void *)0;
}
/* FABRIC-2.md §H.12 step 10: creator-ceiling enforcement, birth-time
* snapshot only, no live sync (§H.3, decided 2026-09-03 -- "if something
* was developed with a particular set of ACLs, it should remain at
* that... otherwise parent changes break the child's program"). For
* every word in child's dictionary also present, by name, in parent's,
* copy parent's CURRENT acl_allow/acl_mode/acl_pinned/acl_ttl onto
* child's matching entry -- caps the child at whatever the parent
* allowed at this exact moment, once, never re-applied afterward. Words
* the child has that the parent doesn't (baby-specific vocabulary) are
* left untouched -- there's nothing to cap them against. vm_find_word()
* is the same C-level lookup FIND itself uses; not modifying FIND, just
* calling the same lookup it does (CLAUDE.md: "FIND is a proven, tested,
* registered word. Never modify it" -- this doesn't). */
static void dictionary_snapshot_acl_from_parent(VM *child, VM *parent) {
DictEntry *ce;
if (!child || !parent) return;
for (ce = child->latest; ce != NULL; ce = ce->link) {
DictEntry *pe = vm_find_word(parent, ce->name, ce->name_len);
if (!pe) continue;
ce->acl_allow = pe->acl_allow;
ce->acl_mode = pe->acl_mode;
ce->acl_pinned = pe->acl_pinned;
ce->acl_ttl = pe->acl_ttl;
}
}
void capsule_vm_registry_init(void *mama_vm_ptr) {
uint32_t i;
vm_node_t *node;
vm_node_t *next;
vm_node_t *mama;
/* Free any nodes from a previous init (defensive) */
node = vm_registry_head;
while (node) {
next = node->next;
kfree(node);
node = next;
}
vm_registry_head = (void *)0;
vm_registry_count = 0;
/* Mama is always the reserved, fixed vm_uuid_hera() id (item 3.8) */
mama = (vm_node_t *)kmalloc(sizeof(vm_node_t));
if (!mama) return;
mama->entry.vm_id = vm_uuid_hera();
mama->entry.state = VM_STATE_LIVE;
mama->entry.birth_capsule_id = 0;
mama->entry.birth_timestamp_ns = 0;
mama->entry.birth_dict_hash = 0;
mama->entry.flags = 0;
mama->entry.parent_vm_id = vm_uuid_hera(); /* self-referential: Hera is the root */
mama->entry.vm_ptr = mama_vm_ptr;
mama->entry.stadium_patron_cell = STADIUM_CELL_NONE; /* set for real by
* stadium_birth_hera()
* separately -- not
* tracked here */
for (i = 0; i < VM_NAME_MAX; i++) mama->entry.name[i] = '\0';
vm_name_copy(mama->entry.name, "Hera");
mama->next = (void *)0;
vm_registry_head = mama;
vm_registry_count = 1;
/* From this point on all console output is prefixed [Hera] */
console_set_vm_name("Hera");
}
static VMRegistryEntry *vm_registry_alloc(void) {
uint32_t i;
vm_node_t *node;
vm_node_t *tail;
node = (vm_node_t *)kmalloc(sizeof(vm_node_t));
if (!node) return (void *)0;
node->entry.vm_id = vm_uuid_none(); /* not yet assigned -- NOT
* vm_uuid_hera(): a
* newly-allocated embryo
* is never Hera (item
* 3.8 caught this exact
* collision class again) */
node->entry.state = VM_STATE_EMBRYO;
node->entry.birth_capsule_id = 0;
node->entry.birth_timestamp_ns = 0;
node->entry.birth_dict_hash = 0;
node->entry.flags = 0;
node->entry.parent_vm_id = vm_uuid_hera(); /* only Hera calls BIRTH
* today; see design doc's "explicitly
* out of scope" for making this dynamic */
node->entry.vm_ptr = (void *)0;
node->entry.stadium_patron_cell = STADIUM_CELL_NONE;
for (i = 0; i < VM_NAME_MAX; i++) node->entry.name[i] = '\0';
node->next = (void *)0;
/* Append to tail */
if (!vm_registry_head) {
vm_registry_head = node;
} else {
tail = vm_registry_head;
while (tail->next) tail = tail->next;
tail->next = node;
}
vm_registry_count++;
return &node->entry;
}
int capsule_vm_registry_get(VMUuid vm_id, VMRegistryEntry *out) {
VMRegistryEntry *entry;
if (!out) return -1;
entry = vm_find_entry_ptr(vm_id);
if (!entry) return -1;
*out = *entry;
return 0;
}
uint32_t capsule_vm_registry_count(void) {
return vm_registry_count;
}
int capsule_vm_registry_get_by_index(uint32_t index, VMRegistryEntry *out) {
vm_node_t *node = vm_registry_head;
uint32_t i = 0;
if (!out) return -1;
while (node) {
if (i == index) { *out = node->entry; return 0; }
node = node->next;
i++;
}
return -1;
}
/* Live population, distinct from vm_registry_count above: vm_registry_count
* is monotonic (incremented on every vm_registry_alloc(), never decremented
* on death), so it counts every VM ever born, not the outer Stadium's
* current occupancy. FABRIC-0.md item 1.5's bound is on LIVE VMs -- a dead or
* stillborn slot doesn't hold Stadium capacity, and gating on the monotonic
* total would mean the fleet could never regrow after any VM's death,
* which contradicts Hera's own kill-then-rebirth lifecycle (TRIPOD-TEST's
* "K soak" check, capsule_vm_physics.c). Not exposed in the public header:
* only capsule_birth_baby's bound check needs it today. */
static uint32_t vm_registry_live_count(void) {
vm_node_t *node = vm_registry_head;
uint32_t live = 0;
while (node) {
if (node->entry.state == VM_STATE_LIVE) live++;
node = node->next;
}
return live;
}
int capsule_vm_find_by_name(const char *name, VMRegistryEntry *out) {
vm_node_t *node;
if (!name || !out) return -1;
node = vm_registry_head;
while (node) {
if (vm_name_eq(node->entry.name, name)) {
*out = node->entry;
return 0;
}
node = node->next;
}
return -1;
}
/* Fold ASCII letter to lowercase (no libc) */
static char vm_to_lower(char c) {
return (c >= 'A' && c <= 'Z') ? (char)(c + 32) : c;
}
/* Case-insensitive ASCII equality (no libc) */
static int vm_name_eq_nocase(const char *a, const char *b) {
while (*a && *b) {
if (vm_to_lower(*a) != vm_to_lower(*b)) return 0;
a++; b++;
}
return *a == *b;
}
/* Case-insensitive ASCII prefix match up to (and not including) a literal
* ':' in `str`, or the whole of `str` if it has no ':' -- capsule names use
* a "namespace:filename" convention (e.g. "hermes:init.4th"), confirmed
* live via a temporary probe (§H.12 step 6) rather than assumed: bare
* vm_name_eq_nocase(capsule_name, "Hermes") never matched. `prefix` has no
* ':' of its own. */
static int vm_name_prefix_eq_nocase(const char *str, const char *prefix) {
while (*str && *str != ':' && *prefix) {
if (vm_to_lower(*str) != vm_to_lower(*prefix)) return 0;
str++; prefix++;
}
if (*prefix) return 0; /* prefix longer than str's namespace segment */
return (*str == '\0' || *str == ':');
}
int capsule_vm_find_by_name_nocase(const char *name, VMRegistryEntry *out) {
vm_node_t *node;
if (!name || !out) return -1;
node = vm_registry_head;
while (node) {
if (vm_name_eq_nocase(node->entry.name, name)) {
*out = node->entry;
return 0;
}
node = node->next;
}
return -1;
}
void capsule_vm_set_state(VMUuid vm_id, uint32_t state) {
VMRegistryEntry *entry = vm_find_entry_ptr(vm_id);
if (entry) entry->state = state;
}
void capsule_vm_set_pending_reap(VMUuid vm_id, int pending) {
VMRegistryEntry *entry = vm_find_entry_ptr(vm_id);
if (entry) entry->pending_reap = pending;
}
void capsule_vm_force_reap(VMUuid vm_id) {
VMRegistryEntry *entry = vm_find_entry_ptr(vm_id);
if (!entry) return;
/* Idempotent, matching capsule_vm_kill()'s own convention -- the
* Stage 3 checkpoint that calls this cannot itself know whether some
* other path (a later re-attach's own teardown, say) already reaped
* this exact VM before the checkpoint got to it. */
if (entry->state == VM_STATE_DEAD) return;
/* Hera is never a switch-signal participant subject to pending_reap
* in the first place, but refuse on principle anyway -- matches
* capsule_vm_kill()'s own unconditional rule. */
if (vm_uuid_is_hera(entry->vm_id)) return;
VM *vm = (VM *)entry->vm_ptr;
/* Deliberately NOT capsule_vm_kill()'s VM_STATE_SWITCHED_OUT refusal
* -- that guard exists precisely for the case this function handles:
* the caller (vm_core.c's Stage 3 checkpoint) has already decided,
* at a safe cooperative point it alone controls, that this parked
* native-stack context will never be resumed. Freeing it here is the
* deferred half of the mark-and-defer design (FABRIC-3.md §XXVIII
* Stage 4) -- see VMRegistryEntry.pending_reap's own doc comment. */
if (entry->stadium_patron_cell != STADIUM_CELL_NONE) {
(void)stadium_evict(entry->stadium_patron_cell);
entry->stadium_patron_cell = STADIUM_CELL_NONE;
}
if (vm) {
vm_cleanup(vm);
sf_free(vm);
}
/* Generic cleanup, independent of whatever subsystem set
* pending_reap -- a reaped VM is no longer a switch-signal
* participant either way. */
sk_vm_switch_signal_unregister(vm_id);
capsule_parity_log_kill(vm_id, entry->name);
console_puts("REAP: ");
console_puts(entry->name[0] ? entry->name : "(unnamed)");
console_println(" tombstone reclaimed");
entry->vm_ptr = (void *)0;
entry->state = VM_STATE_DEAD;
entry->pending_reap = 0;
for (uint32_t i = 0; i < VM_NAME_MAX; i++) entry->name[i] = '\0';
}
void capsule_vm_registry_set_name(VMUuid vm_id, const char *name) {
VMRegistryEntry *entry;
if (!name) return;
entry = vm_find_entry_ptr(vm_id);
if (!entry) return;
vm_name_copy(entry->name, name);
}
/*===========================================================================
* Internal: init.4th dispatch (PERSONALITY layer)
*
* After a baby VM runs its identity capsule, attempt to execute block 1.
* Block 1 is the PERSONALITY layer — the baby's personal init.4th.
* Failure is silent: the block may not exist, which is normal.
*===========================================================================*/
static void dispatch_init_forth(void *vm_ctx) {
/* M9: run baby's personal init.4th from block 1 once per-VM block
* storage is isolated. Until then this is a no-op to avoid executing
* Mama's block 1 content on every child VM. */
(void)vm_ctx;
}
/*===========================================================================
* VM Kill
*===========================================================================*/
int capsule_vm_kill(const char *name) {
VMRegistryEntry *entry;
VM *vm;
VMUuid vm_id;
uint32_t i;
if (!name) return -1;
/* Locate by name (case-insensitive) */
{
vm_node_t *node = vm_registry_head;
entry = (VMRegistryEntry *)0;
while (node) {
if (vm_name_eq_nocase(node->entry.name, name)) {
entry = &node->entry;
break;
}
node = node->next;
}
}
if (!entry) {
console_puts("KILL: ");
console_puts(name);
console_println(" not found");
return -1;
}
/* Hera cannot be killed */
if (vm_uuid_is_hera(entry->vm_id)) {
console_println("KILL: cannot kill Hera");
return -1;
}
/* Already dead — idempotent */
if (entry->state == VM_STATE_DEAD) {
console_puts("KILL: ");
console_puts(name);
console_println(" already dead");
return 0;
}
/* FABRIC-3.md §XXVIII, Stage 2 (2026-09-13): a switched-out VM has a
* live saved context parked on its own native stack -- freeing vm_ptr
* here would leave that saved SP pointing into freed memory, a real
* use-after-free the moment anything ever tried to resume it. Refuse
* rather than silently corrupt; the caller can retry once the VM has
* been switched back in and reaches a normal LIVE state. */
if (entry->state == VM_STATE_SWITCHED_OUT) {
console_puts("KILL: ");
console_puts(name);
console_println(" refused -- switched-out (context parked)");
return -1;
}
vm_id = entry->vm_id;
vm = (VM *)entry->vm_ptr;
/* FABRIC-2.md SS B, VM-COOL: reap this VM's own Stadium patron cell for
* real, dispatching COOL. Refusal (already naturally reclaimed by
* unrelated quota pressure, or never admitted) is silently tolerated --
* KILL tears the VM down unconditionally either way. */
if (entry->stadium_patron_cell != STADIUM_CELL_NONE) {
(void)stadium_evict(entry->stadium_patron_cell);
entry->stadium_patron_cell = STADIUM_CELL_NONE;
}
/* Tear down and free */
if (vm) {
vm_cleanup(vm);
sf_free(vm);
}
entry->vm_ptr = (void *)0;
entry->state = VM_STATE_DEAD;
for (i = 0; i < VM_NAME_MAX; i++) entry->name[i] = '\0';
capsule_parity_log_kill(vm_id, name);
console_puts("KILL: ");
console_puts(name);
console_println(" dead");
return 0;
}
void capsule_vm_kill_all_nonmama(void) {
vm_node_t *node;
VM *vm;
VMUuid vm_id;
node = vm_registry_head;
while (node) {
if (vm_uuid_is_hera(node->entry.vm_id) || node->entry.state == VM_STATE_DEAD) {
node = node->next;
continue;
}
vm_id = node->entry.vm_id;
vm = (VM *)node->entry.vm_ptr;
if (node->entry.stadium_patron_cell != STADIUM_CELL_NONE) {
(void)stadium_evict(node->entry.stadium_patron_cell);
node->entry.stadium_patron_cell = STADIUM_CELL_NONE;
}
/* FABRIC-3.md §XXVIII, Stage 2 (2026-09-13): a switched-out VM has
* a live saved context parked on its own native stack -- skip the
* free (leaked, not corrupted) rather than invalidate memory a
* parked SP still points into. Harmless here specifically: every
* caller of this function (BYE/cold-restart) calls
* arch_cold_reset() immediately afterward, which wipes all memory
* regardless of what this function left allocated. */
if (vm && node->entry.state == VM_STATE_SWITCHED_OUT) {
log_message(LOG_WARN, "capsule_vm_kill_all_nonmama: leaving %s allocated (switched-out)",
node->entry.name);
} else if (vm) {
vm->halted = 1;
vm_cleanup(vm);
sf_free(vm);
}
node->entry.vm_ptr = (void *)0;
node->entry.state = VM_STATE_DEAD;
capsule_parity_log_kill(vm_id, node->entry.name);
node = node->next;
}
}
/* FABRIC-2.md §I.5, 2026-09-04: contributor-capsule trust tier
* (QEMU-vs-real-hardware conditional enforcement, decided in
* conversation). CAPSULE_FLAG_CONTRIB capsules get the same WARN-only
* treatment as everything else under QEMU (timer_calibration_record()->
* vm_mode == 1) -- development/test is meant to run contrib capsules
* freely. On real hardware (vm_mode == 0), a contrib capsule additionally
* requires CAPSULE_SIG_OK -- MISSING/NO_ROOT_KEY, which stay WARN-only
* for every other capsule (no offline signing key on most machines,
* see capsule_birth_mama()'s own comment), are refused here specifically
* because a contributor's capsule has no other provenance to fall back
* on the way this project's own capsules do. Never touches the
* CAPSULE_SIG_INVALID refusal already in place for every capsule --
* additive, not a replacement. */
static int contrib_capsule_refused(uint32_t flags, CapsuleSigResult sr) {
if (!(flags & CAPSULE_FLAG_CONTRIB)) return 0;
if (timer_calibration_record()->vm_mode) return 0; /* QEMU: relaxed */
return sr != CAPSULE_SIG_OK; /* real hardware: must actually verify */
}
/*===========================================================================
* Mama Init
*===========================================================================*/
CapsuleRunResult capsule_birth_mama(
void *mama_vm,
const CapsuleDirHeader *dir,
const CapsuleDesc *descs,
const CapsuleNameEntry *names,
const uint8_t *arena)
{
if (!mama_vm || !dir || !descs || !names || !arena)
return CAPSULE_RUN_ERR_INVALID;
if (!vm_exec_fn || !vm_dict_hash_fn)
return CAPSULE_RUN_ERR_INVALID;
const CapsuleDesc *mama_cap = capsule_find_mama_init(dir, descs);
if (!mama_cap) return CAPSULE_RUN_ERR_INVALID;
CapsuleValidateResult vr = capsule_validate(mama_cap, arena, dir->arena_size, 1);
if (vr != CAPSULE_VALID) return CAPSULE_RUN_ERR_INVALID;
/* Milestone 6 (Phase 8): signature check. Enforced ONLY on INVALID (a
* signature that IS present but does not verify -- unambiguous
* tampering/corruption evidence). MISSING and NO_ROOT_KEY stay
* WARN-only: MISSING is the normal state on every machine without
* access to the offline signing key (CI, any other checkout) --
* refusing on it would brick boot everywhere but the one machine
* that minted this key, not catch anything real. See capsule_sig.h. */
{
int idx = (int)(mama_cap - descs);
CapsuleSigResult sr = capsule_verify_signature(
descs, names, capsule_get_signatures(), arena, dir->desc_count, idx);
if (sr != CAPSULE_SIG_OK) {
log_message(LOG_WARN, "capsule sig: %s: %s",
names[idx].name, capsule_sig_result_str(sr));
if (sr == CAPSULE_SIG_INVALID) return CAPSULE_RUN_ERR_INVALID;
}
}
uint64_t pre_dict_hash = vm_dict_hash_fn(mama_vm);
(void)pre_dict_hash;
const uint8_t *payload = capsule_get_payload(mama_cap, arena);
if (!payload) return CAPSULE_RUN_ERR_INVALID;
int exec_result = vm_exec_fn(mama_vm, (const char *)payload, mama_cap->length);
if (exec_result != 0) return CAPSULE_RUN_ERR_EXEC_FAIL;
uint64_t post_dict_hash = vm_dict_hash_fn(mama_vm);
capsule_parity_log_mama_init(
mama_cap->capsule_id,
mama_cap->content_hash,
post_dict_hash);
{
VMRegistryEntry *mama_entry = vm_find_entry_ptr(vm_uuid_hera());
if (mama_entry) {
mama_entry->birth_capsule_id = mama_cap->capsule_id;
mama_entry->birth_dict_hash = post_dict_hash;
}
}
/* item 3.8: seed the vm_uuid pool now that the Mama capsule's content
* hash is known -- before any baby birth (none happens today, item 0.1),
* so the same capsule booted twice produces the same id sequence. */
vm_uuid_pool_init(mama_cap->content_hash);
return CAPSULE_RUN_OK;
}
/*===========================================================================
* Baby Birth
*===========================================================================*/
CapsuleRunResult capsule_birth_baby(
const char *capsule_name,
const CapsuleDirHeader *dir,
const CapsuleDesc *descs,
const CapsuleNameEntry *names,
const uint8_t *arena,
VMUuid parent,
int skip_pki_sig,
VMUuid *out_vm_id,
void **out_vm_ctx)
{
if (!capsule_name || !dir || !descs || !names || !arena)
return CAPSULE_RUN_ERR_INVALID;
if (!vm_exec_fn || !vm_dict_hash_fn || !vm_alloc_fn)
return CAPSULE_RUN_ERR_INVALID;
/* Locate by name */
const CapsuleDesc *cap = capsule_find_by_name(dir, descs, names, capsule_name);
if (!cap) return CAPSULE_RUN_ERR_INVALID;
if (!CAPSULE_BIRTH_ELIGIBLE(cap->flags)) return CAPSULE_RUN_ERR_NOT_ELIGIBLE;
CapsuleValidateResult vr = capsule_validate(cap, arena, dir->arena_size, 1);
if (vr != CAPSULE_VALID) return CAPSULE_RUN_ERR_INVALID;
/* 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. Skipped entirely when skip_pki_sig is set (RUNCAP,
* FABRIC-2.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);
if (sr != CAPSULE_SIG_OK) {
log_message(LOG_WARN, "capsule sig: %s: %s",
names[idx].name, capsule_sig_result_str(sr));
if (sr == CAPSULE_SIG_INVALID) return CAPSULE_RUN_ERR_INVALID;
}
/* FABRIC-2.md §I.5: contrib trust tier -- see contrib_capsule_
* refused()'s own doc comment. */
if (contrib_capsule_refused(cap->flags, sr)) {
log_message(LOG_WARN, "capsule sig: %s: contrib capsule refused on real hardware (%s)",
names[idx].name, capsule_sig_result_str(sr));
return CAPSULE_RUN_ERR_INVALID;
}
}
if (vm_registry_live_count() >= stadium_max_vm_count()) {
capsule_parity_log_birth_failed(vm_uuid_none(), cap->capsule_id,
CAPSULE_RUN_ERR_FLEET_FULL, 0);
return CAPSULE_RUN_ERR_FLEET_FULL;
}
VMRegistryEntry *entry = vm_registry_alloc();
if (!entry) return CAPSULE_RUN_ERR_INVALID;
VMUuid vm_id = vm_uuid_next();
entry->vm_id = vm_id;
entry->state = VM_STATE_EMBRYO;
entry->birth_capsule_id = cap->capsule_id;
/* Allocate baby VM */
void *new_vm = vm_alloc_fn();
entry->vm_ptr = new_vm;
if (!new_vm) {
entry->state = VM_STATE_STILLBORN;
capsule_parity_log_birth_failed(vm_id, cap->capsule_id,
CAPSULE_RUN_ERR_STILLBORN, 0);
return CAPSULE_RUN_ERR_STILLBORN;
}
/* item 4.2: set before the IDENTITY exec below, so any word this baby
* dispatches during her own init capsule already attributes heat to her
* own reservoir, not vm_uuid_hera()'s (item 4.1's hardcoded default). */
((VM *)new_vm)->stadium_vm_id = vm_id;
/* FABRIC-3.md §XXVIII, Stage 1 (2026-09-13): every VM gets its own
* native stack from the moment it exists, including WIREBIND-birthed
* identity VMs -- purely additive here, nothing executes on it yet
* (that's Stage 2). Allocation failure is not fatal to birth itself:
* a VM with no native stack yet is exactly as functional today as
* every VM has always been (nothing currently runs on a dedicated
* per-VM stack), so this degrades to "not yet switchable," not
* "stillborn." native_stack_top stays 0, the pre-Stage-2 default. */
{
sk_vm_native_stack_t ns;
if (sk_vm_native_stack_alloc(&ns) == 0) {
((VM *)new_vm)->native_stack_paddr = ns.paddr;
((VM *)new_vm)->native_stack_guard_vaddr = ns.guard_vaddr;
((VM *)new_vm)->native_stack_top = ns.stack_top;
} else {
log_message(LOG_WARN, "capsule_birth_baby: native stack allocation failed for %s",
capsule_name);
}
}
/* item 4.6 fix (FABRIC-1.md, 2026-08-18): granted here, before IDENTITY
* exec, not after a confirmed live birth as item 4.1a originally placed
* it. item 4.1a's placement assumed no VM's own IDENTITY code would ever
* need a Stadium quota before birth completes -- true until item 4.6's
* Artemis capsule started auto-running a block-admission stress campaign
* as part of her own init.4th load. Without a quota yet, every
* STADIUM-ADMIT during that campaign refused unconditionally (quota
* slot < 0), 100% of trials, on all three architectures. Trade-off this
* introduces: a VM that dies stillborn below (IDENTITY exec fails) has
* still consumed half of its donor's free list, with no rollback --
* accepted because stadium_grant_quota() failure was already non-fatal
* and a stillbirth here is the rare case, not the common one.
*
* Donor is whichever live VM currently holds the most free cells
* (stadium_best_donor(), FABRIC-3.md SXVI) -- NOT unconditionally
* vm_uuid_hera() as this used to hardcode. Always splitting from Hera
* specifically converges HER free list toward empty after a bounded
* number of grants (each halves what remains), silently refusing every
* later birth once she runs dry even while VMs she granted to earlier
* still hold nearly all of their own share untouched -- the Stadium as
* a whole nowhere near full. Falls back to vm_uuid_hera() only if no VM
* holds a quota yet (stadium_best_donor() returns vm_uuid_none()), which
* should not happen here since stadium_birth_hera() always runs first. */
{
VMUuid donor = stadium_best_donor();
if (vm_uuid_equal(donor, vm_uuid_none())) donor = vm_uuid_hera();
(void)stadium_grant_quota(vm_id, donor);
}
/* FABRIC-2.md SS B, VM-COOL: admit this VM as a patron of its own
* quota -- identity 0 (same convention stadium_birth_hera() uses for
* "patron zero"), heat 0 (no reservoir cost). Admitted unpinned here
* regardless of which VM this is -- pinning (when it applies) happens
* through session_set_pinned() below, after admission, same
* unpinned-then-pin ordering §H.12 step 4 already established for
* Hera (stadium_admit() has no admission-time-special pin handling,
* just copies the candidate header, so this ordering is safe).
*
* FABRIC-2.md §H.12 step 5: fleet-foundation VMs (Hera/Hermes/Artemis)
* are pinned -- permanent, exempt from COOL, per §H.1's decision.
* Ordinary/user VMs stay unpinned, matching the original comment's own
* reasoning here (unrelated quota pressure can naturally evict this
* cell before an explicit KILL runs; tolerated, not a bug -- nothing
* wires COOL's dispatch body to kill anything, so the only visible
* effect is entry->stadium_patron_cell going stale, which the
* KILL-time eviction below already tolerates). The original comment's
* "there is no unpin primitive" concern no longer applies to Hera
* herself (session_set_pinned() now provides one, §H.12 step 3) but
* still correctly describes why ordinary VMs -- which DO get killed --
* must stay unpinned: nothing here ever un-pins a killed ordinary VM,
* so it must never have been pinned to begin with.
*
* Soft failure, same as stadium_grant_quota() above -- a refused
* admission leaves stadium_patron_cell at STADIUM_CELL_NONE, and
* nothing downstream depends on it succeeding. session_register()/
* session_set_pinned() failures are soft-fail the same way (logged,
* non-fatal) -- same reasoning §H.12 step 4 already established for
* Hera. */
{
StadiumPatronHeader vm_patron;
uint8_t *raw = (uint8_t *)&vm_patron;
size_t i;
int is_fleet_foundation =
vm_name_prefix_eq_nocase(capsule_name, "Hera") ||
vm_name_prefix_eq_nocase(capsule_name, "Hermes") ||
vm_name_prefix_eq_nocase(capsule_name, "Artemis");
for (i = 0; i < sizeof(vm_patron); i++) raw[i] = 0;
vm_patron.identity = 0;
vm_patron.heat = 0;
vm_patron.ttl = 0;
vm_patron.link = 0;
vm_patron.contains = STADIUM_CONTAINS_NONE;
vm_patron.mass = 1;
vm_patron.flags = 0;
vm_patron.behaviour = (uint8_t)STADIUM_BEHAVIOUR_COOL;
entry->stadium_patron_cell = stadium_admit(vm_id, &vm_patron);
if (entry->stadium_patron_cell != STADIUM_CELL_NONE) {
Session *s = session_register(vm_id, parent, capsule_name);
if (s) {
s->stadium_cell = entry->stadium_patron_cell;
if (is_fleet_foundation) session_set_pinned(vm_id, 1);
}
}
}
const uint8_t *payload = capsule_get_payload(cap, arena);
if (!payload) {
entry->state = VM_STATE_STILLBORN;
capsule_parity_log_birth_failed(vm_id, cap->capsule_id,
CAPSULE_RUN_ERR_INVALID, 0);
return CAPSULE_RUN_ERR_INVALID;
}
/* IDENTITY: run init capsule */
int exec_result = vm_exec_fn(new_vm, (const char *)payload, cap->length);
if (exec_result != 0) {
uint64_t partial_hash = vm_dict_hash_fn(new_vm);
entry->state = VM_STATE_STILLBORN;
entry->birth_dict_hash = partial_hash;
capsule_parity_log_birth_failed(vm_id, cap->capsule_id,
CAPSULE_RUN_ERR_EXEC_FAIL, partial_hash);
return CAPSULE_RUN_ERR_EXEC_FAIL;
}
/* PERSONALITY: per-VM block storage is M9 scope; no-op until then */
dispatch_init_forth(new_vm);
uint64_t dict_hash = vm_dict_hash_fn(new_vm);
entry->state = VM_STATE_LIVE;
entry->birth_dict_hash = dict_hash;
capsule_parity_log_birth(vm_id, cap->capsule_id, cap->content_hash, dict_hash);
/* §H.12 step 11: applied AFTER dict_hash/parity logging above, not
* before -- a runtime ACL snapshot depends on the parent's CURRENT
* state (which can vary run-to-run once Zuse-granted elevations
* exist), so applying it before the hash would make birth_dict_hash
* no longer a pure function of capsule content, breaking the "same
* capsule booted twice produces the same dict hash" determinism
* invariant this codebase relies on elsewhere. */
{
VMRegistryEntry *parent_entry = vm_find_entry_ptr(parent);
if (parent_entry && parent_entry->vm_ptr) {
dictionary_snapshot_acl_from_parent(new_vm, (VM *)parent_entry->vm_ptr);
}
}
if (out_vm_id) *out_vm_id = vm_id;
if (out_vm_ctx) *out_vm_ctx = new_vm;
return CAPSULE_RUN_OK;
}
/*===========================================================================
* Experiment Execution
*===========================================================================*/
CapsuleRunResult capsule_run_experiment(
void *mama_vm,
const char *capsule_name,
const CapsuleDirHeader *dir,
const CapsuleDesc *descs,
const CapsuleNameEntry *names,
const uint8_t *arena,
uint64_t *out_run_id)
{
if (!mama_vm || !capsule_name || !dir || !descs || !names || !arena)
return CAPSULE_RUN_ERR_INVALID;
if (!vm_exec_fn || !vm_dict_hash_fn)
return CAPSULE_RUN_ERR_INVALID;
const CapsuleDesc *cap = capsule_find_by_name(dir, descs, names, capsule_name);
if (!cap) return CAPSULE_RUN_ERR_INVALID;
if (!CAPSULE_DOE_ELIGIBLE(cap->flags)) return CAPSULE_RUN_ERR_NOT_ELIGIBLE;
CapsuleValidateResult vr = capsule_validate(cap, arena, dir->arena_size, 1);
if (vr != CAPSULE_VALID) return CAPSULE_RUN_ERR_INVALID;
/* 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. */
{
int idx = (int)(cap - descs);
CapsuleSigResult sr = capsule_verify_signature(
descs, names, capsule_get_signatures(), arena, dir->desc_count, idx);
if (sr != CAPSULE_SIG_OK) {
log_message(LOG_WARN, "capsule sig: %s: %s",
names[idx].name, capsule_sig_result_str(sr));
if (sr == CAPSULE_SIG_INVALID) return CAPSULE_RUN_ERR_INVALID;
}
/* FABRIC-2.md §I.5: contrib trust tier -- see contrib_capsule_
* refused()'s own doc comment. */
if (contrib_capsule_refused(cap->flags, sr)) {
log_message(LOG_WARN, "capsule sig: %s: contrib capsule refused on real hardware (%s)",
names[idx].name, capsule_sig_result_str(sr));
return CAPSULE_RUN_ERR_INVALID;
}
}
uint64_t pre_dict_hash = vm_dict_hash_fn(mama_vm);
const uint8_t *payload = capsule_get_payload(cap, arena);
if (!payload) return CAPSULE_RUN_ERR_INVALID;
int exec_result = vm_exec_fn(mama_vm, (const char *)payload, cap->length);
uint64_t post_dict_hash = vm_dict_hash_fn(mama_vm);
CapsuleRunRecord record;
record.run_id = 0;
record.vm_id = vm_uuid_hera(); /* experiments run on Mama's own VM */
record.reserved = 0;
record.capsule_id = cap->capsule_id;
record.capsule_hash = cap->content_hash;
record.pre_dict_hash = pre_dict_hash;
record.post_dict_hash = post_dict_hash;
record.started_ns = 0;
record.ended_ns = 0;
record.result_code = (exec_result == 0) ? CAPSULE_RUN_OK : CAPSULE_RUN_ERR_EXEC_FAIL;
record.flags = cap->flags;
uint64_t run_id = capsule_run_log_record(&record);
capsule_parity_log_run(vm_uuid_hera(), run_id, cap->capsule_id, pre_dict_hash, post_dict_hash);
if (out_run_id) *out_run_id = run_id;
return (exec_result == 0) ? CAPSULE_RUN_OK : CAPSULE_RUN_ERR_EXEC_FAIL;
}