Files
StarForth/src/starkernel/capsule/capsule_birth.c
T
Robert Allan James 5689c397fc Bug-fix sweep: repl reentrancy, virtio/blocksys bounds, identity CRCs, LOG_LINE_MAX
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.
2026-08-28 23:28:10 -04:00

708 lines
25 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) 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;
}