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
301 lines
14 KiB
C
301 lines
14 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_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;
|
||
}
|