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:
@@ -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.
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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 */
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
@@ -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__ */
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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__ */
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user