Fix use-after-free in sk_repl_idle()'s idle-tick VM resolution (FABRIC-3.md §XIII)
Build / build-amd64-iso (push) Waiting to run
Build / build-aarch64-iso (push) Waiting to run
Build / build-riscv64-img (push) Waiting to run

Root-caused a heap-corruption bug that reliably failed WIREBIND identity
attach on the third attach/detach cycle in one boot. sk_console_readline()
and sk_console_getkey() captured `active_vm` once from their caller and
kept passing that same (possibly long-stale) pointer to sk_repl_idle() on
every idle tick serviced while blocked waiting for input. If the VM it
pointed at was killed (WIREBIND detach) mid-block, the existing bailout
only checked a generic "is anyone attached" boolean -- masked as soon as a
different identity attached next -- so blk_vm_flush_all() kept writing
into a freed VM struct sitting on kmalloc's own free list, corrupting the
free list's linked-list metadata itself.

Both idle branches now re-resolve the live active VM fresh from
g_repl_active_vm on every tick, matching the dispatch-side fix already
made for the sibling bug in §XII.3.

Verified: rebuilt amd64, reran the exact three-cycle repro that reliably
corrupted the heap before the fix -- free-list census stayed stable
through the same idle window that previously collapsed to zero. All three
architectures (amd64/aarch64/riscv64) boot clean to the zuse)ok> prompt.

kmalloc_debug_census()/kmalloc_debug_census_bytes() kept as permanent
diagnostic infrastructure; every other temporary probe added during the
investigation was reverted.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EXieurDfDSsDFdnSyusuWo
This commit is contained in:
Robert Allan James
2026-09-10 20:30:41 -04:00
co-authored by Claude Sonnet 5
parent 70421bdd43
commit 9142dda2d6
38 changed files with 236983 additions and 3 deletions
+37
View File
@@ -339,3 +339,40 @@ kmalloc_stats_t kmalloc_get_stats(void)
{
return heap_stats;
}
void kmalloc_debug_census(size_t *out_largest_free, size_t *out_free_count,
size_t *out_used_count, size_t *out_total_blocks)
{
size_t largest = 0, free_count = 0, used_count = 0, total = 0;
heap_block_t *cur = heap_head;
while (cur) {
total++;
if (cur->free) {
free_count++;
if (cur->size > largest) largest = cur->size;
} else {
used_count++;
}
cur = cur->next;
}
if (out_largest_free) *out_largest_free = largest;
if (out_free_count) *out_free_count = free_count;
if (out_used_count) *out_used_count = used_count;
if (out_total_blocks) *out_total_blocks = total;
}
void kmalloc_debug_census_bytes(size_t *out_total_free_bytes, size_t *out_total_used_bytes)
{
size_t free_bytes = 0, used_bytes = 0;
heap_block_t *cur = heap_head;
while (cur) {
if (cur->free) {
free_bytes += cur->size;
} else {
used_bytes += cur->size;
}
cur = cur->next;
}
if (out_total_free_bytes) *out_total_free_bytes = free_bytes;
if (out_total_used_bytes) *out_total_used_bytes = used_bytes;
}
+40 -2
View File
@@ -749,6 +749,11 @@ static int g_console_pending_key = -1;
* the caller's job, same as any standard KEY implementation. */
int sk_console_getkey(VM *active_vm)
{
/* No longer used directly -- the idle branch below re-resolves the
* live active VM itself (FABRIC-3.md SXIII, 2026-09-10) rather than
* trusting this parameter, which can go stale mid-block. Kept in the
* signature: repl.h declares it, other callers still pass one. */
(void)active_vm;
for (;;) {
int c;
if (g_console_pending_key >= 0) {
@@ -763,7 +768,13 @@ int sk_console_getkey(VM *active_vm)
uint64_t now = heartbeat_ticks();
if (now - g_last_beat_tick >= SK_IDLE_BEAT_INTERVAL) {
g_last_beat_tick = now;
sk_repl_idle(active_vm);
/* Same use-after-free class fixed in sk_console_readline()'s
* idle branch (FABRIC-3.md SXIII, 2026-09-10) -- `active_vm`
* is a parameter captured once by the caller before this
* (possibly long) blocking wait for a key began, and can be
* killed mid-wait. Re-resolve fresh every tick instead. */
VM *live_active = g_repl_active_vm ? g_repl_active_vm : (VM *)sk_get_mama_vm();
sk_repl_idle(live_active);
}
arch_relax();
}
@@ -837,6 +848,11 @@ int sk_console_key_available(void)
int sk_console_readline(char* buf, int size, VM* active_vm, int reanchor_prompt)
{
/* No longer used directly -- see sk_console_getkey()'s matching comment;
* the idle branch below re-resolves the live active VM itself instead
* of trusting this parameter across a potentially long block. Kept in
* the signature: repl.h declares it, other callers still pass one. */
(void)active_vm;
int n = 0;
/* TX counter value right after the caller printed its prompt. Any
* console output that lands while this readline blocks (heartbeat
@@ -889,7 +905,29 @@ int sk_console_readline(char* buf, int size, VM* active_vm, int reanchor_prompt)
* design already tolerates. */
if (n == 0 && now - g_last_beat_tick >= SK_IDLE_BEAT_INTERVAL) {
g_last_beat_tick = now;
sk_repl_idle(active_vm);
/* Found live 2026-09-10 (FABRIC-3.md SXIII): `active_vm` is
* this call's parameter, captured once by the caller before
* this (possibly very long) block began -- see the matching
* comment at this function's dispatch-side fix, below, for
* the full mechanism. That fix re-resolves `active` fresh
* right before dispatch, but every idle tick serviced
* *during* this same blocked call used to pass the stale
* parameter straight into sk_repl_idle() -> blk_vm_flush_all(),
* which reads and writes vm->blk_vm_lbn[]/cbuf[]/dirty[]/
* epoch on whatever `active_vm` points at. If that VM was
* killed (WIREBIND detach) while this call sat idle, those
* fields live in a kmalloc block already back on the free
* list -- and blk_vm_check_epoch()'s unconditional field
* writes silently corrupt that block's own free-list
* metadata (next/size/free), observed live as free_blocks/
* largest_free collapsing to 0 a tick or two after a third
* WIREBIND identity attached following two prior attach/
* detach cycles. Re-resolve fresh from the global here too,
* same pattern as the dispatch-side fix -- sk_get_mama_vm()
* is the safe fallback (never freed) matching sk_repl_run()'s
* own `g_repl_active_vm ? g_repl_active_vm : vm` shape. */
VM *live_active = g_repl_active_vm ? g_repl_active_vm : (VM *)sk_get_mama_vm();
sk_repl_idle(live_active);
}
/* Blink the cursor while idle (no key ready this iteration),