Session.parent now comes from the actual birthing VM's own stadium_vm_id, not a hardcoded vm_uuid_hera(). Added a VMUuid parent parameter to capsule_birth_baby() and, one level up, to capsule_console_birth()/capsule_runcap_birth() (neither had a VM* in their own signature, but every caller did). Updated all 6 real call sites: BIRTH, CAPSULE-BIRTH, CONNECT-ARTEMIS, CONNECT-HERMES, RUNCAP-TEST, PAIR-TEST (mama_forth_words.c) and the console+user birth pair in capsule_wirebind_try_attach() (capsule_wirebind.c). Two functions had their vm parameter marked __attribute__((unused)), now genuinely used -- attribute removed. Steps 8 (Session.name from capsule name) and 9 (identity defaults to installed=0) were already satisfied by step 5's existing session_register() call and its identity-zeroing -- confirmed by inspection, no further code needed. Verified 3-arch boot to ok> (amd64/aarch64/riscv64). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
752 lines
28 KiB
C
752 lines
28 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 "starkernel/session.h" /* session_register()/session_set_pinned() -- FABRIC-3.md §H.12 step 5 */
|
||
#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;
|
||
}
|
||
|
||
/* 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_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,
|
||
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-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). 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-3.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);
|
||
|
||
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;
|
||
}
|