Skip to content

Fix WriteBarrier owner and add cellLock for JSCommonJSExtensions::m_registeredFunctions - #30524

Closed
cirospaciari wants to merge 4 commits into
mainfrom
fix-cjs-extensions-celllock-and-owner
Closed

Fix WriteBarrier owner and add cellLock for JSCommonJSExtensions::m_registeredFunctions#30524
cirospaciari wants to merge 4 commits into
mainfrom
fix-cjs-extensions-celllock-and-owner

Conversation

@cirospaciari

Copy link
Copy Markdown
Member

What

Two GC-correctness fixes for JSCommonJSExtensions::m_registeredFunctions in src/jsc/bindings/JSCommonJSExtensions.cpp.

1. Wrong WriteBarrier::set() owner cell

JSCommonJSExtensions__setFunction (line 215) and JSCommonJSExtensions__swapRemove (line 229) pass globalObject as the owner argument to WriteBarrier::set():

extensions->m_registeredFunctions[index].set(globalObject->vm(), globalObject, value);
//                                                              ^^^^^^^^^^^^ should be extensions

WriteBarrier::set(vm, owner, value) registers owner in the GC's remembered set so the write is re-scanned during an eden collection. But it is JSCommonJSExtensions::visitChildrenImpl — not the global object's visitChildren — that scans m_registeredFunctions. With globalObject as the registered owner, an eden collection where extensions is already old/black and value is young never re-marks value, and it can be collected while still referenced. The next time visitChildrenImpl runs, visitor.append(func) reads a dangling cell pointer.

JSCommonJSExtensions__appendFunction (line 208) already passes the correct owner (extensions); these two call sites were the only ones that did not.

2. Missing cellLock() around Vector<WriteBarrier<>> mutation/visit

m_registeredFunctions is a WTF::Vector<WriteBarrier<Unknown>> mutated on the mutator thread by __appendFunction / __setFunction / __swapRemove (append() / takeLast() / removeLast() / clear()) without any lock, while visitChildrenImpl iterates it on parallel mark threads. A Vector reallocation can free the backing buffer while a mark thread is mid-scan, putting garbage JSCell* pointers on the mark stack.

This is the same pattern that was fixed for JSCommonJSModule::m_children in #29995 (bd5149f927). This change applies the same cellLock() discipline to m_registeredFunctions.

Context

Found while auditing the bindings for sources of an intermittent JSC GC segfault on Linux x64 with concurrent GC enabled — SlotVisitor::drain faults on parallel-mark helper threads, and ~InlineCacheHandler() faults during Heap::finalizeUnconditionalFinalizers(). These two issues are correctness bugs regardless of whether they are the root cause of those particular crash reports.

Reproduction hint

BUN_JSC_collectContinuously=1 BUN_JSC_numberOfGCMarkers=8 BUN_JSC_useConcurrentGC=1

with a workload that registers/swaps Module._extensions handlers (e.g. ts-node, source-map-support) under load.

…egisteredFunctions

JSCommonJSExtensions__setFunction and JSCommonJSExtensions__swapRemove pass
globalObject as the owner cell to WriteBarrier::set() while writing to the
extensions object's m_registeredFunctions vector. The write barrier therefore
adds globalObject to the remembered set, but it is
JSCommonJSExtensions::visitChildrenImpl — not the global object's
visitChildren — that scans m_registeredFunctions. Under an eden collection
where the extensions object is already old/black and the new value is young,
the value is never re-marked and can be collected while still referenced.
JSCommonJSExtensions__appendFunction at line 208 already passes the correct
owner (extensions); make the other two call sites consistent.

Also take cellLock() around m_registeredFunctions mutation and visit, matching
the fix that was applied to JSCommonJSModule::m_children in #29995. The same
WTF::Vector<WriteBarrier<>> pattern is mutated on the mutator thread (append /
takeLast / removeLast / clear) without a lock while parallel mark threads
iterate it in visitChildrenImpl; a Vector reallocation can free the backing
buffer mid-scan.
@robobun

robobun commented May 11, 2026

Copy link
Copy Markdown
Collaborator
Updated 7:08 PM PT - May 13th, 2026

@robobun, your commit 7c5ca15 has 1 failures in Build #54147 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 30524

That installs a local version of the PR into your bun-30524 executable, so you can run:

bun-30524 --bun

@coderabbitai

coderabbitai Bot commented May 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Rate limit exceeded

@robobun has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 2 minutes and 58 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4c8eea63-eb5e-4348-9f89-be41112abc9c

📥 Commits

Reviewing files that changed from the base of the PR and between 51c492f and 7c5ca15.

📒 Files selected for processing (2)
  • src/jsc/bindings/JSCommonJSExtensions.cpp
  • test/js/node/module/module-extensions-concurrent-gc.test.ts

Walkthrough

This PR adds thread-safety protections to JSCommonJSExtensions.cpp by acquiring cell locks before accessing m_registeredFunctions in append, remove, and GC marking operations. Additionally, the swap-remove operation corrects a parameter in the replacement entry update.

Changes

CommonJS Module Registration Locking

Layer / File(s) Summary
Append and Remove with Locking
src/jsc/bindings/JSCommonJSExtensions.cpp
JSCommonJSExtensions__appendFunction acquires extensions->cellLock() before vector append. JSCommonJSExtensions__swapRemove acquires the lock before modification and uses extensions instead of globalObject when updating the replacement entry.
GC Visitation with Locking
src/jsc/bindings/JSCommonJSExtensions.cpp
JSCommonJSExtensions::visitChildrenImpl acquires thisObject->cellLock() during m_registeredFunctions iteration to protect against concurrent mutation during parallel garbage collection.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title directly and accurately summarizes the two main changes: fixing the WriteBarrier owner and adding cellLock for JSCommonJSExtensions::m_registeredFunctions.
Description check ✅ Passed The description is comprehensive and well-structured, explaining both bugs with technical context, code examples, and reproduction hints. However, it lacks explicit sections matching the template structure.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/jsc/bindings/JSCommonJSExtensions.cpp`:
- Around line 213-216: The setter JSCommonJSExtensions__setFunction mutates the
vector-backed WriteBarrier storage m_registeredFunctions without acquiring the
same lock used by visitChildrenImpl; wrap the mutation in the
extensions->cellLock() (or equivalent lock guard) before calling
m_registeredFunctions[index].set(...) so writers synchronize with readers in
visitChildrenImpl, ensuring the lock is acquired and released properly around
the set operation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: e31d8141-4603-41e0-bab4-86356268d9c2

📥 Commits

Reviewing files that changed from the base of the PR and between 37bfbed and 51c492f.

📒 Files selected for processing (1)
  • src/jsc/bindings/JSCommonJSExtensions.cpp

Comment thread src/jsc/bindings/JSCommonJSExtensions.cpp

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — mechanical GC-correctness fix that mirrors the existing cellLock() pattern in JSCommonJSModule::m_children.

Extended reasoning...

Overview

This PR touches a single file, src/jsc/bindings/JSCommonJSExtensions.cpp, with two narrow changes to the three extern "C" helpers (__appendFunction / __setFunction / __swapRemove) and to visitChildrenImpl: (1) pass extensions instead of globalObject as the owner cell to WriteBarrier::set(), and (2) wrap mutation and iteration of m_registeredFunctions (a WTF::Vector<WriteBarrier<Unknown>>) in cellLock().

Security risks

None. This is internal JSC GC bookkeeping with no user-facing input handling, auth, crypto, or permission changes.

Level of scrutiny

Low. Both fixes are textbook JSC GC hygiene and copy the exact pattern already applied to JSCommonJSModule::m_children in this codebase (WTF::Locker locker { ->cellLock() } around vector append/clear and around visitor.append in visitChildrenImpl). The WriteBarrier::set owner argument must be the cell whose visitChildren scans the slot — extensions is unambiguously correct here and __appendFunction already did it that way; the other two call sites were just inconsistent.

Other factors

The bug-hunter inline note observes that these three functions currently have no Zig call sites (m_registeredFunctions is always empty), so the change has no runtime effect today. That cuts both ways: it means the PR can't be the actual fix for the reported SlotVisitor::drain segfault, but it also means there is effectively zero regression risk. Hardening latent code to match the established locking pattern is reasonable; whether to delete the dead code instead is a follow-up cleanup decision, not a blocker for this PR. No outstanding human reviewer comments and no CODEOWNERS for this path.

Comment thread src/jsc/bindings/JSCommonJSExtensions.cpp
@cirospaciari

Copy link
Copy Markdown
Member Author

@robobun adopt

@robobun

robobun commented May 14, 2026

Copy link
Copy Markdown
Collaborator

✅ Adopted — ready for merge.

  • 8b7c175 — added cellLock() to JSCommonJSExtensions__setFunction so all three mutators of m_registeredFunctions synchronize with visitChildrenImpl.
  • e6c1837 / 7c5ca15 — added test/js/node/module/module-extensions-concurrent-gc.test.ts, a concurrent-GC regression guard for Module._extensions (put/defineOwnProperty/deletePropertyjsc.Strong) under BUN_JSC_collectContinuously=1.

All review threads resolved.

CI: all Linux (including debian-13-x64-asan), Darwin, and FreeBSD lanes pass. The only red is test/js/bun/test/parallel/test-http-should-emit-close-when-connection-is-aborted.ts timing out on all three Windows lanes — this is a pre-existing Windows HTTP flake unrelated to this diff (it failed identically in #53509 on the commit before adoption, and is currently showing up on #30647, #30648, #30649, #29607 as well). The new test in this PR is skipIf(isWindows). Not re-rolling since the Windows HTTP timeout is consistent rather than intermittent.

Note: the three extern "C" helpers have had no Zig callers since #19231 switched CustomLoader.custom to jsc.Strong, so the hardening is defensive; dead-code removal can be a follow-up.

robobun added 2 commits May 14, 2026 00:27
For consistency with __appendFunction and __swapRemove, take cellLock()
in __setFunction so all mutators of m_registeredFunctions synchronize
with visitChildrenImpl running on parallel mark threads.
Mirrors module-children-concurrent-gc.test.ts (#29995): churns
Module._extensions handlers under BUN_JSC_collectContinuously=1 so the
JSCommonJSExtensions cell is repeatedly visited on concurrent mark
threads while the mutator registers/replaces/deletes handlers, and
asserts the program runs to completion with correct output.
Comment thread test/js/node/module/module-extensions-concurrent-gc.test.ts Outdated
The test exercises put/defineOwnProperty/deleteProperty on the
JSCommonJSExtensions cell under concurrent GC, which routes through
jsc.Strong in Zig rather than the (currently caller-less)
m_registeredFunctions helpers. Make the header and test name describe
what is actually covered, and note explicitly that m_registeredFunctions
stays empty today so the test is a guard for the live paths and for
visitChildrenImpl's locking rather than a direct repro of the
WriteBarrier-owner / cellLock bugs in the dead helpers.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — 7c5ca15 reworded the test header/name as suggested, and all three mutators + visitChildrenImpl now take cellLock() with the correct WriteBarrier owner.

Extended reasoning...

Overview

This PR makes two GC-correctness fixes to src/jsc/bindings/JSCommonJSExtensions.cpp:

  1. Corrects the WriteBarrier::set() owner cell from globalObject to extensions in __setFunction and __swapRemove so eden collections re-scan the right object.
  2. Adds WTF::Locker { cellLock() } around all three m_registeredFunctions mutators and around the iteration in visitChildrenImpl, mirroring the established fix for JSCommonJSModule::m_children in #29995.

It also adds test/js/node/module/module-extensions-concurrent-gc.test.ts, a concurrent-GC smoke test for Module._extensions put/defineOwnProperty/deleteProperty paths, modeled on the existing module-children-concurrent-gc test.

Security risks

None. No user-controlled input parsing, auth, crypto, or network surface is touched. The change is purely internal GC locking/write-barrier hygiene.

Level of scrutiny

Low-to-moderate. The C++ diff is ~10 lines of mechanical lock insertions and two one-token owner-cell corrections that follow an existing precedent in this codebase (#29995 / bd5149f). As noted in the resolved review threads, the three extern "C" helpers have no Zig callers since #19231 switched CustomLoader.custom to jsc.Strong, so m_registeredFunctions is always empty at runtime — the blast radius of these changes is effectively zero today, and they make the code correct should the path ever be re-enabled.

Other factors

  • Bug-hunting system found no issues.
  • Both prior review threads (CodeRabbit's missing lock in __setFunction; my note that the test header/name overstated coverage) are resolved — 8b7c175 added the lock, and 7c5ca15 reworded the test to accurately describe what it exercises (including an explicit note that it cannot observe a regression in the dead helpers).
  • The new test follows the harness conventions (bunEnv, bunExe, tempDir, isWindows skip) and the same shape as the sibling module-children-concurrent-gc test.
  • A maintainer adopted the PR via robobun and confirmed local test passes; prior CI failures were unrelated flakes.

Given the small, pattern-following diff on currently-unreachable code plus an accurately-labeled smoke test, this is safe to approve without further human review. Dead-code removal of m_registeredFunctions and the three helpers remains a clean follow-up.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants