/* StarForth — Steady-State Virtual Machine Runtime Copyright (c) 2023–2025 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; }