From 1984d3675f01ce2ff16547ebd3abf049ea0b2f5d Mon Sep 17 00:00:00 2001 From: "SANO,Masatoshi" Date: Thu, 12 Feb 2026 13:29:36 +0900 Subject: [PATCH] Fix GC control stack stale pointer crashes on ARM64 safepoint builds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On ARM64 safepoint builds with precise GC and separate control/C stacks (!C_STACK_IS_CONTROL_STACK), the GC scans the control stack precisely — every pointer-shaped word is treated as a live reference and transported. However, dead variables in active call frames may hold stale pointers to already-freed from-space objects. Transporting these corrupts the heap, typically manifesting as random crashes after GC or "bad widetag" errors. This does not affect x86/x86-64 (conservative stack scanning pins rather than transports, masking stale pointers) or ARM64 Linux (uses signal-based GC stop, not safepoints). Three coordinated fixes: 1. gencgc.c: Limit control stack scan range. On safepoint builds, GC runs as Lisp code (SUB-GC) on the control stack, so thread->csp includes SUB-GC's own frames which may contain uninitialized data. Temporarily limit scanning to the interrupted code's CSP (saved in the interrupt context by fake_foreign_function_call), which excludes the GC infrastructure frames. 2. gc-common.c (scavenge_control_stack): Validate from-space pointers before transport. For list pointers, verify the target page is PAGE_TYPE_CONS (on precise GC, cons cells are only on cons pages). For other pointers, verify the header widetag is valid and its lowtag matches the pointer's lowtag. Invalid pointers are zeroed. 3. safepoint.c: Scrub stale control stack frames after undo_fake_foreign_function_call in the safepoint handler. This reduces the window for stale pointers to accumulate across GC cycles. All changes are guarded by: #if defined(LISP_FEATURE_SB_SAFEPOINT) && \ !defined(LISP_FEATURE_C_STACK_IS_CONTROL_STACK) No impact on other platforms. Co-Authored-By: Claude Opus 4.6 --- src/runtime/gc-common.c | 34 ++++++++++++++++++++++++++++++++++ src/runtime/gencgc.c | 29 +++++++++++++++++++++++++++++ src/runtime/safepoint.c | 11 +++++++++++ 3 files changed, 74 insertions(+) diff --git a/src/runtime/gc-common.c b/src/runtime/gc-common.c index ace25d5ec..029ccb142 100644 --- a/src/runtime/gc-common.c +++ b/src/runtime/gc-common.c @@ -2442,6 +2442,40 @@ scavenge_control_stack(struct thread *th) #ifdef LISP_FEATURE_MARK_REGION_GC mr_preserve_object(word); #else +#if defined(LISP_FEATURE_SB_SAFEPOINT) && !defined(LISP_FEATURE_C_STACK_IS_CONTROL_STACK) + /* On safepoint builds with precise GC and separate stacks (ARM64), + * dead variables in active frames may contain stale pointers to + * from-space addresses that have been freed/reused. Without stack + * maps, the GC cannot distinguish live from dead variables. + * Transporting garbage data corrupts the heap, so validate that + * from-space pointers actually point to plausible objects before + * allowing scav1 to transport them. Zero invalid slots. */ + { + page_index_t pg = find_page_index((void*)word); + if (pg >= 0 && page_table[pg].gen == from_space) { + lispobj *target = native_pointer(word); + if (!forwarding_pointer_p(target)) { + int valid; + if (lowtag_of(word) == LIST_POINTER_LOWTAG) { + /* Cons pointers should target cons pages */ + valid = (page_table[pg].type == PAGE_TYPE_CONS); + } else { + /* Headered objects: validate header widetag and + * check that the widetag's lowtag matches the + * pointer's lowtag */ + int widetag = *target & WIDETAG_MASK; + valid = other_immediate_lowtag_p(widetag) + && LOWTAG_FOR_WIDETAG(widetag) + && LOWTAG_FOR_WIDETAG(widetag) == lowtag_of(word); + } + if (!valid) { + *object_ptr = 0; + continue; + } + } + } + } +#endif scav1(object_ptr, word); #endif } diff --git a/src/runtime/gencgc.c b/src/runtime/gencgc.c index f2c3d4bd3..442b91a49 100644 --- a/src/runtime/gencgc.c +++ b/src/runtime/gencgc.c @@ -3516,7 +3516,36 @@ garbage_collect_generation(generation_index_t generation, int raise, #if !defined(LISP_FEATURE_MIPS) && defined(reg_CODE) // interrupt contexts already pinned everything they see scavenge_interrupt_contexts(th); #endif +#if defined(LISP_FEATURE_SB_SAFEPOINT) && !defined(LISP_FEATURE_C_STACK_IS_CONTROL_STACK) + /* On safepoint builds with separate control and C stacks (ARM64), + * GC runs as Lisp code (SUB-GC) on the control stack. The current + * thread->csp includes SUB-GC's frames, which may contain stale or + * uninitialized pointers. Precise scanning of these frames crashes + * when it encounters stale pointers to freed from-space objects. + * + * Fix: temporarily limit the scan to the interrupted code's CSP + * (from the interrupt context stored by fake_foreign_function_call). + * The interrupt context registers are already scavenged by + * scavenge_interrupt_contexts above. */ + { + lispobj *saved_csp = access_control_stack_pointer(th); + int ctx_idx = fixnum_value(read_TLS(FREE_INTERRUPT_CONTEXT_INDEX, th)); + if (ctx_idx > 0) { + os_context_t *ctx = nth_interrupt_context(ctx_idx - 1, th); + lispobj *interrupted_csp = + (lispobj*)(uword_t)(*os_context_register_addr(ctx, reg_CSP)); + /* build_fake_control_stack_frames places a 4-word frame above + * the interrupted CSP. Include it in the scan. */ + lispobj *limit = interrupted_csp + 4; + if (limit < saved_csp) + access_control_stack_pointer(th) = limit; + } + scavenge_control_stack(th); + access_control_stack_pointer(th) = saved_csp; + } +#else scavenge_control_stack(th); +#endif } # ifdef LISP_FEATURE_SB_SAFEPOINT diff --git a/src/runtime/safepoint.c b/src/runtime/safepoint.c index e653d29b7..1df7970ef 100644 --- a/src/runtime/safepoint.c +++ b/src/runtime/safepoint.c @@ -1066,6 +1066,17 @@ handle_safepoint_violation(os_context_t *ctx, os_vm_address_t fault_address) fake_foreign_function_call(ctx); thread_in_lisp_raised(ctx); undo_fake_foreign_function_call(ctx); + /* Scrub stale Lisp frames left on the control stack by SUB-GC + * and thruption handlers. undo_fake_foreign_function_call + * zeroed thread->csp (it IS foreign_function_call_active_p on + * ARM64), so temporarily restore it for scrubbing. */ + { + lispobj *csp = (lispobj*)(uword_t) + (*os_context_register_addr(ctx, reg_CSP)); + access_control_stack_pointer(self) = csp; + scrub_thread_control_stack(self); + access_control_stack_pointer(self) = 0; + } #endif return 1; }