Skip to content

worker_threads: close a transferred FileHandle that the worker never received - #43541

Open
robobun wants to merge 6 commits into
mainfrom
robobun/984c160b/close-unreceived-transferred-filehandle
Open

robobun wants to merge 6 commits into
mainfrom
robobun/984c160b/close-unreceived-transferred-filehandle

Conversation

@robobun

@robobun robobun commented Sep 19, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • A FileHandle in the transferList of a node:worker_threads Worker leaks its fd when the worker never starts. That is a missing entry, or a terminate() that stops the thread first. The parent handle is already detached. Node closes the fd.
  • The fd travels as a bare number inside the cloned workerData. Only the worker turns it back into a FileHandle, in unpackJSTransferables (src/js/node/worker_threads.ts:586). A worker that exits earlier never closes it.

Fix

  • Each transferred handle gets a claim: a random id in a native set that all threads share (Worker.cpp). The marker carries the id. The thread that removes the id first owns the fd.
  • The worker claims the fd when it builds the FileHandle. The parent claims it in #onClose and closes every fd that nobody claimed. The constructor rollback claims too.
  • Correct because one lock guards the set, so exactly one thread wins. close fires after the worker VM is destroyed.
  • Verified: 10 new tests in test/js/node/worker_threads/worker_threads.test.ts, 7 fail without the fix. Also the whole file and Node's test-worker-* tests (debug ASAN build). Self-reviewed: 9 concerns raised, 7 addressed, 2 rejected (Notes).

Background

  • Bun cannot transfer a FileHandle natively. packJSTransferables calls kTransfer(), which detaches the handle and returns { fd, flag }. That object replaces the handle inside workerData as a marker.
  • In the worker, node:worker_threads loads before user code. Its module body turns each marker into a new FileHandle.
  • Node keeps the fd in a native TransferData object. Its destructor closes an undelivered fd.
Notes

Repro (bun fh-leak.mjs missing, bun fh-leak.mjs terminate):

import { Worker } from "node:worker_threads";
import { open } from "node:fs/promises";
import { fstatSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";

const scenario = process.argv[2] ?? "missing";
const file = join(tmpdir(), "fh-leak-" + process.pid + ".txt");
writeFileSync(file, "x");
const fh = await open(file, "r");
const fd = fh.fd;
const worker =
  scenario === "missing"
    ? new Worker(join(tmpdir(), "fh-leak-missing-" + process.pid + ".js"), { workerData: { fh }, transferList: [fh] })
    : new Worker("setInterval(() => {}, 1000)", { eval: true, workerData: { fh }, transferList: [fh] });
worker.on("error", () => {});
const exited = new Promise(resolve => worker.on("exit", resolve));
if (scenario === "terminate") worker.terminate();
await exited;
let stillOpen = true;
try { fstatSync(fd); } catch (e) { stillOpen = e.code !== "EBADF"; }
console.log({ scenario, parentFd: fh.fd, stillOpen });
missing terminate
Node v26.3.0 parentFd: -1, stillOpen: false parentFd: -1, stillOpen: false
bun 1.4.3 parentFd: -1, stillOpen: true parentFd: -1, stillOpen: true
this branch (debug and release) parentFd: -1, stillOpen: false parentFd: -1, stillOpen: false

Which line is the fix. The call this.#reclaimJSTransferables?.() in #onClose. The claim exists so that this call, the worker, the rollback and the existing cleanup in finalizeJSTransferables can never close or restore the same fd twice.

Why a claim per handle and not one "workerData was never taken" signal from the native side

Why a native id and not a SharedArrayBuffer flag. The first revision of this PR put a one-slot SharedArrayBuffer in the marker and used Atomics.compareExchange. Review found two ways in which that breaks a transfer that works today. User code can replace the SharedArrayBuffer and Atomics globals. With BUN_JSC_useSharedArrayBuffer=0 the structured clone rejects every shared buffer. The id is a plain number, so neither matters. An object in workerData that imitates a marker cannot hold a live id either.

Timing probes

  • terminate() after a delay (measured on the first revision, with a spy on the parent's Atomics.compareExchange). Release build, 10 runs per delay: the parent wins every claim up to 5 ms, the worker wins every claim from 15 ms. Debug build, 4 runs per delay: the parent wins every claim up to 600 ms. In every run that the parent won, the fd was closed when terminate() resolved.
  • One process per run, terminate() in the same tick as the constructor, debug build: 0 of 450 runs left the fd open.
  • The worker loads node:fs and makes the handle before it claims. With the claim before that load, 1 of 120 runs left the fd open: the terminate() landed in the load.

Tests

  • 7 of the 10 new tests fail without the fix: the missing entry, terminate(), and the 5 marker imitations.
  • 2 tests guard against a second close of the same fd number, so they pass without the fix. I checked each against a build with that one clause of the fix removed. is closed only once when workerData does not reference the handle fails when finalizeJSTransferables closes without a claim. is not closed by the parent when the worker received the handle fails when the worker does not take the claim.
  • is transferred when SharedArrayBuffer is not available guards the choice of a plain number. It runs a subprocess with BUN_JSC_useSharedArrayBuffer=0 and with the globals removed.
  • The terminate() test repeats an attempt in which the fd is still open, up to 10 times. A thread that wins the race against terminate() receives the handle, and then the parent must not close the fd. Without the fix every attempt leaves the fd open.
  • Debug (ASAN) build: worker_threads.test.ts 152 pass. worker-transfer-list, worker-transfer-terminate-stress, worker-async-dispose, worker-shutdown-post-leak and 15787 pass.
  • Node's test/js/node/test/parallel/test-worker-*.js and test-fs-promises-file-handle-read-worker.js with bun <file>: 106 of 107 pass. test-worker-arraybuffer-zerofill.js needs the bun test runner and fails the same way without this change.

Other changes in this diff

  • finalizeJSTransferables (a handle in transferList that workerData does not reference) and the rollback in restoreNeutered go through the same claim. The rollback can run after the worker thread started, when something in the constructor throws late.
  • A marker must now carry a numeric data.fd and a live claim id. An object in workerData that only imitates a marker is delivered as plain data, as in Node. Before, the worker built a FileHandle around the fd value of that object, or threw in its bootstrap.
  • The function that #onClose calls is made at module scope. A closure made inside packJSTransferables keeps the scope of that call alive, and that scope holds the workerData graph of the user.
  • worker.postMessage(fh, [fh]) is not affected. Bun rejects a FileHandle in that transfer list with a DataCloneError and leaves the handle usable.

Self-review. Addressed: a marker imitation can no longer throw in the worker bootstrap, the claim does not depend on globals that user code can replace, the function kept by the Worker no longer holds the workerData graph, the fd-reuse test parks descriptors so the worker thread does not take the number, the tests collect error and wait for exit, the tests release their descriptors on failure, and the comment links Node's source. Rejected:

  • expect(held).toContain(fd) in the two fd-reuse tests. Another thread can take the number first, and then the assertion fails for no fault of the code.
  • Move the close listener up in the constructor. That layout is older than this change, and nothing between the two points throws in practice.

Not changed

Found outside this task. The worker stop ordering tests in the same file log with put(), which adds to the count before it stores the tag. A terminate() between the two leaves a 0 tag, and the test then sees [12, 0]. That happened once on the x64-asan lane and passed on the retry.


[human-review] gate passed · iteration 1 · 3 files touched

fails on main (without fix)
ASAN without fix: 7 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/js/node/worker_threads/worker_threads.test.ts
bun test v1.4.3 (367d939d9)

test/js/node/worker_threads/worker_threads.test.ts:
(pass) support eval in worker [1507.02ms]
(pass) all worker_threads module properties are present [37.25ms]
(pass) markAsUncloneable and markAsUntransferable markers are private, unforgeable, and permanent [34.89ms]
(pass) all worker_threads worker instance properties are present [231.84ms]
(pass) threadId module and worker property is consistent [236.40ms]
(pass) receiveMessageOnPort works across threads [1170.25ms]
(pass) receiveMessageOnPort works as FIFO [18.32ms]
(pass) you can override globalThis.postMessage [1370.86ms]
(pass) support require in eval [1353.21ms]
cwd /workspace/bun
realpath test/js/node/worker_threads/fixture-argv.js
(pass) support require in eval for a file [1249.78ms]
(pass) support require in eval for a file that doesnt exist [1328.54ms]
(pass) support worker eval that throws [1064.75ms]
(pass) execArgv option > inherits the parent's execArgv when falsy or unspecified [5473.08ms]
(p
... (truncated)

release without fix: all passed
bun test v1.4.3-canary.1 (6c5bebdca)

test/js/node/worker_threads/worker_threads.test.ts:
(pass) support eval in worker [18.69ms]
(pass) all worker_threads module properties are present [0.55ms]
(pass) markAsUncloneable and markAsUntransferable markers are private, unforgeable, and permanent [0.59ms]
(pass) all worker_threads worker instance properties are present [2.56ms]
(pass) threadId module and worker property is consistent [3.79ms]
(pass) receiveMessageOnPort works across threads [17.07ms]
(pass) receiveMessageOnPort works as FIFO [0.26ms]
(pass) you can override globalThis.postMessage [24.69ms]
(pass) support require in eval [22.21ms]
cwd /workspace/bun
realpath test/js/node/worker_threads/fixture-argv.js
(pass) support require in eval for a file [22.68ms]
(pass) support require in eval for a file that doesnt exist [14.85ms]
(pass) support worker eval that throws [14.90ms]
(pass) execArgv option > inherits the parent's execArgv when falsy or unspecified [86.24ms]
(pass) execArgv option > provides empty execArgv when passed an empty array [51.98ms]
(pass) execArgv option > can specify an array of strings [50.12ms]
(pass) eval does not leak source code [2873.50
... (truncated)
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/js/node/worker_threads/worker_threads.test.ts
bun test v1.4.3 (367d939d9)

test/js/node/worker_threads/worker_threads.test.ts:
(pass) support eval in worker [1266.47ms]
(pass) all worker_threads module properties are present [28.38ms]
(pass) markAsUncloneable and markAsUntransferable markers are private, unforgeable, and permanent [28.41ms]
(pass) all worker_threads worker instance properties are present [233.24ms]
(pass) threadId module and worker property is consistent [269.73ms]
(pass) receiveMessageOnPort works across threads [1080.74ms]
(pass) receiveMessageOnPort works as FIFO [15.15ms]
(pass) you can override globalThis.postMessage [1554.25ms]
(pass) support require in eval [1575.73ms]
cwd /workspace/bun
realpath test/js/node/worker_threads/fixture-argv.js
(pass) support require in eval for a file [1032.47ms]
(pass) support require in eval for a file that doesnt exist [1001.86ms]
(pass) support worker eval that throws [1514.10ms]
(pass) execArgv option > inherits the parent's execArgv when falsy or unspecified [5043.18ms]
(p
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 969ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/127] gen generated_host_exports.rs
generated_host_exports.rs: 121 exports (host=5, lazy=10, generic=106, rust=0); 245 extern-C blocks audited
[2/127] gen cpp.rs (cppbind)
[3/127] gen JS modules (bundle-modules)
Preprocess modules (7542ms)
Bundle modules (67ms)
Postprocesss modules (151ms)
Bundle Functions (624ms)
Generate Code (45ms)

[8.44s] Bundled "src/js" for production
  2607 kb
  197 internal modules
  13 native modules
  50 internal functions across 16 files
[3/11] cargo bun_runtime → libbun_runtime.a
�[1m�[33mwarning�[0m�[1m: binary `bun_shim_impl` should have a kebab-case name�[0m
   �[1m�[94m|�[0m
�[1m�[94m 1�[0m �[1m�[94m|�[0m /workspace/bun/build/release/rust-target/.../bun_shim_impl
   �[1m�[94m|�[0m                                              �[1m�[33m^^^^^^^^^^^^^�[0m
   �[1m�[94m|�[0m
   �[1m�[94m= �[0m�[1mnote�[0m: `cargo::non_kebab_case_bins` is set to `warn` by default
�[1m�[96mhelp�[0m: to change the binary name to `bun-shim-impl`, convert `bin.name`
  �[1m�[94m--> �[0msrc/install
... (truncated)
diff hotspot
src/js/node/worker_threads.ts                      |  71 ++++++---
 src/jsc/bindings/webcore/Worker.cpp                |  42 ++++-
 test/js/node/worker_threads/worker_threads.test.ts | 174 +++++++++++++++++++++
 3 files changed, 268 insertions(+), 19 deletions(-)

gate history · 2 passed · 0 rejected · iteration 1

evidence per changed file
file                                                reads  edits  tests
src/js/node/worker_threads.ts                          12     13     51
src/jsc/bindings/webcore/Worker.cpp                     2      1     51
test/js/node/worker_threads/worker_threads.test.ts      3      3     51

…received

A FileHandle in transferList travels to the worker as a bare fd inside
workerData. The parent handle is detached at once, and only the worker
turns the marker back into a FileHandle. A worker that exits before it
unpacks its workerData left the fd open for the life of the process.

Each marker now carries a one-slot SharedArrayBuffer. The thread that
flips the slot first owns the fd: the worker when it deserializes the
marker, or the parent when it rolls the transfer back or when the worker
has exited. On 'close' the parent closes every fd that nobody claimed.

An object in workerData that only imitates the marker key has no claim
flag and is now delivered as plain data.
@coderabbitai

coderabbitai Bot commented Sep 19, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review paused — included plan limit reached

Keep your review moving with free on-demand reviews.

  • Run this review for free

On-demand reviews are free for one more day.

  • Ask an admin to make reviews automatic

Open in CodeRabbit

Reviews can continue after your included limit without a manual trigger. An admin must approve usage-based billing.

Promotion and pricing details

On-demand reviews are free for one more day. After that, they cost $0.25 per reviewed file.

Review limit details

Or wait 23 seconds for your next included review.

Check out review usage here.

Limit details: You’ve used all 10 included reviews currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Repository: oven-sh/bun/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Essentials

Run ID: 9c7d9dc4-e4fd-485b-849c-579e61865f24

📥 Commits

Reviewing files that changed from the base of the PR and between 9b7c982 and 805e28e.

📒 Files selected for processing (3)
  • src/js/node/worker_threads.ts
  • src/jsc/bindings/webcore/Worker.cpp
  • test/js/node/worker_threads/worker_threads.test.ts

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

@robobun

robobun commented Sep 19, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 8:04 PM PT - Sep 19th, 2026

❌ @robobun, your commit 805e28e has 2 failures in Build #118781 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 43541

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

bun-43541 --bun

@robobun

robobun commented Sep 19, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status

  • Reproduced on bun 1.4.3 (Linux x64) with the script in the PR description. After the worker exits, fstatSync(fd) still succeeds for a missing entry and for a terminate() in the same tick as the constructor. Node v26.3.0 closes the fd in both cases.
  • With this branch the fd is closed in both cases, on a debug (ASAN) build and on a release build. The new tests pass on every CI lane, Windows and macOS included.
  • I withdraw the "do not merge yet" that an earlier version of this comment had. I wrote it because test/js/bun/spawn/spawn.test.ts was red on the x64-asan lane in three builds of this PR in a row (118637, 118664, 118781), and I suspected the native code in Worker.cpp. I had not read the job logs yet. I have now.
  • What fails: expect(stderr).toBe("") in an idle reader stopped at the highwater mark does not keep the process alive (saturating writer). The stderr of the child is:
    ==39679==WARNING: ptrace appears to be blocked (is seccomp enabled?). LeakSanitizer may hang.
    ==39679==Child exited with signal 42.
    
    This is the exit-time tracer of LeakSanitizer, which cannot use ptrace on that agent. All three job logs have this text (6, 5 and 4 times), no ERROR: AddressSanitizer and no ERROR: LeakSanitizer report. Build 118583 of this PR shows the same two lines in test/js/web/timers/setTimeout.test.js on the same lane.
  • The processes in that test never load node:worker_threads. The native code of this PR is only reachable through that module's binding, so it does not run there. On a release ASAN build of this branch the describe block passed 20 of 20 runs, and on a debug ASAN build the whole file passed 7 of 7.
  • Not explained: why three builds of this PR hit it while the annotations of the last five main builds (118450, 118148, 118067, 117597, 117577) do not mention that test. I do not know. Earlier I also wrote that the test fails on main. I had no evidence for that and withdrew it.
  • Build 118781 also has test/bake/deinitialization.test.ts red on alpine aarch64. This diff does not touch it, and it is in the annotations of 3 of the last 5 main builds, where it passed on a retry.
  • All review threads are answered. The last review asks for a maintainer to look at the design: a native registry of claim ids that all threads share, and the rule that the first thread to take an id owns the fd.
  • The case that this PR cannot cover (the parent is itself a worker and exits first) is tracked in worker_threads: a transferred FileHandle leaks its fd when the parent thread exits before the worker receives it #43542.

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

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Comment thread src/js/node/worker_threads.ts Outdated
Comment thread src/js/node/worker_threads.ts Outdated
Comment thread src/js/node/worker_threads.ts Outdated
Comment thread src/js/node/worker_threads.ts Outdated
The claim flag no longer needs the SharedArrayBuffer, Int32Array and
Atomics globals, which user code can replace or remove. Two native
helpers make the 4 shared bytes and flip them with a compare-and-swap.

A marker is now also checked for its data: an object in workerData that
has the marker key but no numeric data.fd, or no valid claim flag, stays
plain data and cannot throw in the worker bootstrap.

The worker claims the fd after it loaded node:fs and made the handle, so
that a terminate() cannot land in that load with the fd already claimed.
Comment thread src/js/node/worker_threads.ts Outdated
Comment thread src/js/node/worker_threads.ts Outdated
Comment thread src/js/node/worker_threads.ts Outdated
Comment thread src/js/node/worker_threads.ts Outdated
Comment thread src/js/node/worker_threads.ts Outdated
Comment thread src/js/node/worker_threads.ts Outdated
Comment thread src/jsc/bindings/webcore/Worker.cpp Outdated
Comment thread src/jsc/bindings/webcore/Worker.cpp Outdated

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

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Beyond the inline finding, I also checked the new native claim path in src/jsc/bindings/webcore/Worker.cpp (dynamicDowncast, isShared and byteLength guards run before impl()/data() are touched, and the CAS is seq_cst, so a wrong-typed or non-shared argument cannot reach the atomic) and the constructor rollback ordering in src/js/node/worker_threads.ts (the claim is allocated before kTransfer(), so nothing can throw between neutering a handle and recording it for restoreNeutered) — neither turned up a problem.

Extended reasoning...

The second push moved the claim flag into two host functions and added the data.fd shape check on the worker side, which changes the surface I reviewed last time. I re-read both files at HEAD: the C++ functions validate type before any non-throwing accessor, the only throwing call (throwOutOfMemoryError) is under a ThrowScope, and the JS side reaches every lifecycle exit (rollback, finalize, close) through the single closeIfUnclaimed helper so no fd can be closed twice. The one confirmed inline finding (shared buffers rejected by the serializer when useSharedArrayBuffer is disabled) is the remaining item for the author; the terminate-after-boot and nested-worker leaks are documented by the author as out of scope and were already raised earlier, so I am not repeating them.

Comment thread src/jsc/bindings/webcore/Worker.cpp Outdated
The claim of a FileHandle in transit is now a random id in a process-wide
native set. The thread that removes the id owns the fd. The marker
carries the id as a plain number, so the transfer no longer depends on a
SharedArrayBuffer that the structured clone must accept, and an object in
workerData that imitates a marker cannot hold a live id.

Also shorten the comments to one line each.
Comment thread src/js/node/worker_threads.ts Outdated
An object in workerData that fabricates the marker key no longer
deserializes, because it cannot hold a live claim id.

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

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Beyond the inline finding, I also checked the new native claim path in Worker.cpp (the lock is never held across anything that can enter JS, and the argument is type-checked before asNumber) and the one undelivered case the reclaim does not reach — a FileHandle transferred from a script in a disposed Bun.ModuleGraph — which is pre-existing and not introduced by this PR.

Extended reasoning...

This run re-read the reworked mechanism (native 53-bit claim id replacing the earlier SharedArrayBuffer flag) and the worker-side marker shape validation, which address the points raised in prior rounds. The remaining items examined and ruled out were the exception/lock discipline of the two new host functions and the disposed-ModuleGraph undelivered path, which leaks on the base branch as well. The change is not approved here because it adds cross-thread native state and fd-ownership logic that a human should still weigh, and because a posted nit remains open.

Comment thread src/js/node/worker_threads.ts Outdated
A detached handle transfers fd -1. Its claim id stayed in the native set
because closeIfUnclaimed returned on the fd before it took the claim.

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

I re-reviewed the latest push (through 6c5bebdc) and found no bugs; the nit from the previous round is addressed, since closeIfUnclaimed now takes the claim before the fd check. Because this introduces a process-wide native registry and a cross-thread fd-ownership protocol, a human look at the design would still be worthwhile.

What was reviewed:

  • Claim lifecycle: every path that owns a marker (worker deserializeJSTransferable, restoreNeutered, finalizeJSTransferables, the 'close' reclaim) removes the id under the same lock, so an id is never left in the set once the worker has closed.
  • Ordering of the reclaim: WebWorker::shutdown in src/jsc/web_worker.rs posts workerGlobalScopeDestroyed only after the worker VM is deallocated, and thread_main reaches shutdown() even when termination lands before start_vm, so no worker-side claim can race the parent's #onClose.
  • jsFunctionClaimJSTransferable: isNumber() is checked before asNumber(), NaN/Infinity/fractions/out-of-range fall to false, and the 53-bit ids can never collide with WTF HashSet<uint64_t>'s empty (0) or deleted (max) sentinels.
  • Marker imitations in workerData now fall through as plain data (memoized as themselves) rather than throwing in the worker bootstrap; the test.each matrix covers the shapes.
Extended reasoning...

Overview

The PR touches three files: src/js/node/worker_threads.ts (claim ids attached to in-band FileHandle markers, a shared closeIfUnclaimed helper used by rollback, finalize and the new 'close'-time reclaim), src/jsc/bindings/webcore/Worker.cpp (two host functions backed by a process-wide NeverDestroyed<HashSet<uint64_t>> under a static Lock, exposed at binding indices 17/18), and six new serial tests in test/js/node/worker_threads/worker_threads.test.ts. The goal is to close an fd exactly once when a transferred FileHandle is never deserialized by the worker (missing entry, early terminate(), handle not referenced from workerData), matching Node's TransferData destructor.

Security risks

None new. The claim id is a random 53-bit number that user data cannot guess in practice, and jsFunctionClaimJSTransferable validates the argument as an integral number in [1, 2^53) before touching the set, so a forged marker in workerData cannot claim or close a foreign fd; it is delivered as plain data. The registry is process-global by design (the parent VM mints, the worker VM consumes), and every accessor takes s_transferClaimsLock, so the REVIEW.md "never back per-VM state with globals" concern does not apply here: the state is genuinely cross-VM. closeSync failures are swallowed with a comment, which is the same pattern the existing finalizeJSTransferables used.

Level of scrutiny

High. This is cross-thread resource ownership with a new native primitive, and correctness depends on a sequencing argument (the 'close' event is dispatched from workerGlobalScopeDestroyed, which WebWorker::shutdown posts only after the worker VM is deallocated, so all worker-side claim attempts are complete before the parent reclaims). I traced that ordering in src/jsc/web_worker.rs and WorkerMessagingProxy.cpp and it holds, including the early-terminate path where thread_main goes straight to shutdown(). Still, the design choice (per-handle claim vs a native transfer owner) is one a maintainer should weigh, and the previously raised pre-existing gap (a worker that itself spawns a child worker and is torn down before the child's 'close' is delivered) is not addressed by this PR and was already noted in an earlier round.

Other factors

The multi-agent hunt exited on dry_streak with no findings. The single commit since the last review (6c5bebdc) does exactly what the last nit asked: closeIfUnclaimed claims first, then checks fd, so an fd -1 handle no longer leaves its id in the set. Tests are serial with a stated reason (they observe descriptor numbers), drain subprocess pipes concurrently, release resources via using/await using registered before assertions, and the BUN_JSC_useSharedArrayBuffer=0 subprocess test guards the plain-number design choice. I could not compile locally (no debug build and the WebKit headers are not in this checkout), so cryptographicallyRandomNumber<uint64_t>() availability rests on the author's reported debug-build test runs. No CODEOWNERS entry covers the changed files.

This branch has not been deployed

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant