/* 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 "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 #include /* 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 ("~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; /* 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; /* Nothing to verify a regular cert against until Zuse herself has * authenticated this boot. */ if (!mama_vm->zuse_cert_installed) 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_cert_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_cert_installed) 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; } 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)); /* 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. */ /* Register the pairing in the console's own routing table, index 3 * -- the fixed convention sk_repl_dispatch_line() (repl.c) uses. */ { char reg_cmd[sizeof(user_vm_name) + 32]; int n = snprintf(reg_cmd, sizeof(reg_cmd), "S\" %s\" 3 VM-NAME-REG", user_vm_name); if (n > 0 && (size_t)n < sizeof(reg_cmd)) { vm_interpret((VM *)console_ctx, reg_cmd); } } 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; } 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); if (sk_repl_get_active_vm() == (VM *)entry.vm_ptr) { sk_repl_set_active_vm((VM *)0); } capsule_vm_kill(entry.name); 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 §VII follow-on, 2026-09-06: same defect class as * capsule_zuse_boot_logout()'s own fix, found in the same live * verification session -- this used to take no device parameter at * all, so an unrelated device detaching (Zuse's own drive, or general- * purpose USB use) while a WIREBIND user stayed attached would * incorrectly tear down that user's session too. The single-USB- * device constraint this was written under never let a *different* * device be the one detaching while a WIREBIND user's own stayed live * -- stale now that genuine multi-device attach exists. */ if (dev != g_wirebind_attached_dev) return; VMRegistryEntry entry; if (wirebind_resolve_attached(&entry) != 0) 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. */ if (sk_repl_get_active_vm() == (VM *)entry.vm_ptr) { sk_repl_set_active_vm((VM *)0); } capsule_vm_kill(entry.name); g_wirebind_attached_valid = 0; g_wirebind_attached_dev = (struct blkio_dev *) 0; g_wirebind_attached_username[0] = '\0'; console_puts("WIREBIND: "); console_puts(entry.name); console_println(" detached without flush (device already gone)"); } 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"); }