/* 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; }