Four bugs found live verifying the 8 identity thumbdrives (FABRIC-3.md §IX)
Build / build-amd64-iso (push) Canceled after 0s
Build / build-aarch64-iso (push) Canceled after 0s
Build / build-riscv64-img (push) Canceled after 0s

All found by actually running the identity workflow §VII/§VIII made
possible, not by code review:

1. Zuse/WIREBIND cross-contamination on detach: capsule_zuse_boot_logout()
   and capsule_wirebind_unclean_detach() both had no device parameter, so
   an unrelated device detaching (while the real owner's own stayed
   attached) incorrectly tore down the wrong session. Both now compare
   the departing device against their own tracked one, mirroring
   capsule_wirebind.c's pre-existing g_wirebind_attached_dev precedent.

2. Dictionary-entry memory leak: vm_create_word()'s sf_malloc()'d
   DictEntry (plus a second per-entry allocation for transition_metrics)
   was never freed by vm_cleanup(), in both the hosted and kernel
   implementations. Caused a real kernel PANIC after 8-9 repeated VM
   birth/kill cycles in one boot. Fixed by walking vm->latest in both.

3. sf_malloc/sf_free (alloc_kernel.c) was a 4MB bump arena with a
   deliberate no-op free, sized on "VM born once, never killed" -- fix #2
   alone didn't stop the panic because free() itself discarded the
   pointer regardless. Given a real free list (first-fit reuse).

4. Headless-console gate didn't re-engage after a mid-boot logout: the
   original fix (sk_console_mark_login(), one-way sticky) only gated the
   first login of the boot. Replaced with a live check
   (sk_console_identity_present()) re-evaluated continuously, including
   inside sk_console_readline()'s own blocking idle loop -- the console
   is normally sitting blocked there when a hot-unplug logout happens, so
   checking only at the top of the REPL loop wasn't enough.

Also: MINT now verifies its own write (verify_mint(), capsule_mint.c) by
reading back through the same check a real attach performs, rather than
trusting blkio_write()'s BLK_OK alone -- logged via log_message(), not
console_println(), per direct instruction.

Verified live, amd64: the full 8-identity repeated attach/detach cycle
that previously panicked at the same point every time now completes
clean, and a full serial-log sweep found zero bare unauthenticated
prompts anywhere in the run. Three-arch clean-qemu acceptance passed.

Still open, not fixed here: a 3+-simultaneous-device USB enumeration
failure found in a separate live test, not yet root-caused.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018EjXFo7mPXjUMjfJeuUUz4
This commit is contained in:
Robert Allan James
2026-09-06 01:49:13 -04:00
co-authored by Claude Sonnet 5
parent 0bae928aad
commit 2c1b3cd695
23 changed files with 45486 additions and 124 deletions
+79
View File
@@ -19,6 +19,7 @@
#include "starkernel/rng.h"
#include "block_subsystem.h" /* compute_crc64() */
#include "blkio.h"
#include "log.h"
#include <string.h>
#include <stddef.h>
@@ -66,6 +67,80 @@ static int write_devblock(struct blkio_dev *dev, uint32_t devblock,
return 0;
}
/* Read exactly one devblock (4096 bytes) at devblock offset `devblock`,
* mirroring write_devblock() above -- used only by the post-write
* verification pass (2026-09-06), to read back what was just written
* rather than trusting blkio_write()'s BLK_OK return alone. */
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;
}
/* Post-write verification (2026-09-06, direct instruction after a live
* xHCI enumeration failure raised the question of whether MINT itself
* could silently succeed without the data actually being readable back):
* re-reads the identity record and re-runs homeblocks_sig_check() --
* the exact same check a real attach later performs -- instead of trusting
* every write_devblock() call's own BLK_OK return as proof the drive will
* actually read back correctly. blkio_write() succeeding only means the
* BOT command chain completed; it says nothing about whether those bytes
* survive to be read back under real hardware/emulation conditions.
* Logged entirely through log_message() -- LOG_ERROR with the specific
* mismatch on failure, LOG_INFO confirming success -- not console_println,
* so this doesn't add unconditional console/serial noise to every mint;
* the MINT word's own caller (mama_forth_words.c) still reports the final
* pass/fail result to the console either way via its existing console_
* println() switch. Returns 0 if everything reads back correctly, -1
* otherwise. */
static int verify_mint(struct blkio_dev *dev, uint32_t identity_src_offset,
const user_identity_seed_t *written_idrec) {
homeblocks_sig_t verify_sig;
homeblocks_sig_result_t sig_rc =
homeblocks_sig_check(dev, HOMEBLOCKS_SIG_START_FBLOCK, &verify_sig);
if (sig_rc != HOMEBLOCKS_SIG_OK) {
log_message(LOG_ERROR,
"MINT verify: homeblocks_sig_check() did not read back OK (rc=%d)",
(int) sig_rc);
return -1;
}
uint8_t readback_block[4096];
if (read_devblock(dev, identity_src_offset, readback_block) != 0) {
log_message(LOG_ERROR, "MINT verify: identity record read-back failed");
return -1;
}
const user_identity_seed_t *readback_idrec =
(const user_identity_seed_t *) readback_block;
if (readback_idrec->magic != written_idrec->magic ||
readback_idrec->version != written_idrec->version) {
log_message(LOG_ERROR,
"MINT verify: identity record magic/version mismatch on read-back");
return -1;
}
uint64_t want_crc = compute_crc64((const uint8_t *) readback_idrec,
offsetof(user_identity_seed_t, crc));
if (want_crc != readback_idrec->crc) {
log_message(LOG_ERROR, "MINT verify: identity record CRC mismatch on read-back");
return -1;
}
if (memcmp(readback_idrec, written_idrec, sizeof(*written_idrec)) != 0) {
log_message(LOG_ERROR,
"MINT verify: identity record content differs from what was written");
return -1;
}
log_message(LOG_INFO, "MINT verify: identity reads back correctly");
return 0;
}
MintResult capsule_mint_identity(struct blkio_dev *dev, VM *issuer_vm,
const char *full_name, const char *username,
const char *email, const char *phone,
@@ -188,5 +263,9 @@ MintResult capsule_mint_identity(struct blkio_dev *dev, VM *issuer_vm,
(void)blkio_flush((blkio_dev_t *)dev);
if (verify_mint(dev, MINT_IDENTITY_SRC_OFFSET, &idrec) != 0) {
return MINT_ERR_VERIFY_FAILED;
}
return MINT_OK;
}
+18 -4
View File
@@ -204,9 +204,12 @@ void capsule_wirebind_try_attach(struct blkio_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 -- this successful console+user VM birth is
* exactly that login. See repl.h's own doc comment. */
sk_console_mark_login();
* 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. */
@@ -264,7 +267,18 @@ int capsule_wirebind_eject(void) {
return 0;
}
void capsule_wirebind_unclean_detach(void) {
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;
+35 -14
View File
@@ -16,19 +16,26 @@
#include "starkernel/zuse_genesis_marker.h"
#include "starkernel/user_identity_seed.h"
#include "starkernel/console.h"
#include "starkernel/repl.h" /* sk_console_mark_login() -- FABRIC-2.md headless-until-login gate */
#include "block_subsystem.h" /* compute_crc64(), blk_meta_zone_read/write */
#include "blkio.h"
#include <string.h>
#include <stddef.h>
/* FABRIC-2.md §I.8 (re-scoped 2026-09-04): tracks whether the currently
* home-blocks-attached device is Zuse's own -- the single-USB-device
* constraint (§F.8) means capsule_zuse_boot_logout() only needs one flag,
* not a device/uuid comparison, to know a detach event is hers to act on
* (a regular WIREBIND user occupying the one slot instead is tracked
* separately, in capsule_wirebind.c -- the two paths never overlap). */
/* FABRIC-3.md §VII follow-on, 2026-09-06: g_zuse_attached_this_device was a
* bare boolean under the single-USB-device constraint (§F.8) -- "a detach
* event is hers" needed no device comparison because only one device could
* ever be attached at all. That premise is stale (FABRIC-3.md §VII, the
* xHCI/BOT driver now supports genuine simultaneous multi-device attach) --
* confirmed live during this session's own 8-identity verification: with
* Zuse's drive and a WIREBIND target both attached, detaching the *target*
* incorrectly logged Zuse out too, because capsule_zuse_boot_logout() had
* no way to tell "some other device detached" from "my own device
* detached." g_zuse_attached_dev is the fix, mirroring capsule_wirebind.c's
* own g_wirebind_attached_dev precedent exactly -- the boolean stays (still
* useful as a fast "is she attached at all" check) but logout now also
* requires the departing device to match. */
static int g_zuse_attached_this_device = 0;
static struct blkio_dev *g_zuse_attached_dev = (struct blkio_dev *) 0;
/* Read exactly one devblock (4096 bytes) at devblock offset `devblock`,
* as 4 consecutive 1KiB forth-block reads -- mirrors capsule_runcap.c's
@@ -53,7 +60,8 @@ static int genesis_marker_read(zuse_genesis_marker_t *out) {
return 0;
}
static void install_and_activate(VM *mama_vm, const uint8_t seed[32], const uint8_t pubkey[32]) {
static void install_and_activate(VM *mama_vm, struct blkio_dev *dev,
const uint8_t seed[32], const uint8_t pubkey[32]) {
/* vm_zuse_cert_install() is deliberately one-way (returns -1, no-op,
* once vm->zuse_cert_installed is already 1) -- that's a real
* security property (the cert/pubkey must never be re-installed or
@@ -75,13 +83,16 @@ static void install_and_activate(VM *mama_vm, const uint8_t seed[32], const uint
* convention. */
vm_interpret(mama_vm, "ACL-ZUSE-BOOT");
g_zuse_attached_this_device = 1;
g_zuse_attached_dev = dev;
/* Decided 2026-09-05: no console for the running system unless a
* thumbdrive is present. Zuse's own login is not special here --
* "nothing special about zuse as a user except zuse has no ACLs,"
* per direct instruction -- so this is the same shared signal
* capsule_wirebind.c's own successful login sets. See repl.h. */
sk_console_mark_login();
* per direct instruction. Revised 2026-09-06: no longer calls
* sk_console_mark_login() (a one-way sticky flag) -- the gate is now a
* live check (sk_console_identity_present(), repl.c) driven directly
* by mama_vm->zuse_session, which vm_interpret(mama_vm,
* "ACL-ZUSE-BOOT") above already sets. Nothing to call here anymore. */
}
void capsule_zuse_boot_try_attach(struct blkio_dev *dev,
@@ -125,7 +136,7 @@ void capsule_zuse_boot_try_attach(struct blkio_dev *dev,
console_println("Zuse: genesis minted onto attached thumbdrive");
}
install_and_activate(mama_vm, seed, pubkey);
install_and_activate(mama_vm, dev, seed, pubkey);
return;
}
@@ -148,14 +159,24 @@ void capsule_zuse_boot_try_attach(struct blkio_dev *dev,
if (memcmp(idrec.pubkey, marker.zuse_pubkey, 32) != 0) return; /* not Zuse's drive */
console_println("Zuse: identity confirmed from attached thumbdrive");
install_and_activate(mama_vm, idrec.seed, idrec.pubkey);
install_and_activate(mama_vm, dev, idrec.seed, idrec.pubkey);
}
void capsule_zuse_boot_logout(VM *mama_vm) {
void capsule_zuse_boot_logout(VM *mama_vm, struct blkio_dev *dev) {
if (!mama_vm || !g_zuse_attached_this_device) return;
/* The device actually detaching must be hers -- see g_zuse_attached_dev's
* own doc comment above. A different device (a WIREBIND target,
* general-purpose USB use) detaching while her own drive stays
* attached must not touch her session at all. */
if (dev != g_zuse_attached_dev) return;
mama_vm->zuse_session = 0;
g_zuse_attached_this_device = 0;
g_zuse_attached_dev = (struct blkio_dev *) 0;
console_println("Zuse: session ended -- reattach to re-authenticate");
}
struct blkio_dev *capsule_zuse_boot_attached_dev(void) {
return g_zuse_attached_this_device ? g_zuse_attached_dev : (struct blkio_dev *) 0;
}
+12 -1
View File
@@ -607,7 +607,14 @@ void mama_word_kill(VM *vm)
void mama_word_eject(VM *vm)
{
capsule_wirebind_eject();
capsule_zuse_boot_logout(vm);
/* FABRIC-3.md §VII follow-on, 2026-09-06: capsule_zuse_boot_logout()
* now requires the departing device to match the one tracked as
* hers (the abrupt hot-unplug path's own fix) -- EJECT isn't reacting
* to any specific device's detach event, so it passes her own tracked
* device straight back in, which trivially matches when she's
* genuinely attached and no-ops via the existing g_zuse_attached_
* this_device check otherwise. */
capsule_zuse_boot_logout(vm, capsule_zuse_boot_attached_dev());
/* Stack clean on exit */
}
@@ -926,6 +933,10 @@ static void mama_word_mint(VM *vm)
case MINT_ERR_INVALID_PROFILE:
console_println("MINT: refused -- full_name/username missing or a field too long");
break;
case MINT_ERR_VERIFY_FAILED:
console_println("MINT: FAILED -- wrote identity but post-write verification failed "
"(see log for which check)");
break;
}
vm_push(vm, 0);
}