Repository navigation
fix(gc): root IMPLICIT_THIS so moving GC can't strand the dispatch receiver (#1813) - #1825
Merged
Merged
Conversation
…ceiver (#1813) The implicit-`this` thread-local cell holds the NaN-boxed receiver for the duration of a dynamically-dispatched non-arrow method body (set/restored by js_native_call_method and the codegen js_implicit_this_set save/restore around js_native_call_value). The receiver is a live heap object for the whole call, but the cell was plain TLS that no GC root scanner visited. When a moving GC runs during that body — e.g. @perryts/mysql's Pool.acquire runs a nested stdlib pump that drains the handshake IO while nativeScramble allocates — the receiver is evacuated/copied. With no root slot to rewrite, the cell kept the stale pre-move pointer and the body's next `this`-derived dispatch dereferenced relocated memory: the concurrent-load SIGSEGV in js_native_call_method reported in #1813. Objects only move under nursery/RSS pressure, which is why it only reproduced under concurrency (load-dependent heisenbug) and across both auth plugins. Register IMPLICIT_THIS as a GC mutable-root scanner: mark keeps `this` alive, rewrite tracks its relocation. Non-pointer tags (the TAG_UNDEFINED default) are no-ops, so scanning the idle cell is free. Every codegen and runtime dispatch site funnels through this one cell, so a single scanner closes the gap. Adds a deterministic regression test exercising mark + forwarding rewrite + the idle-cell no-op; validated under PERRY_GC_FORCE_EVACUATE=1 PERRY_GC_VERIFY_EVACUATION=1.
This was referenced May 26, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #1813 — the SIGSEGV race in
js_native_call_methodreached via@perryts/mysql'snativeScrambleduring concurrentPool.acquirehandshakes.Root cause: the implicit-
thisthread-local cell (IMPLICIT_THISincrates/perry-runtime/src/object/mod.rs) holds the NaN-boxed receiver for the duration of a dynamically-dispatched non-arrow method body. It is set/restored around the call by bothjs_native_call_methodand the codegenjs_implicit_this_setsave/restore that wrapsjs_native_call_value. The receiver is a live heap object for the whole call — but the cell was plain thread-local storage that no GC root scanner visited.When a moving GC runs during that method body — and under this workload it does:
Pool.acquireruns a nested stdlib pump (js_run_stdlib_pump) that drains the handshake network IO, and the auth path allocates while building the scramble — the receiver gets evacuated (old-gen) or copied (nursery). With no root slot to rewrite, the cell kept the stale pre-move pointer, and the body's nextthis-derived dispatch dereferenced freed/relocated memory → the reported fault insidejs_native_call_method.This explains every symptom:
-c 1) never moves the receiver mid-handshake.mysql_native_passwordSIGSEGV,caching_sha2_passwordwedge): both paths share concurrent connection-establishment that runs the handshake under the nested pump.@perryts/mysqlPOST shape): that one was a stale TLS pointer in the microtask drain; this is a stale TLS pointer in the implicit-thisslot. Same class, different cell.Fix
Register
IMPLICIT_THISas a GC mutable-root scanner (scan_implicit_this_roots_mut), wired intogc_init()alongside the other object scanners. The mark phase keepsthisreachable when the cell is its only root; the rewrite phase follows the forwarding pointer so the cell tracks the receiver's relocation. Non-pointer tags (theTAG_UNDEFINEDdefault, null/int/bool) flow throughvisit_nanbox_bitsas no-ops, so scanning the idle cell between calls is free and safe.Because every codegen and runtime dispatch site funnels through this one cell, a single scanner closes the gap for all of them.
Testing
test_implicit_this_root_scanner_marks_and_rewrites(ingc/tests/runtime_roots.rs): sets the cell to a nursery receiver, asserts the scanner marks it (not swept) and rewrites it to the relocated copy afterset_forwarding_address, and that an idleTAG_UNDEFINEDcell is left untouched. Mirrors the existing receiver-rewrite tests forjs_native_call_method(split/replace/bigint).gc::module: 281 passed, 0 failed.gc::tests::runtime_roots+gc::tests::evacuationunderPERRY_GC_FORCE_EVACUATE=1 PERRY_GC_VERIFY_EVACUATION=1(real moving GC + post-move stale-reference verifier): all green.cargo fmt --all -- --checkclean.I couldn't reproduce the full MySQL-under-concurrency e2e locally (needs the server + package + load), so validation is via the unit tests that exercise the exact moving-GC + implicit-
thisinteraction the crash depends on, plus the force-evacuate/verify stress mode.