Files
LithosAnanke/src/starkernel/capsule/capsule_mint.c
T
Robert Allan JamesandClaude Sonnet 5 1a263555e2 Fix pathological migration scan that stalled WIREBIND identity attach
Root cause (found by a fresh subagent after an extended live-debugging
investigation into "identity 00 attaches slowly/stalls when Zuse never
attached first this boot"): blk_migration_idle_check() was generalized
earlier today to walk every attached device slot uniformly instead of
hardcoding first_disk_slot() (Artemis's own disk). But its per-slot scan
can only early-exit once it finds a devblock that is BOTH "hot" (claimed
and worn) AND "free" -- and a just-attached, never-claimed USB identity
drive can never satisfy the "hot" half by design (claiming only ever
happens via blk_firsttouch_claim(), which only ever targets
first_disk_slot()). So the scan ran to completion -- the drive's entire
~16,000 devblocks, mostly cache misses over slow emulated USB/BOT --
every single idle tick, forever, blocking sk_repl_idle() (and therefore
the console and the storage-attach message round-trip) each time.

Fix, in src/block_subsystem.c: a new has_ever_claimed flag on
blk_dev_slot_t (set in blk_set_meta(), the single choke point every
BLK_FLAG_CLAIMED transition passes through) skips the scan entirely,
O(1), for any slot nothing has ever claimed -- the common case for a
freshly-attached drive. A new migration_scan_lbn resume cursor bounds
*any* slot's per-tick cost to MIGRATION_SCAN_BUDGET (256) devblocks
examined, picking up where the previous tick left off instead of
restarting from start_lbn every time -- restores this function's own
documented "coarse cadence, cheap early-exit" design intent for every
device, not just the one it used to hardcode.

Also along the way (kept, all real improvements, verified live):
- src/starkernel/usb/xhci.c: xhci_wait_bit()/xhci_bot_wait_for_idle()
  had zero yield hints in their MMIO-polling loops; added arch_relax()
  to both (matches virtio_blk.c below) -- a tight loop of nothing but
  MMIO reads can starve TCG's own host-side timer injection under QEMU.
- src/starkernel/virtio/virtio_blk.c: vblk_io()'s spin bound was 33M
  iterations with zero logging on timeout; a single real (still not
  fully root-caused) timeout cost 31+ minutes of CPU before this was
  caught. Reduced to 1M and added a log line naming the failing sector,
  turning a silent, effectively-unbounded stall into a fast, loud
  failure -- callers already tolerate BLKIO_EIO.
- src/starkernel/repl.c: blk_migration_idle_check() deferred for any
  idle tick where a storage-attach message round-trip is still pending,
  to keep the two block-subsystem-touching paths from interleaving; the
  existing MSG-TICK pump now checks the target VM's own dictionary for
  MSG-TICK (vm_find_word + acl_allow) before VM-EXECing into it, instead
  of spamming "VM-EXEC: ERROR" every tick for a VM that doesn't have --
  or isn't allowed -- the word (e.g. a FORTH-79/83-locked-down identity);
  fixed a real -Wunused-variable build break in the EMERGENCY_CONSOLE_
  ENABLED=1 path (unused today, but needed live to reproduce this bug
  with no identity attached at all).
- src/starkernel/capsule/capsule_mint.c: dropped the dead
  S" common:messaging.4th" EXEC / MSG-CD-INIT lines from
  MINT_RESTRICTED_PERSONALITY -- a FORTH-79/83-locked-down identity has
  no legitimate use for a messaging vocabulary it can never call.

Status: the pathological CPU-climbing scan is confirmed fixed (verified
live: CPU stays flat across an extended run instead of climbing without
bound). The WIREBIND storage-attach message round-trip still does not
complete promptly in the "Zuse never attached, other identity attaches
first" scenario -- a separate, still-open issue in the message-delivery
path itself, not the scan. Tracked as follow-on work.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014Ec88YKxxhZGG1RNnune78
2026-09-09 12:08:03 -04:00

301 lines
14 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_mint.c is kernel-only"
#endif
#include "starkernel/capsule_mint.h"
#include "starkernel/homeblocks_sig.h"
#include "starkernel/user_identity_seed.h"
#include "starkernel/x509_ed25519.h"
#include "starkernel/ed25519.h"
#include "starkernel/rng.h"
#include "block_subsystem.h" /* compute_crc64() */
#include "blkio.h"
#include "log.h"
#include <string.h>
#include <stddef.h>
/* Fixed layout, devblock offsets from the drive's own start -- see
* capsule_mint.h's own doc comment. */
#define MINT_CERT_DEVBLOCK 2u
#define MINT_CERT_DEVBLOCKS 1u
#define MINT_IDENTITY_SRC_OFFSET 3u
#define MINT_IDENTITY_SRC_DEVBLOCKS 2u /* 1 seed record + 1 personality source */
#define MINT_METADATA_DEVBLOCKS 5u /* devblocks 0..4 reserved */
/* Real, working, minimal default personality -- FABRIC-2.md §F.6/§F.8's
* "default personality content" question stays open (mint-time-fixed vs.
* user-editable), but the identity this mints must actually do something
* when RUNCAP births it, not just exist. Needs the "Block NNNN" header
* capsule_exec_payload() requires (§F.18) -- discovered live, not
* optional. 4999 is an unused block number (see capsules/ block-map).
*
* Loads common:messaging.4th + MSG-CD-INIT exactly like capsule_console.c's
* own CONSOLE_IDENTITY_SRC -- every live VM except Hera owns a MSG-TICK
* word for her per-idle-tick pump to VM-EXEC into (repl.c's Phase C loop).
* A minted identity's RUNCAP-born VM is not exempt from that pump, so
* omitting this left it spamming "UNKNOWN WORD: 'MSG-TICK'" every idle
* tick forever from the moment WIREBIND made the VM live -- found live
* minting a real second identity (FABRIC-2.md §F.26 follow-up). */
static const char MINT_DEFAULT_PERSONALITY[] =
"Block 4999\n"
"S\" common:messaging.4th\" EXEC\n"
"MSG-CD-INIT\n"
": WELCOME ( -- ) .\" Minted identity -- default personality\" CR ;\n"
"WELCOME\n";
/* Restricted personality: FORTH-79/83 standard words only, everything else
* denied. The actual allowlist and the walk-and-deny logic live entirely in
* capsules/acl-std79.4th (ACL policy belongs in FORTH, never in C, per
* .claude/CLAUDE.md's Word-Level ACL System rules) -- this stub's only job
* is to EXEC that capsule and then call the one word it defines,
* ACL-LOCKDOWN-STD79, as this VM's own last bootstrap step. Block 4998:
* unused in the static capsule block-map (confirmed against
* capsules/BLOCK_MAP.md, 2026-09-07) -- 4999 is MINT_DEFAULT_PERSONALITY's
* own block above, same convention. EXEC itself is deliberately not on the
* std79 allowlist, so it's still usable here (nothing has been locked down
* yet at this point in the script) but becomes permanently denied the
* moment ACL-LOCKDOWN-STD79 finishes -- this is the one and only legitimate
* use of EXEC this VM will ever get. */
static const char MINT_RESTRICTED_PERSONALITY[] =
"Block 4998\n"
"S\" acl-std79.4th\" EXEC\n"
"ACL-LOCKDOWN-STD79\n"
": WELCOME ( -- ) .\" Minted identity -- FORTH-79/83 standard words only\" CR ;\n"
"WELCOME\n";
/* Write exactly one devblock (4096 bytes) at devblock offset `devblock`,
* as 4 consecutive 1KiB forth-block writes -- same convention
* homeblocks_sig_check()'s own read loop already uses. */
static int write_devblock(struct blkio_dev *dev, uint32_t devblock,
const uint8_t *buf4096) {
uint32_t base = devblock * 4u;
for (uint32_t i = 0; i < 4u; i++) {
if (blkio_write((blkio_dev_t *)dev, base + i,
buf4096 + (size_t)i * BLKIO_FORTH_BLOCK_SIZE) != BLKIO_OK) {
return -1;
}
}
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,
uint8_t out_pubkey[32], uint8_t out_seed[32],
MintPersonality personality,
int drive_known_blank) {
if (!dev || !full_name || !username) return MINT_ERR_WRITE_FAIL;
/* full_name/username required and must fit; email/phone may be NULL
* (treated as empty/null) but must fit if given. */
if (full_name[0] == '\0' || strlen(full_name) >= USER_IDENTITY_FULL_NAME_MAX)
return MINT_ERR_INVALID_PROFILE;
if (username[0] == '\0' || strlen(username) >= USER_IDENTITY_USERNAME_MAX)
return MINT_ERR_INVALID_PROFILE;
if (email && strlen(email) >= USER_IDENTITY_EMAIL_MAX)
return MINT_ERR_INVALID_PROFILE;
if (phone && strlen(phone) >= USER_IDENTITY_PHONE_MAX)
return MINT_ERR_INVALID_PROFILE;
/* Refuse to overwrite an already-recognized home-blocks drive --
* mirrors WRITE(10)'s own refuse-on-non-blank-media posture (§F.8,
* decided 2026-08-28: "reasonable by analogy" there, now decided).
* Skipped when the caller already just confirmed HOMEBLOCKS_SIG_BLANK
* itself (drive_known_blank) -- otherwise this repeats the exact same
* full BOT read sequence a second time immediately after the first. */
if (!drive_known_blank) {
homeblocks_sig_t existing;
if (homeblocks_sig_check(dev, HOMEBLOCKS_SIG_START_FBLOCK, &existing)
== HOMEBLOCKS_SIG_OK) {
return MINT_ERR_ALREADY_MINTED;
}
}
/* issuer_vm==NULL is genesis mode (§F.21) -- no existing Zuse to
* require a cert from. */
if (issuer_vm && !issuer_vm->zuse_cert_installed) return MINT_ERR_NO_ZUSE_CERT;
if (!rng_ready()) return MINT_ERR_NO_ENTROPY;
/* Fresh identity keypair. */
uint8_t seed[32], pubkey[32];
if (rng_get_bytes(seed, sizeof(seed)) != 0) return MINT_ERR_NO_ENTROPY;
ed25519_keygen(seed, pubkey);
if (out_pubkey) memcpy(out_pubkey, pubkey, 32);
if (out_seed) memcpy(out_seed, seed, 32);
/* Fresh drive_uuid -- an independent random draw, not derived from
* the identity seed (§F.8 decision 3: "which physical drive," not
* "whose identity"). */
uint8_t drive_uuid[16];
if (rng_get_bytes(drive_uuid, sizeof(drive_uuid)) != 0)
return MINT_ERR_NO_ENTROPY;
uint32_t cert_devblock = 0;
uint32_t cert_devblocks = 0;
if (issuer_vm) {
/* Cert: SubjectPublicKeyInfo = new pubkey, serialNumber =
* drive_uuid, signed by Zuse's own on-device seed (§F.8
* decision 4). Genesis mode (issuer_vm==NULL) skips this
* entirely -- Zuse's own identity isn't verified against a
* separate signer, she's recognized directly by pubkey match
* against zuse_genesis_marker_t (§F.21). */
uint8_t cert[512];
size_t cert_len = x509_build_user_cert(cert, sizeof(cert), pubkey,
drive_uuid, issuer_vm->zuse_cert_seed);
if (cert_len == 0 || cert_len > 4096) return MINT_ERR_CERT_BUILD;
uint8_t cert_block[4096];
memset(cert_block, 0, sizeof(cert_block));
memcpy(cert_block, cert, cert_len);
if (write_devblock(dev, MINT_CERT_DEVBLOCK, cert_block) != 0)
return MINT_ERR_WRITE_FAIL;
cert_devblock = MINT_CERT_DEVBLOCK;
cert_devblocks = MINT_CERT_DEVBLOCKS;
}
/* user_identity_seed_t record. */
user_identity_seed_t idrec;
memset(&idrec, 0, sizeof(idrec));
idrec.magic = USER_IDENTITY_SEED_MAGIC;
idrec.version = USER_IDENTITY_SEED_VERSION;
memcpy(idrec.seed, seed, 32);
memcpy(idrec.pubkey, pubkey, 32);
memcpy(idrec.full_name, full_name, strlen(full_name));
memcpy(idrec.username, username, strlen(username));
if (email) memcpy(idrec.email, email, strlen(email));
if (phone) memcpy(idrec.phone, phone, strlen(phone));
idrec.crc = compute_crc64((const uint8_t *)&idrec, offsetof(user_identity_seed_t, crc));
if (write_devblock(dev, MINT_IDENTITY_SRC_OFFSET, (const uint8_t *)&idrec) != 0)
return MINT_ERR_WRITE_FAIL;
/* Personality source, in the devblock right after the seed record --
* RUNCAP reads exactly this layout. Template selected by `personality`;
* see MintPersonality's own doc comment (capsule_mint.h) for why the
* actual restriction logic isn't here. */
const char *personality_src = (personality == MINT_PERSONALITY_STD79_LOCKDOWN)
? MINT_RESTRICTED_PERSONALITY
: MINT_DEFAULT_PERSONALITY;
size_t personality_len = (personality == MINT_PERSONALITY_STD79_LOCKDOWN)
? sizeof(MINT_RESTRICTED_PERSONALITY) - 1
: sizeof(MINT_DEFAULT_PERSONALITY) - 1;
uint8_t personality_block[4096];
memset(personality_block, 0, sizeof(personality_block));
memcpy(personality_block, personality_src, personality_len);
if (write_devblock(dev, MINT_IDENTITY_SRC_OFFSET + 1u, personality_block) != 0)
return MINT_ERR_WRITE_FAIL;
/* homeblocks_sig_t -- written last, so a crash/power-loss partway
* through minting never leaves a drive that reads as a valid,
* complete home-blocks drive with missing content behind it. */
homeblocks_sig_t sig;
memset(&sig, 0, sizeof(sig));
sig.magic = HOMEBLOCKS_SIG_PACK(HOMEBLOCKS_SIG_VERSION_0);
memcpy(sig.drive_uuid, drive_uuid, 16);
sig.minted_time_ns = 0; /* no monotonic-ns source exists
* anywhere in this codebase yet,
* §F.8's own open item */
sig.metadata_devblocks = MINT_METADATA_DEVBLOCKS;
sig.cert_offset = cert_devblock;
sig.cert_devblocks = cert_devblocks;
sig.identity_src_offset = MINT_IDENTITY_SRC_OFFSET;
sig.identity_src_devblocks = MINT_IDENTITY_SRC_DEVBLOCKS;
sig.hdr_crc = homeblocks_sig_compute_crc(&sig);
uint8_t sig_block[4096];
memcpy(sig_block, &sig, sizeof(sig));
if (write_devblock(dev, HOMEBLOCKS_SIG_START_FBLOCK / 4u, sig_block) != 0)
return MINT_ERR_WRITE_FAIL;
(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;
}