Fix use-after-free in sk_repl_idle()'s idle-tick VM resolution (FABRIC-3.md §XIII)
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:
co-authored by
Claude Sonnet 5
parent
70421bdd43
commit
9142dda2d6
@@ -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
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user