Four bugs found live verifying the 8 identity thumbdrives (FABRIC-3.md §IX)
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:
co-authored by
Claude Sonnet 5
parent
0bae928aad
commit
2c1b3cd695
+155
@@ -1340,3 +1340,158 @@ headless-console work) — no regression in existing `KILL`/`USE` behavior. Not
|
||||
stress-tested against the specific dangling-pointer scenario (kill the actively-`USE`'d VM,
|
||||
then dispatch another command) — the fix is small and directly addresses the confirmed gap,
|
||||
but that specific repro was not re-run live after the fix landed.
|
||||
|
||||
## IX. Four bugs found live minting/verifying the 8 identity thumbdrives — CLOSED 2026-09-06
|
||||
|
||||
All four surfaced by actually exercising the system this project's own documentation exists
|
||||
to describe — the thumbdrive-identity workflow §VII/§VIII made possible — not by code review.
|
||||
Each is documented here with the same discipline: what broke, why, the fix, and what was
|
||||
verified afterward.
|
||||
|
||||
### IX.1 — Zuse/WIREBIND cross-contamination on detach
|
||||
|
||||
**Bug:** with §VII's multi-device fix in place, live-verifying all 8 minted identities (boot
|
||||
with Zuse attached, hot-plug each target drive one at a time, confirm `WIREBIND: <username>
|
||||
attached and ready`) surfaced a real cross-contamination bug: detaching drive `00` printed
|
||||
`Zuse: session ended -- reattach to re-authenticate` even though Zuse's own drive was never
|
||||
touched. Root cause: `capsule_zuse_boot_logout()` (`capsule_zuse_boot.c:164`, called
|
||||
unconditionally from `repl.c`'s per-slot detach loop for every departing device) had no way
|
||||
to tell "some other device detached" from "my own device detached" — its only guard was a
|
||||
bare boolean (`g_zuse_attached_this_device`), not a device comparison, despite its own header
|
||||
doc comment already claiming that no-op existed. The exact same defect class existed in the
|
||||
sibling function, `capsule_wirebind_unclean_detach()` (`capsule_wirebind.c:267`) — also no
|
||||
device parameter, so an unrelated device detaching while a WIREBIND user's own stayed
|
||||
attached would have incorrectly torn down that user's session too (not separately triggered
|
||||
live, found by inspection once the first instance was understood).
|
||||
|
||||
**Fix:** both functions now take the departing `struct blkio_dev *dev` and compare it against
|
||||
their own tracked device (`g_zuse_attached_dev`, new — mirrors `capsule_wirebind.c`'s own
|
||||
pre-existing `g_wirebind_attached_dev` precedent exactly) before acting; every other value is
|
||||
a no-op. `repl.c`'s detach loop passes the actual departing `usb_blk_dev` pointer it already
|
||||
had in scope. `EJECT` (`mama_forth_words.c`'s `mama_word_eject()`, an explicit operator
|
||||
command with no specific detaching device to check against) now calls a new accessor,
|
||||
`capsule_zuse_boot_attached_dev()`, passing her own tracked device straight back in — trivially
|
||||
matches when she's genuinely attached, correctly no-ops via the existing boolean guard
|
||||
otherwise.
|
||||
|
||||
**Verified:** live, amd64 — a full 8-identity sequential attach/detach cycle (Zuse detached
|
||||
first, then bob/rajames, 00-06 one at a time) with zero cross-contamination; `(zuse) ok>`
|
||||
correctly persisted through unrelated detaches before this fix's own follow-on (§IX.3) closed
|
||||
the remaining gap where her session ending correctly still left a bare prompt visible.
|
||||
|
||||
### IX.2 — Dictionary-entry memory leak (`vm_cleanup()`), causing a real kernel PANIC
|
||||
|
||||
**Bug:** repeating the WIREBIND birth/kill cycle 8-9 times in one boot (exactly what
|
||||
identity verification does) produced a genuine kernel PANIC on the 8th-9th cycle every time:
|
||||
cascading `vm_create_word: malloc failed` errors during capsule loading into the newly-born
|
||||
VM, followed by `PANIC: Stadium: eviction selected patron zero (Hera) -- governor invariant
|
||||
broken` and a full system halt. Root cause, found by tracing the allocation path: every
|
||||
dictionary word (`vm_create_word()`, `dictionary_management.c:429`) allocates its own
|
||||
`DictEntry` via `sf_malloc()` — separate from the VM's own `vm->memory` arena entirely — but
|
||||
`vm_cleanup()` (**both** `src/starkernel/vm/vm_core.c:286` kernel and `src/vm_bootstrap.c:370`
|
||||
hosted) freed `vm->memory`, the hotwords cache, rolling window, SSM state, and call stack, but
|
||||
never walked the VM's own dictionary chain (`vm->latest`) to free the individual `DictEntry`
|
||||
allocations — confirmed via grep that no such free existed anywhere in the codebase. Every
|
||||
word a VM ever defined (hundreds per birth, given capsule loading) leaked permanently on kill.
|
||||
Never noticed before this session: the hosted binary normally only calls `vm_cleanup()` once
|
||||
at process exit (the OS reclaims everything anyway), and kernel VMs were normally born once
|
||||
and kept alive for a whole boot, not repeatedly born and killed.
|
||||
|
||||
**Fix:** both `vm_cleanup()` implementations now walk `vm->latest` via each entry's own
|
||||
`->link`, freeing `entry->transition_metrics` (a second, separate per-entry `sf_malloc()`,
|
||||
`dictionary_management.c:455` — also unfreed anywhere) before the entry itself, saving the
|
||||
next-link before each free since freeing destroys it.
|
||||
|
||||
**Verified:** live, amd64, the exact repeated-cycle scenario that panicked — no longer panics
|
||||
alone (see §IX.3, this fix alone was insufficient until the allocator itself was also fixed).
|
||||
|
||||
### IX.3 — `sf_malloc`/`sf_free` was a bump allocator with a deliberate no-op free
|
||||
|
||||
**Bug, found immediately after §IX.2's fix didn't change the panic at all:** the kernel's own
|
||||
`sf_malloc`/`sf_free` (`src/starkernel/vm/alloc_kernel.c`) is a fixed 4MB static bump arena
|
||||
whose `sf_free()` was a **documented, deliberate no-op** — "VM is long-lived (no restart)...
|
||||
free is a no-op... no fragmentation issues in practice." That premise was true until this
|
||||
session's own repeated-birth-kill workflow. §IX.2's fix, while correct application-level
|
||||
hygiene, was functionally inert against this specific allocator: calling `sf_free()`
|
||||
diligently changes nothing when `sf_free()` itself throws the block away regardless.
|
||||
|
||||
**Fix:** `sf_malloc`/`sf_free` now implement a real free list. Every allocation carries a
|
||||
fixed header (`size` + a free-list `next` link, live only while free) immediately before the
|
||||
returned pointer. `sf_free()` pushes the block onto `g_free_list`; `sf_malloc()` searches it
|
||||
first-fit before falling back to bump-allocating a fresh block from the arena, exactly as
|
||||
before. Deliberately no splitting/coalescing — this workload's repeated allocations are for
|
||||
the same capsules loaded into a fresh VM each time, so freed blocks from a just-killed VM's
|
||||
dictionary are typically an exact or near-exact fit for the next VM's own, making first-fit
|
||||
reuse the right simplicity tradeoff. `sf_realloc()` (pre-existing "orphans the old block"
|
||||
behavior) was deliberately left untouched — out of scope for this fix, no evidence it's
|
||||
exercised in the failure path.
|
||||
|
||||
**Verified:** live, amd64, the full 8-identity repeated birth/kill cycle that panicked at the
|
||||
identical point on every prior run (confirmed twice) — no panic, all 9 sessions (Zuse + 8)
|
||||
confirmed in one continuous boot. Three-arch `clean qemu` acceptance also re-run clean after
|
||||
§IX.1/§IX.3/§IX.4 landed together.
|
||||
|
||||
### IX.4 — Headless-until-login gate didn't re-engage after a mid-boot logout
|
||||
|
||||
**Bug:** §VIII's original headless-console gate (`sk_console_mark_login()`/`sk_console_
|
||||
login_occurred()`) was a one-way sticky flag, gating only the very first login of the boot
|
||||
before `sk_repl(mama)` was ever called. Once tripped once, it never reset — a later full
|
||||
logout (nobody attached at all) fell through to a bare, unauthenticated `ok>` instead of going
|
||||
silent again, for the rest of the boot. Found live during the same 8-identity verification
|
||||
run, flagged directly: *"we should never see an unauthorized, no session forth prompt."*
|
||||
Fixing just the top-level loop wasn't enough either: the console is typically sitting blocked
|
||||
inside `sk_console_readline()` waiting for input at the moment a logout happens (via hot-
|
||||
unplug detected from *inside* that same call's own idle-service loop), so a check only at the
|
||||
top of `sk_repl_run()`'s loop wouldn't take effect until a line was actually typed next.
|
||||
|
||||
**Fix:** `sk_console_mark_login()`/`sk_console_login_occurred()` retired entirely, replaced by
|
||||
a live check, `sk_console_identity_present()` (`repl.c`) — mirrors `sk_print_prompt()`'s own
|
||||
`zuse_session`/WIREBIND-username check exactly, evaluated fresh every time rather than latched
|
||||
once. Three call sites updated: `sk_repl_headless_wait()`'s own loop condition; a new check at
|
||||
the top of `sk_repl_run()`'s main loop (re-enters headless wait whenever nobody is currently
|
||||
attached, not just before the very first login); and, critically, inside `sk_console_
|
||||
readline()`'s own idle branch itself — when the identity present at prompt-print time logs out
|
||||
while the call is still blocked waiting for input with nothing typed (`n == 0`), it now
|
||||
returns -1 instead of re-printing a (correctly bare, but still unauthenticated) prompt and
|
||||
continuing to wait. Callers with `reanchor_prompt` nonzero (the REPL's own top-level prompt
|
||||
sites) check for -1 and loop back without printing `" ok"`, landing back at the top-of-loop
|
||||
check, which re-enters silence immediately. `shim.c`'s `fgets()` (`reanchor_prompt == 0`)
|
||||
never receives -1.
|
||||
|
||||
**Verified:** live, amd64, full 8-identity sequential cycle (Zuse's own detach, then each of 8
|
||||
WIREBIND users' own detach) — grepped the entire session's serial log for every prompt-prefix
|
||||
occurrence: 9 total `ok>` lines, every single one carrying a name (`(zuse)`, `(rajames)`,
|
||||
`(00)`...`(06)`) — zero bare, unauthenticated prompts anywhere in the run. Three-arch
|
||||
`clean qemu` acceptance re-run clean with this fix included.
|
||||
|
||||
### IX.5 — `MINT` now verifies its own write
|
||||
|
||||
**Motivation:** a separate live test (attaching Zuse + 8 identities simultaneously without
|
||||
ever detaching any, exercising genuine 3+-device concurrent attach for the first time) hit a
|
||||
4th-device USB enumeration failure (`xhci: not a Mass Storage/SCSI/BOT device`) — a real,
|
||||
still-open defect (not yet root-caused, tracked as follow-on work, not part of this closure).
|
||||
That failure raised a fair question: could `MINT` itself silently report success without the
|
||||
identity actually being readable back afterward? `capsule_mint_identity()`
|
||||
(`capsule_mint.c`) previously trusted every `blkio_write()`'s own `BLK_OK` return as
|
||||
sufficient proof; it never confirmed any of it was actually readable back.
|
||||
|
||||
**Fix:** a new `verify_mint()` (`capsule_mint.c`) runs immediately after all writes and the
|
||||
flush, before returning `MINT_OK`: re-runs `homeblocks_sig_check()` — the exact same check a
|
||||
real later attach performs — then separately re-reads the identity record devblock and checks
|
||||
magic/version/CRC/full-content match against what was written. A new result code,
|
||||
`MINT_ERR_VERIFY_FAILED` (`capsule_mint.h`), is returned (and reported by the `MINT` word's
|
||||
own existing console switch, `mama_forth_words.c`) if any check fails. Logged entirely via
|
||||
`log_message()` (`LOG_ERROR` per specific mismatch, `LOG_INFO` on success) rather than
|
||||
`console_println()`, per direct instruction — no added unconditional console/serial noise;
|
||||
the `MINT` word's own final pass/fail line still reports to the console as it already did for
|
||||
every other `MintResult` case.
|
||||
|
||||
**Not yet re-verified against a real failure** — every mint performed after this fix landed
|
||||
(the original 8 identities were already minted before it existed) has succeeded, so the
|
||||
failure-path logging itself hasn't been exercised live yet, only compiled. Three-arch
|
||||
`clean qemu` acceptance passed with this change included.
|
||||
|
||||
**Still open, not addressed by this closure:** the 3+-simultaneous-device USB enumeration
|
||||
failure that motivated §IX.5 (`xhci: not a Mass Storage/SCSI/BOT device` on a 4th concurrent
|
||||
attach) remains unexplained and unfixed — tracked as the next item, separate from the four
|
||||
closures above.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Capsule Block Manifest — Auto-generated
|
||||
<!-- Generated by mkcapsule --manifest 2026-09-06T01:54:05Z -->
|
||||
<!-- Generated by mkcapsule --manifest 2026-09-06T04:51:32Z -->
|
||||
<!-- DO NOT EDIT — re-run mkcapsule --manifest to refresh. -->
|
||||
<!-- Hand-written justifications and immutability notes live -->
|
||||
<!-- in MANIFEST.md alongside this auto-generated index. -->
|
||||
|
||||
Binary file not shown.
@@ -43,6 +43,19 @@ typedef enum {
|
||||
* user_identity_seed_t's fixed fields, or
|
||||
* email/phone too long (both may be NULL/empty
|
||||
* -- that's "null", not invalid). */
|
||||
MINT_ERR_VERIFY_FAILED, /* every devblock write reported success, but a
|
||||
* post-write read-back (2026-09-06) found the
|
||||
* drive doesn't actually read back as a valid,
|
||||
* complete home-blocks identity -- caught
|
||||
* live: a device that enumerates and accepts
|
||||
* writes can still fail to read back correctly
|
||||
* under real hardware/emulation conditions
|
||||
* (e.g. concurrent multi-device USB load), and
|
||||
* blkio_write() returning BLK_OK is not by
|
||||
* itself proof the bytes landed. The drive may
|
||||
* be left partially or incorrectly minted --
|
||||
* treat identically to MINT_ERR_WRITE_FAIL for
|
||||
* retry purposes. */
|
||||
} MintResult;
|
||||
|
||||
/**
|
||||
|
||||
@@ -109,8 +109,16 @@ int capsule_wirebind_eject(void);
|
||||
* since the last flush is lost, which is correct unclean-removal
|
||||
* semantics. Otherwise identical to capsule_wirebind_eject(): same
|
||||
* active-VM reset-before-kill step, same tracked-state clear.
|
||||
*
|
||||
* FABRIC-3.md §VII follow-on, 2026-09-06: now requires the departing
|
||||
* device to actually be the one tracked as this WIREBIND user's own
|
||||
* (g_wirebind_attached_dev) -- a real bug otherwise, found live once
|
||||
* genuine multi-device attach made a *different* device's detach
|
||||
* reachable while a WIREBIND user's own stayed attached.
|
||||
*
|
||||
* @param dev The device that just detached; every other value is a no-op.
|
||||
*/
|
||||
void capsule_wirebind_unclean_detach(void);
|
||||
void capsule_wirebind_unclean_detach(struct blkio_dev *dev);
|
||||
|
||||
/**
|
||||
* capsule_wirebind_attached_username - The plain username (no "~user"
|
||||
|
||||
@@ -68,17 +68,35 @@ void capsule_zuse_boot_try_attach(struct blkio_dev *dev,
|
||||
* one function covers both the graceful (EJECT) and abrupt (hot-unplug)
|
||||
* call sites identically.
|
||||
*
|
||||
* No-op if the currently-tracked attached device isn't Zuse's own
|
||||
* (nothing to do -- some other identity's drive is what's leaving, or
|
||||
* nothing is attached at all). Clears mama_vm->zuse_session only --
|
||||
* zuse_cert_installed and the cert itself stay put, permanently, per
|
||||
* vm_zuse_cert_install()'s own one-way design; re-attaching her own
|
||||
* drive re-authenticates via capsule_zuse_boot_try_attach() without
|
||||
* re-minting anything.
|
||||
* No-op if `dev` isn't the device currently tracked as Zuse's own (nothing
|
||||
* to do -- some other identity's drive is what's leaving, or nothing is
|
||||
* attached at all) -- FABRIC-3.md §VII follow-on, 2026-09-06: this doc
|
||||
* comment always claimed that no-op, but the check itself was missing
|
||||
* until now (the function took no device parameter at all) -- confirmed
|
||||
* live as a real bug once genuine multi-device attach made it reachable
|
||||
* (detaching an unrelated device logged Zuse out too). Clears
|
||||
* mama_vm->zuse_session only -- zuse_cert_installed and the cert itself
|
||||
* stay put, permanently, per vm_zuse_cert_install()'s own one-way design;
|
||||
* re-attaching her own drive re-authenticates via
|
||||
* capsule_zuse_boot_try_attach() without re-minting anything.
|
||||
*
|
||||
* @param mama_vm Hera's own VM (zuse_session lives here).
|
||||
* @param dev The device that just detached -- compared against the one
|
||||
* tracked as hers; every other value is a no-op.
|
||||
*/
|
||||
void capsule_zuse_boot_logout(VM *mama_vm);
|
||||
void capsule_zuse_boot_logout(VM *mama_vm, struct blkio_dev *dev);
|
||||
|
||||
/**
|
||||
* capsule_zuse_boot_attached_dev - The device currently tracked as Zuse's
|
||||
* own, or NULL if she isn't attached this boot. FABRIC-3.md §VII follow-on,
|
||||
* 2026-09-06: exists so an explicit, operator-initiated logout (EJECT,
|
||||
* mama_forth_words.c) can pass her own device back into
|
||||
* capsule_zuse_boot_logout() without needing to already know it -- unlike
|
||||
* the abrupt hot-unplug path, EJECT isn't reacting to any specific
|
||||
* device's detach event, so there is no other device value available at
|
||||
* that call site to check against.
|
||||
*/
|
||||
struct blkio_dev *capsule_zuse_boot_attached_dev(void);
|
||||
|
||||
#endif /* __STARKERNEL__ */
|
||||
|
||||
|
||||
+25
-31
@@ -131,46 +131,40 @@ int sk_console_key_available(void);
|
||||
* prompt line is their own (shim.c's fgets(), i.e.
|
||||
* QUERY/EXPECT/ACCEPT) pass 0 so "ok> " never gets stamped
|
||||
* onto their mid-word input context.
|
||||
* @return number of characters placed in buf, not counting the NUL
|
||||
* @return number of characters placed in buf, not counting the NUL, or -1
|
||||
* (2026-09-06) when reanchor_prompt is nonzero and the attached
|
||||
* identity logged out while this call was blocked waiting for
|
||||
* input with nothing typed yet -- see repl.c's own doc comment
|
||||
* on this function for what a caller must do with -1.
|
||||
*/
|
||||
int sk_console_readline(char* buf, int size, VM* active_vm, int reanchor_prompt);
|
||||
|
||||
/**
|
||||
* sk_console_mark_login - Record that a real identity has logged in via an
|
||||
* attached thumbdrive, decided in conversation 2026-09-05: no console for
|
||||
* the running system unless a thumbdrive is present -- headless until the
|
||||
* first successful login, regardless of which path performs it (a regular
|
||||
* user's WIREBIND console-VM birth, capsule_wirebind.c, or Zuse's own
|
||||
* attach/genesis-mint, capsule_zuse_boot.c). Both call this on their own
|
||||
* success path; neither is treated as special here, per direct instruction
|
||||
* ("nothing special about zuse as a user except zuse has no ACLs") --
|
||||
* this is one shared signal, not a Zuse-specific carve-out. Idempotent
|
||||
* (a second login, e.g. a second WIREBIND user later, is a harmless no-op).
|
||||
*/
|
||||
void sk_console_mark_login(void);
|
||||
|
||||
/**
|
||||
* sk_console_login_occurred - Whether sk_console_mark_login() has ever
|
||||
* been called this boot. Read by sk_repl_headless_wait()'s own exit
|
||||
* condition; exposed publicly for anything else that needs to know
|
||||
* whether the console is unlocked yet.
|
||||
*/
|
||||
int sk_console_login_occurred(void);
|
||||
|
||||
/**
|
||||
* sk_repl_headless_wait - Idle-service loop with no interactive surface
|
||||
* at all: no banner, no prompt, no console_getc()/readline. Runs
|
||||
* heartbeat_service() and the same SK_IDLE_BEAT_INTERVAL-gated
|
||||
* sk_repl_idle(mama) cadence sk_console_readline()'s own idle branch
|
||||
* uses -- so USB/WIREBIND/Zuse-attach detection, the heartbeat, and all
|
||||
* other idle-tick subsystems keep running -- until sk_console_login_
|
||||
* occurred() becomes true, at which point it returns. Called from
|
||||
* kernel_main.c in place of an immediate sk_repl(mama) call when
|
||||
* EMERGENCY_CONSOLE_ENABLED is off (the new default, 2026-09-05): no
|
||||
* thumbdrive, no prompt, per direct instruction. When
|
||||
* EMERGENCY_CONSOLE_ENABLED is on (the debug/recovery escape hatch),
|
||||
* kernel_main.c skips this and calls sk_repl(mama) immediately instead,
|
||||
* exactly as before this change.
|
||||
* other idle-tick subsystems keep running -- until a real identity is
|
||||
* currently attached (Zuse's own session, or a WIREBIND user), at which
|
||||
* point it returns.
|
||||
*
|
||||
* Revised 2026-09-06: originally exited on a one-way sticky "has anyone
|
||||
* ever logged in this boot" flag (sk_console_mark_login()/sk_console_
|
||||
* login_occurred(), both retired) -- that let a real gap through, found
|
||||
* live: once the flag tripped once, it never reset, so a later full
|
||||
* logout (nobody attached at all) fell through to a bare, unauthenticated
|
||||
* prompt instead of going silent again. This now checks live attach
|
||||
* state instead (repl.c's own sk_console_identity_present()), and is
|
||||
* called from two places: once from kernel_main.c in place of an
|
||||
* immediate sk_repl(mama) call when EMERGENCY_CONSOLE_ENABLED is off (the
|
||||
* default, 2026-09-05) -- no thumbdrive, no prompt, at boot -- and again
|
||||
* from inside sk_repl_run()'s own main loop, every time nobody is
|
||||
* currently attached, so the same silence re-engages after any later
|
||||
* logout mid-boot too. When EMERGENCY_CONSOLE_ENABLED is on (the debug/
|
||||
* recovery escape hatch), neither call site applies -- the console shows
|
||||
* immediately and stays visible regardless of attach state, exactly as
|
||||
* before this change.
|
||||
*
|
||||
* @param mama Hera's own VM instance -- the idle-dispatch target,
|
||||
* same as every other sk_repl_idle() caller uses.
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,11 @@
|
||||
[2J[001;001H[=3h[2J[001;001H[2J[001;001H[8;042;160t[2J[001;001H[2J[001;001H[8;056;240t[2J[001;001HBdsDxe: loading Boot0002 "UEFI QEMU DVD-ROM QM00005 " from PciRoot(0x0)/Pci(0x1F,0x2)/Sata(0x2,0xFFFF,0x0)
|
||||
BdsDxe: starting Boot0002 "UEFI QEMU DVD-ROM QM00005 " from PciRoot(0x0)/Pci(0x1F,0x2)/Sata(0x2,0xFFFF,0x0)
|
||||
[2J[001;001H[=3h[2J[001;001HStarKernel UEFI Loader
|
||||
Loading kernel from ESP...
|
||||
[CKPT 001] Entered efi_main - ConOut live
|
||||
RAW SERIAL UP
|
||||
[CKPT 002] Serial (COM1) initialized
|
||||
Monolithic build - kernel linked directly
|
||||
Collecting boot information...
|
||||
CmdLine: parsed OK
|
||||
[CKPT 004] Command line parsed
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -936,11 +936,16 @@ static void kernel_main_deep(BootInfo *boot_info) {
|
||||
* (Kconfig.heartbeat) rather than adding a second, overlapping one.
|
||||
* When off, sk_repl_headless_wait() runs the same idle-tick services
|
||||
* (heartbeat, USB/WIREBIND/Zuse-attach detection) with no banner, no
|
||||
* prompt, no input surface at all, until sk_console_mark_login()
|
||||
* fires from either login path -- neither is treated as special, per
|
||||
* direct instruction. When on (the debug/recovery escape hatch),
|
||||
* this is skipped entirely and the console shows up immediately,
|
||||
* exactly as before this change. */
|
||||
* prompt, no input surface at all, until a real identity is attached
|
||||
* via either login path -- neither is treated as special, per direct
|
||||
* instruction. This is only the boot-time gate; sk_repl_run()'s own
|
||||
* main loop (repl.c) re-checks the same live condition on every
|
||||
* iteration too, so the console goes silent again after any later
|
||||
* full logout mid-boot, not just before the first-ever login (2026-
|
||||
* 09-06 revision -- see sk_console_identity_present()'s own doc
|
||||
* comment in repl.c for the live bug this closes). When on (the
|
||||
* debug/recovery escape hatch), this is skipped entirely and the
|
||||
* console shows up immediately, exactly as before this change. */
|
||||
#if !EMERGENCY_CONSOLE_ENABLED
|
||||
sk_repl_headless_wait(mama);
|
||||
#endif
|
||||
|
||||
+99
-19
@@ -99,16 +99,26 @@ VM *sk_repl_get_active_vm(void) { return g_repl_active_vm; }
|
||||
|
||||
/*===========================================================================
|
||||
* Headless-until-login gate, decided 2026-09-05: no console for the
|
||||
* running system unless a thumbdrive is present. One shared flag, set by
|
||||
* either login path (capsule_wirebind.c's regular-user console-VM birth,
|
||||
* capsule_zuse_boot.c's own attach/genesis-mint) -- neither is special,
|
||||
* per direct instruction. See repl.h's own doc comments.
|
||||
*===========================================================================*/
|
||||
|
||||
static int g_console_login_occurred = 0;
|
||||
|
||||
void sk_console_mark_login(void) { g_console_login_occurred = 1; }
|
||||
int sk_console_login_occurred(void) { return g_console_login_occurred; }
|
||||
* running system unless a thumbdrive is present.
|
||||
*
|
||||
* Revised 2026-09-06: this was originally a one-way sticky flag
|
||||
* (sk_console_mark_login(), set once by either login path and never
|
||||
* cleared), gating only the very first entry into sk_repl_run() at boot.
|
||||
* That let a real security gap through, found live during this session's
|
||||
* own repeated identity-verification workflow: once anyone logged in even
|
||||
* once, the console stayed visible for the rest of the boot -- a later
|
||||
* full logout (nobody attached at all) fell through to a bare,
|
||||
* unauthenticated "ok>" instead of going silent again. sk_console_
|
||||
* identity_present() replaces the sticky flag with a live check (mirrors
|
||||
* sk_print_prompt()'s own zuse_session/WIREBIND-username check exactly),
|
||||
* and sk_repl_run()'s own main loop now re-checks it every iteration, not
|
||||
* just once before the loop starts -- see its own call site below. */
|
||||
static int sk_console_identity_present(void) {
|
||||
VM *mama_vm = (VM *)sk_get_mama_vm();
|
||||
if (mama_vm && mama_vm->zuse_session) return 1;
|
||||
if (capsule_wirebind_attached_username() != (const char *)0) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*===========================================================================
|
||||
* Currently attached home-blocks device: mirrors g_repl_active_vm's own
|
||||
@@ -377,13 +387,18 @@ static void sk_repl_idle(VM *active_vm)
|
||||
/* FABRIC-2.md §F.10 decision 2 (UNCLEAN, closed alongside EJECT):
|
||||
* the device is already gone -- no-op if WIREBIND never had
|
||||
* anything tracked (general-purpose USB use, not a home-blocks
|
||||
* identity drive). */
|
||||
capsule_wirebind_unclean_detach();
|
||||
* identity drive), OR if the device that left wasn't the one
|
||||
* WIREBIND tracks (FABRIC-3.md §VII follow-on, 2026-09-06 --
|
||||
* genuine multi-device attach means it might be a different
|
||||
* device leaving while a WIREBIND user's own stays attached). */
|
||||
capsule_wirebind_unclean_detach(usb_blk_dev);
|
||||
|
||||
/* FABRIC-2.md §I.8, re-scoped 2026-09-04: Zuse logs out on device
|
||||
* removal exactly like a WIREBIND user -- no-op if the device
|
||||
* that just left wasn't hers. */
|
||||
capsule_zuse_boot_logout((VM *)sk_get_mama_vm());
|
||||
* that just left wasn't hers (FABRIC-3.md §VII follow-on,
|
||||
* 2026-09-06: that no-op is now real, see capsule_zuse_boot_
|
||||
* logout()'s own updated doc comment). */
|
||||
capsule_zuse_boot_logout((VM *)sk_get_mama_vm(), usb_blk_dev);
|
||||
}
|
||||
|
||||
/* FABRIC-0.md/FABRIC-1.md Section V item 6: "a cheap 'anything dirty?
|
||||
@@ -630,11 +645,15 @@ int sk_console_getkey(VM *active_vm)
|
||||
* shape as sk_console_getkey() above, minus the key-reading entirely: no
|
||||
* banner, no prompt, no console_getc()/readline of any kind -- this is
|
||||
* exactly the "no console for the running system unless a thumbdrive is
|
||||
* present" boundary, decided 2026-09-05. Exits the moment
|
||||
* sk_console_login_occurred() becomes true. */
|
||||
* present" boundary, decided 2026-09-05. Exits the moment sk_console_
|
||||
* identity_present() becomes true -- called both once at boot
|
||||
* (kernel_main.c, before the first ever login) and again from inside
|
||||
* sk_repl_run()'s own main loop whenever the last attached identity logs
|
||||
* out mid-boot (2026-09-06 revision, see sk_console_identity_present()'s
|
||||
* own doc comment for why the boot-only version wasn't enough). */
|
||||
void sk_repl_headless_wait(VM *mama)
|
||||
{
|
||||
while (!sk_console_login_occurred()) {
|
||||
while (!sk_console_identity_present()) {
|
||||
heartbeat_service();
|
||||
uint64_t now = heartbeat_ticks();
|
||||
if (now - g_last_beat_tick >= SK_IDLE_BEAT_INTERVAL) {
|
||||
@@ -664,7 +683,17 @@ int sk_console_key_available(void)
|
||||
* Non-blocking poll of console_getc(). While no character is ready the idle
|
||||
* spin services the adaptive heartbeat at SK_IDLE_BEAT_INTERVAL tick cadence.
|
||||
* Supports backspace (0x7F and \b) and ignores other control characters.
|
||||
* Returns the number of characters placed in buf (not counting '\0').
|
||||
* Returns the number of characters placed in buf (not counting '\0'), or
|
||||
* -1 (2026-09-06) when called with reanchor_prompt nonzero and the
|
||||
* identity that was attached when the caller's prompt was printed logs
|
||||
* out while this call is still blocked waiting for input with nothing yet
|
||||
* typed (n == 0) -- callers with reanchor_prompt nonzero (the REPL's own
|
||||
* top-level prompt sites) must check for this and route back to
|
||||
* sk_repl_headless_wait() rather than treating it as an empty line; buf
|
||||
* is left as an empty string in this case too, matching a real empty
|
||||
* line, so a caller that doesn't check the return value degrades to the
|
||||
* pre-fix behavior (an extra harmless " ok") rather than misbehaving.
|
||||
* shim.c's fgets() (reanchor_prompt == 0) never receives -1.
|
||||
*
|
||||
* Public (declared in repl.h): shim.c's fgets()/QUERY's own real body call
|
||||
* this directly -- same line-editing behavior for a mid-word EXPECT/QUERY as
|
||||
@@ -766,6 +795,27 @@ int sk_console_readline(char* buf, int size, VM* active_vm, int reanchor_prompt)
|
||||
* unchanged, so the final state after the chatter dies down is
|
||||
* a fresh prompt on the last visible line, cursor on it.
|
||||
*/
|
||||
/* Headless-until-login gate, 2026-09-06: the identity that was
|
||||
* attached when the caller printed its prompt (sk_print_prompt(),
|
||||
* reflected in reanchor_prompt callers only -- shim.c's fgets()
|
||||
* passes 0 and is unaffected) may have logged out while we sat
|
||||
* here blocked waiting for input -- WIREBIND EJECT/unclean
|
||||
* detach, or Zuse's own logout, both reachable from
|
||||
* sk_repl_idle() just above. Re-printing the prompt in that
|
||||
* case (the block below) would just show a *correct* bare
|
||||
* "ok>" -- true to current state, but still an unauthenticated
|
||||
* interactive surface sitting on screen, which the headless-
|
||||
* until-login design (Kconfig.heartbeat's EMERGENCY_CONSOLE_
|
||||
* ENABLED) exists specifically to prevent. Bail out instead so
|
||||
* the caller (sk_repl_run()'s own main loop) can drop back into
|
||||
* sk_repl_headless_wait() -- confirmed live as a real gap
|
||||
* before this fix (a bare, unauthenticated prompt stayed on
|
||||
* screen after every logout for the rest of the boot). n == 0
|
||||
* only: never abandon a line the user is actively typing. */
|
||||
if (reanchor_prompt && n == 0 && !sk_console_identity_present()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (reanchor_prompt && n == 0 &&
|
||||
console_tx_count() != prompt_tx_mark)
|
||||
{
|
||||
@@ -1005,6 +1055,22 @@ void sk_repl_run(VM *vm)
|
||||
vm->halted = 0;
|
||||
|
||||
while (!vm->halted) {
|
||||
#if !EMERGENCY_CONSOLE_ENABLED
|
||||
/* Headless-until-login gate, revised 2026-09-06: re-checked every
|
||||
* iteration, not just once before this loop starts (kernel_main.c's
|
||||
* own sk_repl_headless_wait() call, still in place, only covers the
|
||||
* very first login of the boot). Whoever was attached may have
|
||||
* logged out since the last iteration (WIREBIND EJECT/unclean
|
||||
* detach, Zuse's own logout) -- if nobody is attached right now,
|
||||
* go back to silent waiting instead of falling through to a bare,
|
||||
* unauthenticated prompt. See sk_console_identity_present()'s own
|
||||
* doc comment for the live bug this closes. */
|
||||
if (!sk_console_identity_present()) {
|
||||
sk_repl_headless_wait(vm);
|
||||
if (vm->halted) break;
|
||||
continue;
|
||||
}
|
||||
#endif
|
||||
/* USE may redirect input to a different VM each iteration */
|
||||
active = g_repl_active_vm ? g_repl_active_vm : vm;
|
||||
|
||||
@@ -1017,7 +1083,21 @@ void sk_repl_run(VM *vm)
|
||||
* sk_print_prompt() extends this with a "(user)" segment. */
|
||||
sk_print_prompt();
|
||||
|
||||
sk_console_readline(input, sizeof(input), active, 1);
|
||||
int n = sk_console_readline(input, sizeof(input), active, 1);
|
||||
|
||||
#if !EMERGENCY_CONSOLE_ENABLED
|
||||
/* n < 0: sk_console_readline() bailed out because the identity
|
||||
* that was attached when this prompt was printed logged out
|
||||
* while we were still blocked waiting for input (2026-09-06 --
|
||||
* see sk_console_readline()'s own doc comment on this return
|
||||
* value). No " ok" here -- nothing was typed, nothing ran --
|
||||
* just loop back to the top, where the check above re-enters
|
||||
* headless silence immediately instead of showing yet another
|
||||
* prompt first. */
|
||||
if (n < 0) {
|
||||
continue;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (input[0] == '\0') {
|
||||
console_puts(" ok\n");
|
||||
|
||||
@@ -23,17 +23,37 @@
|
||||
/*
|
||||
* platform/alloc_kernel.c - Kernel (bare-metal) memory allocator
|
||||
*
|
||||
* Static arena with bump allocation. Free is a no-op.
|
||||
* This is appropriate for kernel use where:
|
||||
* - VM is long-lived (no restart)
|
||||
* - Allocations happen at init time
|
||||
* - Runtime allocations are rare
|
||||
* Static arena, bump-allocated with a real free list on top.
|
||||
*
|
||||
* FABRIC-3.md §VII follow-on, 2026-09-06: this used to bump-allocate only,
|
||||
* with sf_free() a deliberate no-op -- "VM is long-lived (no restart),
|
||||
* allocations happen at init time, runtime allocations are rare... no
|
||||
* fragmentation issues in practice." That premise held until this
|
||||
* session's own repeated identity-verification workflow (WIREBIND
|
||||
* birth/kill cycles, one console+user VM pair per identity, all sharing
|
||||
* this one global arena) needed VMs born and killed repeatedly within a
|
||||
* single boot -- confirmed live: a real kernel PANIC ("malloc failed"
|
||||
* cascading into "Stadium: eviction... governor invariant broken", full
|
||||
* halt) at the exact same cycle count on every run, because freed
|
||||
* dictionary entries (vm_cleanup() was itself also missing this free
|
||||
* before an earlier pass of this same fix) were never actually reclaimed
|
||||
* -- sf_free() threw them away regardless.
|
||||
*
|
||||
* Every allocation now carries a small header (size + free-list link) so
|
||||
* a freed block can be pushed onto g_free_list and reused by a
|
||||
* later sf_malloc() of equal or smaller size (first-fit, no splitting --
|
||||
* deliberately simple: this workload's repeated allocations are for the
|
||||
* same capsules loaded into a fresh VM each time, so freed blocks from a
|
||||
* just-killed VM's dictionary are typically an exact or near-exact fit
|
||||
* for the next VM's own). Falls back to bump-allocating a fresh block
|
||||
* from the arena when no free block is large enough, exactly as before.
|
||||
*
|
||||
* Arena size: 4MB by default (configurable via SF_ARENA_SIZE)
|
||||
*/
|
||||
|
||||
#include "platform_alloc.h"
|
||||
#include <stdint.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#ifndef SF_ARENA_SIZE
|
||||
#define SF_ARENA_SIZE (4 * 1024 * 1024) /* 4MB default */
|
||||
@@ -43,10 +63,22 @@
|
||||
#define SF_ALIGN 8
|
||||
#define SF_ALIGN_UP(x) (((x) + (SF_ALIGN - 1)) & ~(SF_ALIGN - 1))
|
||||
|
||||
/* Per-allocation header, immediately before the pointer sf_malloc()
|
||||
* returns. `next` is meaningful only while the block is on the free
|
||||
* list -- it's live/garbage data for the caller otherwise, matching the
|
||||
* classic free-list-node-in-freed-space technique, just kept as a fixed
|
||||
* header field instead of reusing payload bytes so there's no minimum-
|
||||
* payload-size constraint to worry about. */
|
||||
typedef struct sf_block_header {
|
||||
size_t size; /* payload size, in bytes, SF_ALIGN_UP'd */
|
||||
struct sf_block_header *next; /* free-list link; valid only while free */
|
||||
} sf_block_header_t;
|
||||
|
||||
/* Static arena */
|
||||
static uint8_t g_arena[SF_ARENA_SIZE] __attribute__((aligned(SF_ALIGN)));
|
||||
static size_t g_arena_offset = 0;
|
||||
static int g_initialized = 0;
|
||||
static sf_block_header_t *g_free_list = (sf_block_header_t *)0;
|
||||
|
||||
/* Statistics */
|
||||
static sf_alloc_stats_t g_stats = {0};
|
||||
@@ -64,6 +96,7 @@ static sf_alloc_stats_t g_stats = {0};
|
||||
int sf_alloc_init(void)
|
||||
{
|
||||
g_arena_offset = 0;
|
||||
g_free_list = (sf_block_header_t *)0;
|
||||
g_initialized = 1;
|
||||
|
||||
g_stats.total_bytes = SF_ARENA_SIZE;
|
||||
@@ -76,27 +109,55 @@ int sf_alloc_init(void)
|
||||
}
|
||||
|
||||
/*
|
||||
* @brief Allocate memory from the static kernel arena.
|
||||
* @brief Allocate memory from the static kernel arena, reusing a freed
|
||||
* block first if one is large enough.
|
||||
*
|
||||
* Bump-allocates @p size bytes from @c g_arena, rounding up to @c SF_ALIGN
|
||||
* (8 bytes) to preserve alignment for 64-bit values. Lazily calls
|
||||
* @c sf_alloc_init() on the first invocation if the arena has not been
|
||||
* Rounds @p size up to @c SF_ALIGN (8 bytes). First searches @c
|
||||
* g_free_list for the first block whose payload is >= the requested size
|
||||
* (first-fit, no splitting -- see this file's own top-of-file doc comment
|
||||
* for why that's the right tradeoff for this workload) and reuses it
|
||||
* whole if found. Otherwise bump-allocates a fresh header+payload block
|
||||
* from @c g_arena, exactly as before this fix. Lazily calls @c
|
||||
* sf_alloc_init() on the first invocation if the arena has not been
|
||||
* explicitly initialised. Returns @c NULL for zero-size requests and when
|
||||
* the arena is exhausted.
|
||||
*
|
||||
* @note Because this is a bump allocator there is no reclaim path; once the
|
||||
* arena is full it stays full until the kernel is reset.
|
||||
* neither a free block nor remaining arena space can satisfy the request.
|
||||
*
|
||||
* @param size Number of bytes to allocate.
|
||||
* @return Pointer to the allocated block on success, @c NULL on failure.
|
||||
* @return Pointer to the allocated block's payload on success, @c NULL on
|
||||
* failure.
|
||||
*/
|
||||
void* sf_malloc(size_t size)
|
||||
{
|
||||
if (!g_initialized) sf_alloc_init();
|
||||
if (size == 0) return (void*)0;
|
||||
|
||||
size_t aligned_size = SF_ALIGN_UP(size);
|
||||
size_t new_offset = g_arena_offset + aligned_size;
|
||||
size_t payload = SF_ALIGN_UP(size);
|
||||
|
||||
/* First-fit scan of the free list. */
|
||||
sf_block_header_t **pp = &g_free_list;
|
||||
while (*pp)
|
||||
{
|
||||
if ((*pp)->size >= payload)
|
||||
{
|
||||
sf_block_header_t *blk = *pp;
|
||||
*pp = blk->next;
|
||||
blk->next = (sf_block_header_t *)0;
|
||||
|
||||
g_stats.used_bytes += blk->size;
|
||||
g_stats.alloc_count++;
|
||||
if (g_stats.used_bytes > g_stats.peak_bytes)
|
||||
{
|
||||
g_stats.peak_bytes = g_stats.used_bytes;
|
||||
}
|
||||
return (void *)(blk + 1);
|
||||
}
|
||||
pp = &(*pp)->next;
|
||||
}
|
||||
|
||||
/* No free block large enough -- bump-allocate a fresh one. */
|
||||
size_t header_size = SF_ALIGN_UP(sizeof(sf_block_header_t));
|
||||
size_t total = header_size + payload;
|
||||
size_t new_offset = g_arena_offset + total;
|
||||
|
||||
if (new_offset > SF_ARENA_SIZE)
|
||||
{
|
||||
@@ -104,17 +165,19 @@ void* sf_malloc(size_t size)
|
||||
return (void*)0;
|
||||
}
|
||||
|
||||
void* ptr = &g_arena[g_arena_offset];
|
||||
sf_block_header_t *blk = (sf_block_header_t *)&g_arena[g_arena_offset];
|
||||
g_arena_offset = new_offset;
|
||||
blk->size = payload;
|
||||
blk->next = (sf_block_header_t *)0;
|
||||
|
||||
g_stats.used_bytes = g_arena_offset;
|
||||
g_stats.used_bytes += payload;
|
||||
g_stats.alloc_count++;
|
||||
if (g_stats.used_bytes > g_stats.peak_bytes)
|
||||
{
|
||||
g_stats.peak_bytes = g_stats.used_bytes;
|
||||
}
|
||||
|
||||
return ptr;
|
||||
return (void *)(blk + 1);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -195,35 +258,33 @@ void* sf_realloc(void* ptr, size_t new_size)
|
||||
}
|
||||
|
||||
/*
|
||||
* @brief Release a kernel arena allocation (no-op).
|
||||
* @brief Release a kernel arena allocation, making it available for reuse.
|
||||
*
|
||||
* The kernel bump allocator has no reclaim mechanism — once bytes are
|
||||
* allocated from @c g_arena they remain consumed until the kernel resets.
|
||||
* This function exists solely to satisfy the @c sf_free() contract expected
|
||||
* by shared VM code, and to keep @c g_stats.free_count accurate for
|
||||
* diagnostic purposes.
|
||||
*
|
||||
* Callers must not assume that freed memory is reclaimed or reusable.
|
||||
* FABRIC-3.md §VII follow-on, 2026-09-06: this used to be a documented
|
||||
* no-op (see this file's own top-of-file doc comment for why that
|
||||
* stopped being acceptable). Pushes the block's header onto @c
|
||||
* g_free_list, where a future @c sf_malloc() of equal or smaller size
|
||||
* will find and reuse it -- no coalescing with neighboring free blocks,
|
||||
* matching the same simplicity tradeoff @c sf_malloc()'s first-fit search
|
||||
* makes.
|
||||
*
|
||||
* @param ptr Pointer previously returned by @c sf_malloc() / @c sf_calloc()
|
||||
* (may be @c NULL; silently ignored).
|
||||
* (may be @c NULL; silently ignored). Must not be used again
|
||||
* by the caller after this call, and must not be freed twice.
|
||||
*/
|
||||
void sf_free(void* ptr)
|
||||
{
|
||||
/* Bump allocator: free is a no-op.
|
||||
*
|
||||
* This is acceptable because:
|
||||
* 1. VM allocations happen at init time
|
||||
* 2. VM runs until power-off
|
||||
* 3. No fragmentation issues in practice
|
||||
*
|
||||
* If needed, could implement a simple free list here.
|
||||
*/
|
||||
if (ptr)
|
||||
if (!ptr) return;
|
||||
|
||||
sf_block_header_t *blk = ((sf_block_header_t *)ptr) - 1;
|
||||
blk->next = g_free_list;
|
||||
g_free_list = blk;
|
||||
|
||||
if (g_stats.used_bytes >= blk->size)
|
||||
{
|
||||
g_stats.free_count++;
|
||||
g_stats.used_bytes -= blk->size;
|
||||
}
|
||||
(void)ptr;
|
||||
g_stats.free_count++;
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -321,6 +321,35 @@ void vm_cleanup(VM* vm)
|
||||
vm->ssm_config = NULL;
|
||||
}
|
||||
|
||||
/* FABRIC-3.md §VII follow-on, 2026-09-06: every DictEntry is its own
|
||||
* sf_malloc() (vm_create_word(), dictionary_management.c) -- separate
|
||||
* from vm->memory entirely, so freeing that arena below never touched
|
||||
* them. Neither did anything else in this function, or anywhere else
|
||||
* in the codebase (confirmed by grep before writing this fix) -- every
|
||||
* word a VM ever defined leaked permanently on kill. Never noticed
|
||||
* before: the hosted binary normally only calls this once at process
|
||||
* exit (the OS reclaims everything anyway), and kernel VMs were
|
||||
* normally born once and kept alive for a whole boot, not repeatedly
|
||||
* born and killed -- confirmed live as a real PANIC ("malloc failed"
|
||||
* cascading into "Stadium: eviction... governor invariant broken",
|
||||
* full halt) during this session's own repeated identity-verification
|
||||
* workflow (8 consecutive WIREBIND birth/kill cycles in one boot).
|
||||
* transition_metrics (vm_create_word()'s own second, per-entry
|
||||
* sf_malloc()) must be freed too, before the entry itself -- freeing
|
||||
* entry first would leave no way to reach it. */
|
||||
{
|
||||
DictEntry *dict_entry = vm->latest;
|
||||
while (dict_entry) {
|
||||
DictEntry *next_entry = dict_entry->link;
|
||||
if (dict_entry->transition_metrics) {
|
||||
sf_free(dict_entry->transition_metrics);
|
||||
}
|
||||
sf_free(dict_entry);
|
||||
dict_entry = next_entry;
|
||||
}
|
||||
vm->latest = NULL;
|
||||
}
|
||||
|
||||
if (vm->memory)
|
||||
{
|
||||
vm_host_free(vm, vm->memory);
|
||||
|
||||
@@ -398,6 +398,26 @@ void vm_cleanup(VM* vm)
|
||||
vm->ssm_config = NULL;
|
||||
}
|
||||
|
||||
/* Same fix as the kernel's own vm_cleanup() (src/starkernel/vm/vm_core.c)
|
||||
* -- every DictEntry is its own sf_malloc() (vm_create_word(),
|
||||
* dictionary_management.c), separate from vm->memory entirely, and
|
||||
* nothing anywhere ever freed them. Confirmed live via a kernel-side
|
||||
* repeated birth/kill cycle exhausting the heap; this hosted copy of
|
||||
* vm_cleanup() has the identical gap, just never noticed here because
|
||||
* the hosted binary normally only calls this once at process exit. */
|
||||
{
|
||||
DictEntry *dict_entry = vm->latest;
|
||||
while (dict_entry) {
|
||||
DictEntry *next_entry = dict_entry->link;
|
||||
if (dict_entry->transition_metrics) {
|
||||
sf_free(dict_entry->transition_metrics);
|
||||
}
|
||||
sf_free(dict_entry);
|
||||
dict_entry = next_entry;
|
||||
}
|
||||
vm->latest = NULL;
|
||||
}
|
||||
|
||||
if (vm->memory)
|
||||
{
|
||||
sf_free(vm->memory);
|
||||
|
||||
Reference in New Issue
Block a user