From c53e913c7aedacceda50329ea686bcf3ba0ed707 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 11 Sep 2026 02:08:10 +0000 Subject: [PATCH] node:net: do not push native reads into a socket whose readable side has ended The raw half of a TLS pair still sees the connection's bytes. Since readable: false is honored, pushing them into a raw socket built that way was a push after EOF: it destroyed the raw socket and took the TLS socket down before its data arrived. Node's TLSWrap reads from the handle, so the raw stream's flags do not matter there. --- src/js/node/net.ts | 2 ++ test/js/node/tls/node-tls-connect.test.ts | 29 +++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/src/js/node/net.ts b/src/js/node/net.ts index 527a023a6c40..1aad235f92fc 100644 --- a/src/js/node/net.ts +++ b/src/js/node/net.ts @@ -435,6 +435,8 @@ function tlsHandshakeError(verifyError) { // Node reports a throwing 'data' listener as uncaughtException and keeps reading. function pushDataToSocket(self, socket, buffer) { + // The raw half of a TLS pair still sees the connection's bytes; a readable side that has ended (`readable: false`) takes none. + if (self.readableEnded) return; let full; try { full = self.push(buffer) === false; diff --git a/test/js/node/tls/node-tls-connect.test.ts b/test/js/node/tls/node-tls-connect.test.ts index cbb1c842a4ba..5c18c54e9d6b 100644 --- a/test/js/node/tls/node-tls-connect.test.ts +++ b/test/js/node/tls/node-tls-connect.test.ts @@ -709,6 +709,35 @@ it("'session' and 'keylog' are emitted for a TLSSocket over a duplex stream (tls await once(server, "close"); }); +// The raw half of the TLS pair still sees the connection's (encrypted) bytes. +// Pushing them into a raw socket built with `readable: false` destroyed it with +// ERR_STREAM_PUSH_AFTER_EOF and took the TLS socket down before its data arrived. +// Node's TLSWrap reads from the handle, so the raw stream's flags do not matter. +it.each(["connected", "connecting"])( + "tls.connect({ socket }) works over a %s socket built with readable: false", + async state => { + const server = tls.createServer({ ...COMMON_CERT_ }, socket => { + socket.on("error", () => {}); + socket.end("hello"); + }); + await once(server.listen(0, "127.0.0.1"), "listening"); + try { + const raw = net.connect({ port: (server.address() as AddressInfo).port, host: "127.0.0.1", readable: false }); + const events: string[] = []; + raw.on("error", err => events.push(`raw error ${(err as NodeJS.ErrnoException).code}`)); + if (state === "connected") await once(raw, "connect"); + const client = tls.connect({ socket: raw, rejectUnauthorized: false }, () => events.push("secureConnect")); + client.setEncoding("utf8"); + client.on("data", chunk => events.push(`data ${chunk}`)); + client.on("error", err => events.push(`tls error ${(err as NodeJS.ErrnoException).code}`)); + await once(client, "close"); + expect(events).toEqual(["secureConnect", "data hello"]); + } finally { + server.close(); + } + }, +); + it("a write from inside 'data' does not re-enter 'data' on a TLSSocket over a duplex (tls.connect({ socket }))", async () => { // The TLS engine behind a duplex decrypts into a 64 KiB buffer and emits one // buffer at a time. A write() from inside the 'data' handler used to pump the