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

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

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

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

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

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

Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
rajames
2026-10-01 15:40:09 -04:00
co-authored by Junie
parent 3c709c115b
commit a8b70e88d3
630 changed files with 2646 additions and 2036 deletions
+563
View File
@@ -0,0 +1,563 @@
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
Licensed under the StarForth License, Version 1.0
*/
#ifndef __STARKERNEL__
#error "capsule_wirebind.c is kernel-only"
#endif
#include "starkernel/capsule_wirebind.h"
#include "starkernel/capsule_birth.h"
#include "starkernel/capsule_runcap.h"
#include "starkernel/capsule_console.h"
#include "starkernel/vm_identity.h"
#include "starkernel/user_identity_seed.h"
#include "starkernel/console.h"
#include "starkernel/repl.h" /* sk_repl_get/set_active_vm() -- FABRIC-2.md §F.10 EJECT */
#include "starkernel/capsule_vm_switch_signal.h" /* FABRIC-3.md §XXVIII Stage 4 -- register/unregister */
#include "blkio.h"
#include "block_subsystem.h" /* compute_crc64() */
#include "word_source/include/block_words.h" /* blk_vm_flush_all() -- §F.10 EJECT */
#include "freestanding/stdio.h"
#include <string.h>
#include <stddef.h>
/* FABRIC-2.md §F.10: "the VM currently attached via the home-blocks USB
* path" -- WIREBIND's own to track, set at successful birth in
* capsule_wirebind_try_attach(), read and cleared by both
* capsule_wirebind_eject() and capsule_wirebind_unclean_detach(). Single-
* USB-device constraint (§F.8) means there is never more than one
* candidate, so one tracked id is enough -- no set/list. */
static VMUuid g_wirebind_attached_vm_id;
static int g_wirebind_attached_valid = 0;
/* Plain username (no "~user" registry-name suffix), tracked separately
* from g_wirebind_attached_vm_id -- the registry entry's own name is
* user_vm_name ("<username>~user"), not the bare form the (user) prompt
* segment wants (FABRIC-2.md §I.1/4.4s). */
static char g_wirebind_attached_username[USER_IDENTITY_USERNAME_MAX] = {0};
/* FABRIC-2.md §I.2 overflow trigger, 2026-09-05: the attached drive's own
* blkio_dev pointer, tracked alongside the two fields above so
* capsule_wirebind_overflow_idle_check() can query its free space later
* without a new attach-time argument threaded through the idle loop. */
static struct blkio_dev *g_wirebind_attached_dev = (struct blkio_dev *) 0;
/* FABRIC-3.md §XXVIII Stage 4 (2026-09-14): the singleton above correctly
* models "who is paired to the one physical console" (repl.c's own
* console-relay/`(user)` prompt logic genuinely is single-valued -- there
* is only one serial/framebuffer console). It does NOT correctly model
* "which devices are live," despite g_wirebind_attached_eject()'s own
* header doc claiming a "single-USB-device constraint (§F.8)" -- that
* claim is stale: §XV/§XVI (2026-09-11/12) proved 9 identities genuinely
* simultaneously live via this exact attach path. Every attach after the
* first silently overwrote the singleton, so capsule_wirebind_unclean_
* detach()'s `dev != g_wirebind_attached_dev` check only ever matched the
* *most recently attached* identity -- any earlier identity's drive
* being pulled hit that check, returned early, and orphaned: never torn
* down, its VM and native stack leaked forever, no console/log trace at
* all. Silent because nothing in this project's verification history
* ever detached anything but the single most-recent identity.
*
* This new table is additive and separate from the console singleton
* above (which is untouched, still governs console pairing exactly as
* before) -- it tracks every currently-live WIREBIND identity by its own
* device pointer, so unclean-detach (and Stage 4's own switch-signal
* registration bookkeeping) can resolve *any* attached device to *its
* own* identity, not just the last one. Sized off messaging.4th's own
* VM-MAX (16) minus the 3 slots Hera/Hermes/Artemis permanently reserve
* (capsules/common/messaging.4th: "identities get NEW slots 3-10, never
* renumbered") -- a real, already-agreed system-wide ceiling, not a new
* guess (see feedback_no_hardcoded_small_scale_bounds in project memory:
* Bob has rejected an invented cap here before). */
#define WIREBIND_MAX_LIVE 13u
typedef struct {
int in_use;
struct blkio_dev *dev;
VMUuid user_vm_id;
char username[USER_IDENTITY_USERNAME_MAX];
int switch_slot; /* Stage 4: index into the switch-signal
* table, or -1 if not yet registered
* (wired in the next increment). */
} wirebind_live_entry_t;
static wirebind_live_entry_t g_wirebind_live[WIREBIND_MAX_LIVE];
static wirebind_live_entry_t *wirebind_live_find_by_dev(struct blkio_dev *dev) {
for (uint32_t i = 0; i < WIREBIND_MAX_LIVE; i++) {
if (g_wirebind_live[i].in_use && g_wirebind_live[i].dev == dev) {
return &g_wirebind_live[i];
}
}
return (wirebind_live_entry_t *)0;
}
static wirebind_live_entry_t *wirebind_live_alloc(void) {
for (uint32_t i = 0; i < WIREBIND_MAX_LIVE; i++) {
if (!g_wirebind_live[i].in_use) return &g_wirebind_live[i];
}
return (wirebind_live_entry_t *)0;
}
/* WIREBIND_CERT_MAX_DEVBLOCKS: a sane upper bound on how much cert
* content this reads, independent of whatever sig->cert_devblocks
* claims -- MINT (capsule_mint.c) only ever writes 1 devblock's worth
* (a real cert is ~150-250 bytes), so 4 devblocks (16KB) is already
* generous headroom, not a real constraint. */
#define WIREBIND_CERT_MAX_DEVBLOCKS 4u
/* FABRIC-2.md §I.2 overflow trigger, 2026-09-05: fixed constants, "fixed
* first" per this project's own standing sequencing (MIGRATION_WEAR_
* THRESHOLD, ZUSE_SESSION_TTL_SECONDS -- adaptive only after a real,
* measured baseline exists, not from day one). A single DoE-tunable knob
* apiece, not yet wired to Kconfig -- revisit once real usage data exists.
* 10% is a conservative first guess (extend well before actually full);
* 8 devblocks (24 Forth blocks, ~24 KiB) is a modest first extension, not
* a large land-grab. */
#define WIREBIND_OVERFLOW_FREE_THRESHOLD_PCT 10u
#define WIREBIND_OVERFLOW_CLAIM_DEVBLOCKS 8u
/* Read exactly one devblock (4096 bytes) at devblock offset `devblock`
* -- same convention capsule_runcap.c/capsule_mint.c already use. */
static int read_devblock(struct blkio_dev *dev, uint32_t devblock, uint8_t *buf4096) {
uint32_t base = devblock * 4u;
for (uint32_t i = 0; i < 4u; i++) {
if (blkio_read((blkio_dev_t *)dev, base + i,
buf4096 + (size_t)i * BLKIO_FORTH_BLOCK_SIZE) != BLKIO_OK) {
return -1;
}
}
return 0;
}
int capsule_wirebind_verify_cert(struct blkio_dev *dev,
const homeblocks_sig_t *sig,
VM *mama_vm, VMIdentity *out) {
if (!dev || !sig || !mama_vm || !out) return -1;
/* Genesis-mode Zuse drives have no cert region -- capsule_zuse_boot_
* try_attach() owns that case, not this one. */
if (sig->cert_offset == 0 || sig->cert_devblocks == 0) return -1;
/* FABRIC-3.md, 2026-09-09: verifying a WIREBIND cert needs nothing but
* Zuse's already-public key -- it does not need her live session.
* zuse_root_pubkey_known/zuse_root_pubkey (capsule_zuse_boot_load_
* root_pubkey(), loaded unconditionally from the persistent genesis-
* marker fence as soon as Artemis's own storage is up) is what this
* checks now, not zuse_cert_installed -- that pair stays reserved for
* MINT, which genuinely does need her live private seed. One
* identity's attach must not depend on another identity's live
* presence. */
if (!mama_vm->zuse_root_pubkey_known) return -1;
/* Read the cert region into a local buffer. */
uint32_t n_devblocks = sig->cert_devblocks;
if (n_devblocks > WIREBIND_CERT_MAX_DEVBLOCKS) n_devblocks = WIREBIND_CERT_MAX_DEVBLOCKS;
uint8_t cert_der[WIREBIND_CERT_MAX_DEVBLOCKS * 4096];
for (uint32_t d = 0; d < n_devblocks; d++) {
if (read_devblock(dev, sig->cert_offset + d, cert_der + (size_t)d * 4096) != 0) {
return -1;
}
}
size_t cert_len = (size_t)n_devblocks * 4096;
memset(out, 0, sizeof(*out));
return vm_identity_from_cert(out, cert_der, cert_len,
mama_vm->zuse_root_pubkey, sig->drive_uuid,
0 /* acl_caps: no bits assigned yet, §F.2 */);
}
void capsule_wirebind_try_attach(struct blkio_dev *dev,
const homeblocks_sig_t *sig,
VM *mama_vm) {
if (!dev || !sig || !mama_vm) return;
/* Genesis-mode Zuse drives have no cert region -- capsule_zuse_boot_
* try_attach() already owns that case. Check here too so the right
* "not applicable" case stays silent (matches capsule_wirebind_
* verify_cert()'s own -1, but avoids a misleading refusal message
* for a drive this function was never going to act on anyway). */
if (sig->cert_offset == 0 || sig->cert_devblocks == 0) return;
if (!mama_vm->zuse_root_pubkey_known) return;
VMIdentity identity;
if (capsule_wirebind_verify_cert(dev, sig, mama_vm, &identity) != 0) {
console_println("WIREBIND: cert verification FAILED -- drive refused");
return;
}
/* Verified. Read the drive's own profile for its username. */
if (sig->identity_src_offset == 0 || sig->identity_src_devblocks < 1) {
console_println("WIREBIND: verified cert but no identity_src region -- refusing");
return;
}
user_identity_seed_t idrec;
if (read_devblock(dev, sig->identity_src_offset, (uint8_t *)&idrec) != 0) {
console_println("WIREBIND: verified cert but identity record unreadable -- refusing");
return;
}
/* Same magic -> version -> CRC-64 discipline as capsule_zuse_boot.c:
* the identity record is the same on-disk format, so a corrupt or
* format-mismatched record must be refused rather than trusted. */
if (idrec.magic != USER_IDENTITY_SEED_MAGIC ||
idrec.version != USER_IDENTITY_SEED_VERSION) {
console_println("WIREBIND: verified cert but identity record unreadable -- refusing");
return;
}
uint64_t want_crc = compute_crc64((const uint8_t *)&idrec,
offsetof(user_identity_seed_t, crc));
if (want_crc != idrec.crc) {
console_println("WIREBIND: verified cert but identity record corrupt -- refusing");
return;
}
char username[USER_IDENTITY_USERNAME_MAX];
memcpy(username, idrec.username, sizeof(username));
username[sizeof(username) - 1] = '\0'; /* defensive; MINT already NUL-terminates */
char user_vm_name[USER_IDENTITY_USERNAME_MAX + 8];
size_t ulen = strlen(username);
if (ulen == 0 || ulen + 6 > sizeof(user_vm_name)) {
console_println("WIREBIND: empty or oversized username -- refusing");
return;
}
memcpy(user_vm_name, username, ulen);
memcpy(user_vm_name + ulen, "~user", 6);
/* Idempotent: already bound this session (console VM already live
* under this username) -- nothing to do. Matches BIRTH's own
* idempotent convention. */
{
VMRegistryEntry existing;
if (capsule_vm_find_by_name(username, &existing) == 0 &&
existing.state == VM_STATE_LIVE) {
return;
}
}
VMUuid console_id, user_id;
void *console_ctx = (void *)0;
void *user_ctx = (void *)0;
if (capsule_console_birth(username, mama_vm->stadium_vm_id, &console_id, &console_ctx)
!= CAPSULE_RUN_OK) {
console_println("WIREBIND: console VM birth FAILED");
return;
}
/* FABRIC-3.6.md Phase 4 (Stage E): console_ctx's only use (the
* routing-table VM-NAME-REG call above) is gone -- still populated
* because capsule_console_birth()'s own signature requires an
* out-param regardless, just no longer read here. */
(void)console_ctx;
if (capsule_runcap_birth(dev, sig, user_vm_name, mama_vm->stadium_vm_id, &user_id,
&user_ctx) != CAPSULE_RUN_OK) {
console_println("WIREBIND: user VM birth FAILED");
/* The console VM above already succeeded and is live/registered
* under `username` -- without this, it's an orphan: nothing ever
* points a real user at it (WIREBIND only ever hands the caller
* the user VM's id), so it just sits there permanently holding
* its own kernel-heap arena allocation. Confirmed live 2026-09-07
* (identity-heap-capacity testing): this is exactly what turns a
* single failed identity attach into a permanent net loss of
* heap, compounding every subsequent attempt's odds of failing
* too, rather than a neutral retry. capsule_vm_kill() is the same
* teardown capsule_wirebind_eject()/capsule_wirebind_unclean_
* detach() already use elsewhere in this file -- vm_cleanup() +
* sf_free(), verified to actually reclaim per-word dictionary
* allocations (FABRIC-3.md §IX.2/§IX.3, 2026-09-06). */
capsule_vm_kill(username);
return;
}
/* Install the verified identity onto the user VM -- the one BINDSTEP
* (§F.9) will later re-verify against on every USE. */
if (user_ctx) {
((VM *)user_ctx)->identity = identity;
}
/* FABRIC-2.md §F.10: this is the successful-birth point EJECT/UNCLEAN
* need a tracked VMUuid from -- the user VM, not the console VM
* (blk_vm_flush_all()/capsule_vm_kill() below both act on the VM that
* actually owns block-subsystem state). */
g_wirebind_attached_vm_id = user_id;
g_wirebind_attached_valid = 1;
g_wirebind_attached_dev = dev;
memcpy(g_wirebind_attached_username, username, sizeof(g_wirebind_attached_username));
/* FABRIC-3.md §XXVIII Stage 4 (2026-09-14): additive per-device
* tracking, independent of the console-pairing singleton just above
* (which stays exactly as it was) -- see g_wirebind_live[]'s own doc
* comment. A full table is reported, not silently dropped: running
* out of the real, sourced ceiling here means something is actually
* wrong (more identities live than this system's own VM-MAX allows
* for), worth knowing about rather than quietly leaving a device
* untracked for unclean-detach/switch-signal purposes. */
{
wirebind_live_entry_t *live = wirebind_live_alloc();
if (live) {
live->in_use = 1;
live->dev = dev;
live->user_vm_id = user_id;
/* FABRIC-3.md §XXVIII Stage 4 (2026-09-14): the user VM is
* the one that actually runs FORTH work (VM-EXEC/MSG-TICK
* dispatch target it directly) -- it participates in
* preemptive switching. The console VM does not: it is a
* REPL-session proxy whose own control flow
* sk_repl_get_active_vm() already owns, never itself the
* target of VM-EXEC or MSG-SEND, so it has nothing for a
* switch-in to preempt. Registration failure (table full)
* degrades to "this identity won't be preempted," not to a
* failed attach -- the identity is still fully usable via
* ordinary VM-EXEC/console-relay dispatch, same as before
* Stage 4 existed. */
live->switch_slot = sk_vm_switch_signal_register(user_id);
memcpy(live->username, username, sizeof(live->username));
if (live->switch_slot < 0) {
console_println("WIREBIND: switch-signal table full -- this identity won't be preempted");
}
} else {
console_println("WIREBIND: live-identity table full -- detach/switch tracking degraded for this attach");
}
}
/* Decided 2026-09-05: no console for the running system unless a
* thumbdrive is present. Revised 2026-09-06: this used to call
* sk_console_mark_login() here (a one-way sticky flag) -- the gate is
* now a live check (sk_console_identity_present(), repl.c) driven
* directly by g_wirebind_attached_username being set above, re-checked
* continuously by sk_repl_run()'s own main loop rather than a one-shot
* signal at login time. Nothing to call here anymore. */
/* FABRIC-3.6.md Phase 4 (Stage E, Category B strip): the messaging.4th
* routing-table registration that used to happen here (`S" %s" 3
* VM-NAME-REG`) is gone -- sk_repl_dispatch_line() (repl.c) has not
* read that table since task 3.9's own cutover (it resolves the
* paired identity directly via capsule_vm_find_by_name(), not an
* index), and messaging.4th (which defined VM-NAME-REG) no longer
* exists at all as of this strip. This was already dead, just not
* yet removed -- confirmed by reading, not assumed. */
console_puts("WIREBIND: ");
console_puts(username);
console_println(" attached and ready -- USE it to begin");
}
/* Shared by capsule_wirebind_eject() and capsule_wirebind_unclean_detach():
* resolve the tracked attached-VM id to a live entry, or clear the tracked
* state and report "nothing attached" if it's already gone (idempotent --
* matches capsule_vm_kill()'s own convention). */
static int wirebind_resolve_attached(VMRegistryEntry *out) {
if (!g_wirebind_attached_valid) return -1;
if (capsule_vm_registry_get(g_wirebind_attached_vm_id, out) != 0 ||
out->state != VM_STATE_LIVE) {
g_wirebind_attached_valid = 0;
g_wirebind_attached_username[0] = '\0';
return -1;
}
return 0;
}
/* FABRIC-3.md, 2026-09-09: shared by capsule_wirebind_eject() and
* capsule_wirebind_unclean_detach() -- tears down the *console* VM
* (registered under the bare username, e.g. "00" -- capsule_console_
* birth() in capsule_wirebind_try_attach() above) paired with the user
* VM ("<username>~user") those two functions already kill.
*
* Real bug found live, 2026-09-09: both functions used to compare
* sk_repl_get_active_vm() against entry.vm_ptr, where `entry` comes
* from wirebind_resolve_attached() -- which resolves g_wirebind_
* attached_vm_id, the *user* VM's id (see that field's own doc comment:
* "the user VM, not the console VM"). But USE/g_repl_active_vm always
* points at the *console* VM, never the user VM -- so that comparison
* was never true, the dangling-pointer guard never fired, and the
* console VM was never killed at all here, just silently orphaned
* (paired to a now-dead user VM, but still live and still whatever the
* active REPL session was). Confirmed live: reusing an xHCI port for a
* second identity right after an unclean detach of a first left the
* console stuck showing the first identity's own dead-paired prompt.
*
* Takes the bare username explicitly (FABRIC-3.md §XXVIII Stage 4,
* 2026-09-14 -- was g_wirebind_attached_username directly, which only
* ever names the single console-paired identity; unclean-detach of any
* other simultaneously-live identity needs its own username, not the
* console-pairing singleton's, so this can no longer read the global
* itself). capsule_wirebind_eject() still passes the singleton's own
* username, since EJECT (a typed command) is inherently about "whichever
* identity is currently console-paired." */
static void wirebind_teardown_console(const char *username) {
if (!username || username[0] == '\0') return;
VMRegistryEntry console_entry;
if (capsule_vm_find_by_name(username, &console_entry) != 0 ||
console_entry.state != VM_STATE_LIVE) {
return;
}
if (sk_repl_get_active_vm() == (VM *)console_entry.vm_ptr) {
sk_repl_set_active_vm((VM *)0);
}
capsule_vm_kill(console_entry.name);
}
static void wirebind_live_remove(wirebind_live_entry_t *e) {
if (!e) return;
/* FABRIC-3.md §XXVIII Stage 4 (2026-09-14): centralized here rather
* than at each of eject()/unclean_detach()'s own call sites -- both
* paths that legitimately free this VM (as opposed to the tombstone
* path, which does not call this while a VM is still switched-out)
* must release its switch-signal slot the same way, or repeated
* attach/detach cycles exhaust the bounded slot table. No-op if it
* was never registered (table was full at attach time). */
if (e->switch_slot >= 0) {
sk_vm_switch_signal_unregister(e->user_vm_id);
}
e->in_use = 0;
e->dev = (struct blkio_dev *) 0;
e->username[0] = '\0';
e->switch_slot = -1;
}
int capsule_wirebind_eject(void) {
VMRegistryEntry entry;
if (wirebind_resolve_attached(&entry) != 0) {
console_println("EJECT: nothing attached via WIREBIND");
return -1;
}
/* Drive is still physically present -- flush before it's safe to
* remove (§F.10 decision 1). */
blk_vm_flush_all((VM *)entry.vm_ptr);
wirebind_teardown_console(g_wirebind_attached_username);
capsule_vm_kill(entry.name);
wirebind_live_remove(wirebind_live_find_by_dev(g_wirebind_attached_dev));
g_wirebind_attached_valid = 0;
g_wirebind_attached_dev = (struct blkio_dev *) 0;
g_wirebind_attached_username[0] = '\0';
console_puts("EJECT: ");
console_puts(entry.name);
console_println(" flushed and detached");
return 0;
}
void capsule_wirebind_unclean_detach(struct blkio_dev *dev) {
/* FABRIC-3.md §XXVIII Stage 4 (2026-09-14): was `dev !=
* g_wirebind_attached_dev`, comparing against the single console-
* pairing global -- correct only for the single most-recently-
* attached identity (see g_wirebind_live[]'s own doc comment above
* for the real bug this caused: every earlier identity's device-pull
* was silently ignored, leaking that VM forever). Now resolves via
* the per-device live table instead, so any of several simultaneously
* live identities' own device-pull is handled correctly. */
wirebind_live_entry_t *live = wirebind_live_find_by_dev(dev);
if (!live) return; /* not a tracked WIREBIND device, or already torn down */
VMRegistryEntry entry;
if (capsule_vm_registry_get(live->user_vm_id, &entry) != 0 ||
entry.state == VM_STATE_DEAD || entry.state == VM_STATE_STILLBORN) {
wirebind_live_remove(live);
return;
}
/* Device is already gone (§F.10 decision 2) -- no flush attempted;
* data since the last flush is lost, matching real-OS unclean-removal
* semantics. The console VM has no switch-signal participation (only
* user VMs do, once Stage 4's registration lands) so it is always
* safely LIVE and killable regardless of the user VM's own state
* below. */
wirebind_teardown_console(live->username);
int killed = (capsule_vm_kill(entry.name) == 0);
/* Only clear the console-pairing singleton if THIS device is the one
* it currently tracks -- an unrelated identity's own detach must
* never disturb who is paired to the one physical console. */
if (g_wirebind_attached_valid && g_wirebind_attached_dev == dev) {
g_wirebind_attached_valid = 0;
g_wirebind_attached_dev = (struct blkio_dev *) 0;
g_wirebind_attached_username[0] = '\0';
}
if (killed) {
console_puts("WIREBIND: ");
console_puts(entry.name);
console_println(" detached without flush (device already gone)");
wirebind_live_remove(live);
} else {
/* capsule_vm_kill() refused -- VM_STATE_SWITCHED_OUT, a live
* register/stack context is parked on this VM's own native
* stack. Mark it for the Stage 3 checkpoint (vm_core.c) to reap
* on its own next attempt to resume this VM, instead of freeing
* memory a parked context still points into -- see
* VMRegistryEntry.pending_reap's own doc comment. The live-table
* entry stays in place until then: capsule_wirebind_reap_idle_
* check() below removes it once capsule_vm_force_reap() has
* actually run and the VM reads back DEAD. */
capsule_vm_set_pending_reap(live->user_vm_id, 1);
console_puts("WIREBIND: ");
console_puts(entry.name);
console_println(" device gone but VM is switched-out -- deferred reap");
}
}
void capsule_wirebind_reap_idle_check(void) {
/* FABRIC-3.md §XXVIII Stage 4 (2026-09-14): the Stage 3 checkpoint
* (vm_core.c) is the only thing that actually frees a pending_reap
* VM -- see capsule_vm_force_reap()'s own doc comment for why (it
* must run at a cooperative point the switcher itself controls, not
* from this idle tick). This sweep only cleans up the now-stale
* live-table entry once that has already happened; it is not the
* reap itself and never calls capsule_vm_kill()/force_reap(). */
for (uint32_t i = 0; i < WIREBIND_MAX_LIVE; i++) {
if (!g_wirebind_live[i].in_use) continue;
VMRegistryEntry entry;
if (capsule_vm_registry_get(g_wirebind_live[i].user_vm_id, &entry) != 0 ||
entry.state == VM_STATE_DEAD) {
wirebind_live_remove(&g_wirebind_live[i]);
}
}
}
const char *capsule_wirebind_attached_username(void) {
if (!g_wirebind_attached_valid) return (const char *)0;
return g_wirebind_attached_username;
}
void capsule_wirebind_overflow_idle_check(void) {
if (!g_wirebind_attached_valid || !g_wirebind_attached_dev) return;
VMRegistryEntry entry;
if (capsule_vm_registry_get(g_wirebind_attached_vm_id, &entry) != 0 ||
entry.state != VM_STATE_LIVE || !entry.vm_ptr) {
return;
}
uint64_t free_blocks, total_blocks;
if (blk_get_device_free_blocks(g_wirebind_attached_dev, &free_blocks, &total_blocks)
!= BLK_OK || total_blocks == 0) {
return;
}
if (free_blocks * 100u >= total_blocks * WIREBIND_OVERFLOW_FREE_THRESHOLD_PCT) {
return; /* plenty of room left on the drive itself */
}
uint8_t owner_fp[8];
memcpy(owner_fp, ((VM *)entry.vm_ptr)->identity.owner_pubkey, sizeof(owner_fp));
if (blk_owner_has_claim(owner_fp)) {
return; /* already extended once -- not a growth loop, see header doc */
}
uint32_t chain_head;
if (blk_firsttouch_claim(owner_fp, WIREBIND_OVERFLOW_CLAIM_DEVBLOCKS, &chain_head)
!= BLK_OK) {
console_println("WIREBIND: overflow trigger fired but Artemis has no room to extend into");
return;
}
console_puts("WIREBIND: ");
console_puts(g_wirebind_attached_username);
console_println(" running low on drive space -- extended onto system-resident storage");
}