Files
LithosAnanke/kernel/src/capsule/capsule_wirebind.c
T
rajamesandJunie a8b70e88d3 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>
2026-10-01 15:40:09 -04:00

564 lines
26 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
StarForth — Steady-State Virtual Machine Runtime
Copyright (c) 2023–2025 Robert A. James
All rights reserved.
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");
}