Files
LithosAnanake/src/starkernel/capsule/capsule_wirebind.c
T
Robert Allan JamesandClaude Sonnet 5 4d4ab59189 Build the FIRSTTOUCH overflow trigger flagged in FABRIC-2.md §I.2
The overflow-triggered migration path (a WIREBIND-attached identity's own
drive running low on space) was scoped but never built -- only the
trigger-detection call site was missing, per this section's own text.

- capsule_wirebind.c now tracks the attached blkio_dev* alongside the
  already-tracked VM id, set in try_attach() and cleared in both
  EJECT/UNCLEAN paths.
- New capsule_wirebind_overflow_idle_check(), called once per idle tick
  in repl.c right alongside blk_migration_idle_check() (same cadence):
  reads the attached drive's free/total via blk_get_device_free_blocks(),
  and if free space is below a fixed 10% threshold, extends the
  identity's pool with a one-time blk_firsttouch_claim() of 8 additional
  devblocks on Artemis's system-resident device.
- New blk_owner_has_claim(owner_fp) in block_subsystem.c answers the
  debounce question blk_firsttouch_claim()'s own doc comment had left
  open: a disk scan, not a RAM flag, so the already-extended answer
  survives reboot/reattach, matching BMAPFMT's "ownership travels with
  the block" model.

Premise checked before building (does a WIREBIND-attached drive actually
give a real free/total signal, or does it stay PROVISIONAL/raw): traced
repl.c's attach sequence and confirmed blk_subsys_attach_device() runs on
the same dev pointer right after WIREBIND, and a WIREBIND-eligible drive
is always already STFR/v2-formatted, so the signal is real. Premise held,
unlike the BAM item's overstated one.

Verified with the mandatory 3-arch QEMU acceptance (identical dictionary
hashes, no regression) plus a live logic test of blk_owner_has_claim():
a temporary TEST-OWNER-CLAIM word, run once via SK_CMD and reverted,
confirmed it correctly detects the claiming owner and rejects an
unrelated one. The low-disk-space-triggers-a-claim path itself is not
verified end-to-end -- that needs a real minted WIREBIND-user thumbdrive
with deliberately tiny capacity, out of scope for this pass; noted as
such in the FABRIC-2.md §I.2 closure note rather than overclaimed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018EjXFo7mPXjUMjfJeuUUz4
2026-09-05 16:17:19 -04:00

325 lines
13 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 "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;
/* 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");
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));
/* 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(void) {
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");
}