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>
564 lines
26 KiB
C
564 lines
26 KiB
C
/*
|
||
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");
|
||
}
|