Repository navigation
Stale awaited Promise reaches js_async_step_chain under PERRY_GC_SCHEDULE_SEED=1 RATE=1 + PROTECT_FROMSPACE (cc, safepoint 4746) #9506
Description
Found while validating #9497 (the fix for #9446). With the trampoline frame fixed, the same seeded run paired with the from-space quarantine no longer faults at #9446's site — it faults later, on a different stale pointer:
PERRY_GC_SCHEDULE_SEED=1 PERRY_GC_SCHEDULE_RATE=1 PERRY_GC_PROTECT_FROMSPACE=1 PERRY_GC_PROTECT_FROMSPACE_DEPTH=800 \
<cc binary> -p --input-format stream-json --output-format stream-json --verbose (stdin: one "hi" user record, fresh HOME)
[gc-schedule] FAILURE (signal 11) under seed=1
[gc-schedule] safepoints=4746 scheduled_collections=4736
[gc-fromspace-protect] FAULT: signal 11 at 0x40e1fc845a0
This address is RETIRED FROM-SPACE. The evacuating minor moved or
freed the object here and the holder kept the pre-collection address.
block=0x40e1fc80000 +17824 retired_bytes=61496 retired_by_minor=#4744
last-known object: user_ptr=0x40e1fc845a8 obj_type=5 size=80
The faulting instruction IS the stale use.
obj_type=5 is a Promise. Symbolized backtrace (PERRY_KEEP_SYMBOLS=1 build of cli_2.1.112.js at f1e9c370a + #9497):
#0 js_async_step_chain+0x2b1 crates/perry-runtime/src/promise/async_step.rs
#1 perry_closure_claude_ccstress_cli_2_1_112_js__81658+0x3697 (compiled async step body)
#2 perry_runtime::promise::async_step::call_async_step_body+0x352
#3 perry_closure_claude_ccstress_cli_2_1_112_js__40562+0x26c
#4 js_native_call_value+0x5d6
#5 perry_runtime::object::native_call_method::common_methods::dispatch_common+0x11d4
#6 js_native_call_method+0x2dc1
#7 js_typed_feedback_native_call_method_by_id+0x61
#8 perry_closure_claude_ccstress_cli_2_1_112_js__10818+0x6e4
#9 perry_runtime::closure::registry::dispatch_rest_bundled+0xf0e
#10 perry_closure_claude_ccstress_cli_2_1_112_js__82812+0x306
#11 perry_runtime::promise::microtasks::pump_protected+0x81de
#12 …run_microtasks… / perry_sjlj_try
The faulting instruction:
js_async_step_chain+675: call *…(%rip) # perry_runtime::buffer::header::is_registered_buffer_slow
js_async_step_chain+681: test %al,%al ; jne …
js_async_step_chain+689: cmpb $0x5,-0x8(%rbp) # <- fault; rbp = 0x40e1fc845a8, r14 = 0x7ffd040e1fc845a8 (the nanboxed `value`)
That is the inlined js_value_is_promise(value) GC-header check at the top of the promise branch — so the stale pointer is the value argument as passed in by the compiled async step body (frame #1, __81658), not something js_async_step_chain derived itself. The retiring minor is #4744 and the use is at safepoint 4746, two collections later. Nothing in js_async_step_chain before that point allocates on the native-promise path (adapt_foreign_promise_value only acts on V8 handles), so the awaited promise was already dangling when await lowered to the runtime call — which points at the async lowering holding the awaited operand across a collection point (CLAUDE.md lists the async-to-generator body-local boxing as a known-weak area).
Not chased further here. For scale: on the pre-#9497 binary the quarantine's first fault is at safepoint 818 (a dynamic-dispatch receiver, the #9417 shape #9479 closed); with #9479 and #9497 in, this is the first stale use the instrument finds on this workload. The unprotected seeded run completes (6645 forced collections, Not logged in), so this one is silent without the quarantine.
Artifacts on the build host: /root/claude-9446/app_fixed (binary with symbols), /root/claude-9446/w_fixedprot2/gdb.out (full gdb session with registers), /root/claude-9446/seedprot2.sh (the gdb driver).
Activity
proggeramlug commentedon Sep 4, 2026
Re-ran this issue's own reproducer — same env, same single
"hi"stream-json record on stdin, freshHOME— against a freshly built coherent bundle at currentmain(0e3ce3812, one runtime stamp), with this issue's own binary/root/claude-9446/app_fixedas a paired control in the same environment.retired_set=#Nlines)app_fixed(f1e9c370a+ #9497 — the binary this issue was filed against)mainrun 1mainrun 2mainrun 3The control reproduces this issue exactly, down to the numbers in the report:
so the harness and the instrument are both right, and this is a real before/after, not a configuration that quietly stopped exercising the subject.
The subject was live on every
mainrun. Each quarantined 6 400+ retired page-sets, ran 6 400+ copying minors and moved ~950 000 objects — and each ran past safepoint 4746, where the control faults, to ~6 600 safepoints and a normalNot logged inexit. A green run here is the detector finding nothing, not the detector never arming.Attribution
Not bisected. The window between this issue's binary and current
maincarries several rooting fixes that could each account for a stale awaited Promise (#9673rooting the callback across everyArray.prototypeiteration loop,#9660rooting web-adapter values across GC,#9628/#9629in the collector itself,#9611holding the wasm imports object behind a token), and each bisect point is a ~35-minute bundle compile on a shared host. I did not want to spend that to name a commit for an issue that is already closed by the evidence above — but the reproducer and the control binary are both still on the build host if anyone wants to.Closing as fixed. The async-step /
AsyncStepChainlifetime suspicion in the original report was never confirmed and should not be carried forward as a known defect; if this shape returns, the quarantine will name the real holder again.Artifacts on the build host:
/root/cc9575/repro9506b.sh(the driver used here),/root/cc9575/err9506_{control,main1,main2,main3}.txt(fullPERRY_GC_DIAG=1logs),/root/cc9575/app_main(themainbundle).