Under a seeded GC schedule, the moment/parse_format package workload (moment 2.31.0) faults on a stale from-space closure pointer during moment's module init. The stale use is in js_register_function_prototype_method → synthetic_class_id_for_function, which is the Func.prototype.x = fn registration path.
main: 295473cd5c4f0eceed5c5cd04b180e7533195641. It contains #11610 (be39bbf), which fixed the qs crash with a similar shape (#11550). On the same build, qs/parse_nested is 0/200 clean, so this one is separate.
Failure rate
PERRY_GC_SCHEDULE_RATE=0.05 PERRY_GC_PROTECT_FROMSPACE=1, seeds 1–200, args 300 50, output compared to Node 26.5.1 (checksum cfca1856):
22/200 fail, all SIGSEGV (rc 139): 3 13 19 21 32 65 75 90 91 94 97 101 107 120 133 140 144 152 155 162 173 183
These are the same seeds PR #11634 reported as pre-existing on its older base (3, 13, 19, 21, 32 within 1–40; 22/200). It is deterministic: seed 3 faulted on 2 of 2 re-runs. Every failure has the same signature: obj_type=4 (GC_TYPE_CLOSURE), size=56, retired_by_minor=#1, around safepoint 14. That means the first copying minor, during module init.
Without PERRY_GC_PROTECT_FROMSPACE, seeds 3, 13 and 19 exit 0 (77, 70 and 64 copying minors). I did not diff their output against Node, so a silent wrong answer there is possible but unverified.
Repro
cargo build --release -p perry -p perry-runtime-static -p perry-stdlib-static
cd benchmarks/packages && npm ci --ignore-scripts
PERRY_KEEP_SYMBOLS=1 PERRY_NO_AUTO_OPTIMIZE=1 PERRY_RUNTIME_DIR=../../target/release \
../../target/release/perry compile moment/parse_format.ts -o /tmp/msym
PERRY_GC_SCHEDULE_SEED=3 PERRY_GC_SCHEDULE_RATE=0.05 PERRY_GC_PROTECT_FROMSPACE=1 /tmp/msym 300 50
Fault report (seed 3, symbolized via addr2line)
[gc-schedule] FAILURE (signal 11) under seed=3
[gc-schedule] safepoints=14 scheduled_collections=2
[gc-fromspace-protect] FAULT: signal 11 at 0x50e74416f30
This address is RETIRED FROM-SPACE. The evacuating minor moved or
freed the object here and the holder kept the pre-collection address.
block=... +94000 retired_bytes=518824 retired_by_minor=#1
last-known object: user_ptr=0x50e74416f38 obj_type=4 size=56
The faulting instruction IS the stale use. Backtrace:
perry_runtime::arena::quarantine::fromspace_fault_handler
perry_runtime::gc::schedule::schedule_fault_handler
?? (inlined; likely is_callable_function_value reading the closure header)
perry_runtime::object::class_registry::prototype_methods::synthetic_class_id_for_function
js_register_function_prototype_method
perry_closure_node_modules_moment_moment_js__11
perry_closure_node_modules_moment_moment_js__433
perry_closure_node_modules_moment_moment_js__1
perry_closure_node_modules_moment_moment_js__0
node_modules_moment_moment_js__init_body
...run_path_initializer / js_run_module_init_catching / main
Notes (not verified)
- The
func_value passed to js_register_function_prototype_method from moment closure __11 still holds the closure's pre-minor address. So the codegen-side holder of Func across the Func.prototype.x = fn store is not rooted, or its root is not rewritten, across a preceding collection point. --trace hir --focus / --trace llvm on closure __11 should show which local it is.
- There is a related hazard in
synthetic_class_id_for_function itself: FUNCTION_CLASS_IDS is keyed by the closure's NaN-boxed bits. A closure that moves in a copying minor after its first registration would look up under a new key and get a fresh synthetic class id. That would split F.prototype methods from new F() instances. This is not the fault above, but it is the same path.
Not run
Only Linux x86_64 (perrymaster), release build with CARGO_PROFILE_RELEASE_CODEGEN_UNITS=16, PERRY_NO_AUTO_OPTIMIZE=1. I did not check the other moment workload (diff_duration) or the macOS/arm64 path, and I did not dig into the root cause.
Under a seeded GC schedule, the
moment/parse_formatpackage workload (moment 2.31.0) faults on a stale from-space closure pointer during moment's module init. The stale use is injs_register_function_prototype_method→synthetic_class_id_for_function, which is theFunc.prototype.x = fnregistration path.main:
295473cd5c4f0eceed5c5cd04b180e7533195641. It contains #11610 (be39bbf), which fixed the qs crash with a similar shape (#11550). On the same build,qs/parse_nestedis 0/200 clean, so this one is separate.Failure rate
PERRY_GC_SCHEDULE_RATE=0.05 PERRY_GC_PROTECT_FROMSPACE=1, seeds 1–200, args300 50, output compared to Node 26.5.1 (checksum cfca1856):22/200 fail, all SIGSEGV (rc 139): 3 13 19 21 32 65 75 90 91 94 97 101 107 120 133 140 144 152 155 162 173 183
These are the same seeds PR #11634 reported as pre-existing on its older base (3, 13, 19, 21, 32 within 1–40; 22/200). It is deterministic: seed 3 faulted on 2 of 2 re-runs. Every failure has the same signature:
obj_type=4(GC_TYPE_CLOSURE),size=56,retired_by_minor=#1, around safepoint 14. That means the first copying minor, during module init.Without
PERRY_GC_PROTECT_FROMSPACE, seeds 3, 13 and 19 exit 0 (77, 70 and 64 copying minors). I did not diff their output against Node, so a silent wrong answer there is possible but unverified.Repro
Fault report (seed 3, symbolized via addr2line)
Notes (not verified)
func_valuepassed tojs_register_function_prototype_methodfrom moment closure__11still holds the closure's pre-minor address. So the codegen-side holder ofFuncacross theFunc.prototype.x = fnstore is not rooted, or its root is not rewritten, across a preceding collection point.--trace hir --focus/--trace llvmon closure__11should show which local it is.synthetic_class_id_for_functionitself:FUNCTION_CLASS_IDSis keyed by the closure's NaN-boxed bits. A closure that moves in a copying minor after its first registration would look up under a new key and get a fresh synthetic class id. That would splitF.prototypemethods fromnew F()instances. This is not the fault above, but it is the same path.Not run
Only Linux x86_64 (perrymaster), release build with
CARGO_PROFILE_RELEASE_CODEGEN_UNITS=16,PERRY_NO_AUTO_OPTIMIZE=1. I did not check the other moment workload (diff_duration) or the macOS/arm64 path, and I did not dig into the root cause.