Skip to content

net: stop re-emitting post-upgrade bytes on a named-pipe socket after tls.connect({ socket }) - #32243

Closed
robobun wants to merge 5 commits into
mainfrom
farm/3457cafa/fix-namedpipe-tls-reemit
Closed

robobun wants to merge 5 commits into
mainfrom
farm/3457cafa/fix-namedpipe-tls-reemit

Conversation

@robobun

@robobun robobun commented Jun 13, 2026 •

Copy link
Copy Markdown
Collaborator

Follow-up to #32241 (fix for #32239), carved out as #32242.

Summary

tls.connect({ socket }) over a Windows named pipe re-emits post-upgrade bytes as cleartext data on the original net.Socket. #32241 fixed this for the TCP path; this PR covers the sibling named-pipe path, which #32241 could not reuse.

Root cause

Socket.prototype.connect (src/js/node/net.ts) has two code paths for an existing socket:

  • TCP (socket.upgradeTLS): swaps the native handle with connection._handle = raw.
  • Duplex / named pipe (upgradeDuplexToTLS): does not replace connection._handle.

A named-pipe net.Socket reuses the Duplex wrapper (isNamedPipeSocket(socket) is true only on Windows). Because its native pipe handle is left in place, it keeps firing SocketHandlers2.data with self = connection after the upgrade. That handler runs self.push(buffer), which emits a public data event. The TLS layer was fed through that same public event (connection.on("data", events[0])), so post-upgrade ciphertext reached both the TLS feeder (intended) and any pre-existing user data listener.

In a STARTTLS pattern the re-entrant listener calls tls.connect({ socket }) again on the already-upgraded socket (the #32239 "Invalid socket" re-entry).

The #32241 kupgradedToTLS flag cannot be reused here: suppressing self.push(buffer) is exactly what would starve the only source of the data events the feeder consumes, breaking TLS over named pipes entirely.

Fix

Route the named-pipe handle's post-upgrade reads straight to the TLS data feeder instead of through the public data event:

The fixing lines are the connection[kDuplexTLSData] = events[0] assignments in the two upgradeDuplex branches plus the feeder check in every raw data handler. isNamedPipeSocket is true for any pipe-variant handle, so the feeder can be set on a client named-pipe socket (SocketHandlers2.data, or the onread variant) or a server-accepted named-pipe socket (ServerHandlers.data); the fd-connect path (SocketHandlers.data) is covered defensively. A plain Duplex transport keeps connection.on("data", events[0]) (it has no native handle to feed from, and its behavior already matches Node).

Scope

Testing

test/js/node/tls/node-tls-namedpipes.test.ts gains two STARTTLS regressions driven over a named pipe (it.if(isWindows)), modeled on #32241's proven TCP test: a mock peer greets, replies PROCEED, then sends mock TLS records. Without the fix those bytes re-enter the upgrader's data handler and attempt a second upgrade (upgrades > 1); with the fix exactly one upgrade happens and it never fails with Invalid socket. One test upgrades a client net.connect socket (SocketHandlers2.data), the other upgrades a server-accepted socket (ServerHandlers.data).

This path is reachable only on Windows, so the fail-before/after check on a Linux runner cannot exercise it (the test is skipped there). Verification done on Linux:

  • Debug build + the touched cross-platform paths pass: node-tls-upgrade.test.ts, node-tls-duplex-close-throw-uaf.test.ts, node-tls-socket-allow-half-open-option.test.ts.
  • The change is a behavioral no-op off Windows: kDuplexTLSData is only set when isNamedPipeSocket is true, and the namedPipe computation is equivalent to the previous upgradeDuplex = isNamedPipeSocket(socket) on every other input.
  • Confirmed the pure-Duplex sibling re-emits identically in Node and Bun (so it is Node's documented behavior, not a bug, and is intentionally left unchanged).

The regression test runs on Windows CI.

@robobun

robobun commented Jun 13, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 10:11 AM PT - Jun 17th, 2026

✅ @robobun, your commit 8aaefe4ff49d6056c948ee69db93d56cec6ac6fa passed in Build #63143! 🎉


🧪   To try this PR locally:

bunx bun-pr 32243

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

bun-32243 --bun

@coderabbitai

coderabbitai Bot commented Jun 13, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Routes post-TLS-upgrade bytes for named-pipe sockets to a per-socket TLS feeder so upgraded TLS data is handled by TLS instead of being re-emitted as cleartext; updates multiple data paths, both upgrade branches, and socket reuse cleanup. Adds Windows STARTTLS regression tests.

Changes

Named-pipe TLS upgrade data routing

Layer / File(s) Summary
Symbol and data-path routing
src/js/node/net.ts
Introduces kDuplexTLSData, initializes it on Socket, and updates SocketHandlers.data, SocketHandlers2.data, ServerHandlers.data, and the onread.callback path to forward buffers to the feeder when present instead of normal push/pause/data handling.
Synchronous connect() TLS upgrade routing
src/js/node/net.ts
Detects named-pipe sockets in the immediate net.Socket.connect → tls.connect({ socket }) upgrade path and stores the TLS feeder on connection[kDuplexTLSData] instead of attaching a data listener.
Deferred (once connect) TLS upgrade routing
src/js/node/net.ts
Applies the same named-pipe detection and feeder assignment in the deferred upgrade branch that waits for connection.once("connect", ...).
Handle reuse cleanup and Windows STARTTLS tests
src/js/node/net.ts, test/js/node/tls/node-tls-namedpipes.test.ts
Clears kDuplexTLSData in initSocketHandle to avoid stale feeder leakage and adds two Windows-only STARTTLS regression tests covering client-initiated and server-accepted named-pipe TLS upgrades asserting a single upgrade and no cleartext re-emission.

Possibly related issues

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: preventing post-upgrade byte re-emission on named-pipe sockets after tls.connect({ socket }).
Description check ✅ Passed The description covers what changed and how it was verified, with clear Summary, Fix, Scope, and Testing sections matching the template intent.
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.

@robobun

robobun commented Jun 13, 2026 •

Copy link
Copy Markdown
Collaborator Author

Update: the two named-pipe TLS regression tests now pass on Windows CI (all windows x64 ... test-bun lanes green on build #62277), confirming the fix on the only platform where this path runs.

The red on that build is unrelated flake on lanes this diff does not touch: serve-body-leak, streams-leak, spawn-streaming-stdin, bun-install, cli/run/multi-run, bake/dev-and-prod hmr, issue/30205 (napi), and sql-mysql (all with retries), plus cli/init/init.test.ts, which is deterministic and pre-existing on main (bun init scaffolds CLAUDE.md when the claude binary is on the agent's PATH, src/cli/init_command.rs:1500, Windows-exempt). None reference named-pipe TLS.

Because that init failure is environment-deterministic and unrelated, a re-run cannot turn CI fully green on its own. The change itself is green where it matters. Flagging for a maintainer.

Comment thread src/js/node/net.ts Resolved

@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 `@test/js/node/tls/node-tls-namedpipes.test.ts`:
- Around line 190-203: The test currently treats any "close" event as a
successful terminal outcome, which masks cases where the socket closes before
TLS is actually completed; change the bare close handlers so they signal failure
instead of success: update the serverSocket.on("close") and
tlsSocket.on("close") callbacks to resolve with a non-empty message (e.g.
resolve({ upgrades, message: "closed before secureConnect" })) so only a real
TLS terminal event (tlsSocket.on("secureConnect") or an error) counts as success
and the final assertion can fail when post-upgrade bytes are dropped.
🪄 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: 3b31593e-2854-4484-934c-5db652bdaed5

📥 Commits

Reviewing files that changed from the base of the PR and between 19c855a and 855e441.

📒 Files selected for processing (2)
  • src/js/node/net.ts
  • test/js/node/tls/node-tls-namedpipes.test.ts

robobun added 5 commits June 17, 2026 16:25
… tls.connect({ socket })

On Windows, tls.connect({ socket }) over a named pipe upgrades through the
Duplex TLS wrapper. Unlike the TCP path, it leaves connection._handle in
place, so the native pipe handle keeps firing SocketHandlers2.data after the
upgrade. The post-upgrade bytes were pushed onto the original socket and
re-emitted as cleartext data, reaching both the TLS feeder and any
pre-existing user data listener. In a STARTTLS pattern the re-entrant
listener upgrades the already-upgraded socket again.

Route the named-pipe handle's post-upgrade reads straight to the TLS data
feeder (kDuplexTLSData) instead of self.push, so the original socket goes
quiet after the upgrade. Pure Duplex transports are unchanged (they have no
native handle and match Node's behavior).
…lers

isNamedPipeSocket() is true for any pipe-variant handle, including a
server-accepted named-pipe socket whose active data handler is
ServerHandlers.data (not SocketHandlers2.data). connect() sets
kDuplexTLSData for those too, so the feeder has to be consulted there
as well, otherwise tls.connect({ socket: serverAcceptedPipeSocket })
would starve the TLS layer (no data listener, and ServerHandlers.data
ignored the feeder).

Mirror the SocketHandlers2.data check in ServerHandlers.data and
SocketHandlers.data (the fd-connect path), and add a server-accepted
named-pipe STARTTLS regression.
…arser

Tag each terminal outcome and require the mock records to surface as a TLS
`error` (outcome === "tls-error"). A bare socket close or a completed
handshake now fails the assertion instead of passing silently, so a feeder
that dropped post-upgrade bytes could not masquerade as success.
… bytes

On Windows the upgraded TLS socket emits secureConnect (the handshake
completes) rather than a parser error, so requiring outcome === "tls-error"
failed in CI. Both a completed handshake and a parser error prove the
post-upgrade bytes reached the TLS layer via the feeder; only the raw socket
closing with the TLS layer idle indicates a dropped feeder. Assert the outcome
is not that raw-close sentinel instead of a specific TLS result. The
re-emission guard (upgrades === 1) is unchanged.
Keep the durable setup and assertion explanations (mock STARTTLS protocol,
why the mock records reach the TLS parser, what the outcome/upgrades
assertions guard); drop the 'before the fix' narrative, the #32239
cross-reference, and the SocketHandlers2/ServerHandlers internal names that a
refactor could rename. Per root CLAUDE.md, that context lives in the PR
description.
@robobun

robobun commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator Author

#36534 covers this on current main, including the server-side upgradeDuplexToTLS sites and hasUnflushedWrites trigger added since this PR was opened.

@robobun

robobun commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator Author

Closing in favor of #36534, which covers the named-pipe path on current main (including the server-side and unflushed-writes stream-level sites added since this was opened) and carries the accepted-named-pipe test from this PR.

@robobun robobun closed this Aug 13, 2026
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