Reorganize source tree: kernel/, v3/, v4/ split and board infrastructure

Source tree reorganization:
- Move StarForth v3 engine to v3/ (src/, include/, Makefile)
- Move kernel to kernel/ (src/, include/, linker/, Makefile)
- Create v4/ skeleton for F18-ISA golden model (DECOMPOSITION.md, JUSTIFICATION.md)
- Move FABRIC-0..4.md to docs/fabric/
- Move ONTOLOGY.md and ROADMAP.md to docs/

Board infrastructure:
- Add boards/ser5/, boards/raspi/, boards/milkv/, boards/zynq7020/
- Each board has board.mk (ISA, CPU flags, boot recipe) and README.md
- Root Makefile becomes thin dispatcher: boot_image, all, clean, docs take TARGET
- make boot_image TARGET=SER5|RASPI|MILKV builds one GPT/MBR image per board
- ZYNQ7020 target exists but stops with clear error (ARMv7 port not built yet)
- scripts/mkdiskimage.sh builds disk images for all boards

Docs pipeline:
- docs/book/ with LaTeX master (main.tex) and Makefile
- pandoc converts Markdown to LaTeX at build time
- Two Lua filters: table-widths.lua (wide tables wrap), code-breaks.lua (inline code breaks)
- make docs builds single PDF (754 pages, 0 missing characters)
- make docs TARGET=<board> adds board appendix
- build/docs/<book|board>/meta.tex stamps git commit into PDF

Bug fixes:
- 42 include paths that only worked by accident now use correct relative paths
- clang-18 hardcode replaced with configurable CC variable (fixed aarch64 build)
- Pi 5: kernel_2712.img linked at 0x80000, .bss zeroed, memory reserved
- Doxyfile, .clang-tidy, README.md, Kconfig paths updated

Verified:
- Hosted v3 build passes 1012 tests, 0 failures
- SER5 image boots in QEMU (OVMF), POST passes, K exact (65536 = Q48_ONE)
- Milk-V image boots in QEMU (OpenSBI + U-Boot + bootefi), POST passes
- make clean TARGET=<board> removes only that board and its ISA objects
- make all builds all boards, hosted v3, and docs in one run

Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
rajames
2026-10-01 15:40:09 -04:00
co-authored by Junie
parent 3c709c115b
commit a8b70e88d3
630 changed files with 2646 additions and 2036 deletions
+20
View File
@@ -0,0 +1,20 @@
# src/starkernel/capsule/
Kernel-side capsule system: locating, validating, and executing the
immutable, content-addressed `.4th` initialization payloads in
`capsules/` (embedded into the kernel image via `tools/mkcapsule.c`).
- `capsule_find.c` — locates a capsule by name in the generated capsule
directory.
- `capsule_validate.c` — validates a capsule's content hash before use.
- `capsule_birth.c` — the birth protocol: allocate VM ID → execute
IDENTITY (capsule code) → execute PERSONALITY (block 1 from ramdrive) →
log parity record.
- `capsule_run.c` — capsule execution against a VM instance.
- `capsule_loader.c` — loads capsule block content into kernel ramdrive
block RAM, writing each `Block N` header to its literal numbered block
address (not reordered).
- `capsule_vm_hooks.c` — VM integration hooks for capsule execution.
- `capsule_vm_physics.c` — physics-runtime bindings for capsule-birthed
VMs.
- `mama_forth_words.c` — Mama VM-specific FORTH word bindings.
+973
View File
@@ -0,0 +1,973 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
This file is part of the StarForth project.
Licensed under the StarForth License, Version 1.0 (the "License");
you may not use this file except in compliance with the License.
*/
/**
* capsule_birth.c - VM Birth Protocol Implementation (M7.1)
*
* Mama init, baby birth, and experiment execution.
* Freestanding - no libc dependency.
*
* Birth sequence for a baby VM:
* 1. Find capsule by name (capsule_find_by_name)
* 2. Assert CAPSULE_BIRTH_ELIGIBLE
* 3. Validate content hash
* 4. vm_alloc_hook() — fresh VM
* 5. vm_exec_hook(payload) — IDENTITY (init capsule from Hera's store)
* 6. vm_exec_hook("1 LOAD") — PERSONALITY (baby's personal init.4th from block 1, if present)
* 7. Log parity record
*/
#include "starkernel/capsule_birth.h"
#include "starkernel/capsule.h"
#include "starkernel/capsule_run.h"
#include "starkernel/capsule_generated.h" /* capsule_get_signatures() */
#include "starkernel/capsule_sig.h"
#include "starkernel/kmalloc.h"
#include "starkernel/console.h"
#include "starkernel/vm/stadium.h" /* item 4.1a -- stadium_grant_quota() */
#include "starkernel/vm/arena.h" /* FABRIC-3.md §XXVIII Stage 1 -- sk_vm_native_stack_alloc() */
#include "starkernel/session.h" /* session_register()/session_set_pinned() -- FABRIC-2.md §H.12 step 5 */
#include "starkernel/timer.h" /* timer_calibration_record()->vm_mode -- FABRIC-2.md §I.5 CONTRIB trust tier */
#include "starkernel/capsule_vm_switch_signal.h" /* FABRIC-3.md §XXVIII Stage 4 -- capsule_vm_force_reap() */
#include "starkernel/vm/kernel_hermes.h" /* FABRIC-3.6.md task 3.2 -- sk_hermes_channel_subscribe() */
#include "word_source/include/framebuffer_words.h" /* FABRIC-3.6.md task 1.8 -- register_framebuffer_words() */
#include "vm.h"
#include "platform_alloc.h"
/* No LOG_LINE_MAX include-order constraint anymore: vm.h's own
* LOG_LINE_MAX (persistent block-log, 64) and log.h's in-memory line
* length (renamed LOG_MSG_LINE_MAX, 256) are distinct names, so include
* order no longer redefines anything (the -Werror collision found
* 2026-08-26 wiring capsule signature logging is structurally gone). */
#include "log.h"
/*===========================================================================
* VM Execution Hooks
*===========================================================================*/
static CapsuleExecFn vm_exec_fn = 0;
static CapsuleDictHashFn vm_dict_hash_fn = 0;
static CapsuleVMAllocFn vm_alloc_fn = 0;
void capsule_birth_set_hooks(
CapsuleExecFn exec_fn,
CapsuleDictHashFn dict_hash_fn,
CapsuleVMAllocFn vm_alloc_fn_arg)
{
vm_exec_fn = exec_fn;
vm_dict_hash_fn = dict_hash_fn;
vm_alloc_fn = vm_alloc_fn_arg;
}
/*===========================================================================
* VM Registry — dynamic linked list, heap-allocated via kmalloc
*===========================================================================*/
typedef struct vm_node {
VMRegistryEntry entry;
struct vm_node *next;
} vm_node_t;
static vm_node_t *vm_registry_head = (void *)0;
static uint32_t vm_registry_count = 0;
/* item 3.8: vm_id generation moved to vm_uuid_next()'s deterministic pool;
* the monotonic next_vm_id counter this replaced is gone. Hera's id is
* vm_uuid_hera() (fixed, reserved), not drawn from the pool. */
/* Copy at most VM_NAME_MAX-1 chars, always null-terminate */
static void vm_name_copy(char *dst, const char *src) {
uint32_t i;
for (i = 0; i < (VM_NAME_MAX - 1u) && src[i]; i++)
dst[i] = src[i];
dst[i] = '\0';
}
/* Case-sensitive equality test (no libc) */
static int vm_name_eq(const char *a, const char *b) {
while (*a && *b) {
if (*a != *b) return 0;
a++; b++;
}
return *a == *b;
}
/* Internal: return mutable pointer into registry node for vm_id */
static VMRegistryEntry *vm_find_entry_ptr(VMUuid vm_id) {
vm_node_t *node = vm_registry_head;
while (node) {
if (vm_uuid_equal(node->entry.vm_id, vm_id)) return &node->entry;
node = node->next;
}
return (void *)0;
}
/* FABRIC-2.md §H.12 step 10: creator-ceiling enforcement, birth-time
* snapshot only, no live sync (§H.3, decided 2026-09-03 -- "if something
* was developed with a particular set of ACLs, it should remain at
* that... otherwise parent changes break the child's program"). For
* every word in child's dictionary also present, by name, in parent's,
* copy parent's CURRENT acl_allow/acl_mode/acl_pinned/acl_ttl onto
* child's matching entry -- caps the child at whatever the parent
* allowed at this exact moment, once, never re-applied afterward. Words
* the child has that the parent doesn't (baby-specific vocabulary) are
* left untouched -- there's nothing to cap them against. vm_find_word()
* is the same C-level lookup FIND itself uses; not modifying FIND, just
* calling the same lookup it does (CLAUDE.md: "FIND is a proven, tested,
* registered word. Never modify it" -- this doesn't). */
static void dictionary_snapshot_acl_from_parent(VM *child, VM *parent) {
DictEntry *ce;
if (!child || !parent) return;
for (ce = child->latest; ce != NULL; ce = ce->link) {
DictEntry *pe = vm_find_word(parent, ce->name, ce->name_len);
if (!pe) continue;
ce->acl_allow = pe->acl_allow;
ce->acl_mode = pe->acl_mode;
ce->acl_pinned = pe->acl_pinned;
ce->acl_ttl = pe->acl_ttl;
}
}
void capsule_vm_registry_init(void *mama_vm_ptr) {
uint32_t i;
vm_node_t *node;
vm_node_t *next;
vm_node_t *mama;
/* Free any nodes from a previous init (defensive) */
node = vm_registry_head;
while (node) {
next = node->next;
kfree(node);
node = next;
}
vm_registry_head = (void *)0;
vm_registry_count = 0;
/* Mama is always the reserved, fixed vm_uuid_hera() id (item 3.8) */
mama = (vm_node_t *)kmalloc(sizeof(vm_node_t));
if (!mama) return;
mama->entry.vm_id = vm_uuid_hera();
mama->entry.state = VM_STATE_LIVE;
mama->entry.birth_capsule_id = 0;
mama->entry.birth_timestamp_ns = 0;
mama->entry.birth_dict_hash = 0;
mama->entry.flags = 0;
mama->entry.parent_vm_id = vm_uuid_hera(); /* self-referential: Hera is the root */
mama->entry.vm_ptr = mama_vm_ptr;
mama->entry.stadium_patron_cell = STADIUM_CELL_NONE; /* set for real by
* stadium_birth_hera()
* separately -- not
* tracked here */
for (i = 0; i < VM_NAME_MAX; i++) mama->entry.name[i] = '\0';
vm_name_copy(mama->entry.name, "Hera");
mama->next = (void *)0;
vm_registry_head = mama;
vm_registry_count = 1;
/* From this point on all console output is prefixed [Hera] */
console_set_vm_name("Hera");
}
static VMRegistryEntry *vm_registry_alloc(void) {
uint32_t i;
vm_node_t *node;
vm_node_t *tail;
node = (vm_node_t *)kmalloc(sizeof(vm_node_t));
if (!node) return (void *)0;
node->entry.vm_id = vm_uuid_none(); /* not yet assigned -- NOT
* vm_uuid_hera(): a
* newly-allocated embryo
* is never Hera (item
* 3.8 caught this exact
* collision class again) */
node->entry.state = VM_STATE_EMBRYO;
node->entry.birth_capsule_id = 0;
node->entry.birth_timestamp_ns = 0;
node->entry.birth_dict_hash = 0;
node->entry.flags = 0;
node->entry.parent_vm_id = vm_uuid_hera(); /* only Hera calls BIRTH
* today; see design doc's "explicitly
* out of scope" for making this dynamic */
node->entry.vm_ptr = (void *)0;
node->entry.stadium_patron_cell = STADIUM_CELL_NONE;
for (i = 0; i < VM_NAME_MAX; i++) node->entry.name[i] = '\0';
node->next = (void *)0;
/* Append to tail */
if (!vm_registry_head) {
vm_registry_head = node;
} else {
tail = vm_registry_head;
while (tail->next) tail = tail->next;
tail->next = node;
}
vm_registry_count++;
return &node->entry;
}
int capsule_vm_registry_get(VMUuid vm_id, VMRegistryEntry *out) {
VMRegistryEntry *entry;
if (!out) return -1;
entry = vm_find_entry_ptr(vm_id);
if (!entry) return -1;
*out = *entry;
return 0;
}
uint32_t capsule_vm_registry_count(void) {
return vm_registry_count;
}
int capsule_vm_registry_get_by_index(uint32_t index, VMRegistryEntry *out) {
vm_node_t *node = vm_registry_head;
uint32_t i = 0;
if (!out) return -1;
while (node) {
if (i == index) { *out = node->entry; return 0; }
node = node->next;
i++;
}
return -1;
}
/* Live population, distinct from vm_registry_count above: vm_registry_count
* is monotonic (incremented on every vm_registry_alloc(), never decremented
* on death), so it counts every VM ever born, not the outer Stadium's
* current occupancy. FABRIC-0.md item 1.5's bound is on LIVE VMs -- a dead or
* stillborn slot doesn't hold Stadium capacity, and gating on the monotonic
* total would mean the fleet could never regrow after any VM's death,
* which contradicts Hera's own kill-then-rebirth lifecycle (TRIPOD-TEST's
* "K soak" check, capsule_vm_physics.c). Not exposed in the public header:
* only capsule_birth_baby's bound check needs it today. */
static uint32_t vm_registry_live_count(void) {
vm_node_t *node = vm_registry_head;
uint32_t live = 0;
while (node) {
if (node->entry.state == VM_STATE_LIVE) live++;
node = node->next;
}
return live;
}
int capsule_vm_find_by_name(const char *name, VMRegistryEntry *out) {
vm_node_t *node;
if (!name || !out) return -1;
node = vm_registry_head;
while (node) {
if (vm_name_eq(node->entry.name, name)) {
*out = node->entry;
return 0;
}
node = node->next;
}
return -1;
}
/* Fold ASCII letter to lowercase (no libc) */
static char vm_to_lower(char c) {
return (c >= 'A' && c <= 'Z') ? (char)(c + 32) : c;
}
/* Case-insensitive ASCII equality (no libc) */
static int vm_name_eq_nocase(const char *a, const char *b) {
while (*a && *b) {
if (vm_to_lower(*a) != vm_to_lower(*b)) return 0;
a++; b++;
}
return *a == *b;
}
/* Case-insensitive ASCII prefix match up to (and not including) a literal
* ':' in `str`, or the whole of `str` if it has no ':' -- capsule names use
* a "namespace:filename" convention (e.g. "hermes:init.4th"), confirmed
* live via a temporary probe (§H.12 step 6) rather than assumed: bare
* vm_name_eq_nocase(capsule_name, "Hermes") never matched. `prefix` has no
* ':' of its own. */
static int vm_name_prefix_eq_nocase(const char *str, const char *prefix) {
while (*str && *str != ':' && *prefix) {
if (vm_to_lower(*str) != vm_to_lower(*prefix)) return 0;
str++; prefix++;
}
if (*prefix) return 0; /* prefix longer than str's namespace segment */
return (*str == '\0' || *str == ':');
}
int capsule_vm_find_by_name_nocase(const char *name, VMRegistryEntry *out) {
vm_node_t *node;
if (!name || !out) return -1;
node = vm_registry_head;
while (node) {
if (vm_name_eq_nocase(node->entry.name, name)) {
*out = node->entry;
return 0;
}
node = node->next;
}
return -1;
}
void capsule_vm_set_state(VMUuid vm_id, uint32_t state) {
VMRegistryEntry *entry = vm_find_entry_ptr(vm_id);
if (entry) entry->state = state;
}
void capsule_vm_set_pending_reap(VMUuid vm_id, int pending) {
VMRegistryEntry *entry = vm_find_entry_ptr(vm_id);
if (entry) entry->pending_reap = pending;
}
void capsule_vm_force_reap(VMUuid vm_id) {
VMRegistryEntry *entry = vm_find_entry_ptr(vm_id);
if (!entry) return;
/* Idempotent, matching capsule_vm_kill()'s own convention -- the
* Stage 3 checkpoint that calls this cannot itself know whether some
* other path (a later re-attach's own teardown, say) already reaped
* this exact VM before the checkpoint got to it. */
if (entry->state == VM_STATE_DEAD) return;
/* Hera is never a switch-signal participant subject to pending_reap
* in the first place, but refuse on principle anyway -- matches
* capsule_vm_kill()'s own unconditional rule. */
if (vm_uuid_is_hera(entry->vm_id)) return;
VM *vm = (VM *)entry->vm_ptr;
/* Deliberately NOT capsule_vm_kill()'s VM_STATE_SWITCHED_OUT refusal
* -- that guard exists precisely for the case this function handles:
* the caller (vm_core.c's Stage 3 checkpoint) has already decided,
* at a safe cooperative point it alone controls, that this parked
* native-stack context will never be resumed. Freeing it here is the
* deferred half of the mark-and-defer design (FABRIC-3.md §XXVIII
* Stage 4) -- see VMRegistryEntry.pending_reap's own doc comment. */
if (entry->stadium_patron_cell != STADIUM_CELL_NONE) {
(void)stadium_evict(entry->stadium_patron_cell);
entry->stadium_patron_cell = STADIUM_CELL_NONE;
}
if (vm) {
vm_cleanup(vm);
sf_free(vm);
}
/* Generic cleanup, independent of whatever subsystem set
* pending_reap -- a reaped VM is no longer a switch-signal
* participant either way. */
sk_vm_switch_signal_unregister(vm_id);
capsule_parity_log_kill(vm_id, entry->name);
console_puts("REAP: ");
console_puts(entry->name[0] ? entry->name : "(unnamed)");
console_println(" tombstone reclaimed");
entry->vm_ptr = (void *)0;
entry->state = VM_STATE_DEAD;
entry->pending_reap = 0;
for (uint32_t i = 0; i < VM_NAME_MAX; i++) entry->name[i] = '\0';
}
void capsule_vm_registry_set_name(VMUuid vm_id, const char *name) {
VMRegistryEntry *entry;
if (!name) return;
entry = vm_find_entry_ptr(vm_id);
if (!entry) return;
vm_name_copy(entry->name, name);
}
/*===========================================================================
* Internal: init.4th dispatch (PERSONALITY layer)
*
* After a baby VM runs its identity capsule, attempt to execute block 1.
* Block 1 is the PERSONALITY layer — the baby's personal init.4th.
* Failure is silent: the block may not exist, which is normal.
*===========================================================================*/
static void dispatch_init_forth(void *vm_ctx) {
/* M9: run baby's personal init.4th from block 1 once per-VM block
* storage is isolated. Until then this is a no-op to avoid executing
* Mama's block 1 content on every child VM. */
(void)vm_ctx;
}
/*===========================================================================
* VM Kill
*===========================================================================*/
int capsule_vm_kill(const char *name) {
VMRegistryEntry *entry;
VM *vm;
VMUuid vm_id;
uint32_t i;
if (!name) return -1;
/* Locate by name (case-insensitive) */
{
vm_node_t *node = vm_registry_head;
entry = (VMRegistryEntry *)0;
while (node) {
if (vm_name_eq_nocase(node->entry.name, name)) {
entry = &node->entry;
break;
}
node = node->next;
}
}
if (!entry) {
console_puts("KILL: ");
console_puts(name);
console_println(" not found");
return -1;
}
/* Hera cannot be killed */
if (vm_uuid_is_hera(entry->vm_id)) {
console_println("KILL: cannot kill Hera");
return -1;
}
/* Already dead — idempotent */
if (entry->state == VM_STATE_DEAD) {
console_puts("KILL: ");
console_puts(name);
console_println(" already dead");
return 0;
}
/* FABRIC-3.md §XXVIII, Stage 2 (2026-09-13): a switched-out VM has a
* live saved context parked on its own native stack -- freeing vm_ptr
* here would leave that saved SP pointing into freed memory, a real
* use-after-free the moment anything ever tried to resume it. Refuse
* rather than silently corrupt; the caller can retry once the VM has
* been switched back in and reaches a normal LIVE state. */
if (entry->state == VM_STATE_SWITCHED_OUT) {
console_puts("KILL: ");
console_puts(name);
console_println(" refused -- switched-out (context parked)");
return -1;
}
vm_id = entry->vm_id;
vm = (VM *)entry->vm_ptr;
/* FABRIC-2.md SS B, VM-COOL: reap this VM's own Stadium patron cell for
* real, dispatching COOL. Refusal (already naturally reclaimed by
* unrelated quota pressure, or never admitted) is silently tolerated --
* KILL tears the VM down unconditionally either way. */
if (entry->stadium_patron_cell != STADIUM_CELL_NONE) {
(void)stadium_evict(entry->stadium_patron_cell);
entry->stadium_patron_cell = STADIUM_CELL_NONE;
}
/* Tear down and free */
if (vm) {
vm_cleanup(vm);
sf_free(vm);
}
entry->vm_ptr = (void *)0;
entry->state = VM_STATE_DEAD;
for (i = 0; i < VM_NAME_MAX; i++) entry->name[i] = '\0';
capsule_parity_log_kill(vm_id, name);
console_puts("KILL: ");
console_puts(name);
console_println(" dead");
return 0;
}
void capsule_vm_kill_all_nonmama(void) {
vm_node_t *node;
VM *vm;
VMUuid vm_id;
node = vm_registry_head;
while (node) {
if (vm_uuid_is_hera(node->entry.vm_id) || node->entry.state == VM_STATE_DEAD) {
node = node->next;
continue;
}
vm_id = node->entry.vm_id;
vm = (VM *)node->entry.vm_ptr;
if (node->entry.stadium_patron_cell != STADIUM_CELL_NONE) {
(void)stadium_evict(node->entry.stadium_patron_cell);
node->entry.stadium_patron_cell = STADIUM_CELL_NONE;
}
/* FABRIC-3.md §XXVIII, Stage 2 (2026-09-13): a switched-out VM has
* a live saved context parked on its own native stack -- skip the
* free (leaked, not corrupted) rather than invalidate memory a
* parked SP still points into. Harmless here specifically: every
* caller of this function (BYE/cold-restart) calls
* arch_cold_reset() immediately afterward, which wipes all memory
* regardless of what this function left allocated. */
if (vm && node->entry.state == VM_STATE_SWITCHED_OUT) {
log_message(LOG_WARN, "capsule_vm_kill_all_nonmama: leaving %s allocated (switched-out)",
node->entry.name);
} else if (vm) {
vm->halted = 1;
vm_cleanup(vm);
sf_free(vm);
}
node->entry.vm_ptr = (void *)0;
node->entry.state = VM_STATE_DEAD;
capsule_parity_log_kill(vm_id, node->entry.name);
node = node->next;
}
}
/* FABRIC-2.md §I.5, 2026-09-04: contributor-capsule trust tier
* (QEMU-vs-real-hardware conditional enforcement, decided in
* conversation). CAPSULE_FLAG_CONTRIB capsules get the same WARN-only
* treatment as everything else under QEMU (timer_calibration_record()->
* vm_mode == 1) -- development/test is meant to run contrib capsules
* freely. On real hardware (vm_mode == 0), a contrib capsule additionally
* requires CAPSULE_SIG_OK -- MISSING/NO_ROOT_KEY, which stay WARN-only
* for every other capsule (no offline signing key on most machines,
* see capsule_birth_mama()'s own comment), are refused here specifically
* because a contributor's capsule has no other provenance to fall back
* on the way this project's own capsules do. Never touches the
* CAPSULE_SIG_INVALID refusal already in place for every capsule --
* additive, not a replacement. */
static int contrib_capsule_refused(uint32_t flags, CapsuleSigResult sr) {
if (!(flags & CAPSULE_FLAG_CONTRIB)) return 0;
if (timer_calibration_record()->vm_mode) return 0; /* QEMU: relaxed */
return sr != CAPSULE_SIG_OK; /* real hardware: must actually verify */
}
/*===========================================================================
* Mama Init
*===========================================================================*/
CapsuleRunResult capsule_birth_mama(
void *mama_vm,
const CapsuleDirHeader *dir,
const CapsuleDesc *descs,
const CapsuleNameEntry *names,
const uint8_t *arena)
{
if (!mama_vm || !dir || !descs || !names || !arena)
return CAPSULE_RUN_ERR_INVALID;
if (!vm_exec_fn || !vm_dict_hash_fn)
return CAPSULE_RUN_ERR_INVALID;
const CapsuleDesc *mama_cap = capsule_find_mama_init(dir, descs);
if (!mama_cap) return CAPSULE_RUN_ERR_INVALID;
CapsuleValidateResult vr = capsule_validate(mama_cap, arena, dir->arena_size, 1);
if (vr != CAPSULE_VALID) return CAPSULE_RUN_ERR_INVALID;
/* Milestone 6 (Phase 8): signature check. Enforced ONLY on INVALID (a
* signature that IS present but does not verify -- unambiguous
* tampering/corruption evidence). MISSING and NO_ROOT_KEY stay
* WARN-only: MISSING is the normal state on every machine without
* access to the offline signing key (CI, any other checkout) --
* refusing on it would brick boot everywhere but the one machine
* that minted this key, not catch anything real. See capsule_sig.h. */
{
int idx = (int)(mama_cap - descs);
CapsuleSigResult sr = capsule_verify_signature(
descs, names, capsule_get_signatures(), arena, dir->desc_count, idx);
if (sr != CAPSULE_SIG_OK) {
log_message(LOG_WARN, "capsule sig: %s: %s",
names[idx].name, capsule_sig_result_str(sr));
if (sr == CAPSULE_SIG_INVALID) return CAPSULE_RUN_ERR_INVALID;
}
}
uint64_t pre_dict_hash = vm_dict_hash_fn(mama_vm);
(void)pre_dict_hash;
const uint8_t *payload = capsule_get_payload(mama_cap, arena);
if (!payload) return CAPSULE_RUN_ERR_INVALID;
int exec_result = vm_exec_fn(mama_vm, (const char *)payload, mama_cap->length);
if (exec_result != 0) return CAPSULE_RUN_ERR_EXEC_FAIL;
uint64_t post_dict_hash = vm_dict_hash_fn(mama_vm);
capsule_parity_log_mama_init(
mama_cap->capsule_id,
mama_cap->content_hash,
post_dict_hash);
{
VMRegistryEntry *mama_entry = vm_find_entry_ptr(vm_uuid_hera());
if (mama_entry) {
mama_entry->birth_capsule_id = mama_cap->capsule_id;
mama_entry->birth_dict_hash = post_dict_hash;
}
}
/* item 3.8: seed the vm_uuid pool now that the Mama capsule's content
* hash is known -- before any baby birth (none happens today, item 0.1),
* so the same capsule booted twice produces the same id sequence. */
vm_uuid_pool_init(mama_cap->content_hash);
return CAPSULE_RUN_OK;
}
/*===========================================================================
* Baby Birth
*===========================================================================*/
CapsuleRunResult capsule_birth_baby(
const char *capsule_name,
const CapsuleDirHeader *dir,
const CapsuleDesc *descs,
const CapsuleNameEntry *names,
const uint8_t *arena,
VMUuid parent,
int skip_pki_sig,
VMUuid *out_vm_id,
void **out_vm_ctx)
{
if (!capsule_name || !dir || !descs || !names || !arena)
return CAPSULE_RUN_ERR_INVALID;
if (!vm_exec_fn || !vm_dict_hash_fn || !vm_alloc_fn)
return CAPSULE_RUN_ERR_INVALID;
/* Locate by name */
const CapsuleDesc *cap = capsule_find_by_name(dir, descs, names, capsule_name);
if (!cap) return CAPSULE_RUN_ERR_INVALID;
if (!CAPSULE_BIRTH_ELIGIBLE(cap->flags)) return CAPSULE_RUN_ERR_NOT_ELIGIBLE;
CapsuleValidateResult vr = capsule_validate(cap, arena, dir->arena_size, 1);
if (vr != CAPSULE_VALID) return CAPSULE_RUN_ERR_INVALID;
/* Milestone 6 (Phase 8): enforced only on INVALID -- see the fuller
* comment in capsule_birth_mama() above for why MISSING/NO_ROOT_KEY
* stay WARN-only. Skipped entirely when skip_pki_sig is set (RUNCAP,
* FABRIC-2.md §F.6/F.18): capsule_get_signatures() is the compile-
* time-baked array, indexed against the build-time capsule_descriptors[]
* -- meaningless for a heap-built directory sourced from a thumbdrive,
* where idx 0 would just compare against whatever real capsule happens
* to occupy that slot. That content's trust already comes from a
* separate root (CERTVERIFY, run by the caller before this). */
if (!skip_pki_sig) {
int idx = (int)(cap - descs);
CapsuleSigResult sr = capsule_verify_signature(
descs, names, capsule_get_signatures(), arena, dir->desc_count, idx);
if (sr != CAPSULE_SIG_OK) {
log_message(LOG_WARN, "capsule sig: %s: %s",
names[idx].name, capsule_sig_result_str(sr));
if (sr == CAPSULE_SIG_INVALID) return CAPSULE_RUN_ERR_INVALID;
}
/* FABRIC-2.md §I.5: contrib trust tier -- see contrib_capsule_
* refused()'s own doc comment. */
if (contrib_capsule_refused(cap->flags, sr)) {
log_message(LOG_WARN, "capsule sig: %s: contrib capsule refused on real hardware (%s)",
names[idx].name, capsule_sig_result_str(sr));
return CAPSULE_RUN_ERR_INVALID;
}
}
if (vm_registry_live_count() >= stadium_max_vm_count()) {
capsule_parity_log_birth_failed(vm_uuid_none(), cap->capsule_id,
CAPSULE_RUN_ERR_FLEET_FULL, 0);
return CAPSULE_RUN_ERR_FLEET_FULL;
}
VMRegistryEntry *entry = vm_registry_alloc();
if (!entry) return CAPSULE_RUN_ERR_INVALID;
VMUuid vm_id = vm_uuid_next();
entry->vm_id = vm_id;
entry->state = VM_STATE_EMBRYO;
entry->birth_capsule_id = cap->capsule_id;
/* Allocate baby VM */
void *new_vm = vm_alloc_fn();
entry->vm_ptr = new_vm;
if (!new_vm) {
entry->state = VM_STATE_STILLBORN;
capsule_parity_log_birth_failed(vm_id, cap->capsule_id,
CAPSULE_RUN_ERR_STILLBORN, 0);
return CAPSULE_RUN_ERR_STILLBORN;
}
/* item 4.2: set before the IDENTITY exec below, so any word this baby
* dispatches during her own init capsule already attributes heat to her
* own reservoir, not vm_uuid_hera()'s (item 4.1's hardcoded default). */
((VM *)new_vm)->stadium_vm_id = vm_id;
/* FABRIC-3.md §XXVIII, Stage 1 (2026-09-13): every VM gets its own
* native stack from the moment it exists, including WIREBIND-birthed
* identity VMs -- purely additive here, nothing executes on it yet
* (that's Stage 2). Allocation failure is not fatal to birth itself:
* a VM with no native stack yet is exactly as functional today as
* every VM has always been (nothing currently runs on a dedicated
* per-VM stack), so this degrades to "not yet switchable," not
* "stillborn." native_stack_top stays 0, the pre-Stage-2 default. */
{
sk_vm_native_stack_t ns;
if (sk_vm_native_stack_alloc(&ns) == 0) {
((VM *)new_vm)->native_stack_paddr = ns.paddr;
((VM *)new_vm)->native_stack_guard_vaddr = ns.guard_vaddr;
((VM *)new_vm)->native_stack_top = ns.stack_top;
} else {
log_message(LOG_WARN, "capsule_birth_baby: native stack allocation failed for %s",
capsule_name);
}
}
/* item 4.6 fix (FABRIC-1.md, 2026-08-18): granted here, before IDENTITY
* exec, not after a confirmed live birth as item 4.1a originally placed
* it. item 4.1a's placement assumed no VM's own IDENTITY code would ever
* need a Stadium quota before birth completes -- true until item 4.6's
* Artemis capsule started auto-running a block-admission stress campaign
* as part of her own init.4th load. Without a quota yet, every
* STADIUM-ADMIT during that campaign refused unconditionally (quota
* slot < 0), 100% of trials, on all three architectures. Trade-off this
* introduces: a VM that dies stillborn below (IDENTITY exec fails) has
* still consumed half of its donor's free list, with no rollback --
* accepted because stadium_grant_quota() failure was already non-fatal
* and a stillbirth here is the rare case, not the common one.
*
* Donor is whichever live VM currently holds the most free cells
* (stadium_best_donor(), FABRIC-3.md SXVI) -- NOT unconditionally
* vm_uuid_hera() as this used to hardcode. Always splitting from Hera
* specifically converges HER free list toward empty after a bounded
* number of grants (each halves what remains), silently refusing every
* later birth once she runs dry even while VMs she granted to earlier
* still hold nearly all of their own share untouched -- the Stadium as
* a whole nowhere near full. Falls back to vm_uuid_hera() only if no VM
* holds a quota yet (stadium_best_donor() returns vm_uuid_none()), which
* should not happen here since stadium_birth_hera() always runs first. */
{
VMUuid donor = stadium_best_donor();
if (vm_uuid_equal(donor, vm_uuid_none())) donor = vm_uuid_hera();
(void)stadium_grant_quota(vm_id, donor);
}
/* FABRIC-2.md SS B, VM-COOL: admit this VM as a patron of its own
* quota -- identity 0 (same convention stadium_birth_hera() uses for
* "patron zero"), heat 0 (no reservoir cost). Admitted unpinned here
* regardless of which VM this is -- pinning (when it applies) happens
* through session_set_pinned() below, after admission, same
* unpinned-then-pin ordering §H.12 step 4 already established for
* Hera (stadium_admit() has no admission-time-special pin handling,
* just copies the candidate header, so this ordering is safe).
*
* FABRIC-2.md §H.12 step 5: fleet-foundation VMs (Hera/Hermes/Artemis)
* are pinned -- permanent, exempt from COOL, per §H.1's decision.
* Ordinary/user VMs stay unpinned, matching the original comment's own
* reasoning here (unrelated quota pressure can naturally evict this
* cell before an explicit KILL runs; tolerated, not a bug -- nothing
* wires COOL's dispatch body to kill anything, so the only visible
* effect is entry->stadium_patron_cell going stale, which the
* KILL-time eviction below already tolerates). The original comment's
* "there is no unpin primitive" concern no longer applies to Hera
* herself (session_set_pinned() now provides one, §H.12 step 3) but
* still correctly describes why ordinary VMs -- which DO get killed --
* must stay unpinned: nothing here ever un-pins a killed ordinary VM,
* so it must never have been pinned to begin with.
*
* Soft failure, same as stadium_grant_quota() above -- a refused
* admission leaves stadium_patron_cell at STADIUM_CELL_NONE, and
* nothing downstream depends on it succeeding. session_register()/
* session_set_pinned() failures are soft-fail the same way (logged,
* non-fatal) -- same reasoning §H.12 step 4 already established for
* Hera. */
{
StadiumPatronHeader vm_patron;
uint8_t *raw = (uint8_t *)&vm_patron;
size_t i;
/* FABRIC-3.6.md Phase 4 (Stage E), 2026-09-22: "Hermes" retired
* from this OR-chain -- FABRIC-3.5.md SXXXIV.4's own ruling
* named this exact moment ("removed in Stage E, with the strip,
* not with the Tripod change"). The chain temporarily held four
* names (Hera, Hermes, Artemis, Hestia) through Phases 1-3,
* deliberately, per that same ruling; three names now. */
int is_fleet_foundation =
vm_name_prefix_eq_nocase(capsule_name, "Hera") ||
vm_name_prefix_eq_nocase(capsule_name, "Artemis") ||
vm_name_prefix_eq_nocase(capsule_name, "Hestia");
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);
}
}
/* FABRIC-3.6.md task 1.8 (item 33): PLOT/FB-WIDTH/FB-HEIGHT
* (framebuffer_words.c) are no longer registered unconditionally
* in register_forth79_words() -- Hestia is the sole owner of the
* drawing fabric, and this is the one place a VM's name is known
* at the point its dictionary is actually built (vm_init_with_host()
* itself takes no name). Every other VM simply never gets these
* words now, which is the point -- confirmed live, task 0.8 found
* Hermes/Artemis could both call FB-WIDTH despite never loading
* fabric.4th. */
if (vm_name_prefix_eq_nocase(capsule_name, "Hestia")) {
register_framebuffer_words((VM *)new_vm);
}
}
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;
/* FABRIC-3.6.md task 3.2 (B1 / FABRIC-3.5.md §XLV.1): every VM joins
* the common channel at birth. This is the single choke point every
* non-Hera birth passes through (BIRTH, WIREBIND identities, DoE
* workload births alike) -- Hera herself is subscribed explicitly in
* kernel_main.c, the one birth path that never reaches here. Soft
* failure, same posture as every other call in this function -- a
* refused subscription (table exhausted) is logged nowhere special
* and does not fail the birth itself. */
(void)sk_hermes_channel_subscribe(SK_HERMES_CHANNEL_COMMON, vm_id);
capsule_parity_log_birth(vm_id, cap->capsule_id, cap->content_hash, dict_hash);
/* §H.12 step 11: applied AFTER dict_hash/parity logging above, not
* before -- a runtime ACL snapshot depends on the parent's CURRENT
* state (which can vary run-to-run once Zuse-granted elevations
* exist), so applying it before the hash would make birth_dict_hash
* no longer a pure function of capsule content, breaking the "same
* capsule booted twice produces the same dict hash" determinism
* invariant this codebase relies on elsewhere. */
{
VMRegistryEntry *parent_entry = vm_find_entry_ptr(parent);
if (parent_entry && parent_entry->vm_ptr) {
dictionary_snapshot_acl_from_parent(new_vm, (VM *)parent_entry->vm_ptr);
}
}
if (out_vm_id) *out_vm_id = vm_id;
if (out_vm_ctx) *out_vm_ctx = new_vm;
return CAPSULE_RUN_OK;
}
/*===========================================================================
* Experiment Execution
*===========================================================================*/
CapsuleRunResult capsule_run_experiment(
void *mama_vm,
const char *capsule_name,
const CapsuleDirHeader *dir,
const CapsuleDesc *descs,
const CapsuleNameEntry *names,
const uint8_t *arena,
uint64_t *out_run_id)
{
if (!mama_vm || !capsule_name || !dir || !descs || !names || !arena)
return CAPSULE_RUN_ERR_INVALID;
if (!vm_exec_fn || !vm_dict_hash_fn)
return CAPSULE_RUN_ERR_INVALID;
const CapsuleDesc *cap = capsule_find_by_name(dir, descs, names, capsule_name);
if (!cap) return CAPSULE_RUN_ERR_INVALID;
if (!CAPSULE_DOE_ELIGIBLE(cap->flags)) return CAPSULE_RUN_ERR_NOT_ELIGIBLE;
CapsuleValidateResult vr = capsule_validate(cap, arena, dir->arena_size, 1);
if (vr != CAPSULE_VALID) return CAPSULE_RUN_ERR_INVALID;
/* Milestone 6 (Phase 8): enforced only on INVALID -- see the fuller
* comment in capsule_birth_mama() above for why MISSING/NO_ROOT_KEY
* stay WARN-only. */
{
int idx = (int)(cap - descs);
CapsuleSigResult sr = capsule_verify_signature(
descs, names, capsule_get_signatures(), arena, dir->desc_count, idx);
if (sr != CAPSULE_SIG_OK) {
log_message(LOG_WARN, "capsule sig: %s: %s",
names[idx].name, capsule_sig_result_str(sr));
if (sr == CAPSULE_SIG_INVALID) return CAPSULE_RUN_ERR_INVALID;
}
/* FABRIC-2.md §I.5: contrib trust tier -- see contrib_capsule_
* refused()'s own doc comment. */
if (contrib_capsule_refused(cap->flags, sr)) {
log_message(LOG_WARN, "capsule sig: %s: contrib capsule refused on real hardware (%s)",
names[idx].name, capsule_sig_result_str(sr));
return CAPSULE_RUN_ERR_INVALID;
}
}
uint64_t pre_dict_hash = vm_dict_hash_fn(mama_vm);
const uint8_t *payload = capsule_get_payload(cap, arena);
if (!payload) return CAPSULE_RUN_ERR_INVALID;
int exec_result = vm_exec_fn(mama_vm, (const char *)payload, cap->length);
uint64_t post_dict_hash = vm_dict_hash_fn(mama_vm);
CapsuleRunRecord record;
record.run_id = 0;
record.vm_id = vm_uuid_hera(); /* experiments run on Mama's own VM */
record.reserved = 0;
record.capsule_id = cap->capsule_id;
record.capsule_hash = cap->content_hash;
record.pre_dict_hash = pre_dict_hash;
record.post_dict_hash = post_dict_hash;
record.started_ns = 0;
record.ended_ns = 0;
record.result_code = (exec_result == 0) ? CAPSULE_RUN_OK : CAPSULE_RUN_ERR_EXEC_FAIL;
record.flags = cap->flags;
uint64_t run_id = capsule_run_log_record(&record);
capsule_parity_log_run(vm_uuid_hera(), run_id, cap->capsule_id, pre_dict_hash, post_dict_hash);
if (out_run_id) *out_run_id = run_id;
return (exec_result == 0) ? CAPSULE_RUN_OK : CAPSULE_RUN_ERR_EXEC_FAIL;
}
+92
View File
@@ -0,0 +1,92 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
Licensed under the StarForth License, Version 1.0
*/
#ifndef __STARKERNEL__
#error "capsule_console.c is kernel-only"
#endif
#include "starkernel/capsule_console.h"
#include "starkernel/capsule.h"
#include "starkernel/capsule_birth.h"
#include "starkernel/xxhash64.h"
#include "starkernel/kmalloc.h"
#include <string.h>
/* FABRIC-3.6.md Phase 4 (Stage E), 2026-09-22: used to load
* common:messaging.4th + MSG-CD-INIT here -- "just enough vocabulary
* to send/receive messages" -- but a console-proxy VM's own traffic
* (CONSOLE-CMD-EVENT) has gone through kernel-Hermes directly
* (sk_hermes_send_one(), repl.c) since task 3.9, with no dependency on
* this VM's own dictionary containing any messaging vocabulary at all.
* common:messaging.4th no longer exists as of this strip, so this
* needed to come out regardless. Genuinely empty now, past the
* mandatory "Block NNNN" header capsule_exec_payload() requires
* (§F.18) -- confirmed live that an empty body is a safe no-op when
* interpreted (task 3.8's own finding: vm_interpret() on an empty
* string is a no-op, no error, no UNKNOWN WORD). 4997 is an unused
* block number. */
static const char CONSOLE_IDENTITY_SRC[] =
"Block 4997\n";
CapsuleRunResult capsule_console_birth(const char *console_name, VMUuid parent,
VMUuid *out_vm_id, void **out_vm_ctx)
{
if (!console_name) return CAPSULE_RUN_ERR_INVALID;
size_t source_len = sizeof(CONSOLE_IDENTITY_SRC) - 1u;
uint8_t *arena = (uint8_t *)kmalloc(source_len);
if (!arena) return CAPSULE_RUN_ERR_STILLBORN;
memcpy(arena, CONSOLE_IDENTITY_SRC, source_len);
/* Heap-built single-entry directory -- same shape RUNCAP already
* established (§F.6/§F.18); never freed, matches that precedent
* (a VM's IDENTITY exec reads directly from this arena). */
CapsuleNameEntry name_entry;
memset(name_entry.name, 0, sizeof(name_entry.name));
{
size_t n = strlen(console_name);
if (n >= CAPSULE_NAME_MAX) n = CAPSULE_NAME_MAX - 1u;
memcpy(name_entry.name, console_name, n);
}
CapsuleDesc desc;
memset(&desc, 0, sizeof(desc));
desc.magic = CAPSULE_MAGIC_PACK(CAPSULE_VERSION_0, CAPSULE_HASH_XXHASH64);
desc.content_hash = xxhash64_capsule(arena, source_len);
desc.capsule_id = desc.content_hash;
desc.offset = 0;
desc.length = source_len;
desc.flags = CAPSULE_FLAG_ACTIVE | CAPSULE_FLAG_PRODUCTION;
desc.owner_vm = 0;
desc.birth_count = 0;
desc.created_ns = 0;
CapsuleDirHeader dir;
memset(&dir, 0, sizeof(dir));
dir.magic = CAPSULE_DIR_MAGIC;
dir.arena_base = (uint64_t)(uintptr_t)arena;
dir.arena_size = source_len;
dir.desc_count = 1;
dir.desc_capacity = 1;
dir.name_count = 1;
dir.dir_hash = 0;
CapsuleRunResult r = capsule_birth_baby(
console_name, &dir, &desc, &name_entry, arena, parent,
1 /* skip_pki_sig -- not build-time content, same rationale as RUNCAP */,
out_vm_id, out_vm_ctx);
/* capsule_birth_baby() never sets the registry entry's own .name --
* found live in RUNCAP (§F.18), same fix needed here. */
if (r == CAPSULE_RUN_OK && out_vm_id) {
capsule_vm_registry_set_name(*out_vm_id, console_name);
}
return r;
}
+105
View File
@@ -0,0 +1,105 @@
/*
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_find.c - Capsule Lookup (M7.1)
*
* Lookup functions for capsule descriptors by ID, name, and role.
* Freestanding - no libc dependency.
*/
#include "starkernel/capsule.h"
/*===========================================================================
* Internal: string comparison (no libc)
*===========================================================================*/
static int capsule_streq(const char *a, const char *b) {
while (*a && *b) {
if (*a != *b) return 0;
a++;
b++;
}
return *a == *b;
}
/*===========================================================================
* capsule_find_by_id
*===========================================================================*/
const CapsuleDesc *capsule_find_by_id(
const CapsuleDirHeader *dir,
const CapsuleDesc *descs,
uint64_t id)
{
if (!dir || !descs) return (void *)0;
uint32_t i;
for (i = 0; i < dir->desc_count; i++) {
if (descs[i].capsule_id == id) {
return &descs[i];
}
}
return (void *)0;
}
/*===========================================================================
* capsule_find_by_name
*===========================================================================*/
const CapsuleDesc *capsule_find_by_name(
const CapsuleDirHeader *dir,
const CapsuleDesc *descs,
const CapsuleNameEntry *names,
const char *name)
{
if (!dir || !descs || !names || !name) return (void *)0;
uint32_t i;
for (i = 0; i < dir->desc_count; i++) {
if (capsule_streq(names[i].name, name)) {
return &descs[i];
}
}
return (void *)0;
}
/*===========================================================================
* capsule_find_mama_init
*===========================================================================*/
const CapsuleDesc *capsule_find_mama_init(
const CapsuleDirHeader *dir,
const CapsuleDesc *descs)
{
if (!dir || !descs) return (void *)0;
uint32_t i;
for (i = 0; i < dir->desc_count; i++) {
if (CAPSULE_IS_MAMA_INIT(descs[i].flags)) {
return &descs[i];
}
}
return (void *)0;
}
/*===========================================================================
* capsule_get_payload
*===========================================================================*/
const uint8_t *capsule_get_payload(
const CapsuleDesc *desc,
const uint8_t *arena_base)
{
if (!desc || !arena_base) return (void *)0;
return arena_base + desc->offset;
}
+600
View File
@@ -0,0 +1,600 @@
/*
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.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* capsule_loader.c - Block Capsule Loader and Executor (M7.1)
*
* Parses a capsule payload for "Block <num>" headers and implements
* the full init sequence:
*
* 1. Write blocks to ramdrive (2048+) — establishes device content
* so FORTH can call LOAD N later if needed.
* 2. Execute each block directly from payload bytes via exec_block_with_retry.
* exec_block_with_retry receives a C pointer into the payload; it does
* NOT use the block device, so no dedicated-RAM copy is needed here.
* 3. Zero dedicated RAM slots so userspace can reuse them.
*
* The ramdrive (2048+) is the fallback device when no physical block
* device is mounted.
*/
#include "starkernel/capsule_loader.h"
#include "starkernel/capsule.h"
#include "starkernel/capsule_run.h"
#include "block_subsystem.h"
#include "vm.h"
#ifdef __STARKERNEL__
#include "starkernel/console.h"
#include "log.h"
#endif
/* Ramdrive starts at this block number; dest = source - CAPSULE_RAM_OFFSET */
#define CAPSULE_RAM_OFFSET 2048u
#define LOADER_BLOCK_SIZE 1024u
/* FORTH block geometry: 16 lines × 64 characters = 1024 bytes */
#define CAPSULE_BLOCK_LINES 16
#define CAPSULE_LINE_COLS 64
/*
* Execution line buffer — capsule text (the .4th source representation)
* may use lines longer than the canonical 64-column block storage limit.
* This buffer covers the execution path; CAPSULE_LINE_COLS covers storage
* and the 16-bit bitmask geometry.
*/
#define CAPSULE_EXEC_LINE_MAX 256
/*
* Forward-definition retry budget.
*
* When a block line triggers an error (typically UNKNOWN WORD for a
* forward reference) the loader skips that line and retries the entire
* block from the top. Each retry sets one bit in a uint16_t bitmask
* (one bit per line 0-15). When the mask reaches 0xFFFF all 16 line
* slots are exhausted and the block is declared unloadable; the loader
* panics with a per-line report.
*
* 16 skipped lines per 1024-byte block matches the FORTH block geometry
* exactly — if every line is unresolvable the capsule is fundamentally
* broken.
*/
#define CAPSULE_MAX_DEFERRED CAPSULE_BLOCK_LINES
#ifdef __STARKERNEL__
/*===========================================================================
* capsule_blk_init - Initialize block subsystem and attach the kernel ramdrive.
*
* Sets up the unified block address space:
* LBN 0..2047: fast RAM (ram_buf)
* LBN 2048..2048+KRD_MAX_BLOCKS: ramdrive (krd_buf, volatile)
*
* krd_buf must be pre-zeroed; KRD_MAX_BLOCKS 1 KiB slots are registered
* as a raw device via blk_subsys_add_raw_device().
* Disk device (if any) is attached separately after this call.
*===========================================================================*/
#define KRD_MAX_BLOCKS 1024u
int capsule_blk_init(void *vm, uint8_t *ram_buf, size_t ram_size, uint8_t *krd_buf)
{
int rc = blk_subsys_init((VM *)vm, ram_buf, ram_size);
if (rc != 0) return rc;
return blk_subsys_add_raw_device(krd_buf, KRD_MAX_BLOCKS);
}
#endif /* __STARKERNEL__ */
/*===========================================================================
* Internal: header detection
*
* Tests whether the bytes at p begin a "Block <num>" header line.
* On match writes the block number to *out_num and the byte position
* immediately after the header's newline to *out_after.
* Returns 1 on match, 0 otherwise.
*===========================================================================*/
static int is_block_header(const uint8_t *p, const uint8_t *end,
uint32_t *out_num, const uint8_t **out_after)
{
if ((size_t)(end - p) < 7u) return 0;
if (p[0] != 'B') return 0;
if (p[1] != 'l') return 0;
if (p[2] != 'o') return 0;
if (p[3] != 'c') return 0;
if (p[4] != 'k') return 0;
if (p[5] != ' ') return 0;
const uint8_t *q = p + 6;
if (q >= end || *q < '0' || *q > '9') return 0;
uint32_t num = 0;
while (q < end && *q >= '0' && *q <= '9') {
num = num * 10u + (uint32_t)(*q - '0');
q++;
}
while (q < end && *q != '\n') q++;
if (q < end) q++;
*out_num = num;
*out_after = q;
return 1;
}
/*===========================================================================
* Internal: write one block to the ramdrive (device region, 2048+)
*===========================================================================*/
static void write_ramdrive_block(uint32_t block_num,
const uint8_t *content,
uint32_t content_len)
{
uint8_t *buf = blk_get_buffer(block_num, 1);
if (!buf) return;
uint32_t i;
for (i = 0; i < LOADER_BLOCK_SIZE; i++) buf[i] = 0;
uint32_t copy_len = (content_len > LOADER_BLOCK_SIZE)
? LOADER_BLOCK_SIZE : content_len;
for (i = 0; i < copy_len; i++) buf[i] = content[i];
blk_update(block_num);
}
/*===========================================================================
* Internal: uint32 to decimal string
* Returns number of characters written (not including null terminator).
*===========================================================================*/
static size_t u32_to_dec(uint32_t val, char *buf, size_t size)
{
char tmp[11];
size_t len = 0;
if (size < 2u) return 0;
if (val == 0u) {
tmp[len++] = '0';
} else {
while (val > 0u && len < 10u) {
tmp[len++] = '0' + (char)(val % 10u);
val /= 10u;
}
}
size_t out = 0;
while (len > 0u && out < size - 1u) buf[out++] = tmp[--len];
buf[out] = '\0';
return out;
}
/*===========================================================================
* capsule_load_blocks
*===========================================================================*/
int capsule_load_blocks(const uint8_t *payload, uint64_t length,
uint32_t *out_entry_block)
{
if (!payload || length == 0) return -1;
if (out_entry_block) *out_entry_block = 0;
const uint8_t *p = payload;
const uint8_t *end = payload + length;
int blocks_written = 0;
int in_block = 0;
int entry_set = 0;
uint32_t current_block_num = 0;
const uint8_t *block_start = (void *)0;
while (p < end) {
uint32_t block_num;
const uint8_t *content_start;
if (is_block_header(p, end, &block_num, &content_start)) {
if (in_block) {
uint32_t clen = (uint32_t)(p - block_start);
write_ramdrive_block(current_block_num, block_start, clen);
blocks_written++;
}
current_block_num = block_num;
block_start = content_start;
in_block = 1;
if (!entry_set && out_entry_block) {
*out_entry_block = block_num;
entry_set = 1;
}
p = content_start;
} else {
while (p < end && *p != '\n') p++;
if (p < end) p++;
}
}
if (in_block) {
uint32_t clen = (uint32_t)(end - block_start);
write_ramdrive_block(current_block_num, block_start, clen);
blocks_written++;
}
return blocks_written;
}
/*===========================================================================
* exec_block_with_retry — execute one block with forward-reference tolerance
*
* Executes the block line by line. Lines 0-15 (the canonical FORTH block
* geometry) can be deferred via a uint16_t bitmask; lines >= 16 execute
* unconditionally — an error there is non-deferrable and fails the block.
*
* On any error in lines 0-15 the offending line is recorded in the bitmask
* and the entire block is retried from the top. Deferred lines are skipped
* in O(1) on every subsequent pass.
*
* Terminates when:
* a) block executes without error → returns 0 (deferred lines are
* forward references to be resolved later; logged but not fatal)
* b) deferred_mask == 0xFFFF (all 16 bitmask slots exhausted) → returns -1
* c) an error occurs on line >= 16 (beyond defer window) → returns -1
*
* VM compile/interpret mode is reset to MODE_INTERPRET before each retry
* so a half-compiled colon definition cannot corrupt the next attempt.
*===========================================================================*/
static int popcount16(uint16_t v)
{
int c = 0;
while (v) { c += (int)(v & 1u); v >>= 1; }
return c;
}
static int exec_block_with_retry(VM *vm, const uint8_t *block_start,
size_t block_len, uint32_t block_num)
{
uint16_t deferred_mask = 0u;
char deferred_text[CAPSULE_BLOCK_LINES][CAPSULE_LINE_COLS + 1];
int j;
for (j = 0; j < CAPSULE_BLOCK_LINES; j++) deferred_text[j][0] = '\0';
for (;;) {
/* Reset interpret/compile state before each attempt */
vm->error = 0;
vm->mode = MODE_INTERPRET;
vm->state_var = 0;
vm_store_cell(vm, vm->state_addr, 0);
int error_this_pass = 0;
int error_line = 0; /* set before use; init silences compiler */
const uint8_t *lp = block_start;
const uint8_t *lend = block_start + block_len;
int line_index = 0;
while (lp < lend) {
const uint8_t *nl = lp;
while (nl < lend && *nl != '\n') nl++;
/* Only lines 0-15 can be deferred via the bitmask */
int in_defer_range = (line_index < CAPSULE_BLOCK_LINES);
/* O(1) skip check via bitmask — skipped only if in range AND bit set */
if (!in_defer_range || !(deferred_mask & (1u << (unsigned)line_index))) {
char line[CAPSULE_EXEC_LINE_MAX];
size_t line_len = (size_t)(nl - lp);
size_t i;
/* Clamp only for the execution buffer; block geometry enforced
at storage time, not here */
if (line_len >= CAPSULE_EXEC_LINE_MAX)
line_len = CAPSULE_EXEC_LINE_MAX - 1u;
for (i = 0; i < line_len; i++) line[i] = (char)lp[i];
line[line_len] = '\0';
if (line_len > 0) {
vm_interpret(vm, line);
/* ABORT: stop this block's remaining lines cleanly.
* Not a failure (system_word_abort() clears vm->error),
* so return 0 rather than -1 -- capsule_exec_payload
* treats any non-zero return as fatal and would stop
* loading the rest of the capsule payload, which would
* silently break word definitions in later blocks that
* have nothing to do with why this one aborted. */
if (vm->abort_requested) {
vm->abort_requested = 0;
vm->error = 0;
vm->mode = MODE_INTERPRET;
vm->state_var = 0;
vm_store_cell(vm, vm->state_addr, 0);
return 0;
}
if (vm->error) {
vm->error = 0;
vm->mode = MODE_INTERPRET;
vm->state_var = 0;
vm_store_cell(vm, vm->state_addr, 0);
if (!in_defer_range) {
/* Beyond bitmask window — cannot defer, block fails */
#ifdef __STARKERNEL__
{
char nbuf[12], lbuf[4];
u32_to_dec(block_num, nbuf, sizeof(nbuf));
u32_to_dec((uint32_t)line_index, lbuf, sizeof(lbuf));
console_puts("[CAPSULE][FAIL] block ");
console_puts(nbuf);
console_puts(" line ");
console_puts(lbuf);
console_puts(": error beyond defer window\r\n");
}
#endif
return -1;
}
error_this_pass = 1;
error_line = line_index;
/* Save truncated text for diagnostics */
if (deferred_text[line_index][0] == '\0') {
size_t tlen = line_len < CAPSULE_LINE_COLS
? line_len : CAPSULE_LINE_COLS;
for (i = 0; i < tlen; i++)
deferred_text[line_index][i] = line[i];
deferred_text[line_index][tlen] = '\0';
}
break;
}
}
}
lp = (nl < lend) ? nl + 1 : lend;
line_index++;
}
if (!error_this_pass) {
/* Block passed — log any deferred lines as unresolved forward refs */
#ifdef __STARKERNEL__
if (deferred_mask != 0u) {
char nbuf[12], dbuf[4];
u32_to_dec(block_num, nbuf, sizeof(nbuf));
u32_to_dec((uint32_t)popcount16(deferred_mask), dbuf, sizeof(dbuf));
console_puts("[CAPSULE][DEFER] block ");
console_puts(nbuf);
console_puts(": ");
console_puts(dbuf);
console_puts(" forward ref(s) unresolved at load time:\r\n");
for (j = 0; j < CAPSULE_BLOCK_LINES; j++) {
if (deferred_mask & (1u << (unsigned)j)) {
char lbuf[4];
u32_to_dec((uint32_t)j, lbuf, sizeof(lbuf));
console_puts(" line ");
console_puts(lbuf);
console_puts(": ");
console_puts(deferred_text[j]);
console_puts("\r\n");
}
}
}
#endif
/* Always leave interpreter in interpret mode after a block completes.
* A deferred ';' in a nested capsule (e.g. doe.4th block 2107) can
* strand the VM in compile mode; resetting here prevents the caller's
* next ':' from seeing a spurious "nested colon" error. */
vm->error = 0;
vm->mode = MODE_INTERPRET;
vm->state_var = 0;
vm_store_cell(vm, vm->state_addr, 0);
return 0;
}
/* Budget exhaustion: all 16 line slots are deferred */
if (deferred_mask == 0xFFFFu) {
#ifdef __STARKERNEL__
{
char nbuf[12];
u32_to_dec(block_num, nbuf, sizeof(nbuf));
console_puts("[CAPSULE][PANIC] block ");
console_puts(nbuf);
console_puts(": exceeded 16 deferred lines — capsule cannot load\r\n");
for (j = 0; j < CAPSULE_BLOCK_LINES; j++) {
if (deferred_text[j][0] != '\0') {
char lbuf[4];
u32_to_dec((uint32_t)j, lbuf, sizeof(lbuf));
console_puts(" line ");
console_puts(lbuf);
console_puts(": ");
console_puts(deferred_text[j]);
console_puts("\r\n");
}
}
}
#endif
return -1;
}
/* Defer the errored line — set its bit */
deferred_mask |= (1u << (unsigned)error_line);
#ifdef __STARKERNEL__
{
char nbuf[12], lbuf[4], dbuf[4];
u32_to_dec(block_num, nbuf, sizeof(nbuf));
u32_to_dec((uint32_t)error_line, lbuf, sizeof(lbuf));
u32_to_dec((uint32_t)popcount16(deferred_mask), dbuf, sizeof(dbuf));
console_puts("[CAPSULE][DEFER] block ");
console_puts(nbuf);
console_puts(" (");
console_puts(dbuf);
console_puts("/16) line ");
console_puts(lbuf);
console_puts(": ");
console_puts(deferred_text[error_line]);
console_puts("\r\n");
}
#endif
/* Retry the block with this line now in the skip bitmask */
}
}
/*===========================================================================
* capsule_exec_payload — unified block parse + populate + execute
*
* For each "Block <num>" section in the payload:
* 1. Write content to block device; warn and truncate if > 1KB
* 2. Execute content line-by-line via exec_block_with_retry
*
* Block numbers are arbitrary; order is irrelevant.
* No LOAD is injected — FORTH code calls LOAD itself if needed.
*===========================================================================*/
int capsule_exec_payload(void *vm_opaque, const uint8_t *payload, uint64_t length)
{
VM *vm = (VM *)vm_opaque;
if (!vm || !payload || length == 0) return -1;
const uint8_t *p = payload;
const uint8_t *end = payload + length;
int in_block = 0;
uint32_t current_block_num = 0;
const uint8_t *block_start = (void *)0;
for (;;) {
uint32_t next_num = 0;
const uint8_t *content_start = (void *)0;
int at_end = (p >= end);
int is_hdr = !at_end &&
is_block_header(p, end, &next_num, &content_start);
if (in_block && (is_hdr || at_end)) {
/* Flush completed block */
const uint8_t *block_end = at_end ? end : p;
size_t block_len = (size_t)(block_end - block_start);
if (block_len > LOADER_BLOCK_SIZE) {
#ifdef __STARKERNEL__
char nbuf[12];
u32_to_dec(current_block_num, nbuf, sizeof(nbuf));
console_puts("WARN: block ");
console_puts(nbuf);
console_puts(" exceeds 1KB, truncating\n");
#endif
block_len = LOADER_BLOCK_SIZE;
}
/* 1. Populate ramdrive (needed if FORTH later calls LOAD N) */
write_ramdrive_block(current_block_num, block_start, (uint32_t)block_len);
/* 2. Execute with forward-reference retry (up to 16 deferred lines) */
log_message(LOG_INFO, "exec_block: block %u len %zu", current_block_num, block_len);
if (exec_block_with_retry(vm, block_start, block_len,
current_block_num) != 0)
return -1;
}
if (at_end) break;
if (is_hdr) {
current_block_num = next_num;
block_start = content_start;
in_block = 1;
p = content_start;
} else {
/* Pre-block or unrecognised line — skip */
while (p < end && *p != '\n') p++;
if (p < end) p++;
}
}
return 0;
}
/*===========================================================================
* capsule_clear_blocks — zero dedicated RAM slots (N-2048) for userspace
*===========================================================================*/
void capsule_clear_blocks(const uint8_t *payload, uint64_t length)
{
if (!payload || length == 0) return;
const uint8_t *p = payload;
const uint8_t *end = payload + length;
while (p < end) {
uint32_t block_num;
const uint8_t *content_start;
if (is_block_header(p, end, &block_num, &content_start)) {
uint8_t *buf = blk_get_buffer(block_num, 1);
if (buf) {
uint32_t i;
for (i = 0; i < LOADER_BLOCK_SIZE; i++) buf[i] = 0;
blk_update(block_num);
}
p = content_start;
} else {
while (p < end && *p != '\n') p++;
if (p < end) p++;
}
}
}
/*===========================================================================
* capsule_exec_init — full load / execute / clear sequence
*===========================================================================*/
CapsuleRunResult capsule_exec_init(
void *vm_opaque,
const char *capsule_name,
const CapsuleDirHeader *dir,
const CapsuleDesc *descs,
const CapsuleNameEntry *names,
const uint8_t *arena)
{
if (!vm_opaque || !capsule_name || !dir || !descs || !names || !arena)
return CAPSULE_RUN_ERR_INVALID;
VM *vm = (VM *)vm_opaque;
/* Locate and validate capsule */
const CapsuleDesc *cap = capsule_find_by_name(dir, descs, names, capsule_name);
if (!cap) return CAPSULE_RUN_ERR_INVALID;
CapsuleValidateResult vr = capsule_validate(cap, arena, dir->arena_size, 1);
if (vr != CAPSULE_VALID) return CAPSULE_RUN_ERR_INVALID;
const uint8_t *payload = capsule_get_payload(cap, arena);
if (!payload) return CAPSULE_RUN_ERR_INVALID;
/* Populate block device + execute. Block content is left in place
* afterward -- capsule_exec_payload() already commits it to real
* ramdrive storage (write_ramdrive_block()) specifically so FORTH
* code can address it by number; a Standard BLOCK/LOAD on that same
* number must be able to read back what EXEC just ran, the same way
* any other block-storage write persists until something explicitly
* overwrites or blanks it. Previously this called capsule_clear_blocks()
* here, zeroing the slots immediately after execution -- that made
* capsule content invisible to LOAD the instant EXEC returned, which
* is not how FORTH-79 BLOCK/LOAD is supposed to behave, and is also
* the reason a FORTH-79/83-locked identity (no EXEC in its own
* dictionary, see capsules/acl-std79.4th) had no standard-compliant
* way to ever load capsule-authored source: LOAD was on the allowlist
* but nothing was ever left for it to find. Found and fixed 2026-09-10. */
int rc = capsule_exec_payload(vm, payload, cap->length);
return (rc == 0) ? CAPSULE_RUN_OK : CAPSULE_RUN_ERR_EXEC_FAIL;
}
+387
View File
@@ -0,0 +1,387 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
Licensed under the StarForth License, Version 1.0
*/
#ifndef __STARKERNEL__
#error "capsule_mint.c is kernel-only"
#endif
#include "starkernel/capsule_mint.h"
#include "starkernel/homeblocks_sig.h"
#include "starkernel/user_identity_seed.h"
#include "starkernel/x509_ed25519.h"
#include "starkernel/ed25519.h"
#include "starkernel/rng.h"
#include "block_subsystem.h" /* compute_crc64() */
#include "blkio.h"
#include "log.h"
#include <string.h>
#include <stddef.h>
/* blkio_ram.c's own backend entry points -- no header exists for these
* (blkio_factory.c, the vendored hosted-only caller, declares them the
* same way: plain externs, not a dedicated blkio_ram.h). blkio_factory.c
* itself stays out of the kernel build (it unconditionally references
* blkio_file.c's real fopen()/fread() file I/O, which has no freestanding
* kernel equivalent); blkio_ram.c alone -- pure memcpy over a caller
* buffer -- was pulled into Makefile.starkernel's kernel build for this
* function specifically (FABRIC-3.md §XXXII.2, 2026-09-16). */
extern size_t blkio_ram_state_size(void);
extern const blkio_vtable_t *blkio_ram_vtable(void);
extern int blkio_ram_init_state(void *state_mem, size_t state_len,
uint8_t *base, uint32_t total_blocks,
uint32_t fbs, uint8_t read_only,
void **out_opaque);
/* Fixed layout, devblock offsets from the drive's own start -- see
* capsule_mint.h's own doc comment. */
#define MINT_CERT_DEVBLOCK 2u
#define MINT_CERT_DEVBLOCKS 1u
#define MINT_IDENTITY_SRC_OFFSET 3u
#define MINT_IDENTITY_SRC_DEVBLOCKS 2u /* 1 seed record + 1 personality source */
#define MINT_METADATA_DEVBLOCKS 5u /* devblocks 0..4 reserved */
/* Real, working, minimal default personality -- FABRIC-2.md §F.6/§F.8's
* "default personality content" question stays open (mint-time-fixed vs.
* user-editable), but the identity this mints must actually do something
* when RUNCAP births it, not just exist. Needs the "Block NNNN" header
* capsule_exec_payload() requires (§F.18) -- discovered live, not
* optional. 4999 is an unused block number (see capsules/ block-map).
*
* FABRIC-3.6.md Phase 4 (Stage E), 2026-09-22: used to load
* common:messaging.4th + MSG-CD-INIT here too, for the same reason
* capsule_console.c's own CONSOLE_IDENTITY_SRC did -- the old MSG-TICK
* idle-pump gate (repl.c's Phase C loop) spammed "UNKNOWN WORD:
* 'MSG-TICK'" on any VM missing the word, found live minting a real
* second identity (FABRIC-2.md §F.26 follow-up). That entire pump
* mechanism is gone as of this same strip (sk_repl_idle() now calls
* sk_hermes_drain_checkpoint() directly, no FORTH word of any kind
* required on the target VM), so the failure mode this once worked
* around no longer exists either -- this can come out clean, not just
* because common:messaging.4th itself no longer exists to load. */
static const char MINT_DEFAULT_PERSONALITY[] =
"Block 4999\n"
": WELCOME ( -- ) .\" Minted identity -- default personality\" CR ;\n"
"WELCOME\n";
/* Restricted personality: FORTH-79/83 standard words only, everything else
* denied. The actual allowlist and the walk-and-deny logic live entirely in
* capsules/acl-std79.4th (ACL policy belongs in FORTH, never in C, per
* .claude/CLAUDE.md's Word-Level ACL System rules) -- this stub's only job
* is to EXEC that capsule and then call the one word it defines,
* ACL-LOCKDOWN-STD79, as this VM's own last bootstrap step. Block 4998:
* unused in the static capsule block-map (confirmed against
* capsules/BLOCK_MAP.md, 2026-09-07) -- 4999 is MINT_DEFAULT_PERSONALITY's
* own block above, same convention. EXEC itself is deliberately not on the
* std79 allowlist, so it's still usable here (nothing has been locked down
* yet at this point in the script) but becomes permanently denied the
* moment ACL-LOCKDOWN-STD79 finishes -- this is the one and only legitimate
* use of EXEC this VM will ever get. */
static const char MINT_RESTRICTED_PERSONALITY[] =
"Block 4998\n"
"S\" acl-std79.4th\" EXEC\n"
"ACL-LOCKDOWN-STD79\n"
": WELCOME ( -- ) .\" Minted identity -- FORTH-79/83 standard words only\" CR ;\n"
"WELCOME\n";
/* Write exactly one devblock (4096 bytes) at devblock offset `devblock`,
* as 4 consecutive 1KiB forth-block writes -- same convention
* homeblocks_sig_check()'s own read loop already uses. */
static int write_devblock(struct blkio_dev *dev, uint32_t devblock,
const uint8_t *buf4096) {
uint32_t base = devblock * 4u;
for (uint32_t i = 0; i < 4u; i++) {
if (blkio_write((blkio_dev_t *)dev, base + i,
buf4096 + (size_t)i * BLKIO_FORTH_BLOCK_SIZE) != BLKIO_OK) {
return -1;
}
}
return 0;
}
/* Read exactly one devblock (4096 bytes) at devblock offset `devblock`,
* mirroring write_devblock() above -- used only by the post-write
* verification pass (2026-09-06), to read back what was just written
* rather than trusting blkio_write()'s BLK_OK return alone. */
static int read_devblock(struct blkio_dev *dev, uint32_t devblock,
uint8_t *buf4096) {
uint32_t base = devblock * 4u;
for (uint32_t i = 0; i < 4u; i++) {
if (blkio_read((blkio_dev_t *)dev, base + i,
buf4096 + (size_t)i * BLKIO_FORTH_BLOCK_SIZE) != BLKIO_OK) {
return -1;
}
}
return 0;
}
/* Post-write verification (2026-09-06, direct instruction after a live
* xHCI enumeration failure raised the question of whether MINT itself
* could silently succeed without the data actually being readable back):
* re-reads the identity record and re-runs homeblocks_sig_check() --
* the exact same check a real attach later performs -- instead of trusting
* every write_devblock() call's own BLK_OK return as proof the drive will
* actually read back correctly. blkio_write() succeeding only means the
* BOT command chain completed; it says nothing about whether those bytes
* survive to be read back under real hardware/emulation conditions.
* Logged entirely through log_message() -- LOG_ERROR with the specific
* mismatch on failure, LOG_INFO confirming success -- not console_println,
* so this doesn't add unconditional console/serial noise to every mint;
* the MINT word's own caller (mama_forth_words.c) still reports the final
* pass/fail result to the console either way via its existing console_
* println() switch. Returns 0 if everything reads back correctly, -1
* otherwise. */
static int verify_mint(struct blkio_dev *dev, uint32_t identity_src_offset,
const user_identity_seed_t *written_idrec) {
homeblocks_sig_t verify_sig;
homeblocks_sig_result_t sig_rc =
homeblocks_sig_check(dev, HOMEBLOCKS_SIG_START_FBLOCK, &verify_sig);
if (sig_rc != HOMEBLOCKS_SIG_OK) {
log_message(LOG_ERROR,
"MINT verify: homeblocks_sig_check() did not read back OK (rc=%d)",
(int) sig_rc);
return -1;
}
uint8_t readback_block[4096];
if (read_devblock(dev, identity_src_offset, readback_block) != 0) {
log_message(LOG_ERROR, "MINT verify: identity record read-back failed");
return -1;
}
const user_identity_seed_t *readback_idrec =
(const user_identity_seed_t *) readback_block;
if (readback_idrec->magic != written_idrec->magic ||
readback_idrec->version != written_idrec->version) {
log_message(LOG_ERROR,
"MINT verify: identity record magic/version mismatch on read-back");
return -1;
}
uint64_t want_crc = compute_crc64((const uint8_t *) readback_idrec,
offsetof(user_identity_seed_t, crc));
if (want_crc != readback_idrec->crc) {
log_message(LOG_ERROR, "MINT verify: identity record CRC mismatch on read-back");
return -1;
}
if (memcmp(readback_idrec, written_idrec, sizeof(*written_idrec)) != 0) {
log_message(LOG_ERROR,
"MINT verify: identity record content differs from what was written");
return -1;
}
log_message(LOG_INFO, "MINT verify: identity reads back correctly");
return 0;
}
MintResult capsule_mint_identity(struct blkio_dev *dev, VM *issuer_vm,
const char *full_name, const char *username,
const char *email, const char *phone,
uint8_t out_pubkey[32], uint8_t out_seed[32],
MintPersonality personality,
int drive_known_blank) {
if (!dev || !full_name || !username) return MINT_ERR_WRITE_FAIL;
/* full_name/username required and must fit; email/phone may be NULL
* (treated as empty/null) but must fit if given. */
if (full_name[0] == '\0' || strlen(full_name) >= USER_IDENTITY_FULL_NAME_MAX)
return MINT_ERR_INVALID_PROFILE;
if (username[0] == '\0' || strlen(username) >= USER_IDENTITY_USERNAME_MAX)
return MINT_ERR_INVALID_PROFILE;
if (email && strlen(email) >= USER_IDENTITY_EMAIL_MAX)
return MINT_ERR_INVALID_PROFILE;
if (phone && strlen(phone) >= USER_IDENTITY_PHONE_MAX)
return MINT_ERR_INVALID_PROFILE;
/* Refuse to overwrite an already-recognized home-blocks drive --
* mirrors WRITE(10)'s own refuse-on-non-blank-media posture (§F.8,
* decided 2026-08-28: "reasonable by analogy" there, now decided).
* Skipped when the caller already just confirmed HOMEBLOCKS_SIG_BLANK
* itself (drive_known_blank) -- otherwise this repeats the exact same
* full BOT read sequence a second time immediately after the first. */
if (!drive_known_blank) {
homeblocks_sig_t existing;
if (homeblocks_sig_check(dev, HOMEBLOCKS_SIG_START_FBLOCK, &existing)
== HOMEBLOCKS_SIG_OK) {
return MINT_ERR_ALREADY_MINTED;
}
}
/* issuer_vm==NULL is genesis mode (§F.21) -- no existing Zuse to
* require a cert from. */
if (issuer_vm && !issuer_vm->zuse_cert_installed) return MINT_ERR_NO_ZUSE_CERT;
if (!rng_ready()) return MINT_ERR_NO_ENTROPY;
/* Fresh identity keypair. */
uint8_t seed[32], pubkey[32];
if (rng_get_bytes(seed, sizeof(seed)) != 0) return MINT_ERR_NO_ENTROPY;
ed25519_keygen(seed, pubkey);
if (out_pubkey) memcpy(out_pubkey, pubkey, 32);
if (out_seed) memcpy(out_seed, seed, 32);
/* Fresh drive_uuid -- an independent random draw, not derived from
* the identity seed (§F.8 decision 3: "which physical drive," not
* "whose identity"). */
uint8_t drive_uuid[16];
if (rng_get_bytes(drive_uuid, sizeof(drive_uuid)) != 0)
return MINT_ERR_NO_ENTROPY;
uint32_t cert_devblock = 0;
uint32_t cert_devblocks = 0;
if (issuer_vm) {
/* Cert: SubjectPublicKeyInfo = new pubkey, serialNumber =
* drive_uuid, signed by Zuse's own on-device seed (§F.8
* decision 4). Genesis mode (issuer_vm==NULL) skips this
* entirely -- Zuse's own identity isn't verified against a
* separate signer, she's recognized directly by pubkey match
* against zuse_genesis_marker_t (§F.21). */
uint8_t cert[512];
size_t cert_len = x509_build_user_cert(cert, sizeof(cert), pubkey,
drive_uuid, issuer_vm->zuse_cert_seed);
if (cert_len == 0 || cert_len > 4096) return MINT_ERR_CERT_BUILD;
uint8_t cert_block[4096];
memset(cert_block, 0, sizeof(cert_block));
memcpy(cert_block, cert, cert_len);
if (write_devblock(dev, MINT_CERT_DEVBLOCK, cert_block) != 0)
return MINT_ERR_WRITE_FAIL;
cert_devblock = MINT_CERT_DEVBLOCK;
cert_devblocks = MINT_CERT_DEVBLOCKS;
}
/* user_identity_seed_t record. */
user_identity_seed_t idrec;
memset(&idrec, 0, sizeof(idrec));
idrec.magic = USER_IDENTITY_SEED_MAGIC;
idrec.version = USER_IDENTITY_SEED_VERSION;
memcpy(idrec.seed, seed, 32);
memcpy(idrec.pubkey, pubkey, 32);
memcpy(idrec.full_name, full_name, strlen(full_name));
memcpy(idrec.username, username, strlen(username));
if (email) memcpy(idrec.email, email, strlen(email));
if (phone) memcpy(idrec.phone, phone, strlen(phone));
idrec.crc = compute_crc64((const uint8_t *)&idrec, offsetof(user_identity_seed_t, crc));
if (write_devblock(dev, MINT_IDENTITY_SRC_OFFSET, (const uint8_t *)&idrec) != 0)
return MINT_ERR_WRITE_FAIL;
/* Personality source, in the devblock right after the seed record --
* RUNCAP reads exactly this layout. Template selected by `personality`;
* see MintPersonality's own doc comment (capsule_mint.h) for why the
* actual restriction logic isn't here. */
const char *personality_src = (personality == MINT_PERSONALITY_STD79_LOCKDOWN)
? MINT_RESTRICTED_PERSONALITY
: MINT_DEFAULT_PERSONALITY;
size_t personality_len = (personality == MINT_PERSONALITY_STD79_LOCKDOWN)
? sizeof(MINT_RESTRICTED_PERSONALITY) - 1
: sizeof(MINT_DEFAULT_PERSONALITY) - 1;
uint8_t personality_block[4096];
memset(personality_block, 0, sizeof(personality_block));
memcpy(personality_block, personality_src, personality_len);
if (write_devblock(dev, MINT_IDENTITY_SRC_OFFSET + 1u, personality_block) != 0)
return MINT_ERR_WRITE_FAIL;
/* homeblocks_sig_t -- written last, so a crash/power-loss partway
* through minting never leaves a drive that reads as a valid,
* complete home-blocks drive with missing content behind it. */
homeblocks_sig_t sig;
memset(&sig, 0, sizeof(sig));
sig.magic = HOMEBLOCKS_SIG_PACK(HOMEBLOCKS_SIG_VERSION_0);
memcpy(sig.drive_uuid, drive_uuid, 16);
sig.minted_time_ns = 0; /* no monotonic-ns source exists
* anywhere in this codebase yet,
* §F.8's own open item */
sig.metadata_devblocks = MINT_METADATA_DEVBLOCKS;
sig.cert_offset = cert_devblock;
sig.cert_devblocks = cert_devblocks;
sig.identity_src_offset = MINT_IDENTITY_SRC_OFFSET;
sig.identity_src_devblocks = MINT_IDENTITY_SRC_DEVBLOCKS;
sig.hdr_crc = homeblocks_sig_compute_crc(&sig);
uint8_t sig_block[4096];
memcpy(sig_block, &sig, sizeof(sig));
if (write_devblock(dev, HOMEBLOCKS_SIG_START_FBLOCK / 4u, sig_block) != 0)
return MINT_ERR_WRITE_FAIL;
(void)blkio_flush((blkio_dev_t *)dev);
if (verify_mint(dev, MINT_IDENTITY_SRC_OFFSET, &idrec) != 0) {
return MINT_ERR_VERIFY_FAILED;
}
return MINT_OK;
}
MintResult capsule_mint_identity_scratch(VM *issuer_vm,
const char *full_name, const char *username,
const char *email, const char *phone,
MintPersonality personality,
uint8_t out_uuid[16], uint8_t out_cert[4096]) {
if (!out_uuid || !out_cert) return MINT_ERR_WRITE_FAIL;
/* MINT_METADATA_DEVBLOCKS 4KiB devblocks, expressed in blkio's own
* 1KiB unit -- same conversion write_devblock()/read_devblock() above
* already use. Static, not stack: 20480 bytes is fine on either, but
* this buffer's whole reason to exist is to be reused call after call
* without re-touching any real device, so giving it a fixed home
* matches g_wirebind_attached_username's own precedent for one-shot,
* non-reentrant kernel-side scratch state. */
static uint8_t scratch_data[MINT_METADATA_DEVBLOCKS * 4u * BLKIO_FORTH_BLOCK_SIZE];
static uint8_t scratch_state[64]; /* comfortably larger than blkio_ram's
* own state struct; blkio_factory_
* open() checks the real size itself
* and fails BLKIO_ENOSPACE if this
* ever stops being enough. */
memset(scratch_data, 0, sizeof(scratch_data));
if (sizeof(scratch_state) < blkio_ram_state_size()) return MINT_ERR_WRITE_FAIL;
void *opaque = NULL;
if (blkio_ram_init_state(scratch_state, sizeof(scratch_state),
scratch_data, MINT_METADATA_DEVBLOCKS * 4u,
0, 0, &opaque) != BLKIO_OK) {
return MINT_ERR_WRITE_FAIL;
}
blkio_dev_t dev;
const blkio_params_t p = {
.forth_block_size = 0,
.total_blocks = MINT_METADATA_DEVBLOCKS * 4u,
.opaque = opaque
};
if (blkio_open(&dev, blkio_ram_vtable(), &p) != BLKIO_OK) {
return MINT_ERR_WRITE_FAIL;
}
/* Fresh, zeroed buffer every call -- never a stale sig to misread as
* "already minted," so the real homeblocks_sig_check() pass inside
* capsule_mint_identity() is redundant here, not skipped for speed. */
MintResult r = capsule_mint_identity((struct blkio_dev *)&dev, issuer_vm,
full_name, username, email, phone,
(uint8_t *)0, (uint8_t *)0,
personality, 1 /* drive_known_blank */);
if (r != MINT_OK) {
blkio_close(&dev);
return r;
}
uint8_t sig_block[4096];
if (read_devblock((struct blkio_dev *)&dev, HOMEBLOCKS_SIG_START_FBLOCK / 4u,
sig_block) != 0) {
blkio_close(&dev);
return MINT_ERR_VERIFY_FAILED;
}
memcpy(out_uuid, sig_block + offsetof(homeblocks_sig_t, drive_uuid), 16);
if (read_devblock((struct blkio_dev *)&dev, MINT_CERT_DEVBLOCK, out_cert) != 0) {
blkio_close(&dev);
return MINT_ERR_VERIFY_FAILED;
}
blkio_close(&dev);
return MINT_OK;
}
+288
View File
@@ -0,0 +1,288 @@
/*
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.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* capsule_run.c - DoE Run Logging and Parity (M7.1)
*
* Ring buffer for capsule execution logging and parity record emission.
* Freestanding - no libc dependency except for console output hooks.
*/
#include "starkernel/capsule_run.h"
#include "starkernel/capsule.h"
#ifdef __STARKERNEL__
#include "starkernel/console.h" /* parity output goes straight to the console */
#endif
/* Parity output sink.
*
* In the monolithic kernel/PE build, taking the address of a cross-translation-
* unit function (e.g. &console_puts) is resolved through an absolute
* R_X86_64_32S relocation that the PE base-relocator cannot fix up, so a stored
* console function pointer holds a garbage address and faults #GP when called.
* Direct calls to console_puts/console_putc are PC-relative and always valid, so
* the kernel parity path calls the console directly rather than via a pointer.
* The hosted build keeps the redirectable function-pointer hooks. */
#ifdef __STARKERNEL__
#define PARITY_HAVE_SINK 1
#define PARITY_EMIT(s) console_puts(s)
#else
#define PARITY_HAVE_SINK (parity_puts != 0)
#define PARITY_EMIT(s) do { if (parity_puts) parity_puts(s); } while (0)
#endif
/*===========================================================================
* Run Log Ring Buffer
*===========================================================================*/
static CapsuleRunRecord run_log[CAPSULE_MAX_RUN_RECORDS];
static uint32_t run_log_head = 0; /* Next write position */
static uint32_t run_log_count = 0; /* Total records logged */
static uint64_t run_id_counter = 0; /* Sequential run ID */
void capsule_run_log_init(void) {
run_log_head = 0;
run_log_count = 0;
run_id_counter = 0;
/* Zero the ring buffer */
for (uint32_t i = 0; i < CAPSULE_MAX_RUN_RECORDS; i++) {
run_log[i].run_id = 0;
run_log[i].vm_id = vm_uuid_none(); /* not {0,0} -- that's Hera's reserved id (item 3.8) */
run_log[i].reserved = 0;
run_log[i].capsule_id = 0;
run_log[i].capsule_hash = 0;
run_log[i].pre_dict_hash = 0;
run_log[i].post_dict_hash = 0;
run_log[i].started_ns = 0;
run_log[i].ended_ns = 0;
run_log[i].result_code = 0;
run_log[i].flags = 0;
}
}
uint64_t capsule_run_log_record(const CapsuleRunRecord *record) {
if (!record) {
return 0;
}
/* Assign run ID */
uint64_t id = ++run_id_counter;
/* Copy record to ring buffer */
CapsuleRunRecord *slot = &run_log[run_log_head];
*slot = *record;
slot->run_id = id;
/* Advance head (circular) */
run_log_head = (run_log_head + 1) % CAPSULE_MAX_RUN_RECORDS;
/* Track total count (saturates at buffer size for wrap detection) */
if (run_log_count < CAPSULE_MAX_RUN_RECORDS) {
run_log_count++;
}
return id;
}
int capsule_run_log_get(uint64_t run_id, CapsuleRunRecord *out) {
if (!out || run_id == 0 || run_id > run_id_counter) {
return -1;
}
/* Search ring buffer for matching run_id */
for (uint32_t i = 0; i < CAPSULE_MAX_RUN_RECORDS; i++) {
if (run_log[i].run_id == run_id) {
*out = run_log[i];
return 0;
}
}
return -1; /* Not found (may have been overwritten) */
}
uint32_t capsule_run_log_count(void) {
return run_log_count;
}
/*===========================================================================
* Parity Logging
*
* These functions emit structured parity records to the console/serial.
* Format is designed for deterministic verification and debugging.
*
* Output goes through a console hook that can be redirected to:
* - Serial port (kernel mode)
* - stdout (hosted mode)
* - Debug port 0x402 (QEMU)
*===========================================================================*/
/* Console output hook - to be set by platform layer */
static void (*parity_putc)(char c) = 0;
static void (*parity_puts)(const char *s) = 0;
void capsule_parity_set_output(void (*putc_fn)(char), void (*puts_fn)(const char *)) {
parity_putc = putc_fn;
parity_puts = puts_fn;
}
/* Helper: output hex value */
static void parity_put_hex64(uint64_t val) {
static const char hex[] = "0123456789abcdef";
char buf[19]; /* "0x" + 16 hex + null */
buf[0] = '0';
buf[1] = 'x';
for (int i = 15; i >= 0; i--) {
buf[2 + (15 - i)] = hex[(val >> (i * 4)) & 0xF];
}
buf[18] = '\0';
PARITY_EMIT(buf);
}
/* item 3.8: VMUuid printed as its two hex64 halves, hyphen-separated --
* reuses parity_put_hex64 rather than a new hex-formatting routine. */
static void parity_put_uuid(VMUuid id) {
parity_put_hex64(id.hi);
PARITY_EMIT("-");
parity_put_hex64(id.lo);
}
static void parity_put_u32(uint32_t val) {
char buf[12];
int i = 11;
buf[i--] = '\0';
if (val == 0) {
buf[i--] = '0';
} else {
while (val > 0 && i >= 0) {
buf[i--] = '0' + (val % 10);
val /= 10;
}
}
PARITY_EMIT(&buf[i + 1]);
}
static void parity_put_u64(uint64_t val) {
char buf[21];
int i = 20;
buf[i--] = '\0';
if (val == 0) {
buf[i--] = '0';
} else {
while (val > 0 && i >= 0) {
buf[i--] = '0' + (val % 10);
val /= 10;
}
}
PARITY_EMIT(&buf[i + 1]);
}
void capsule_parity_log_birth(
VMUuid vm_id,
uint64_t capsule_id,
uint64_t capsule_hash,
uint64_t dict_hash)
{
if (!PARITY_HAVE_SINK) return;
PARITY_EMIT("PARITY:BIRTH vm_id=");
parity_put_uuid(vm_id);
PARITY_EMIT(" capsule_id=");
parity_put_hex64(capsule_id);
PARITY_EMIT(" mode=p capsule_hash=");
parity_put_hex64(capsule_hash);
PARITY_EMIT(" dict_hash=");
parity_put_hex64(dict_hash);
PARITY_EMIT("\n");
}
void capsule_parity_log_birth_failed(
VMUuid vm_id,
uint64_t capsule_id,
CapsuleRunResult error,
uint64_t partial_dict_hash)
{
if (!PARITY_HAVE_SINK) return;
PARITY_EMIT("PARITY:BIRTH_FAILED vm_id=");
parity_put_uuid(vm_id);
PARITY_EMIT(" capsule_id=");
parity_put_hex64(capsule_id);
PARITY_EMIT(" error=");
parity_put_u32((uint32_t)error);
PARITY_EMIT(" partial_dict_hash=");
parity_put_hex64(partial_dict_hash);
PARITY_EMIT("\n");
}
void capsule_parity_log_run(
VMUuid vm_id,
uint64_t run_id,
uint64_t capsule_id,
uint64_t pre_dict_hash,
uint64_t post_dict_hash)
{
if (!PARITY_HAVE_SINK) return;
PARITY_EMIT("PARITY:RUN vm_id=");
parity_put_uuid(vm_id);
PARITY_EMIT(" run_id=");
parity_put_u64(run_id);
PARITY_EMIT(" capsule_id=");
parity_put_hex64(capsule_id);
PARITY_EMIT(" mode=e pre_dict=");
parity_put_hex64(pre_dict_hash);
PARITY_EMIT(" post_dict=");
parity_put_hex64(post_dict_hash);
PARITY_EMIT("\n");
}
/*===========================================================================
* Mama Init Parity
*===========================================================================*/
void capsule_parity_log_mama_init(
uint64_t capsule_id,
uint64_t capsule_hash,
uint64_t dict_hash)
{
if (!PARITY_HAVE_SINK) return;
PARITY_EMIT("PARITY:MAMA_INIT capsule_id=");
parity_put_hex64(capsule_id);
PARITY_EMIT(" mode=m capsule_hash=");
parity_put_hex64(capsule_hash);
PARITY_EMIT(" dict_hash=");
parity_put_hex64(dict_hash);
PARITY_EMIT("\n");
}
void capsule_parity_log_kill(VMUuid vm_id, const char *name)
{
if (!PARITY_HAVE_SINK) return;
PARITY_EMIT("PARITY:KILL vm_id=");
parity_put_uuid(vm_id);
PARITY_EMIT(" name=");
if (name) PARITY_EMIT(name);
PARITY_EMIT("\n");
}
+144
View File
@@ -0,0 +1,144 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
Licensed under the StarForth License, Version 1.0
*/
#ifndef __STARKERNEL__
#error "capsule_runcap.c is kernel-only"
#endif
#include "starkernel/capsule_runcap.h"
#include "starkernel/capsule.h"
#include "starkernel/capsule_birth.h"
#include "starkernel/xxhash64.h"
#include "starkernel/kmalloc.h"
#include "blkio.h"
#include <string.h>
CapsuleRunResult capsule_runcap_birth(
struct blkio_dev *dev,
const homeblocks_sig_t *sig,
const char *vm_name,
VMUuid parent,
VMUuid *out_vm_id,
void **out_vm_ctx)
{
if (!dev || !sig || !vm_name || !out_vm_id) return CAPSULE_RUN_ERR_INVALID;
/* 0 = never minted (homeblocks_sig.h's own field doc). */
if (sig->identity_src_offset == 0) return CAPSULE_RUN_ERR_INVALID;
/* First devblock is the user_identity_seed_t record (MINT, §F.8);
* everything after it is raw FORTH source. Need at least one devblock
* of actual source beyond the seed. */
if (sig->identity_src_devblocks < 2) return CAPSULE_RUN_ERR_INVALID;
uint32_t source_devblocks = sig->identity_src_devblocks - 1u;
uint64_t source_len = (uint64_t)source_devblocks * 4096u;
uint8_t *arena = (uint8_t *)kmalloc((size_t)source_len);
if (!arena) return CAPSULE_RUN_ERR_STILLBORN;
/* homeblocks_sig_t's own offset/devblocks fields are in 4KiB
* devblocks; blkio_read() works in 1KiB forth-blocks (same *4
* conversion homeblocks_sig_check() already uses for its own
* sig_start_fblock). Skip the seed devblock (+1 devblock = +4
* forth-blocks) before reading source content. */
uint32_t base_fblock = (sig->identity_src_offset + 1u) * 4u;
uint32_t source_fblocks = source_devblocks * 4u;
uint32_t i;
for (i = 0; i < source_fblocks; i++) {
if (blkio_read((blkio_dev_t *)dev, base_fblock + i,
arena + (size_t)i * BLKIO_FORTH_BLOCK_SIZE) != BLKIO_OK) {
return CAPSULE_RUN_ERR_INVALID;
}
}
/* FABRIC-3.md SXXV follow-up (2026-09-13): source_len above is the
* FULL reserved region (source_devblocks worth of 4KiB devblocks) --
* MINT (capsule_mint.c) only ever writes a short personality string
* into it and memset()s the rest of that one devblock to 0 first, but
* never touches whatever devblocks beyond the first were reserved for
* identity_src, and this loop reads all of them regardless. Real
* content is a handful of lines; everything past it, for the whole
* remaining reserved region, is zero bytes with no "Block N" header
* of its own to stop capsule_exec_payload()'s own block-flush scan --
* confirmed live as the actual cause of "WARN: block 4998 exceeds
* 1KB, truncating", firing for every std79-locked identity, every
* boot, since before this was found: the parser had no way to tell
* "real content ends here" from "just more of this same block," so it
* walked the whole zero-padded region as one oversized block. FORTH
* source never legitimately contains a raw NUL, so it's a safe,
* ordinary C-string-style terminator here: trim source_len down to
* the first NUL actually found, restoring the block-parser's normal
* behavior instead of teaching it a special case for this one
* caller's own padding convention. */
{
uint64_t real_len = source_len;
uint64_t k;
for (k = 0; k < source_len; k++) {
if (arena[k] == '\0') { real_len = k; break; }
}
source_len = real_len;
}
/* Heap-built single-entry directory -- exact shape §F.6 traced
* against capsule_birth_baby()'s own parameters, not a new mechanism.
* Never freed: matches kernel_main.c's own compile-time-directory
* heap copy at Mama's birth, which is also never freed -- the VM's
* IDENTITY exec reads directly from this arena. */
CapsuleNameEntry name_entry;
memset(name_entry.name, 0, sizeof(name_entry.name));
{
size_t n = strlen(vm_name);
if (n >= CAPSULE_NAME_MAX) n = CAPSULE_NAME_MAX - 1u;
memcpy(name_entry.name, vm_name, n);
}
CapsuleDesc desc;
memset(&desc, 0, sizeof(desc));
desc.magic = CAPSULE_MAGIC_PACK(CAPSULE_VERSION_0, CAPSULE_HASH_XXHASH64);
desc.content_hash = xxhash64_capsule(arena, (size_t)source_len);
desc.capsule_id = desc.content_hash; /* content-addressed invariant */
desc.offset = 0;
desc.length = source_len;
desc.flags = CAPSULE_FLAG_ACTIVE | CAPSULE_FLAG_PRODUCTION;
desc.owner_vm = 0;
desc.birth_count = 0;
desc.created_ns = 0; /* no monotonic-ns source exists anywhere in this
* codebase yet, §F.8's own open item -- matches
* CapsuleDesc.created_ns's existing hardcoded-0
* precedent at mkcapsule generation time. */
CapsuleDirHeader dir;
memset(&dir, 0, sizeof(dir));
dir.magic = CAPSULE_DIR_MAGIC;
dir.arena_base = (uint64_t)(uintptr_t)arena;
dir.arena_size = source_len;
dir.desc_count = 1;
dir.desc_capacity = 1;
dir.name_count = 1;
dir.dir_hash = 0; /* not verified anywhere in the birth path today */
CapsuleRunResult r = capsule_birth_baby(
vm_name, &dir, &desc, &name_entry, arena, parent,
1 /* skip_pki_sig -- trust comes from CERTVERIFY, a separate root */,
out_vm_id, out_vm_ctx);
/* capsule_birth_baby() itself never sets the registry entry's own
* .name -- every existing caller (mama_word_birth, CONNECT-HERMES/
* -ARTEMIS) does this as a separate step after a successful birth,
* via capsule_vm_registry_set_name(). Found live: skipping this left
* a freshly RUNCAP-born VM's registry name empty, which the idle-loop
* pump (FABRIC-2.md Phase C) then read as a zero-length name and
* refused ("VM name too long or empty") every tick. */
if (r == CAPSULE_RUN_OK && out_vm_id) {
capsule_vm_registry_set_name(*out_vm_id, vm_name);
}
return r;
}
+68
View File
@@ -0,0 +1,68 @@
/* capsule_sig.c -- see capsule_sig.h. */
#include "starkernel/capsule_sig.h"
#include "starkernel/x509_ed25519.h"
#include "starkernel/ed25519.h"
#include <string.h>
#define SNAKEOIL_CERT_NAME "pki:snakeoil-intermediate.der"
/* Cached once per boot: the cert doesn't change mid-boot, and every
* capsule birth (Mama, then every child VM) would otherwise re-find and
* re-parse the same cert. -1 = not yet attempted, 0 = failed, 1 = ready. */
static int pubkey_state = -1;
static uint8_t cached_pubkey[32];
static int find_and_cache_pubkey(const CapsuleDesc *descs,
const CapsuleNameEntry *names,
const uint8_t *arena_base,
uint32_t desc_count) {
if (pubkey_state != -1) return pubkey_state;
pubkey_state = 0; /* assume failure unless every step below succeeds */
for (uint32_t i = 0; i < desc_count; i++) {
if (strcmp(names[i].name, SNAKEOIL_CERT_NAME) == 0) {
const CapsuleDesc *cert_desc = &descs[i];
if (x509_extract_ed25519_pubkey(arena_base + cert_desc->offset,
(size_t)cert_desc->length,
cached_pubkey) == 0) {
pubkey_state = 1;
}
break;
}
}
return pubkey_state;
}
CapsuleSigResult capsule_verify_signature(
const CapsuleDesc *descs, const CapsuleNameEntry *names,
const CapsuleSigEntry *sigs, const uint8_t *arena_base,
uint32_t desc_count, int index) {
if (!descs || !names || !sigs || !arena_base || index < 0 ||
(uint32_t)index >= desc_count) {
return CAPSULE_SIG_NO_ROOT_KEY; /* malformed call -- treat like "can't check" */
}
if (!find_and_cache_pubkey(descs, names, arena_base, desc_count)) {
return CAPSULE_SIG_NO_ROOT_KEY;
}
if (!sigs[index].has_sig) {
return CAPSULE_SIG_MISSING;
}
const CapsuleDesc *d = &descs[index];
int ok = ed25519_verify(cached_pubkey, arena_base + d->offset,
(size_t)d->length, sigs[index].sig);
return ok ? CAPSULE_SIG_OK : CAPSULE_SIG_INVALID;
}
const char *capsule_sig_result_str(CapsuleSigResult result) {
switch (result) {
case CAPSULE_SIG_OK: return "ok";
case CAPSULE_SIG_MISSING: return "missing (unsigned)";
case CAPSULE_SIG_INVALID: return "INVALID -- signature does not verify";
case CAPSULE_SIG_NO_ROOT_KEY: return "no root key (cert not found/parseable)";
default: return "unknown";
}
}
+133
View File
@@ -0,0 +1,133 @@
/*
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.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* capsule_validate.c - Capsule Validation (M7.1)
*
* Validates capsule descriptors for structural integrity and content hash.
* Freestanding - no libc dependency.
*/
#include "starkernel/capsule.h"
#include "starkernel/xxhash64.h"
/*===========================================================================
* Validation Result Strings
*===========================================================================*/
static const char *validate_result_strings[] = {
"valid",
"bad magic",
"bad version",
"bad hash algorithm",
"bounds error",
"mode invalid",
"revoked but active",
"hash mismatch",
"null pointer",
};
const char *capsule_validate_result_str(CapsuleValidateResult result) {
if ((size_t)result < sizeof(validate_result_strings) / sizeof(validate_result_strings[0])) {
return validate_result_strings[result];
}
return "unknown error";
}
/*===========================================================================
* Validation
*===========================================================================*/
CapsuleValidateResult capsule_validate(
const CapsuleDesc *desc,
const uint8_t *arena_base,
uint64_t arena_size,
int verify_hash)
{
/* Check for null pointers */
if (!desc) {
return CAPSULE_ERR_NULL_PTR;
}
/* 1. Check magic signature */
uint32_t sig = CAPSULE_MAGIC_GET_SIG(desc->magic);
if (sig != (uint32_t)CAPSULE_DESC_MAGIC) {
return CAPSULE_ERR_BAD_MAGIC;
}
/* 2. Check version */
uint8_t version = CAPSULE_MAGIC_GET_VERSION(desc->magic);
if (version != CAPSULE_VERSION_0) {
return CAPSULE_ERR_BAD_VERSION;
}
/* 3. Check hash algorithm is known */
uint8_t hash_alg = CAPSULE_MAGIC_GET_HASHALG(desc->magic);
if (hash_alg > CAPSULE_HASH_BLAKE3) {
return CAPSULE_ERR_BAD_HASH_ALG;
}
/* 4. Check bounds: offset + length <= arena_size */
if (arena_base && arena_size > 0) {
if (desc->offset > arena_size ||
desc->length > arena_size ||
desc->offset + desc->length > arena_size) {
return CAPSULE_ERR_BOUNDS;
}
}
/* 5. Check mode flags: exactly one of (p) or (e) must be set */
if (!CAPSULE_MODE_VALID(desc->flags)) {
return CAPSULE_ERR_MODE_INVALID;
}
/* 6. Reject if REVOKED and ACTIVE are both set — data inconsistency */
if ((desc->flags & CAPSULE_FLAG_REVOKED) &&
(desc->flags & CAPSULE_FLAG_ACTIVE)) {
return CAPSULE_ERR_REVOKED_ACTIVE;
}
/* 7. Verify content hash if requested */
if (verify_hash && arena_base && desc->length > 0) {
/* Currently only xxHash64 is supported */
if (hash_alg != CAPSULE_HASH_XXHASH64) {
/* Can't verify unsupported hash algorithms */
return CAPSULE_ERR_BAD_HASH_ALG;
}
const uint8_t *payload = arena_base + desc->offset;
uint64_t computed_hash = xxhash64_capsule(payload, (size_t)desc->length);
if (computed_hash != desc->content_hash) {
return CAPSULE_ERR_HASH_MISMATCH;
}
/* Also verify capsule_id == content_hash (content-addressed invariant) */
if (desc->capsule_id != desc->content_hash) {
return CAPSULE_ERR_HASH_MISMATCH;
}
}
return CAPSULE_VALID;
}
/* capsule_find_by_id, capsule_get_payload, capsule_find_mama_init are in capsule_find.c */
+127
View File
@@ -0,0 +1,127 @@
/*
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_vm_hooks.c - VM Hook Implementations for Capsule Birth (M7.1)
*
* Concrete implementations of the three function pointers required by
* capsule_birth_set_hooks():
*
* capsule_exec_hook -- wraps vm_interpret()
* capsule_dict_hash_hook -- xxhash64 walk of the live dictionary
* capsule_vm_alloc_hook -- allocates and initialises a fresh VM
*
* Call capsule_vm_hooks_register() once during VM bootstrap (after
* register_forth79_words) to wire everything up.
*/
#include "starkernel/capsule_birth.h"
#include "starkernel/capsule_loader.h"
#include "starkernel/xxhash64.h"
#include "vm.h"
#include "platform_alloc.h"
#include "word_source/include/mama_forth_words.h"
#ifdef __STARKERNEL__
#include "starkernel/hal/hal.h" /* sk_hal_host_services() */
#endif
/*===========================================================================
* exec hook ( vm_ctx, code, code_len ) -> 0 on success
*===========================================================================*/
static int capsule_exec_hook(void *vm_ctx, const char *code, uint64_t code_len) {
return capsule_exec_payload(vm_ctx, (const uint8_t *)code, code_len);
}
/*===========================================================================
* dict hash hook ( vm_ctx ) -> uint64_t
*
* Walks the live dictionary linked list and hashes each word's name and
* execution heat. Provides a cheap, deterministic snapshot of dictionary
* state for parity logging.
*===========================================================================*/
static uint64_t capsule_dict_hash_hook(void *vm_ctx) {
VM *vm = (VM *)vm_ctx;
if (!vm) return 0;
XXHash64State state;
xxhash64_reset(&state, 0);
DictEntry *entry = vm->latest;
while (entry) {
/* Hash: word name */
xxhash64_update(&state, entry->name, entry->name_len);
/* Hash: execution heat (captures runtime activity level) */
xxhash64_update(&state, &entry->execution_heat,
sizeof(entry->execution_heat));
entry = entry->link;
}
return xxhash64_digest(&state);
}
/*===========================================================================
* vm alloc hook () -> void* (VM*)
*
* Allocates a fresh VM on the heap, initialises it fully (including word
* registration and physics subsystems), and returns it as an opaque pointer.
* Returns NULL on allocation failure.
*===========================================================================*/
static void *capsule_vm_alloc_hook(void) {
VM *vm = (VM *)sf_malloc(sizeof(VM));
if (!vm) return (void *)0;
#ifdef __STARKERNEL__
vm_init_with_host(vm, sk_hal_host_services());
#else
vm_init(vm);
#endif
/* vm_init sets vm->error if memory allocation failed internally */
if (vm->error) {
vm_cleanup(vm);
sf_free(vm);
return (void *)0;
}
/* Enable interpreter — child VMs skip sk_vm_bootstrap_parity, must set manually */
vm_enable_interpreter(vm);
/* Register STOP + EXEC via the dedicated function in mama_forth_words.c.
* Do NOT pass mama_word_stop / mama_word_exec as function-pointer arguments
* here: cross-TU function-pointer loads generate R_X86_64_REX_GOTPCRELX,
* which the PE32+ linker resolves by dereferencing the function address
* (reading machine-code bytes) instead of loading the address itself. */
register_child_vm_words(vm);
return (void *)vm;
}
/*===========================================================================
* Registration
*===========================================================================*/
/**
* capsule_vm_hooks_register - Wire the three hooks into the capsule subsystem.
*
* Must be called after vm_init() completes on Mama's VM, before any capsule
* birth or experiment operations.
*/
void capsule_vm_hooks_register(void) {
capsule_birth_set_hooks(
capsule_exec_hook,
capsule_dict_hash_hook,
capsule_vm_alloc_hook
);
}
+573
View File
@@ -0,0 +1,573 @@
/*
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_vm_physics.c - Dynamic VM Fleet Physics Implementation
*
* See capsule_vm_physics.h and
* docs/working/architecture/VM-PHYSICS-DYNAMIC-FLEET-DESIGN-20260705.md
* for the full design rationale.
*
* Concurrency: no locking. The only interrupt-driven kernel path
* (heartbeat_tick, from the timer ISR) touches only its own isolated
* TimeTrustState and never calls into mama_word_*, vm_interpret, or any
* capsule/dictionary state. HEARTBEAT_THREAD_ENABLED=0 means no separate
* OS thread runs physics logic either. BIRTH/KILL/VM-EXEC/VM-CALL/
* VM-STEP all execute synchronously in Hera's single interpreter
* context with nothing able to preempt them mid-transfer.
*
* L8 coupling (rev q): the only place this file reaches outside its own
* VMPhysics/VMFleetWindow state is l8_regime_modulation_q16(), a
* read-only look at Hera's own ssm_l8_state via sk_get_mama_vm() --
* modulating the fleet's empirically-recovered transfer rate uniformly,
* never deciding which VM receives heat. Still a passive observer: this
* changes how fast the fleet's own rate estimate responds, not who runs
* or who gets touched.
*/
#include "starkernel/capsule_vm_physics.h"
#include "starkernel/capsule_birth.h"
#include "starkernel/kmalloc.h"
#include "starkernel/q48_16.h"
#include "starkernel/console.h"
#include "starforth_config.h" /* STADIUM_CAPACITY_TICK */
#include "starkernel/vm/bootstrap/sk_vm_bootstrap.h" /* sk_get_mama_vm */
#include "vm.h" /* VM, vm->ssm_l8_state */
#include "ssm_jacquard.h" /* ssm_l8_state_t, CDConfigTable */
/* Word-level ROLLING_WINDOW_SIZE defaults to 4096 because word executions
* number in the millions. Fleet touches are real VM-EXEC/VM-CALL/VM-STEP
* dispatches -- dozens to low hundreds over a full boot + acceptance run.
* 64 gives the regression enough samples to stabilize without being
* wastefully oversized for a 3-8 VM fleet. Compile-time constant, same
* tuning-knob convention as ROLLING_WINDOW_SIZE. */
#define VM_FLEET_WINDOW_DEPTH 64
/* |fleet_heat_sum - Q48_ONE| < 5% of Q48_ONE, mirrors fleet-k.4th's
* K-EPSILON (3277). */
#define VM_PHYSICS_EPSILON_Q48 3277ULL
typedef struct {
uint64_t execution_heat_q48;
uint64_t last_active_tick; /* fleet_heartbeat_tick_count value at last touch */
int touched; /* 0 until the first touch; explicit rather than
* overloading last_active_tick == 0, which is a
* legitimate tick value a first touch can land on */
int is_live;
} VMPhysics;
typedef struct vm_physics_node {
VMUuid vm_id;
VMPhysics physics;
struct vm_physics_node *next;
} vm_physics_node_t;
/* kmalloc-backed linked list, same pattern as capsule_birth.c's
* vm_registry_head/vm_registry_count -- unbounded, not a fixed array. */
static vm_physics_node_t *vm_physics_head = (void *)0;
/* One recorded touch's inputs to the transfer law (amount = elapsed_ticks *
* slope >> 16, clamped to available heat) -- not a derived heat value.
* Rev o (VM-FLEET-ATTRACTOR-DESIGN-20260705.md) replaced the earlier
* heat-snapshot design (rev m) with this: since the transfer law is known
* exactly, the true rate can be recovered directly from any touch that
* wasn't clamped (rate = amount / elapsed_ticks), rather than curve-fitting
* a reconstructed trajectory against a synthetic time axis.
*
* Restated on the virtual tick (FABRIC-0.md item 2.1, 2026-08-04): this used
* to be elapsed_us, wall-clock microseconds -- non-reproducible under TCG
* per GAP-A1 (FABRIC-0.md §25.7.1). Now counts fleet_heartbeat_tick_count
* ticks, which is execution-paced (incremented once per vm_tick() call,
* confirmed at vm_runtime.c:143). */
typedef struct {
uint64_t elapsed_ticks; /* since this vm_id's previous touch; 0 if there
* was no previous touch to compare against */
uint64_t amount; /* elapsed_ticks * slope >> 16 at touch time,
* i.e. the rate-implied transfer before any
* clamp; 0 if not computed (first touch, zero
* slope, or non-monotonic tick) */
int clamped; /* 1 if the heat actually moved was capped below
* `amount` by what the rest of the fleet held --
* carries no rate information, excluded from
* the estimator rather than treated as a zero */
} VMFleetTouchSample;
typedef struct {
VMFleetTouchSample touch_samples[VM_FLEET_WINDOW_DEPTH];
uint32_t head;
uint32_t count; /* saturates at VM_FLEET_WINDOW_DEPTH */
int is_warm; /* count >= VM_FLEET_WINDOW_DEPTH */
} VMFleetWindow;
/* One instance, fleet-wide -- not per-VM state. */
static VMFleetWindow fleet_window = { { { 0, 0, 0 } }, 0, 0, 0 };
/* Below this many informative (unclamped) samples in a full window, skip
* the fit and keep the current slope rather than trust a handful of
* points -- same "skip, don't substitute a degenerate value" philosophy
* as the is_warm gate. Arbitrary but conservative relative to the
* 64-deep window: needs an eighth of the window to be informative. */
#define VM_PHYSICS_MIN_INFORMATIVE_SAMPLES 8
/* Shared adaptive slope, fit from aggregate fleet statistics (one slope
* for the whole fleet), exactly mirroring how the word engine infers one
* decay_slope_q48 per VM instance rather than one per word.
*
* Seeded at 65536/3 (Q48.16 for 1/3), matching the word engine's actual
* bootstrap value (src/vm_bootstrap.c / src/starkernel/vm/vm_bootstrap.c:
* "vm->decay_slope_q48 = (1ULL << 16) / 3"). The old comment on that field
* in include/vm.h ("starts at 2:1 = 131072") is stale relative to the real
* code -- 1/3 is what's actually seeded there, so 1/3 is what's mirrored
* here. This was previously seeded at literal 0, which cannot bootstrap:
* vm_physics_touch's transfer amount is elapsed_ticks * slope >> 16, so a
* zero slope moves zero heat on every touch forever, which starves
* vm_physics_tick's regression of the very heat-trajectory signal it
* needs to infer a better slope -- a closed loop with no way out. Found
* via doe_log.c's per-VM heat CSV columns showing a dead-flat trajectory
* across a full boot (VM-FLEET-ATTRACTOR-DESIGN-20260705.md rev f).
*
* UNVALIDATED after the ns-to-tick restatement (FABRIC-0.md item 2.1,
* 2026-08-04): this value was calibrated against elapsed wall-clock
* microseconds. Elapsed fleet-heartbeat *ticks* between touches is a
* different quantity at a different scale, and the seed has not been
* re-fit against it. Left as-is rather than guessed at -- the acceptance
* boot test below only checks determinism, not transfer magnitude; a real
* re-tune is DoE work (item 5.1), not something to invent here. */
static uint64_t fleet_transfer_slope_q48 = 65536ULL / 3;
static uint64_t slope_fit_quality_q48 = 0;
/* Fleet-wide heartbeat tick, distinct from any VM's own per-VM
* HeartbeatState.tick_count -- see the rationale in the header. Advances
* from whichever VM's own heartbeat cycle happens to fire, so the
* readiness signal reflects the fleet's aggregate activity rather than
* any one VM's (in practice, Hera's) personal word-execution rate.
* Declared here (rather than just above vm_physics_heartbeat_tick, where
* it used to live) so vm_physics_touch() can read the current tick
* without a forward declaration -- it is now this counter's second
* reader, alongside vm_physics_heartbeat_tick()'s own gating use. */
static uint64_t fleet_heartbeat_tick_count = 0;
static uint64_t fleet_last_capacity_tick = 0; /* renamed 2026-08-15 from
fleet_last_inference_tick -- gated on
STADIUM_CAPACITY_TICK now, not HEARTBEAT_
INFERENCE_FREQUENCY, see vm_physics_heartbeat_tick() */
static vm_physics_node_t *vm_physics_find(VMUuid vm_id)
{
vm_physics_node_t *n = vm_physics_head;
while (n) {
if (vm_uuid_equal(n->vm_id, vm_id)) return n;
n = n->next;
}
return (void *)0;
}
/**
* vm_physics_transfer - The one conservative primitive everything else
* is a special case of. Subtracts amount from from (clamped at 0) and
* adds the same amount to to. Nothing is created or destroyed:
* sum(execution_heat for all LIVE VMs) is invariant across any call.
*/
static void vm_physics_transfer(VMPhysics *from, VMPhysics *to, uint64_t amount_q48)
{
uint64_t moved = (amount_q48 > from->execution_heat_q48)
? from->execution_heat_q48
: amount_q48;
from->execution_heat_q48 -= moved;
to->execution_heat_q48 += moved;
}
void vm_physics_init(VMUuid vm_id)
{
vm_physics_node_t *node = vm_physics_find(vm_id);
/* Hera (vm_uuid_hera()) is the fleet's single structural root -- the one
* entry capsule_vm_kill refuses to ever kill, and the same fixed
* point vm_physics_find_root_id below walks every parent chain up
* to. She is where the fleet's whole Q48_ONE initially resides;
* every other VM joins an already-nonzero fleet and starts at 0,
* per the "cold mass added to a closed system" reasoning -- which
* requires a nonzero sum to already exist, so Hera's own
* registration is where that sum is seeded. */
int is_root = vm_uuid_is_hera(vm_id);
if (node) {
node->physics.execution_heat_q48 = is_root ? Q48_ONE : 0;
node->physics.last_active_tick = 0;
node->physics.touched = 0;
node->physics.is_live = 1;
return;
}
node = (vm_physics_node_t *)kmalloc(sizeof(vm_physics_node_t));
if (!node) return;
node->vm_id = vm_id;
node->physics.execution_heat_q48 = is_root ? Q48_ONE : 0;
node->physics.last_active_tick = 0;
node->physics.touched = 0;
node->physics.is_live = 1;
node->next = vm_physics_head;
vm_physics_head = node;
}
/**
* vm_physics_find_root_id - Walk parent_vm_id edges up to Hera
*
* Every VM's parent_vm_id (VMRegistryEntry, capsule_run.h) is set once
* at its own birth and never rewritten -- including a dead VM's, so the
* walk continues correctly through an already-killed intermediate
* parent without any special dead-parent handling. Hera's own entry is
* self-referential (parent_vm_id == vm_id == vm_uuid_hera()), which is
* what stops the walk: she is the only node with no outbound edge.
*
* The 64-hop guard is defensive only -- a well-formed chain terminates
* in O(fleet depth) hops, nowhere near 64. It exists so a corrupted
* chain (which should never happen) returns the starting vm_id instead
* of spinning.
*/
static VMUuid vm_physics_find_root_id(VMUuid vm_id)
{
VMUuid cursor = vm_id;
uint32_t guard;
for (guard = 0; guard < 64; guard++) {
VMRegistryEntry entry;
if (capsule_vm_registry_get(cursor, &entry) != 0) break;
if (vm_uuid_equal(entry.parent_vm_id, cursor)) break; /* self-referential: root */
cursor = entry.parent_vm_id;
}
return cursor;
}
void vm_physics_retire(VMUuid vm_id)
{
vm_physics_node_t *dying = vm_physics_find(vm_id);
vm_physics_node_t *root;
VMUuid root_id;
if (!dying || !dying->physics.is_live) return;
root_id = vm_physics_find_root_id(vm_id);
root = !vm_uuid_equal(root_id, vm_id) ? vm_physics_find(root_id) : (void *)0;
if (root) {
/* The dying VM's entire remaining heat flows to the root it
* chains up to (in practice always Hera today) -- no division,
* no weighting policy, no "no survivors" case, since a root is
* always there by construction. This is also what makes
* TRIPOD-TEST's kill-then-rebirth "K soak" check meaningful: a
* root VM (e.g. Hermes) being killed and later respawned by
* Hera doesn't lose heat across the gap, it just parks at Hera
* in the meantime. */
vm_physics_transfer(&dying->physics, &root->physics,
dying->physics.execution_heat_q48);
}
/* root_id == vm_id means the dying VM IS the root itself (Hera --
* shouldn't happen, she can't be killed) or the chain was broken
* (shouldn't happen, see vm_physics_find_root_id). Either way
* there's nowhere conservation-preserving to send the remainder;
* it is dropped along with the dying entry. */
dying->physics.execution_heat_q48 = 0;
dying->physics.is_live = 0;
}
void vm_physics_touch(VMUuid vm_id)
{
vm_physics_node_t *target = vm_physics_find(vm_id);
VMFleetTouchSample sample = { 0, 0, 0 };
uint64_t now_tick = fleet_heartbeat_tick_count;
if (!target || !target->physics.is_live) return;
/* Restated on the virtual tick (FABRIC-0.md item 2.1, 2026-08-04): this
* used to gate on target->physics.last_active_ns > 0 and scale by
* elapsed wall-clock microseconds -- non-reproducible run to run under
* TCG (GAP-A1, FABRIC-0.md §25.7.1). fleet_heartbeat_tick_count is
* execution-paced (vm_runtime.c:143), so elapsed_ticks is now a pure
* function of the execution stream. `touched` replaces the old
* `> 0` sentinel: tick 0 is a legitimate value for a genuine first
* touch to land on, so "never touched" needs its own flag rather than
* overloading the tick value. */
if (fleet_transfer_slope_q48 > 0 &&
target->physics.touched &&
now_tick > target->physics.last_active_tick) {
uint64_t elapsed_ticks = now_tick - target->physics.last_active_tick;
uint64_t amount = (elapsed_ticks * fleet_transfer_slope_q48) >> 16;
sample.elapsed_ticks = elapsed_ticks;
sample.amount = amount;
if (amount > 0) {
/* Pull toward the touched VM, proportional to each other LIVE
* VM's current heat -- same fan-out shape as retire, mirror
* direction. Clamped to what the rest of the fleet actually
* holds, so this can never manufacture heat. */
vm_physics_node_t *n;
uint64_t others_total = 0;
uint64_t moved_total;
for (n = vm_physics_head; n; n = n->next) {
if (n != target && n->physics.is_live) {
others_total += n->physics.execution_heat_q48;
}
}
/* moved_total == 0 when others_total == 0 (nothing to pull --
* the fleet is already fully concentrated elsewhere), handled
* uniformly by the same clamp as a partial cap: either way
* the actual transfer fell short of what the rate implied, so
* this touch carries no usable rate information (rev o). */
moved_total = (amount > others_total) ? others_total : amount;
sample.clamped = (moved_total != amount);
if (moved_total > 0) {
for (n = vm_physics_head; n; n = n->next) {
if (n != target && n->physics.is_live) {
uint64_t share = (moved_total * n->physics.execution_heat_q48)
/ others_total;
vm_physics_transfer(&n->physics, &target->physics, share);
}
}
}
}
}
target->physics.last_active_tick = now_tick;
target->physics.touched = 1;
/* Record this touch's transfer-law inputs, not a derived heat value
* (rev o, VM-FLEET-ATTRACTOR-DESIGN-20260705.md) -- see
* VMFleetTouchSample for why. */
fleet_window.touch_samples[fleet_window.head] = sample;
fleet_window.head = (fleet_window.head + 1) % VM_FLEET_WINDOW_DEPTH;
if (fleet_window.count < VM_FLEET_WINDOW_DEPTH) {
fleet_window.count++;
}
fleet_window.is_warm = (fleet_window.count >= VM_FLEET_WINDOW_DEPTH);
}
/**
* l8_regime_modulation_q16 - Read-only Q16 multiplier from Hera's own L8
*
* Gives L8 a real, passive-observer-respecting causal channel into the
* fleet's transfer rate (rev q, VM-FLEET-ATTRACTOR-DESIGN-20260705.md):
* previously nothing L8 did could affect fleet dynamics at all (rev j's
* architectural diagnosis for its null DoE result). Reuses Hera's
* adaptive table's current_regime -- the same 3-bit entropy/cv/temporal
* classification the bandit already stratifies its own scores by,
* rather than inventing a new signal -- as a proxy for fleet-wide
* dispatch activity, since all VM-EXEC/VM-CALL flows through her.
*
* modulation = (popcount(regime) + 1) * 0.5, i.e. 0.5x-2.0x: more of the
* three high-signal bits active scales the fleet's already-recovered
* rate up (track change more aggressively), fewer scales it down (damp
* toward the raw empirical rate). No per-VM favoritism -- this scales
* whatever vm_physics_tick() already computed uniformly, it doesn't
* decide who receives heat.
*
* Returns Q16 65536 (neutral 1.0x) if Hera or her adaptive table isn't
* available, so a missing signal never blocks or corrupts the estimate
* this multiplies.
*/
static uint32_t l8_regime_modulation_q16(void)
{
VM *mama = (VM*)sk_get_mama_vm();
ssm_l8_state_t *l8;
uint32_t popcount;
if (!mama || !mama->ssm_l8_state) return 65536u;
l8 = (ssm_l8_state_t*)mama->ssm_l8_state;
if (!l8->table) return 65536u; /* legacy path tracks no regime */
popcount = (uint32_t)(((l8->table->current_regime >> 2) & 1u) +
((l8->table->current_regime >> 1) & 1u) +
(l8->table->current_regime & 1u));
return (popcount + 1u) * 32768u;
}
void vm_physics_tick(uint64_t now_ns)
{
uint64_t rates[VM_FLEET_WINDOW_DEPTH];
uint32_t rate_count = 0;
uint32_t i;
(void)now_ns; /* Unlike the log-linear regression this replaced, the
* estimator below needs no external time reference --
* each sample already carries its own elapsed_ticks. */
/* Mirrors vm_tick_inference_engine's is_warm gate: skip rather than
* substitute a default slope. Expected to stay unwarmed (or warm
* with a low-quality fit) at the current 3-VM Tripod scale -- there
* simply aren't enough distinct touch events yet. */
if (!fleet_window.is_warm) return;
/* Direct rate recovery, not curve-fitting (rev o,
* VM-FLEET-ATTRACTOR-DESIGN-20260705.md, superseding the log-linear
* OLS approach of rev b-n): the transfer law is known exactly
* (amount = elapsed_ticks * slope >> 16), so any unclamped touch can
* be inverted directly for an exact per-sample rate. Order doesn't
* matter here -- a median over the observation window, not a
* time-indexed fit -- so this walks the fixed-size array directly
* rather than replaying ring order from fleet_window.head. */
for (i = 0; i < VM_FLEET_WINDOW_DEPTH; i++) {
VMFleetTouchSample *s = &fleet_window.touch_samples[i];
if (s->clamped || s->amount == 0 || s->elapsed_ticks == 0) continue;
rates[rate_count++] = (s->amount << 16) / s->elapsed_ticks;
}
/* slope_fit_quality_q48 as the informative fraction of the window --
* a real measure now, not a fixed placeholder. Reported even when
* there's too little to fit, same as a partial-sample regression
* would report low confidence rather than nothing at all. */
slope_fit_quality_q48 = ((uint64_t)rate_count << 16) / VM_FLEET_WINDOW_DEPTH;
/* Too few informative samples to trust: keep the current slope
* rather than overwrite it with an estimate from a handful of
* points (or zero) -- the same "skip, don't substitute a degenerate
* value" philosophy as the is_warm gate above. */
if (rate_count < VM_PHYSICS_MIN_INFORMATIVE_SAMPLES) return;
/* Insertion sort for the median -- rate_count <= VM_FLEET_WINDOW_DEPTH
* (64), no library qsort in this freestanding kernel-only file. */
for (i = 1; i < rate_count; i++) {
uint64_t key = rates[i];
uint32_t j = i;
while (j > 0 && rates[j - 1] > key) {
rates[j] = rates[j - 1];
j--;
}
rates[j] = key;
}
fleet_transfer_slope_q48 = (rates[rate_count / 2] * l8_regime_modulation_q16()) >> 16;
}
/* Gated on STADIUM_CAPACITY_TICK, not HEARTBEAT_INFERENCE_FREQUENCY (fixed
* 2026-08-15, FABRIC-1.md §12 Q5): fleet_heartbeat_tick_count is fed by
* EVERY live VM's own vm_tick() (see the comment above this counter's
* declaration), so it advances several times faster in wall-clock terms
* than a single VM's own tick_count -- comparing it against
* HEARTBEAT_INFERENCE_FREQUENCY (written assuming a single VM's stream)
* meant this fired far more often than §22.4's required ~1000:1 separation
* from a VM's own heat-inference loop. Still one clock, one counter --
* this is a bigger flat threshold on the same counter, not a second tick
* source or anything that reads live VM count to adjust itself. */
void vm_physics_heartbeat_tick(uint64_t now_ns)
{
fleet_heartbeat_tick_count++;
if ((fleet_heartbeat_tick_count - fleet_last_capacity_tick) >= STADIUM_CAPACITY_TICK) {
vm_physics_tick(now_ns);
fleet_last_capacity_tick = fleet_heartbeat_tick_count;
}
}
uint64_t vm_physics_fleet_heat_sum(void)
{
uint64_t total = 0;
vm_physics_node_t *n;
for (n = vm_physics_head; n; n = n->next) {
if (n->physics.is_live) {
total += n->physics.execution_heat_q48;
}
}
return total;
}
int vm_physics_conserved(void)
{
uint64_t sum = vm_physics_fleet_heat_sum();
uint64_t diff = (sum > Q48_ONE) ? (sum - Q48_ONE) : (Q48_ONE - sum);
return diff < VM_PHYSICS_EPSILON_Q48;
}
uint64_t vm_physics_heat_of(VMUuid vm_id)
{
vm_physics_node_t *n = vm_physics_find(vm_id);
if (!n || !n->physics.is_live) return 0;
return n->physics.execution_heat_q48;
}
/* Decimal print, same shape as parity.c's parity_put_u64 -- freestanding,
* no snprintf dependency. */
static void console_put_u64(uint64_t val)
{
char buf[21];
int i = 20;
buf[i--] = '\0';
if (val == 0) {
buf[i--] = '0';
} else {
while (val > 0 && i >= 0) {
buf[i--] = (char)('0' + (val % 10));
val /= 10;
}
}
console_puts(&buf[i + 1]);
}
void vm_physics_status(void)
{
uint64_t sum = vm_physics_fleet_heat_sum();
console_puts("VM-PHYSICS: fleet_heat_sum=");
console_put_u64(sum);
console_println("");
console_puts("VM-PHYSICS: conserved=");
console_println(vm_physics_conserved() ? "CONSERVED" : "DRIFTED");
console_puts("VM-PHYSICS: fleet_transfer_slope_q48=");
console_put_u64(fleet_transfer_slope_q48);
console_println("");
console_puts("VM-PHYSICS: slope_fit_quality_q48=");
console_put_u64(slope_fit_quality_q48);
console_println("");
console_puts("VM-PHYSICS: fleet_window_warm=");
console_println(fleet_window.is_warm ? "YES" : "NO");
/* Per-VM breakdown (VM-FLEET-ATTRACTOR-DESIGN-20260705.md transparency
* requirement): the aggregate above always reads Q.1 when conserved,
* which says nothing about how heat is actually split among live VMs. */
{
vm_physics_node_t *n;
for (n = vm_physics_head; n; n = n->next) {
VMRegistryEntry entry;
if (!n->physics.is_live) continue;
console_puts("VM-PHYSICS: vm_id=");
{
char uuid_buf[37];
vm_uuid_format(n->vm_id, uuid_buf);
console_puts(uuid_buf);
}
if (capsule_vm_registry_get(n->vm_id, &entry) == 0) {
console_puts(" name=");
console_puts(entry.name);
}
console_puts(" heat_q48=");
console_put_u64(n->physics.execution_heat_q48);
console_println("");
}
}
}
@@ -0,0 +1,282 @@
/*
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.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* capsule_vm_switch_signal.c - see the header for the full contract
* (FABRIC-3.md §XXVIII, Stage 3, 2026-09-13).
*/
#ifdef __STARKERNEL__
#include "starkernel/capsule_vm_switch_signal.h"
#include "vm.h"
#include "starkernel/vm/switch.h" /* sk_vm_switch_current_vm() -- see below */
#include "starkernel/vm/stadium.h" /* stadium_max_vm_count() -- task 3.1's sizing bound */
#include "starkernel/kmalloc.h"
#include "starkernel/console.h"
#include <stddef.h>
/* FABRIC-3.6.md task 3.1 (2026-09-21, ruled B2 / FABRIC-3.5.md §XLV.2):
* replaces the old #define SK_SWITCH_MAX_SLOTS 16 (FABRIC-3.md §XXVIII
* Stage 4, 2026-09-14 -- itself a bump from 8, pinned to messaging.4th's
* VM-MAX constant). The table is now kmalloc'd at
* sk_vm_switch_signal_boot_init() to stadium_max_vm_count() entries --
* every switch-signal participant is a Stadium VM, so this reuses that
* bound directly rather than re-deriving a separate RAM budget, mirroring
* session.c's session_boot_init() (the direct precedent for this pattern,
* itself mirroring stadium.c's own StadiumVMQuota table). See
* feedback_no_hardcoded_small_scale_bounds in project memory -- a fixed
* cap here was rejected before; this closes the same class of bound. */
/* Ticks a non-running participant must accumulate readiness before a
* switch to it is requested. Simple linear accumulate-then-threshold,
* not the fancier relaxation law a later pass might want -- correctness
* and a clean single-writer/single-reader story mattered more than the
* exact law for this first cut (this stage's own design-review latitude,
* per the plan). Reset to 0 the instant a slot becomes current, so a
* VM that just ran needs a full threshold's worth of ticks idle before
* it can be selected again -- a crude but real fairness property. */
#define SK_SWITCH_READINESS_THRESHOLD 50u
typedef struct {
VMUuid vm_id;
uint32_t readiness;
int has_work; /* Stage 3 follow-on (2026-09-14): set by
* sk_vm_switch_signal_mark_work() (mainline,
* MSG-SEND hook), cleared here in tick() the
* instant this slot becomes current -- see that
* function's own doc comment in the header. */
} sk_switch_slot_entry_t;
static sk_switch_slot_entry_t *g_slots = (sk_switch_slot_entry_t *)0;
static int g_slot_capacity; /* 0 until sk_vm_switch_signal_boot_init() succeeds */
static int g_slot_count;
static int g_pending;
static VMUuid g_pending_target;
/* DoE CSV exposure (FABRIC-3.md §XXVIII Stage 3 follow-on, 2026-09-13). */
static int g_current_slot_cached = -1;
static uint64_t g_switch_count;
static uint32_t g_ticks_since_switch;
/* Freestanding: no libc printf. Prints an unsigned decimal, no leading
* zeros -- same small helper stadium.c/stadium_blocks.c each carry their
* own copy of. */
static void console_put_u64(uint64_t v) {
char buf[21];
int i = 20;
buf[20] = '\0';
if (v == 0) {
console_puts("0");
return;
}
while (v > 0 && i > 0) {
buf[--i] = (char)('0' + (v % 10));
v /= 10;
}
console_puts(&buf[i]);
}
static int slot_for_vm_id(VMUuid vm_id) {
int i;
for (i = 0; i < g_slot_count; i++) {
if (vm_uuid_equal(g_slots[i].vm_id, vm_id)) return i;
}
return -1;
}
int sk_vm_switch_signal_boot_init(void) {
size_t max_vm_count = stadium_max_vm_count();
sk_switch_slot_entry_t *slots;
size_t i;
if (max_vm_count == 0) return -1; /* Stadium not yet initialized */
slots = (sk_switch_slot_entry_t *)kmalloc(max_vm_count * sizeof(sk_switch_slot_entry_t));
if (!slots) return -1;
for (i = 0; i < max_vm_count; i++) {
slots[i].vm_id = vm_uuid_none();
slots[i].readiness = 0;
slots[i].has_work = 0;
}
g_slots = slots;
g_slot_capacity = (int)max_vm_count;
g_slot_count = 0;
console_puts("Switch-signal: ");
console_put_u64((uint64_t)max_vm_count);
console_println(" slots");
return 0;
}
int sk_vm_switch_signal_register(VMUuid vm_id) {
if (g_slot_count >= g_slot_capacity) return -1;
g_slots[g_slot_count].vm_id = vm_id;
g_slots[g_slot_count].readiness = 0;
g_slots[g_slot_count].has_work = 0;
return g_slot_count++;
}
int sk_vm_switch_signal_unregister(VMUuid vm_id) {
int slot = slot_for_vm_id(vm_id);
if (slot < 0) return -1;
for (int i = slot; i < g_slot_count - 1; i++) {
g_slots[i] = g_slots[i + 1];
}
g_slot_count--;
/* A pending switch targeting this VM must not survive its removal --
* the checkpoint in execute_colon_word() would otherwise try to
* switch into a VM no longer tracked here. */
if (g_pending && vm_uuid_equal(g_pending_target, vm_id)) {
g_pending = 0;
}
/* g_current_slot_cached is a DoE-observable value only (see its own
* declaration comment); if compaction just invalidated it, the very
* next tick() recomputes it from scratch via slot_for_vm_id(), so a
* momentarily-stale cache here is harmless. */
return 0;
}
void sk_vm_switch_signal_tick(void) {
VM *current;
int current_slot, i;
/* FABRIC-3.md §XXVIII Stage 3 follow-on correction (2026-09-14): was
* vm_log_attributed_vm() -- wrong source. That tracks nested
* vm_interpret() calls for log attribution and has no way to know a
* VM is running while parked inside switch.c's own raw-C trampoline
* (sk_vm_switch_entry()), which never itself calls vm_interpret(). A
* VM that only ever ran the trampoline was therefore never seen as
* "current," so its own readiness/has_work below never reset --
* tick() re-armed the very next timer tick, forever, once nothing
* else happened to be independently flipping attribution around it
* (found live: a switch storm, 101% CPU, log frozen). sk_vm_switch_
* current_vm() is authoritative instead: the switch mechanism itself
* updates it on every switch, so it reflects who is physically
* running regardless of whether that VM ever calls vm_interpret(). */
current = sk_vm_switch_current_vm();
if (!current) return; /* boot: no switch has happened yet, not seeded */
current_slot = slot_for_vm_id(current->stadium_vm_id);
if (current_slot < 0) return; /* current VM isn't a registered participant */
g_current_slot_cached = current_slot;
g_ticks_since_switch++;
for (i = 0; i < g_slot_count; i++) {
if (i == current_slot) {
g_slots[i].readiness = 0;
g_slots[i].has_work = 0;
} else {
g_slots[i].readiness++;
}
}
if (!g_pending) {
for (i = 0; i < g_slot_count; i++) {
if (i != current_slot &&
g_slots[i].readiness >= SK_SWITCH_READINESS_THRESHOLD &&
g_slots[i].has_work) {
g_pending = 1;
g_pending_target = g_slots[i].vm_id;
break;
}
}
}
}
VMUuid sk_vm_switch_signal_take_pending(void) {
if (!g_pending) return vm_uuid_none();
g_pending = 0;
return g_pending_target;
}
void sk_vm_switch_signal_note_switch_performed(VMUuid target_id) {
/* FABRIC-3.md §XXVIII Stage 3 follow-on correction (2026-09-14):
* reset the target's own readiness/has_work HERE, deterministically,
* at the switch decision point -- not by waiting for tick()'s
* current-slot polling to observe it running. That polling depends on
* sk_vm_switch_current_vm() catching a real ISR tick during whatever
* window the target is genuinely "current" for, and for a VM whose
* only production behavior is switch.c's placeholder trampoline (an
* immediate yield-back), that window can be as small as a few
* instructions -- too narrow for a timer tick to reliably land in.
* Without this, the target's eligibility never clears, tick() re-arms
* on the very next timer tick, and the result is a continuous switch
* storm (found live: 101% CPU, zero forward progress). This makes
* correctness independent of ISR timing; current_slot (via
* sk_vm_switch_current_vm()/tick()) remains useful as a DoE-observable
* value and for excluding the running slot from readiness accrual,
* but is no longer load-bearing for storm prevention. */
int slot = slot_for_vm_id(target_id);
if (slot >= 0) {
g_slots[slot].readiness = 0;
g_slots[slot].has_work = 0;
}
g_switch_count++;
g_ticks_since_switch = 0;
}
void sk_vm_switch_signal_mark_work(VMUuid vm_id) {
int slot = slot_for_vm_id(vm_id);
if (slot < 0) return;
g_slots[slot].has_work = 1;
}
uint64_t sk_vm_switch_signal_switch_count(void) {
return g_switch_count;
}
uint32_t sk_vm_switch_signal_ticks_since_switch(void) {
return g_ticks_since_switch;
}
int sk_vm_switch_signal_current_slot(void) {
return g_current_slot_cached;
}
int sk_vm_switch_signal_slot_count(void) {
return g_slot_count;
}
int sk_vm_switch_signal_slot_capacity(void) {
return g_slot_capacity;
}
uint32_t sk_vm_switch_signal_readiness(int slot) {
if (slot < 0 || slot >= g_slot_count) return 0;
return g_slots[slot].readiness;
}
uint32_t sk_vm_switch_signal_readiness_of(VMUuid vm_id) {
int slot = slot_for_vm_id(vm_id);
if (slot < 0) return 0;
return g_slots[slot].readiness;
}
#endif /* __STARKERNEL__ */
+563
View File
@@ -0,0 +1,563 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
Licensed under the StarForth License, Version 1.0
*/
#ifndef __STARKERNEL__
#error "capsule_wirebind.c is kernel-only"
#endif
#include "starkernel/capsule_wirebind.h"
#include "starkernel/capsule_birth.h"
#include "starkernel/capsule_runcap.h"
#include "starkernel/capsule_console.h"
#include "starkernel/vm_identity.h"
#include "starkernel/user_identity_seed.h"
#include "starkernel/console.h"
#include "starkernel/repl.h" /* sk_repl_get/set_active_vm() -- FABRIC-2.md §F.10 EJECT */
#include "starkernel/capsule_vm_switch_signal.h" /* FABRIC-3.md §XXVIII Stage 4 -- register/unregister */
#include "blkio.h"
#include "block_subsystem.h" /* compute_crc64() */
#include "word_source/include/block_words.h" /* blk_vm_flush_all() -- §F.10 EJECT */
#include "freestanding/stdio.h"
#include <string.h>
#include <stddef.h>
/* FABRIC-2.md §F.10: "the VM currently attached via the home-blocks USB
* path" -- WIREBIND's own to track, set at successful birth in
* capsule_wirebind_try_attach(), read and cleared by both
* capsule_wirebind_eject() and capsule_wirebind_unclean_detach(). Single-
* USB-device constraint (§F.8) means there is never more than one
* candidate, so one tracked id is enough -- no set/list. */
static VMUuid g_wirebind_attached_vm_id;
static int g_wirebind_attached_valid = 0;
/* Plain username (no "~user" registry-name suffix), tracked separately
* from g_wirebind_attached_vm_id -- the registry entry's own name is
* user_vm_name ("<username>~user"), not the bare form the (user) prompt
* segment wants (FABRIC-2.md §I.1/4.4s). */
static char g_wirebind_attached_username[USER_IDENTITY_USERNAME_MAX] = {0};
/* FABRIC-2.md §I.2 overflow trigger, 2026-09-05: the attached drive's own
* blkio_dev pointer, tracked alongside the two fields above so
* capsule_wirebind_overflow_idle_check() can query its free space later
* without a new attach-time argument threaded through the idle loop. */
static struct blkio_dev *g_wirebind_attached_dev = (struct blkio_dev *) 0;
/* FABRIC-3.md §XXVIII Stage 4 (2026-09-14): the singleton above correctly
* models "who is paired to the one physical console" (repl.c's own
* console-relay/`(user)` prompt logic genuinely is single-valued -- there
* is only one serial/framebuffer console). It does NOT correctly model
* "which devices are live," despite g_wirebind_attached_eject()'s own
* header doc claiming a "single-USB-device constraint (§F.8)" -- that
* claim is stale: §XV/§XVI (2026-09-11/12) proved 9 identities genuinely
* simultaneously live via this exact attach path. Every attach after the
* first silently overwrote the singleton, so capsule_wirebind_unclean_
* detach()'s `dev != g_wirebind_attached_dev` check only ever matched the
* *most recently attached* identity -- any earlier identity's drive
* being pulled hit that check, returned early, and orphaned: never torn
* down, its VM and native stack leaked forever, no console/log trace at
* all. Silent because nothing in this project's verification history
* ever detached anything but the single most-recent identity.
*
* This new table is additive and separate from the console singleton
* above (which is untouched, still governs console pairing exactly as
* before) -- it tracks every currently-live WIREBIND identity by its own
* device pointer, so unclean-detach (and Stage 4's own switch-signal
* registration bookkeeping) can resolve *any* attached device to *its
* own* identity, not just the last one. Sized off messaging.4th's own
* VM-MAX (16) minus the 3 slots Hera/Hermes/Artemis permanently reserve
* (capsules/common/messaging.4th: "identities get NEW slots 3-10, never
* renumbered") -- a real, already-agreed system-wide ceiling, not a new
* guess (see feedback_no_hardcoded_small_scale_bounds in project memory:
* Bob has rejected an invented cap here before). */
#define WIREBIND_MAX_LIVE 13u
typedef struct {
int in_use;
struct blkio_dev *dev;
VMUuid user_vm_id;
char username[USER_IDENTITY_USERNAME_MAX];
int switch_slot; /* Stage 4: index into the switch-signal
* table, or -1 if not yet registered
* (wired in the next increment). */
} wirebind_live_entry_t;
static wirebind_live_entry_t g_wirebind_live[WIREBIND_MAX_LIVE];
static wirebind_live_entry_t *wirebind_live_find_by_dev(struct blkio_dev *dev) {
for (uint32_t i = 0; i < WIREBIND_MAX_LIVE; i++) {
if (g_wirebind_live[i].in_use && g_wirebind_live[i].dev == dev) {
return &g_wirebind_live[i];
}
}
return (wirebind_live_entry_t *)0;
}
static wirebind_live_entry_t *wirebind_live_alloc(void) {
for (uint32_t i = 0; i < WIREBIND_MAX_LIVE; i++) {
if (!g_wirebind_live[i].in_use) return &g_wirebind_live[i];
}
return (wirebind_live_entry_t *)0;
}
/* WIREBIND_CERT_MAX_DEVBLOCKS: a sane upper bound on how much cert
* content this reads, independent of whatever sig->cert_devblocks
* claims -- MINT (capsule_mint.c) only ever writes 1 devblock's worth
* (a real cert is ~150-250 bytes), so 4 devblocks (16KB) is already
* generous headroom, not a real constraint. */
#define WIREBIND_CERT_MAX_DEVBLOCKS 4u
/* FABRIC-2.md §I.2 overflow trigger, 2026-09-05: fixed constants, "fixed
* first" per this project's own standing sequencing (MIGRATION_WEAR_
* THRESHOLD, ZUSE_SESSION_TTL_SECONDS -- adaptive only after a real,
* measured baseline exists, not from day one). A single DoE-tunable knob
* apiece, not yet wired to Kconfig -- revisit once real usage data exists.
* 10% is a conservative first guess (extend well before actually full);
* 8 devblocks (24 Forth blocks, ~24 KiB) is a modest first extension, not
* a large land-grab. */
#define WIREBIND_OVERFLOW_FREE_THRESHOLD_PCT 10u
#define WIREBIND_OVERFLOW_CLAIM_DEVBLOCKS 8u
/* Read exactly one devblock (4096 bytes) at devblock offset `devblock`
* -- same convention capsule_runcap.c/capsule_mint.c already use. */
static int read_devblock(struct blkio_dev *dev, uint32_t devblock, uint8_t *buf4096) {
uint32_t base = devblock * 4u;
for (uint32_t i = 0; i < 4u; i++) {
if (blkio_read((blkio_dev_t *)dev, base + i,
buf4096 + (size_t)i * BLKIO_FORTH_BLOCK_SIZE) != BLKIO_OK) {
return -1;
}
}
return 0;
}
int capsule_wirebind_verify_cert(struct blkio_dev *dev,
const homeblocks_sig_t *sig,
VM *mama_vm, VMIdentity *out) {
if (!dev || !sig || !mama_vm || !out) return -1;
/* Genesis-mode Zuse drives have no cert region -- capsule_zuse_boot_
* try_attach() owns that case, not this one. */
if (sig->cert_offset == 0 || sig->cert_devblocks == 0) return -1;
/* FABRIC-3.md, 2026-09-09: verifying a WIREBIND cert needs nothing but
* Zuse's already-public key -- it does not need her live session.
* zuse_root_pubkey_known/zuse_root_pubkey (capsule_zuse_boot_load_
* root_pubkey(), loaded unconditionally from the persistent genesis-
* marker fence as soon as Artemis's own storage is up) is what this
* checks now, not zuse_cert_installed -- that pair stays reserved for
* MINT, which genuinely does need her live private seed. One
* identity's attach must not depend on another identity's live
* presence. */
if (!mama_vm->zuse_root_pubkey_known) return -1;
/* Read the cert region into a local buffer. */
uint32_t n_devblocks = sig->cert_devblocks;
if (n_devblocks > WIREBIND_CERT_MAX_DEVBLOCKS) n_devblocks = WIREBIND_CERT_MAX_DEVBLOCKS;
uint8_t cert_der[WIREBIND_CERT_MAX_DEVBLOCKS * 4096];
for (uint32_t d = 0; d < n_devblocks; d++) {
if (read_devblock(dev, sig->cert_offset + d, cert_der + (size_t)d * 4096) != 0) {
return -1;
}
}
size_t cert_len = (size_t)n_devblocks * 4096;
memset(out, 0, sizeof(*out));
return vm_identity_from_cert(out, cert_der, cert_len,
mama_vm->zuse_root_pubkey, sig->drive_uuid,
0 /* acl_caps: no bits assigned yet, §F.2 */);
}
void capsule_wirebind_try_attach(struct blkio_dev *dev,
const homeblocks_sig_t *sig,
VM *mama_vm) {
if (!dev || !sig || !mama_vm) return;
/* Genesis-mode Zuse drives have no cert region -- capsule_zuse_boot_
* try_attach() already owns that case. Check here too so the right
* "not applicable" case stays silent (matches capsule_wirebind_
* verify_cert()'s own -1, but avoids a misleading refusal message
* for a drive this function was never going to act on anyway). */
if (sig->cert_offset == 0 || sig->cert_devblocks == 0) return;
if (!mama_vm->zuse_root_pubkey_known) return;
VMIdentity identity;
if (capsule_wirebind_verify_cert(dev, sig, mama_vm, &identity) != 0) {
console_println("WIREBIND: cert verification FAILED -- drive refused");
return;
}
/* Verified. Read the drive's own profile for its username. */
if (sig->identity_src_offset == 0 || sig->identity_src_devblocks < 1) {
console_println("WIREBIND: verified cert but no identity_src region -- refusing");
return;
}
user_identity_seed_t idrec;
if (read_devblock(dev, sig->identity_src_offset, (uint8_t *)&idrec) != 0) {
console_println("WIREBIND: verified cert but identity record unreadable -- refusing");
return;
}
/* Same magic -> version -> CRC-64 discipline as capsule_zuse_boot.c:
* the identity record is the same on-disk format, so a corrupt or
* format-mismatched record must be refused rather than trusted. */
if (idrec.magic != USER_IDENTITY_SEED_MAGIC ||
idrec.version != USER_IDENTITY_SEED_VERSION) {
console_println("WIREBIND: verified cert but identity record unreadable -- refusing");
return;
}
uint64_t want_crc = compute_crc64((const uint8_t *)&idrec,
offsetof(user_identity_seed_t, crc));
if (want_crc != idrec.crc) {
console_println("WIREBIND: verified cert but identity record corrupt -- refusing");
return;
}
char username[USER_IDENTITY_USERNAME_MAX];
memcpy(username, idrec.username, sizeof(username));
username[sizeof(username) - 1] = '\0'; /* defensive; MINT already NUL-terminates */
char user_vm_name[USER_IDENTITY_USERNAME_MAX + 8];
size_t ulen = strlen(username);
if (ulen == 0 || ulen + 6 > sizeof(user_vm_name)) {
console_println("WIREBIND: empty or oversized username -- refusing");
return;
}
memcpy(user_vm_name, username, ulen);
memcpy(user_vm_name + ulen, "~user", 6);
/* Idempotent: already bound this session (console VM already live
* under this username) -- nothing to do. Matches BIRTH's own
* idempotent convention. */
{
VMRegistryEntry existing;
if (capsule_vm_find_by_name(username, &existing) == 0 &&
existing.state == VM_STATE_LIVE) {
return;
}
}
VMUuid console_id, user_id;
void *console_ctx = (void *)0;
void *user_ctx = (void *)0;
if (capsule_console_birth(username, mama_vm->stadium_vm_id, &console_id, &console_ctx)
!= CAPSULE_RUN_OK) {
console_println("WIREBIND: console VM birth FAILED");
return;
}
/* FABRIC-3.6.md Phase 4 (Stage E): console_ctx's only use (the
* routing-table VM-NAME-REG call above) is gone -- still populated
* because capsule_console_birth()'s own signature requires an
* out-param regardless, just no longer read here. */
(void)console_ctx;
if (capsule_runcap_birth(dev, sig, user_vm_name, mama_vm->stadium_vm_id, &user_id,
&user_ctx) != CAPSULE_RUN_OK) {
console_println("WIREBIND: user VM birth FAILED");
/* The console VM above already succeeded and is live/registered
* under `username` -- without this, it's an orphan: nothing ever
* points a real user at it (WIREBIND only ever hands the caller
* the user VM's id), so it just sits there permanently holding
* its own kernel-heap arena allocation. Confirmed live 2026-09-07
* (identity-heap-capacity testing): this is exactly what turns a
* single failed identity attach into a permanent net loss of
* heap, compounding every subsequent attempt's odds of failing
* too, rather than a neutral retry. capsule_vm_kill() is the same
* teardown capsule_wirebind_eject()/capsule_wirebind_unclean_
* detach() already use elsewhere in this file -- vm_cleanup() +
* sf_free(), verified to actually reclaim per-word dictionary
* allocations (FABRIC-3.md §IX.2/§IX.3, 2026-09-06). */
capsule_vm_kill(username);
return;
}
/* Install the verified identity onto the user VM -- the one BINDSTEP
* (§F.9) will later re-verify against on every USE. */
if (user_ctx) {
((VM *)user_ctx)->identity = identity;
}
/* FABRIC-2.md §F.10: this is the successful-birth point EJECT/UNCLEAN
* need a tracked VMUuid from -- the user VM, not the console VM
* (blk_vm_flush_all()/capsule_vm_kill() below both act on the VM that
* actually owns block-subsystem state). */
g_wirebind_attached_vm_id = user_id;
g_wirebind_attached_valid = 1;
g_wirebind_attached_dev = dev;
memcpy(g_wirebind_attached_username, username, sizeof(g_wirebind_attached_username));
/* FABRIC-3.md §XXVIII Stage 4 (2026-09-14): additive per-device
* tracking, independent of the console-pairing singleton just above
* (which stays exactly as it was) -- see g_wirebind_live[]'s own doc
* comment. A full table is reported, not silently dropped: running
* out of the real, sourced ceiling here means something is actually
* wrong (more identities live than this system's own VM-MAX allows
* for), worth knowing about rather than quietly leaving a device
* untracked for unclean-detach/switch-signal purposes. */
{
wirebind_live_entry_t *live = wirebind_live_alloc();
if (live) {
live->in_use = 1;
live->dev = dev;
live->user_vm_id = user_id;
/* FABRIC-3.md §XXVIII Stage 4 (2026-09-14): the user VM is
* the one that actually runs FORTH work (VM-EXEC/MSG-TICK
* dispatch target it directly) -- it participates in
* preemptive switching. The console VM does not: it is a
* REPL-session proxy whose own control flow
* sk_repl_get_active_vm() already owns, never itself the
* target of VM-EXEC or MSG-SEND, so it has nothing for a
* switch-in to preempt. Registration failure (table full)
* degrades to "this identity won't be preempted," not to a
* failed attach -- the identity is still fully usable via
* ordinary VM-EXEC/console-relay dispatch, same as before
* Stage 4 existed. */
live->switch_slot = sk_vm_switch_signal_register(user_id);
memcpy(live->username, username, sizeof(live->username));
if (live->switch_slot < 0) {
console_println("WIREBIND: switch-signal table full -- this identity won't be preempted");
}
} else {
console_println("WIREBIND: live-identity table full -- detach/switch tracking degraded for this attach");
}
}
/* Decided 2026-09-05: no console for the running system unless a
* thumbdrive is present. Revised 2026-09-06: this used to call
* sk_console_mark_login() here (a one-way sticky flag) -- the gate is
* now a live check (sk_console_identity_present(), repl.c) driven
* directly by g_wirebind_attached_username being set above, re-checked
* continuously by sk_repl_run()'s own main loop rather than a one-shot
* signal at login time. Nothing to call here anymore. */
/* FABRIC-3.6.md Phase 4 (Stage E, Category B strip): the messaging.4th
* routing-table registration that used to happen here (`S" %s" 3
* VM-NAME-REG`) is gone -- sk_repl_dispatch_line() (repl.c) has not
* read that table since task 3.9's own cutover (it resolves the
* paired identity directly via capsule_vm_find_by_name(), not an
* index), and messaging.4th (which defined VM-NAME-REG) no longer
* exists at all as of this strip. This was already dead, just not
* yet removed -- confirmed by reading, not assumed. */
console_puts("WIREBIND: ");
console_puts(username);
console_println(" attached and ready -- USE it to begin");
}
/* Shared by capsule_wirebind_eject() and capsule_wirebind_unclean_detach():
* resolve the tracked attached-VM id to a live entry, or clear the tracked
* state and report "nothing attached" if it's already gone (idempotent --
* matches capsule_vm_kill()'s own convention). */
static int wirebind_resolve_attached(VMRegistryEntry *out) {
if (!g_wirebind_attached_valid) return -1;
if (capsule_vm_registry_get(g_wirebind_attached_vm_id, out) != 0 ||
out->state != VM_STATE_LIVE) {
g_wirebind_attached_valid = 0;
g_wirebind_attached_username[0] = '\0';
return -1;
}
return 0;
}
/* FABRIC-3.md, 2026-09-09: shared by capsule_wirebind_eject() and
* capsule_wirebind_unclean_detach() -- tears down the *console* VM
* (registered under the bare username, e.g. "00" -- capsule_console_
* birth() in capsule_wirebind_try_attach() above) paired with the user
* VM ("<username>~user") those two functions already kill.
*
* Real bug found live, 2026-09-09: both functions used to compare
* sk_repl_get_active_vm() against entry.vm_ptr, where `entry` comes
* from wirebind_resolve_attached() -- which resolves g_wirebind_
* attached_vm_id, the *user* VM's id (see that field's own doc comment:
* "the user VM, not the console VM"). But USE/g_repl_active_vm always
* points at the *console* VM, never the user VM -- so that comparison
* was never true, the dangling-pointer guard never fired, and the
* console VM was never killed at all here, just silently orphaned
* (paired to a now-dead user VM, but still live and still whatever the
* active REPL session was). Confirmed live: reusing an xHCI port for a
* second identity right after an unclean detach of a first left the
* console stuck showing the first identity's own dead-paired prompt.
*
* Takes the bare username explicitly (FABRIC-3.md §XXVIII Stage 4,
* 2026-09-14 -- was g_wirebind_attached_username directly, which only
* ever names the single console-paired identity; unclean-detach of any
* other simultaneously-live identity needs its own username, not the
* console-pairing singleton's, so this can no longer read the global
* itself). capsule_wirebind_eject() still passes the singleton's own
* username, since EJECT (a typed command) is inherently about "whichever
* identity is currently console-paired." */
static void wirebind_teardown_console(const char *username) {
if (!username || username[0] == '\0') return;
VMRegistryEntry console_entry;
if (capsule_vm_find_by_name(username, &console_entry) != 0 ||
console_entry.state != VM_STATE_LIVE) {
return;
}
if (sk_repl_get_active_vm() == (VM *)console_entry.vm_ptr) {
sk_repl_set_active_vm((VM *)0);
}
capsule_vm_kill(console_entry.name);
}
static void wirebind_live_remove(wirebind_live_entry_t *e) {
if (!e) return;
/* FABRIC-3.md §XXVIII Stage 4 (2026-09-14): centralized here rather
* than at each of eject()/unclean_detach()'s own call sites -- both
* paths that legitimately free this VM (as opposed to the tombstone
* path, which does not call this while a VM is still switched-out)
* must release its switch-signal slot the same way, or repeated
* attach/detach cycles exhaust the bounded slot table. No-op if it
* was never registered (table was full at attach time). */
if (e->switch_slot >= 0) {
sk_vm_switch_signal_unregister(e->user_vm_id);
}
e->in_use = 0;
e->dev = (struct blkio_dev *) 0;
e->username[0] = '\0';
e->switch_slot = -1;
}
int capsule_wirebind_eject(void) {
VMRegistryEntry entry;
if (wirebind_resolve_attached(&entry) != 0) {
console_println("EJECT: nothing attached via WIREBIND");
return -1;
}
/* Drive is still physically present -- flush before it's safe to
* remove (§F.10 decision 1). */
blk_vm_flush_all((VM *)entry.vm_ptr);
wirebind_teardown_console(g_wirebind_attached_username);
capsule_vm_kill(entry.name);
wirebind_live_remove(wirebind_live_find_by_dev(g_wirebind_attached_dev));
g_wirebind_attached_valid = 0;
g_wirebind_attached_dev = (struct blkio_dev *) 0;
g_wirebind_attached_username[0] = '\0';
console_puts("EJECT: ");
console_puts(entry.name);
console_println(" flushed and detached");
return 0;
}
void capsule_wirebind_unclean_detach(struct blkio_dev *dev) {
/* FABRIC-3.md §XXVIII Stage 4 (2026-09-14): was `dev !=
* g_wirebind_attached_dev`, comparing against the single console-
* pairing global -- correct only for the single most-recently-
* attached identity (see g_wirebind_live[]'s own doc comment above
* for the real bug this caused: every earlier identity's device-pull
* was silently ignored, leaking that VM forever). Now resolves via
* the per-device live table instead, so any of several simultaneously
* live identities' own device-pull is handled correctly. */
wirebind_live_entry_t *live = wirebind_live_find_by_dev(dev);
if (!live) return; /* not a tracked WIREBIND device, or already torn down */
VMRegistryEntry entry;
if (capsule_vm_registry_get(live->user_vm_id, &entry) != 0 ||
entry.state == VM_STATE_DEAD || entry.state == VM_STATE_STILLBORN) {
wirebind_live_remove(live);
return;
}
/* Device is already gone (§F.10 decision 2) -- no flush attempted;
* data since the last flush is lost, matching real-OS unclean-removal
* semantics. The console VM has no switch-signal participation (only
* user VMs do, once Stage 4's registration lands) so it is always
* safely LIVE and killable regardless of the user VM's own state
* below. */
wirebind_teardown_console(live->username);
int killed = (capsule_vm_kill(entry.name) == 0);
/* Only clear the console-pairing singleton if THIS device is the one
* it currently tracks -- an unrelated identity's own detach must
* never disturb who is paired to the one physical console. */
if (g_wirebind_attached_valid && g_wirebind_attached_dev == dev) {
g_wirebind_attached_valid = 0;
g_wirebind_attached_dev = (struct blkio_dev *) 0;
g_wirebind_attached_username[0] = '\0';
}
if (killed) {
console_puts("WIREBIND: ");
console_puts(entry.name);
console_println(" detached without flush (device already gone)");
wirebind_live_remove(live);
} else {
/* capsule_vm_kill() refused -- VM_STATE_SWITCHED_OUT, a live
* register/stack context is parked on this VM's own native
* stack. Mark it for the Stage 3 checkpoint (vm_core.c) to reap
* on its own next attempt to resume this VM, instead of freeing
* memory a parked context still points into -- see
* VMRegistryEntry.pending_reap's own doc comment. The live-table
* entry stays in place until then: capsule_wirebind_reap_idle_
* check() below removes it once capsule_vm_force_reap() has
* actually run and the VM reads back DEAD. */
capsule_vm_set_pending_reap(live->user_vm_id, 1);
console_puts("WIREBIND: ");
console_puts(entry.name);
console_println(" device gone but VM is switched-out -- deferred reap");
}
}
void capsule_wirebind_reap_idle_check(void) {
/* FABRIC-3.md §XXVIII Stage 4 (2026-09-14): the Stage 3 checkpoint
* (vm_core.c) is the only thing that actually frees a pending_reap
* VM -- see capsule_vm_force_reap()'s own doc comment for why (it
* must run at a cooperative point the switcher itself controls, not
* from this idle tick). This sweep only cleans up the now-stale
* live-table entry once that has already happened; it is not the
* reap itself and never calls capsule_vm_kill()/force_reap(). */
for (uint32_t i = 0; i < WIREBIND_MAX_LIVE; i++) {
if (!g_wirebind_live[i].in_use) continue;
VMRegistryEntry entry;
if (capsule_vm_registry_get(g_wirebind_live[i].user_vm_id, &entry) != 0 ||
entry.state == VM_STATE_DEAD) {
wirebind_live_remove(&g_wirebind_live[i]);
}
}
}
const char *capsule_wirebind_attached_username(void) {
if (!g_wirebind_attached_valid) return (const char *)0;
return g_wirebind_attached_username;
}
void capsule_wirebind_overflow_idle_check(void) {
if (!g_wirebind_attached_valid || !g_wirebind_attached_dev) return;
VMRegistryEntry entry;
if (capsule_vm_registry_get(g_wirebind_attached_vm_id, &entry) != 0 ||
entry.state != VM_STATE_LIVE || !entry.vm_ptr) {
return;
}
uint64_t free_blocks, total_blocks;
if (blk_get_device_free_blocks(g_wirebind_attached_dev, &free_blocks, &total_blocks)
!= BLK_OK || total_blocks == 0) {
return;
}
if (free_blocks * 100u >= total_blocks * WIREBIND_OVERFLOW_FREE_THRESHOLD_PCT) {
return; /* plenty of room left on the drive itself */
}
uint8_t owner_fp[8];
memcpy(owner_fp, ((VM *)entry.vm_ptr)->identity.owner_pubkey, sizeof(owner_fp));
if (blk_owner_has_claim(owner_fp)) {
return; /* already extended once -- not a growth loop, see header doc */
}
uint32_t chain_head;
if (blk_firsttouch_claim(owner_fp, WIREBIND_OVERFLOW_CLAIM_DEVBLOCKS, &chain_head)
!= BLK_OK) {
console_println("WIREBIND: overflow trigger fired but Artemis has no room to extend into");
return;
}
console_puts("WIREBIND: ");
console_puts(g_wirebind_attached_username);
console_println(" running low on drive space -- extended onto system-resident storage");
}
+211
View File
@@ -0,0 +1,211 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
Licensed under the StarForth License, Version 1.0
*/
#ifndef __STARKERNEL__
#error "capsule_zuse_boot.c is kernel-only"
#endif
#include "starkernel/capsule_zuse_boot.h"
#include "starkernel/capsule_mint.h"
#include "starkernel/zuse_genesis_marker.h"
#include "starkernel/user_identity_seed.h"
#include "starkernel/console.h"
#include "block_subsystem.h" /* compute_crc64(), blk_meta_zone_read/write */
#include "blkio.h"
#include <string.h>
#include <stddef.h>
/* FABRIC-3.md §VII follow-on, 2026-09-06: g_zuse_attached_this_device was a
* bare boolean under the single-USB-device constraint (§F.8) -- "a detach
* event is hers" needed no device comparison because only one device could
* ever be attached at all. That premise is stale (FABRIC-3.md §VII, the
* xHCI/BOT driver now supports genuine simultaneous multi-device attach) --
* confirmed live during this session's own 8-identity verification: with
* Zuse's drive and a WIREBIND target both attached, detaching the *target*
* incorrectly logged Zuse out too, because capsule_zuse_boot_logout() had
* no way to tell "some other device detached" from "my own device
* detached." g_zuse_attached_dev is the fix, mirroring capsule_wirebind.c's
* own g_wirebind_attached_dev precedent exactly -- the boolean stays (still
* useful as a fast "is she attached at all" check) but logout now also
* requires the departing device to match. */
static int g_zuse_attached_this_device = 0;
static struct blkio_dev *g_zuse_attached_dev = (struct blkio_dev *) 0;
/* Read exactly one devblock (4096 bytes) at devblock offset `devblock`,
* as 4 consecutive 1KiB forth-block reads -- mirrors capsule_runcap.c's
* and homeblocks_sig_check()'s own read convention. */
static int read_devblock(struct blkio_dev *dev, uint32_t devblock, uint8_t *buf4096) {
uint32_t base = devblock * 4u;
for (uint32_t i = 0; i < 4u; i++) {
if (blkio_read((blkio_dev_t *)dev, base + i,
buf4096 + (size_t)i * BLKIO_FORTH_BLOCK_SIZE) != BLKIO_OK) {
return -1;
}
}
return 0;
}
static int genesis_marker_read(zuse_genesis_marker_t *out) {
if (blk_meta_zone_read(0, (uint8_t *)out) != 0) return -1;
if (out->magic != ZUSE_GENESIS_MARKER_MAGIC) return -1;
if (out->version != ZUSE_GENESIS_MARKER_VERSION) return -1;
uint64_t want_crc = compute_crc64((const uint8_t *)out, offsetof(zuse_genesis_marker_t, crc));
if (want_crc != out->crc) return -1;
return 0;
}
static void install_and_activate(VM *mama_vm, struct blkio_dev *dev,
const uint8_t seed[32], const uint8_t pubkey[32]) {
/* vm_zuse_cert_install() is deliberately one-way (returns -1, no-op,
* once vm->zuse_cert_installed is already 1) -- that's a real
* security property (the cert/pubkey must never be re-installed or
* swapped) and stays untouched here. Its return is NOT used to gate
* ACL-ZUSE-BOOT below anymore (FABRIC-2.md §I.8, re-scoped
* 2026-09-04): re-authenticating after capsule_zuse_boot_logout()
* needs ACL-ZUSE-BOOT to re-run and re-set zuse_session even when
* the cert itself was already installed from an earlier boot-time
* attach -- gating it on install's success/failure made re-login
* impossible, since a second install always fails by design. */
vm_zuse_cert_install(mama_vm, seed, pubkey);
/* Real defect found live 2026-09-22 verifying FABRIC-3.6.md task 3.9:
* capsule_zuse_boot_load_root_pubkey() is the only other writer of
* zuse_root_pubkey_known, and it only runs once, at Artemis's
* boot-time virtio-blk attach (kernel_main.c) -- before genesis mint
* has happened on a fresh artemis.img, so it finds no marker yet and
* leaves the flag 0. Nothing re-triggers it after genesis mint
* completes. Silent result: capsule_wirebind_try_attach()'s own
* `if (!mama_vm->zuse_root_pubkey_known) return;` gate (this file's
* sibling function) then refuses every identity attach for the rest
* of the boot, with no message at all -- exactly the failure mode
* FABRIC-3.5.md SXXXV.0 names. Fixed at the source: the pubkey this
* function was just handed (from genesis mint, above, or from an
* already-Zuse re-attach, below) IS the root pubkey -- activate it
* directly here instead of relying on a disk re-read that may never
* happen this session. Matches vm_zuse_cert_install()'s own one-way
* idempotence (harmless to set twice). */
memcpy(mama_vm->zuse_root_pubkey, pubkey, 32);
mama_vm->zuse_root_pubkey_known = 1;
/* zuse.4th's ACL-ZUSE-BOOT self-activated once already at Mama's own
* birth, when no cert was installed yet (the thumbdrive wasn't
* attached at that early, one-shot point) -- ACL-PIN only blocks
* *redefinition*, not re-execution, so re-running the same policy
* word here is the correct, already-designed way to activate
* zuse_session now that a cert genuinely exists. No new C-side auth
* logic; policy stays in ACL.4th/zuse.4th per this project's own
* convention. */
vm_interpret(mama_vm, "ACL-ZUSE-BOOT");
g_zuse_attached_this_device = 1;
g_zuse_attached_dev = dev;
/* Decided 2026-09-05: no console for the running system unless a
* thumbdrive is present. Zuse's own login is not special here --
* "nothing special about zuse as a user except zuse has no ACLs,"
* per direct instruction. Revised 2026-09-06: no longer calls
* sk_console_mark_login() (a one-way sticky flag) -- the gate is now a
* live check (sk_console_identity_present(), repl.c) driven directly
* by mama_vm->zuse_session, which vm_interpret(mama_vm,
* "ACL-ZUSE-BOOT") above already sets. Nothing to call here anymore. */
}
void capsule_zuse_boot_try_attach(struct blkio_dev *dev,
homeblocks_sig_result_t sig_rc,
const homeblocks_sig_t *sig,
VM *mama_vm) {
if (!dev || !mama_vm) return;
/* FABRIC-2.md §I.8, re-scoped 2026-09-04: gate on zuse_session, not
* zuse_cert_installed. zuse_cert_installed never clears (one-way,
* see install_and_activate()'s own comment) and would permanently
* block re-authentication after a logout; zuse_session does clear
* (capsule_zuse_boot_logout()), which is exactly the "already have
* Zuse THIS SESSION" check this early-return is actually for. */
if (mama_vm->zuse_session) return;
zuse_genesis_marker_t marker;
int have_marker = (genesis_marker_read(&marker) == 0);
if (!have_marker) {
if (sig_rc != HOMEBLOCKS_SIG_BLANK) return; /* not eligible for genesis */
uint8_t seed[32], pubkey[32];
MintResult r = capsule_mint_identity(dev, (VM *)0, "Zuse", "zuse",
(const char *)0, (const char *)0,
pubkey, seed,
MINT_PERSONALITY_DEFAULT, /* Zuse is the superuser -- never restricted */
1 /* sig_rc already confirmed BLANK above */);
if (r != MINT_OK) {
console_println("Zuse: genesis mint failed");
return;
}
zuse_genesis_marker_t wm;
memset(&wm, 0, sizeof(wm));
wm.magic = ZUSE_GENESIS_MARKER_MAGIC;
wm.version = ZUSE_GENESIS_MARKER_VERSION;
memcpy(wm.zuse_pubkey, pubkey, 32);
wm.crc = compute_crc64((const uint8_t *)&wm, offsetof(zuse_genesis_marker_t, crc));
if (blk_meta_zone_write(0, (const uint8_t *)&wm) != 0) {
console_println("Zuse: genesis minted but fence marker write FAILED (not persistent)");
} else {
console_println("Zuse: genesis minted onto attached thumbdrive");
}
install_and_activate(mama_vm, dev, seed, pubkey);
return;
}
/* Marker present: genesis already happened, on some thumbdrive.
* Only act if THIS attach is that drive. */
if (sig_rc != HOMEBLOCKS_SIG_OK || !sig) return;
if (sig->identity_src_offset == 0 || sig->identity_src_devblocks < 1) return;
user_identity_seed_t idrec;
if (read_devblock(dev, sig->identity_src_offset, (uint8_t *)&idrec) != 0) return;
/* Same magic -> version -> CRC-64 discipline as genesis_marker_read():
* this record carries Zuse's private key (the seed), so a corrupt or
* format-mismatched record must be refused, never loaded -- a bad CRC
* could otherwise install a garbage seed as Zuse's identity. */
if (idrec.magic != USER_IDENTITY_SEED_MAGIC) return;
if (idrec.version != USER_IDENTITY_SEED_VERSION) return;
uint64_t want_crc = compute_crc64((const uint8_t *)&idrec,
offsetof(user_identity_seed_t, crc));
if (want_crc != idrec.crc) return;
if (memcmp(idrec.pubkey, marker.zuse_pubkey, 32) != 0) return; /* not Zuse's drive */
console_println("Zuse: identity confirmed from attached thumbdrive");
install_and_activate(mama_vm, dev, idrec.seed, idrec.pubkey);
}
void capsule_zuse_boot_logout(VM *mama_vm, struct blkio_dev *dev) {
if (!mama_vm || !g_zuse_attached_this_device) return;
/* The device actually detaching must be hers -- see g_zuse_attached_dev's
* own doc comment above. A different device (a WIREBIND target,
* general-purpose USB use) detaching while her own drive stays
* attached must not touch her session at all. */
if (dev != g_zuse_attached_dev) return;
mama_vm->zuse_session = 0;
g_zuse_attached_this_device = 0;
g_zuse_attached_dev = (struct blkio_dev *) 0;
console_println("Zuse: session ended -- reattach to re-authenticate");
}
struct blkio_dev *capsule_zuse_boot_attached_dev(void) {
return g_zuse_attached_this_device ? g_zuse_attached_dev : (struct blkio_dev *) 0;
}
void capsule_zuse_boot_load_root_pubkey(VM *mama_vm) {
if (!mama_vm || mama_vm->zuse_root_pubkey_known) return;
zuse_genesis_marker_t marker;
if (genesis_marker_read(&marker) != 0) return; /* no genesis yet -- nothing to load */
memcpy(mama_vm->zuse_root_pubkey, marker.zuse_pubkey, 32);
mama_vm->zuse_root_pubkey_known = 1;
}
File diff suppressed because it is too large Load Diff
+43
View File
@@ -0,0 +1,43 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
Licensed under the StarForth License, Version 1.0
*/
#ifndef __STARKERNEL__
#error "vm_identity.c is kernel-only"
#endif
#include "starkernel/vm_identity.h"
#include "starkernel/x509_ed25519.h"
#include <string.h>
int vm_identity_has_cap(const VMIdentity *id, uint32_t cap) {
if (!id || !id->installed) return 0;
return (id->acl_caps & cap) == cap;
}
int vm_identity_from_cert(VMIdentity *out, const uint8_t *der, size_t der_len,
const uint8_t issuer_pubkey[32],
const uint8_t drive_uuid[16], uint32_t acl_caps) {
if (!out || !der || !issuer_pubkey || !drive_uuid) return -1;
if (x509_verify_signature(der, der_len, issuer_pubkey) != 0) return -1;
uint8_t serial[32];
size_t serial_len = 0;
if (x509_extract_serial(der, der_len, serial, sizeof(serial), &serial_len) != 0)
return -1;
if (serial_len != 16 || memcmp(serial, drive_uuid, 16) != 0) return -1;
uint8_t owner_pubkey[32];
if (x509_extract_ed25519_pubkey(der, der_len, owner_pubkey) != 0) return -1;
memcpy(out->owner_pubkey, owner_pubkey, 32);
out->installed = 1;
out->acl_caps = acl_caps;
return 0;
}
+137
View File
@@ -0,0 +1,137 @@
/*
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.
You may obtain a copy of the License at:
https://github.com/star.4th@proton.me/StarForth/LICENSE.txt
This software is provided "AS IS", WITHOUT WARRANTY OF ANY KIND,
express or implied, including but not limited to the warranties of
merchantability, fitness for a particular purpose, and noninfringement.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* vm_uuid.c - 128-bit VM identifiers (FABRIC-0.md punch list item 3.8)
*
* See vm_uuid.h for the design rationale (no RNG source exists on any of
* the three ISAs uniformly, so this is deterministic, not random).
*/
#include "starkernel/vm_uuid.h"
#ifdef __STARKERNEL__
#include <stddef.h>
#define VM_UUID_POOL_SIZE 16
static uint64_t vm_uuid_prng_state = 0;
static int vm_uuid_seeded = 0;
static VMUuid vm_uuid_pool[VM_UUID_POOL_SIZE];
static size_t vm_uuid_pool_pos = VM_UUID_POOL_SIZE; /* "empty": first next() call refills */
VMUuid vm_uuid_hera(void) {
VMUuid id;
id.hi = 0;
id.lo = 0;
return id;
}
int vm_uuid_is_hera(VMUuid id) {
return id.hi == 0 && id.lo == 0;
}
VMUuid vm_uuid_none(void) {
VMUuid id;
id.hi = 0xFFFFFFFFFFFFFFFFULL;
id.lo = 0xFFFFFFFFFFFFFFFFULL;
return id;
}
int vm_uuid_is_none(VMUuid id) {
return id.hi == 0xFFFFFFFFFFFFFFFFULL && id.lo == 0xFFFFFFFFFFFFFFFFULL;
}
int vm_uuid_equal(VMUuid a, VMUuid b) {
return a.hi == b.hi && a.lo == b.lo;
}
/* splitmix64 -- public-domain, well-known, minimal. Deterministic given a
* seed; not cryptographically random, not claimed to be. */
static uint64_t splitmix64_next(uint64_t *state) {
uint64_t z = (*state += 0x9E3779B97F4A7C15ULL);
z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9ULL;
z = (z ^ (z >> 27)) * 0x94D049BB133111EBULL;
return z ^ (z >> 31);
}
static void vm_uuid_pool_refill(void) {
size_t i;
for (i = 0; i < VM_UUID_POOL_SIZE; i++) {
uint64_t hi = splitmix64_next(&vm_uuid_prng_state);
uint64_t lo = splitmix64_next(&vm_uuid_prng_state);
/* RFC 4122 version 4 / variant bits -- cosmetic shape only, not a
* claim these are cryptographically random. */
hi = (hi & 0xFFFFFFFFFFFF0FFFULL) | 0x0000000000004000ULL;
lo = (lo & 0x3FFFFFFFFFFFFFFFULL) | 0x8000000000000000ULL;
vm_uuid_pool[i].hi = hi;
vm_uuid_pool[i].lo = lo;
}
vm_uuid_pool_pos = 0;
}
void vm_uuid_pool_init(uint64_t seed) {
vm_uuid_prng_state = seed;
vm_uuid_seeded = 1;
vm_uuid_pool_refill();
}
VMUuid vm_uuid_next(void) {
VMUuid id;
if (!vm_uuid_seeded) {
/* Safety net, not the intended path -- callers should seed from the
* Mama capsule's content hash before the first non-Hera VM birth. */
vm_uuid_pool_init(0x9E3779B97F4A7C15ULL);
}
if (vm_uuid_pool_pos >= VM_UUID_POOL_SIZE) {
vm_uuid_pool_refill();
}
id = vm_uuid_pool[vm_uuid_pool_pos];
vm_uuid_pool_pos++;
return id;
}
void vm_uuid_format(VMUuid id, char *buf) {
static const char hex[] = "0123456789abcdef";
int pos = 0;
int i;
uint64_t halves[2];
/* Group boundaries per RFC 4122: 8-4-4-4-12 hex digits, dashes after
* digit 8, 12, 16, 20 (of 32 total). hi supplies digits 0-15, lo
* supplies digits 16-31. */
halves[0] = id.hi;
halves[1] = id.lo;
for (i = 0; i < 32; i++) {
uint64_t v = (i < 16) ? halves[0] : halves[1];
int shift = (15 - (i % 16)) * 4;
buf[pos++] = hex[(v >> shift) & 0xF];
if (i == 7 || i == 11 || i == 15 || i == 19) buf[pos++] = '-';
}
buf[pos] = '\0';
}
#endif /* __STARKERNEL__ */
+107
View File
@@ -0,0 +1,107 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
Licensed under the StarForth License, Version 1.0
*/
#ifndef __STARKERNEL__
#error "zuse_eligibility.c is kernel-only"
#endif
#include "starkernel/zuse_eligibility.h"
#include "starkernel/zuse_eligibility_list.h"
#include "block_subsystem.h" /* compute_crc64(), blk_meta_zone_read/write */
#include <string.h>
#include <stddef.h>
/* Read and validate one devblock of the chain. Mirrors
* capsule_zuse_boot.c's genesis_marker_read() convention exactly: magic
* mismatch, version mismatch, and CRC mismatch are all merged into a
* single -1 ("treat as absent/corrupt, not present") -- the fence region
* is never zeroed at format time (block_subsystem.c's blk_commit_format()
* only zeroes BAM/reloc), so blank foreign bytes and genuine corruption
* are indistinguishable and must be handled identically here. */
static int list_devblock_read(uint32_t devblock_from_top, zuse_eligibility_devblock_t *out) {
if (blk_meta_zone_read(devblock_from_top, (uint8_t *)out) != 0) return -1;
if (out->magic != ZUSE_ELIGIBILITY_LIST_MAGIC) return -1;
if (out->version != ZUSE_ELIGIBILITY_LIST_VERSION) return -1;
if (out->count > ZUSE_ELIGIBILITY_ENTRIES_PER_DEVBLOCK) return -1;
uint64_t want_crc = compute_crc64((const uint8_t *)out,
offsetof(zuse_eligibility_devblock_t, crc));
if (want_crc != out->crc) return -1;
return 0;
}
static int list_devblock_write(uint32_t devblock_from_top, zuse_eligibility_devblock_t *blk) {
blk->crc = compute_crc64((const uint8_t *)blk, offsetof(zuse_eligibility_devblock_t, crc));
return blk_meta_zone_write(devblock_from_top, (const uint8_t *)blk) == 0 ? 0 : -1;
}
int zuse_eligibility_is_member(const uint8_t pubkey[32]) {
uint32_t devblock = ZUSE_ELIGIBILITY_LIST_HEAD_DEVBLOCK;
zuse_eligibility_devblock_t blk;
for (;;) {
if (list_devblock_read(devblock, &blk) != 0) return 0; /* fail-closed */
for (uint32_t i = 0; i < blk.count; i++) {
if (memcmp(blk.entries[i], pubkey, 32) == 0) return 1;
}
if (blk.next_devblock_from_top == ZUSE_ELIGIBILITY_LIST_NO_NEXT) return 0;
devblock = blk.next_devblock_from_top;
}
}
int zuse_eligibility_add(const uint8_t pubkey[32]) {
uint32_t devblock = ZUSE_ELIGIBILITY_LIST_HEAD_DEVBLOCK;
zuse_eligibility_devblock_t blk;
if (list_devblock_read(devblock, &blk) != 0) {
/* List doesn't exist yet (blank fence, or this is genuinely the
* first entry ever added) -- create the head devblock fresh. */
memset(&blk, 0, sizeof(blk));
blk.magic = ZUSE_ELIGIBILITY_LIST_MAGIC;
blk.version = ZUSE_ELIGIBILITY_LIST_VERSION;
blk.count = 1;
blk.next_devblock_from_top = ZUSE_ELIGIBILITY_LIST_NO_NEXT;
memcpy(blk.entries[0], pubkey, 32);
return list_devblock_write(devblock, &blk);
}
/* Walk the existing chain: bail out early (idempotent success) if
* already present, otherwise track the tail devblock to append to. */
for (;;) {
for (uint32_t i = 0; i < blk.count; i++) {
if (memcmp(blk.entries[i], pubkey, 32) == 0) return 0; /* already eligible */
}
if (blk.next_devblock_from_top == ZUSE_ELIGIBILITY_LIST_NO_NEXT) break;
devblock = blk.next_devblock_from_top;
if (list_devblock_read(devblock, &blk) != 0) return -1; /* corrupt mid-chain */
}
if (blk.count < ZUSE_ELIGIBILITY_ENTRIES_PER_DEVBLOCK) {
memcpy(blk.entries[blk.count], pubkey, 32);
blk.count++;
return list_devblock_write(devblock, &blk);
}
/* Tail devblock is full -- chain a fresh one drawn from the
* already-reserved fence budget, one devblock_from_top past the
* current tail (blk_meta_zone_write() itself refuses any offset at
* or beyond the on-disk meta_fence_blocks reservation, so an
* exhausted budget surfaces here as an ordinary write failure). */
uint32_t new_devblock = devblock + 1;
zuse_eligibility_devblock_t new_blk;
memset(&new_blk, 0, sizeof(new_blk));
new_blk.magic = ZUSE_ELIGIBILITY_LIST_MAGIC;
new_blk.version = ZUSE_ELIGIBILITY_LIST_VERSION;
new_blk.count = 1;
new_blk.next_devblock_from_top = ZUSE_ELIGIBILITY_LIST_NO_NEXT;
memcpy(new_blk.entries[0], pubkey, 32);
if (list_devblock_write(new_devblock, &new_blk) != 0) return -1;
blk.next_devblock_from_top = new_devblock;
return list_devblock_write(devblock, &blk);
}