Skip to content

node:cluster: one handle for each descriptor that workers name - #44185

Open
robobun wants to merge 9 commits into
mainfrom
robobun/148c7915/cluster-one-handle-per-fd
Open

robobun wants to merge 9 commits into
mainfrom
robobun/148c7915/cluster-one-handle-per-fd

Conversation

@robobun

@robobun robobun commented Sep 29, 2026 •

Copy link
Copy Markdown
Collaborator

Base: main. Until #44172 merges, this diff contains it (Notes). Merge before #44015.

Problem

  • In a cluster worker, a second listen({ fd: 3 }) answers listening. The primary closes its own descriptor 3 under the first server, and the worker never leaves on disconnect. A debug build prints close(3[BADF]) = EBADF. This is an indication of a file descriptor UAF.
  • queryServer (src/js/internal/cluster/primary.ts:294) makes a second handle for a descriptor that a live handle holds. That handle closes it when its worker leaves.
  • node:cluster: refuse a descriptor of the wrong kind in the primary #44172 alone lets two more call orders reach this, for example udp4, net, udp4 (Notes).

Fix

  • queryServer looks for the holder of the number before it makes a handle. A held number gets EEXIST, as in node v26.3.0 under SCHED_RR.
  • Correct: that branch alone makes handles. One handle holds a descriptor and closes it one time.
  • Self-reviewed: 38 concerns raised, 23 addressed, 12 in part, 1 rejected, 2 left (Notes).
  • Verified: test/js/node/cluster.test.ts (13 of 15 new tests fail on node:cluster: refuse a descriptor of the wrong kind in the primary #44172 alone), 97 vendored cluster tests.

Background

  • A holder is a SharedHandle, a RoundRobinHandle, or a dgram socket of the primary.
  • Weighed: EEXIST for a repeated key only, which misses udp4 then udp6. A native owner table, which adds two locks to each port listen.

Downsides

Notes

The base and the two parts of the diff.

The worker that never leaves. One worker under SCHED_NONE asks for one descriptor of the primary in the given order and closes nothing. Then the primary calls worker.disconnect(). All four columns are release builds. "main" is 367d939. The next two are c0924e1 without and with the part of this PR. "Never leaves" is no exit in 12 s.

asks main #44172 alone this PR with #44172 node v26.3.0
net, net listening two times, never leaves as main bind EEXIST, leaves with code 0 worker dies, ERR_INTERNAL_ASSERTION
udp4, udp4 listening two times, never leaves as main open EEXIST, leaves with code 0 worker dies
udp4, net, udp4 listen EINVAL, open EINVAL, leaves bind EINVAL, listening, never leaves bind EINVAL, open EEXIST, leaves with code 0 bind EINVAL, worker dies
net, udp4, net open EINVAL, bind EINVAL, leaves open EINVAL, listening, never leaves open EINVAL, bind EEXIST, leaves with code 0 open EINVAL, worker dies
  • The cause is in the worker. It keeps one handle for each key (shared() in src/js/internal/cluster/child.ts), and a disconnect closes the handles of that table. The second handle under a key replaces the first, so the first stays open and keeps the worker alive. A debug build stops before that, at $assert(handles.has(key) === false).
  • Rows 3 and 4 are the reason to merge this PR soon after node:cluster: refuse a descriptor of the wrong kind in the primary #44172. On main the refused ask of the other kind closes the descriptor, so the third ask fails and the worker leaves. node:cluster: refuse a descriptor of the wrong kind in the primary #44172 keeps the descriptor open, so the third ask is the second ask of rows 1 and 2.
  • The test file has a table row for row 3, and two tests that run rows 3 and 4 with the disconnect. After a wrong answer the primary of those two tests kills the worker, so a build without the fix fails on the answers and not on the time limit.

Two questions for a maintainer.

  1. The errno. A held descriptor gets EEXIST. Node v26.3.0 gives EEXIST under SCHED_RR, from listen in its primary. cluster: report EADDRINUSE when a worker listens twice on the same port nodejs/node#65020 is open and answers EADDRINUSE when a worker asks again for a key that it holds. If Bun follows that PR later, the 8 rows where one worker asks two times under one key change their errno.
  2. The owner. Here, as in node, the handle that adopts a descriptor closes it when its last worker leaves. The other rule is that the primary only lends the descriptor and nothing in the cluster closes it. That rule changes what left means in every row.

Where this comes from. Nobody reported it. A review of #44015 found it by reading. #31829 lists under known limitations: "server.listen({ fd }) on an fd that is already listening resolves rather than failing with EEXIST".

Affected on main (Linux, measured with the fixture of the test):

  • SCHED_NONE: each order in the table.
  • SCHED_RR, the default: tls servers, dgram sockets, and two kinds of server on one descriptor. A second plain net listen gets EEXIST already, because epoll refuses the second listener of the primary.
  • macOS: not measured on main. By reading, kqueue does not refuse that second listener. With this PR the table passes on macOS in CI (see Tests run).

The rows. One descriptor of the primary, asks in order. I ran each row on node v26.3.0 and on main with the fixture of the test, one row in one process. "main" is the release build of 367d939, whose cluster code equals main except for type annotations.

policy asks main node v26.3.0 this PR
SCHED_NONE net, net, then net in a second worker listening, descriptor closed, second worker gets ENOBUFS worker dies, ERR_INTERNAL_ASSERTION bind EEXIST -17, then the second worker listens
SCHED_NONE udp4, udp4 listening, descriptor closed worker dies, ERR_INTERNAL_ASSERTION open EEXIST -17
SCHED_NONE udp4, then udp6 in a second worker listening listening open EEXIST -17
SCHED_NONE net on a port, then net on the socket that the primary made listening listening bind EEXIST -17
SCHED_NONE one server calls listen({ fd }) two times before the first answer listening listening bind EEXIST -17
SCHED_NONE a udp4 socket of the primary, then udp4 in a worker listening, descriptor closed under the socket of the primary open EEXIST -17 open EEXIST -17
SCHED_NONE udp4, net, then udp4 listen EINVAL 22, descriptor closed, then open EINVAL bind EINVAL -22, then the worker dies bind EINVAL -22, then open EEXIST -17
SCHED_RR net, net, then net in a second worker bind EEXIST -17 bind EEXIST -17 bind EEXIST -17
SCHED_RR tls, tls listening, descriptor closed bind EEXIST -17 bind EEXIST -17
SCHED_RR net, tls listening, descriptor closed, client gets no answer worker dies, TypeError in tls.Server._setServerData(null) bind EEXIST -17, client served
SCHED_RR tls, net listening, descriptor closed bind EEXIST -17 bind EEXIST -17
SCHED_RR udp4, then net bind EINVAL -22 bind EINVAL -22 bind EINVAL -22
SCHED_RR net on a port, then tls on the socket that the primary made listening bind EEXIST -17 bind EEXIST -17

The three orders that node serves and this PR refuses. They are rows 3, 4 and 5. In rows 3 and 4, main and node v26.3.0 both answer listening, and both then close the number two times. I measured the result of each close() of the primary on that number with gdb: 0, then -9 (EBADF), on main and on node. With one ask the result is 0 only. The second close is harmless unless another file took the number in between. So in these rows the visible change is an error where main and node serve. Row 5 is one server that calls listen({ fd }) two times in one tick. Under SCHED_RR both runtimes refuse it already.

Self-review. 38 concerns.

  • Rejected, 1. Check the kind before the lookup for each query: it adds 2 syscalls to each served { fd } query.
  • Addressed after the first push: the lookup uses handles.$forEach. The Map has its key type now, because the $ methods do not resolve on Map<any, any>.
  • Left, 2. Rows where a worker leaves or dies while a descriptor is held. A lookup in the socket layer for a descriptor that the loop polls.
  • In part, 12. The main ones: no tracking issue is filed yet, no issue is filed at nodejs/node for its second close, no Windows test, and no bunHint on the new answer. The message stays bind EEXIST, the message of node.

Not covered.

  • A worker names the descriptor of a listening server of the primary. Under SCHED_NONE a handle still adopts it. When the worker closes its server, the primary closes the descriptor under its own server, and a client gets no answer (measured on main and on this PR). Node answers bind EEXIST, because libuv refuses a descriptor that its loop watches already (uv_tcp_open). The cluster code cannot see that holder. A walk over the listen sockets of the loop can, but that is native code in packages/bun-usockets, and this PR has none.
  • Windows has no test. The lookup runs there too, below the dgram branch that answers ENOTSUP. A held number gets EINVAL there and not EEXIST, because clusterValidateFd answers EINVAL for every descriptor on Windows. By reading, all the numbers are SOCKET handles there: Bun.listen({ fd }) takes one, and the fd of a listener and of clusterRawBind give one. I could not run Windows, and no test names a descriptor there.
  • The same worker asks again for a port or a path that it holds. That is the case of cluster: report EADDRINUSE when a worker listens twice on the same port nodejs/node#65020. It is left out on purpose, because its errno waits for that PR.
  • Two primaries in one process. Each has its own table of handles.
  • The data field of the new answer has no test. Nothing in Bun reads it.

Costs.

  • A listen or a bind that names no descriptor: one more condition in queryServer. On a release build of the first version of this change, which had the same condition, that was 5 bytecode instructions, 8 for a dgram port bind, and no call. I did not measure it again on this code.
  • A { fd } query that reaches this branch: one forEach over the live handles. A dgram query also calls jsDgramIsFdAdopted, which takes one lock.
  • Builtin JS (release codegen of node:cluster: refuse a descriptor of the wrong kind in the primary #44172 and of this PR, wc -c): primary.js 8601 to 9298 B (+697), SharedHandle.js 1705 to 1788 B (+83), RoundRobinHandle.js 5212 to 5313 B (+101). The other three cluster modules are byte-identical.
  • Host functions (GeneratedJS2Native.h): 117 to 117. This PR has no native change.
  • Text of bun (size, release builds of c0924e1 without and with the part of this PR): 80,705,669 to 80,706,550 B (+881). That is the builtin JS above. Main at the same base (f4d755a) is 80,705,363 B.

Tests run.

  • CI build 122733 on 8810596: 181 of 181 jobs pass. cluster.test.ts has 0 fail and no retry on the 11 lanes that run it. Linux (7 lanes, one with ASAN) and macOS (2 lanes): 62 pass, 1 skip. Windows (2 lanes): 27 pass, 38 skip, because the tests of this PR are POSIX only.

On a debug build of the merged branch in my environment (--timeout 240000, because one debug process takes about 4 s to start on this machine):

  • bun bd test test/js/node/cluster.test.ts: 63 pass.
  • On the source of node:cluster: refuse a descriptor of the wrong kind in the primary #44172: 13 of the 15 new tests fail. They are 11 of the 13 new rows and the 2 tests with the disconnect. The 2 rows that pass are the SCHED_RR rows "net, net" and "udp4, then net". The table says why.
  • On the source of main, which is what the test proof of this PR runs: 24 of the 28 tests of the two PRs fail.
  • Four changes to the fix, one at a time, each break a row: no dgram lookup (1 row), no fd of RoundRobinHandle (2 rows), no fd of SharedHandle (7 rows), holder before kind (4 rows). I measured this on the first version of this PR.
  • The 97 vendored test files with cluster or listen-fd in their name exit 0.
  • tsc --noEmit -p src/js/tsconfig.json passes.

no test proof · iteration 0 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/js/node/cluster.test.ts

A worker names a descriptor of the primary with listen({ fd }) or
bind({ fd }). The primary accepted a stream socket for a datagram query
and a datagram socket for a stream query. The worker then failed to use
its copy, gave the handle back, and the primary closed its own
descriptor. For the descriptor of a listening server of the primary,
that stopped the server.

clusterValidateFd now takes the kind of the query. A descriptor of
another kind, a datagram socket that is not AF_INET or AF_INET6, and a
number that is not an integer get EINVAL from the primary, as in node
v26.3.0. The primary leaves the descriptor open.
@robobun

robobun commented Sep 29, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: ready for review, CI is green. The base is main. Until #44172 merges, the diff contains it. The PR body says which part is which.

How to reproduce on main

The parent passes a listening TCP socket to the primary as descriptor 3. The primary forks one worker under SCHED_NONE. The worker calls listen({ fd: 3 }) on one net.Server and, when it listens, on a second one. The primary prints the state of its descriptor 3 after each answer.

runtime second answer descriptor 3 of the primary
bun, main listening EBADF
node v26.3.0 the worker dies with ERR_INTERNAL_ASSERTION closed when the worker is gone
node v26.3.0, SCHED_RR bind EEXIST, errno -17 open
bun, this PR, both policies bind EEXIST, errno -17 open

The fixture is fdQueryFixture in test/js/node/cluster.test.ts. The row is "net, net, then net in a second worker". On a release build of main, that worker also never leaves when the primary calls worker.disconnect(). The two tests named "a worker leaves on disconnect ..." cover that.

Two decisions that need a maintainer (details in the Notes of the PR body)

  1. EEXIST, or the EADDRINUSE that cluster: report EADDRINUSE when a worker listens twice on the same port nodejs/node#65020 proposes for a repeated key.
  2. Three call orders that node v26.3.0 and main serve get EEXIST here. In two of them both runtimes close the number two times.

Merge order. #44172 alone lets two more call orders reach this bug (udp4, net, udp4 and net, udp4, net on one descriptor). Merge this PR soon after #44172, or merge this PR alone and close #44172.

CI. Build 122733 on 8810596: 181 of 181 jobs pass. test/js/node/cluster.test.ts has 0 fail and no retry on the 11 lanes that run it: 62 pass and 1 skip on Linux (7 lanes, one with ASAN) and on macOS (2 lanes), 27 pass and 38 skip on Windows (2 lanes).

Tests on the merged branch (8810596), debug build: test/js/node/cluster.test.ts 63 pass, 0 fail. The 97 vendored cluster test files exit 0.

Review. Each finding of the review has an answer in its thread. One thread is open: a worker that names the descriptor of a listening server of the primary. It is under "Not covered" in the PR body.

@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/internal/cluster/primary.ts Outdated
Comment thread src/js/internal/cluster/primary.ts Outdated
Comment thread src/js/internal/cluster/primary.ts Outdated
…eger

Under SCHED_RR the listener of the primary refuses fd 3.5, and the worker
gets bind EINVAL as in node. The row passes without the change of this PR.
A handle of the primary closes the descriptor it holds when its last
worker leaves. queryServer made a second handle for a descriptor that a
live handle held: for a second listen({ fd }) of one worker, and for a
query with another key for the same number (udp4 and udp6). The second
handle closed the descriptor under the first one, and the first one
closed the number again later.

queryServer now looks for the holder of the number before it makes a
handle: a SharedHandle, a RoundRobinHandle, or a dgram socket of the
primary itself. A held number gets EEXIST, the answer of node v26.3.0
under SCHED_RR. The kind of the descriptor is checked first, so a wrong
kind still gets EINVAL.
The lookup runs below the Windows dgram branch, so it needs no platform
condition. It uses the private forEach of the Map.
@robobun
robobun force-pushed the robobun/148c7915/cluster-one-handle-per-fd branch 2 times, most recently from 2e3e0a1 to f7522d2 Compare September 29, 2026 07:24
@robobun

robobun commented Sep 29, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 12:24 AM PT - Oct 2nd, 2026

✅ @robobun, your commit 881059602284a943e243fad58157c3108f463178 passed in Build #122733! 🎉


🧪   To try this PR locally:

bunx bun-pr 44185

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

bun-44185 --bun

@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 reviewed the two new commits and found no bugs; the push addresses the earlier inline points (the holder lookup now goes through handles.$forEach, and the win32 gate is gone so the lookup runs on every platform). Because this deliberately diverges from node v26.3.0 in three call orders (udp4 then udp6, a port listen followed by naming the created socket, and one server listening twice before the first ack now get EEXIST where node serves), a maintainer should still decide whether that divergence and the EEXIST-vs-EADDRINUSE errno question are the behavior Bun wants.

What was reviewed:

  • Both lazily-bound Rust functions exist with the stated arity (cluster_validate_fd in src/runtime/node/node_cluster_binding.rs, js_dgram_is_fd_adopted in src/runtime/socket/udp_socket.rs), and neither can throw or return a non-number on a numeric fd.
  • The fd getters on SharedHandle/RoundRobinHandle read plain fields and the native Listener fd getter, which returns a number on every platform, so the handle.fd === fd comparison cannot throw.
  • The check sits only on the handle === undefined path after the ENOTSUP guard, and the refusal reply carries data consistent with the other error replies in queryServer.
  • The 12 new test rows assert exact {code, syscall, errno} and cover both scheduling policies; the fd-scan for fd: "created" is bounded and fails loudly if it does not find exactly one new socket.
Extended reasoning...

The diff adds a holder lookup in the node:cluster primary (src/js/internal/cluster/primary.ts) that refuses a worker's { fd } query with UV_EEXIST when a SharedHandle, RoundRobinHandle, or a primary dgram socket already holds that descriptor, plus fd accessors on the two handle classes and 12 new rows in test/js/node/cluster.test.ts. It touches no auth, crypto, or injection surface; the only descriptor-level work is a read-only getsockopt/getpeername/getsockname in an existing Rust helper. The code is small and I found no bugs, and the second push resolved both of my earlier inline comments. Deferring rather than approving because the PR intentionally changes observable node:cluster behavior away from node in three call orders and the author explicitly leaves the errno choice (EEXIST vs. the EADDRINUSE proposed in nodejs/node#65020) and the ownership rule to a maintainer, which is a compat design decision rather than a mechanical change.

Still open from earlier reviews (1):

  • Unresolved: 1 minor or pre-existing.

@robobun
robobun changed the base branch from robobun/148c7915/cluster-fd-kind-check to main October 2, 2026 00:50
@coderabbitai

coderabbitai Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

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

Review profile: ASSERTIVE

Plan: Essentials

Run ID: 72244f39-7c17-4592-8060-2a5b2717207e

📥 Commits

Reviewing files that changed from the base of the PR and between c0924e1 and 8810596.

📒 Files selected for processing (1)
  • test/js/node/cluster.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 1 remain after this review.


Walkthrough

Cluster descriptor validation now checks socket type, descriptor ownership, and datagram address family before creating cluster handles. Descriptor accessors and tests cover stream and datagram operations under both cluster scheduling policies.

Changes

Cluster file descriptor validation

Layer / File(s) Summary
Native descriptor validation
src/runtime/node/node_cluster_binding.rs
cluster_validate_fd selects stream or datagram validation, rejects fractional descriptors, and checks that datagram sockets use IPv4 or IPv6.
Cluster query validation
src/js/internal/cluster/SharedHandle.ts, src/js/internal/cluster/RoundRobinHandle.ts, src/js/internal/cluster/primary.ts
The handle wrappers expose their descriptors and pass the socket type to validation. The primary checks descriptor ownership and validation results before creating a handle.
Descriptor validation and ownership tests
test/js/node/cluster.test.ts
Tests cover invalid socket types, descriptor adoption, repeated operations, ownership, and connectivity under both scheduling policies.

Suggested reviewers: dylan-conway

Priority: ➖ Normal

Merge Risk: ⚪ Minimal · up to 88105

The change tightens cluster descriptor validation and ownership detection, and the new tests exercise it under both scheduling policies. No concrete merge-blocking issue was established.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title clearly summarizes the primary change: preventing multiple cluster handles from being created for the same worker-named descriptor.
Description check ✅ Passed The description explains the problem, fix, scope, limitations, and verification results. It does not use the exact template headings, but it provides the required information in equivalent sections.
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


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

… and the disconnect after it

The order udp4, net, udp4 on one datagram socket reaches the second udp4
ask with the descriptor open. A worker with two handles under one key
does not leave on disconnect.

@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 on lines +246 to +253
function errnoOfFdQuery(message) {
const fd = message.fd;
const isUdp = message.addressType === "udp4" || message.addressType === "udp6";
let held = isUdp && (isFdOfDgramSocket ??= $newRustFunction("udp_socket.rs", "jsDgramIsFdAdopted", 1))(fd);
if (!held) {
handles.$forEach(handle => {
if (handle.fd === fd) held = true;
});

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.

🟣 pre-existing, not blocking: pre-existing: a primary that itself listens on an inherited descriptor and whose worker names the same number still loses that listener and closes the number twice, exactly as on the base branch. The holder lookup in errnoOfFdQuery at primary.ts:246-253 only sees the dgram registry and the entries of handles; a net.Server or Bun.listen listener of the primary is not a holder it can see, so a SharedHandle adopts the descriptor and closes it when the worker leaves. Fix: treat a descriptor that a live listener of the primary holds as held too, returning EEXIST like the other holders; the PR description lists this under "Not covered".
A small fix can ride a push you are already making; otherwise a short reply is enough.

Why this was flagged

Trigger: in the primary, net.createServer().listen({ fd: 3 }), then a worker calls server.listen({ fd: 3 }) under SCHED_NONE (or tls under SCHED_RR). queryServer reaches errnoOfFdQuery at src/js/internal/cluster/primary.ts:246; handles.$forEach at primary.ts:251-253 finds no handle because the primary's own net.Server is not in handles. errnoOfFdQuery returns 0, and primary.ts:325 constructs a SharedHandle; SharedHandle.ts:22 validateFd(3, false) passes and SharedHandle.ts:26 records { fd: 3 }. When the worker closes its server, close() at primary.ts:376-383 calls remove(), and SharedHandle.ts:67 closeRawHandle(3) closes the descriptor that the primary's own listener still polls; the primary's server silently stops accepting, and its later server.close() closes number 3 a second time. The base branch behaves the same; the diff adds the holder lookup but it cannot see this holder. No safeguard covers it: validateFd checks kind and connectedness only.

Verification: Trigger: the primary does net.createServer().listen({ fd: N }) and a worker calls listen({ fd: N }) under SCHED_NONE. That listener is never inserted into handles, so the holder lookup in errnoOfFdQuery (src/js/internal/cluster/primary.ts:249-253) finds nothing and returns 0. SharedHandle.remove (SharedHandle.ts:67) calls closeRawHandle(fd), closing the descriptor under the primary's live listener.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This case is real. It is the first entry under "Not covered" in the PR body, and I left it out of this PR on purpose.

I measured it with release builds. The primary listens on descriptor 3 with its own net.Server. One worker under SCHED_NONE calls listen({ fd: 3 }) and closes its server when it listens. Then a client connects.

runtime answer to the worker after that
node v26.3.0 bind EEXIST descriptor 3 is open, the primary serves the client
bun, main listening descriptor 3 of the primary is EBADF, the client gets no answer
bun, this PR listening the same as main

Node has this answer from libuv: uv_tcp_open returns UV_EEXIST for a descriptor that its loop watches already (tcp.c). The cluster code of Bun cannot see a listener that it did not make. A walk over the listen sockets of the loop can see it, but that is native code in packages/bun-usockets. This PR changes three builtin JS files and no native code, so that lookup is a change of its own.

Without the fix the worker has two handles under one key and never leaves
on disconnect. The primary kills it, so the test fails on the answers.

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

Code review found no issues

No high-confidence issues detected in this change.

Still open from earlier reviews (1):

  • Unresolved: 1 minor or pre-existing.

@robobun

robobun commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

The base of this PR is now main. Before, it was the branch of #44172.

The PR body has the measurements.

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