Code review fixes, all compile clean (hosted gcc + aarch64/riscv64 kernel flags):
- repl.c (H1): reentrancy guards on the MSG-TICK idle pump. sk_repl_idle()
now defers when Hera is mid-interpret (g_mama_interpreting) or when its
own vm_interpret is on the stack (g_idle_pump_active), so a blocking
KEY/EXPECT/QUERY inside a dispatched line can no longer re-enter the
interpreter and clobber the in-flight input buffer.
- virtio_rng.c: clamp device-returned used_len to VRNG_BUF_SIZE before the
caller's data_buf copy, closing a device-controlled OOB read.
- block_subsystem.c: first-write path now keys off created_time==0 instead
of dead magic==0 so fresh blocks get a real created_time stamp; first_free/
last_allocated fixed to absolute Forth LBNs (set in blk_compute_fresh_geometry
from slot->start_lbn, no longer the wrong physical-BAM-index values from
compute_totals_from_B); physical-bounds guard on blk_meta_zone_read/write
prevents unsigned underflow on a corrupt fence >= device size.
- capsule_zuse_boot.c / capsule_wirebind.c: identity seed validated magic ->
version -> CRC-64 (compute_crc64 over offsetof(crc)) before trusting it,
so a corrupt/format-mismatched record is refused, never loaded.
- log.h / starkernel/log.h: unused LOG_LINE_MAX 256 renamed LOG_MSG_LINE_MAX
to lift the include-order collision with vm.h's LOG_LINE_MAX 64; stale
include-order comments dropped (kernel_main.c, shim.c, capsule_birth.c).
- FABRIC-3.md: three stale-doc carry-forward items closed [x] with cbe7b49
notes.
Real KEY/?TERMINAL/QUERY/EXPECT bodies (console WIP):
- repl.h/repl.c: sk_console_getkey()/sk_console_key_available()/
sk_console_readline() public bodies; non-destructive peek buffers the
found byte so a following KEY returns it.
- shim.c: getchar()/fgetc()/fgets()/sf_terminal_ready() routed through the
real console paths instead of stubs; sf_terminal_ready() in platform_io.h
with sf_terminal_ready() implemented for the hosted build (linux/io.c,
POSIX select on fd 0) wired into Makefile.
- io_words.c: ?TERMINAL now returns actual terminal-readiness, not constant 0.
Artifacts: minted disk/artemis.img + rebuilt lfs kernel; BLOCK_MAP.md,
doe csv + qemu log regenerated.
708 lines
25 KiB
C
708 lines
25 KiB
C
/*
|
||
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 "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;
|
||
}
|
||
|
||
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.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;
|
||
}
|
||
|
||
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_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;
|
||
}
|
||
|
||
vm_id = entry->vm_id;
|
||
vm = (VM *)entry->vm_ptr;
|
||
|
||
/* FABRIC-3.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;
|
||
}
|
||
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;
|
||
}
|
||
}
|
||
|
||
/*===========================================================================
|
||
* 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,
|
||
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-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);
|
||
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;
|
||
}
|
||
}
|
||
|
||
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;
|
||
|
||
/* item 4.6 fix (FABRIC-2.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 Hera'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. */
|
||
(void)stadium_grant_quota(vm_id, vm_uuid_hera());
|
||
|
||
/* FABRIC-3.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), unpinned (unlike Hera --
|
||
* there is no unpin primitive, and pinning here would make the
|
||
* explicit KILL-time eviction below unreachable without adding one).
|
||
* Unpinned means unrelated quota pressure on this VM's own words/
|
||
* blocks could naturally evict this cell before KILL ever runs; that
|
||
* is 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 (stadium_evict() simply refuses if it's already gone).
|
||
* 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. */
|
||
{
|
||
StadiumPatronHeader vm_patron;
|
||
uint8_t *raw = (uint8_t *)&vm_patron;
|
||
size_t i;
|
||
|
||
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);
|
||
}
|
||
|
||
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);
|
||
|
||
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;
|
||
}
|
||
}
|
||
|
||
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;
|
||
}
|