Fix GC control stack stale pointer crashes on ARM64 safepoint builds

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 <noreply@anthropic.com>
This commit is contained in:
SANO,Masatoshi 2026-02-12 13:29:36 +09:00 committed by Stas Boukarev
parent 6aa247c9d4
commit 1984d3675f
3 changed files with 74 additions and 0 deletions

View file

@ -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
}

View file

@ -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

View file

@ -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;
}