Repository navigation
node:tls: test bytesRead on the sockets that a TLS upgrade wraps - #42373
Conversation
The socket under tls.connect({ socket }) and under
new tls.TLSSocket(socket, { isServer: true }) keeps counting in node:
the handle counts a read before TLSWrap consumes it, and the upgrade
keeps the handle, so the bytes read before a STARTTLS upgrade stay in
the count.
Cover the wrapped socket with no reader, with readable: false and with
an onread buffer, and both sides of a STARTTLS exchange. On main the
onread row reports 0.
|
Status: merged into the branch of #42313 as b293a54. The 4 tests now land with #42313. Requested in #42304 (comment). How I reproduced the case that is wrong on main: import net from "node:net";
import tls from "node:tls";
import { once } from "node:events";
// key and cert: any self-signed pair for localhost
const server = tls.createServer({ key, cert }, s => s.end("banner"));
await once(server.listen(0, "127.0.0.1"), "listening");
const raw = net.connect({
port: server.address().port,
host: "127.0.0.1",
onread: { buffer: Buffer.alloc(64), callback() {} },
});
await once(raw, "connect");
const secure = tls.connect({ socket: raw, ca: cert, servername: "localhost" });
secure.resume();
await once(secure, "close");
console.log(raw.bytesRead, secure.bytesRead);
server.close();
|
b293a54
into
robobun/8c2e67ac/socket-bytes-read
There was a problem hiding this comment.
LGTM — test-only addition that mirrors the file's existing patterns.
What was reviewed:
- Both new tests use
port: 0,once()(auto-rejects on'error'), andtry/finallycleanup registered before assertions; no sleeps or per-test timeouts. - The
test.eachrows cover the same three raw-socket variants as the existing test above them; the STARTTLS test wires server-sideerror/closeto the awaited resolver so it can't hang. - Exact plaintext byte counts are asserted with
.toBe/.toEqual; only handshake-size-dependent values use.toBeGreaterThan, which the PR justifies against Node v26.3.0 numbers and the cited nodejs/node permalink.
Extended reasoning...
Overview
This PR appends two test cases (72 lines) to test/js/node/tls/node-tls-upgrade.test.ts, exercising bytesRead accounting on both the raw and TLS sockets when tls.connect({ socket }) / new tls.TLSSocket(socket, { isServer: true }) wrap an existing net.Socket. The first is a test.each over three raw-socket configurations (no reader, readable: false, onread buffer); the second models a full STARTTLS exchange and checks that pre-upgrade byte counts are preserved and continue to grow on both peers. No production code is touched.
Security risks
None. The change adds tests only, uses in-process servers on 127.0.0.1 with port: 0, and reuses the harness-provided self-signed cert already imported by the file. Nothing touches auth, crypto primitives, or trust decisions.
Level of scrutiny
Low-to-moderate. Test-only changes to an existing Node-compat test file, following the exact structure of the two tests already in the file (same imports, same once/try-finally/Promise.withResolvers shape, same server setup). I checked against CLAUDE.md, test/CLAUDE.md, and REVIEW.md testing rules: port: 0 is used, no time-based waits, cleanup is in finally before any assertion can throw, once() from events rejects on 'error' so failure paths surface, related assertions use .toEqual on a combined object, and the variant matrix uses test.each. The nodejs/node source permalink and observed-in-Node comments satisfy the Node-compat citation convention.
Other factors
The toBeGreaterThan assertions on raw.bytesRead are the loosest checks here, but they're the strongest invariant available since TLS handshake record sizes differ between Node and Bun (documented in the PR notes with concrete numbers), while the plaintext counts (6, 2, 7, 8) are asserted exactly. No CODEOWNERS entry covers this path. The bug-hunting run exited on dry_streak with no findings, and there are no outstanding third-party reviews on the timeline.
Follow-up to #42304 (closed in favor of #42313). Stacked on #42313. Tests only.
Problem
bytesReadto a counter on the native socket. Its TLS upgrade path has no test: therawhalf of the[raw, tls]pair inherits the count of the handle it replaces.net.Socketwith anonreadbuffer thattls.connect({ socket })wraps reportsbytesRead === 0. Node v26.3.0 reports 3055, the TLS records.Fix
test/js/node/tls/node-tls-upgrade.test.ts. Three rows (no reader,readable: false,an onread buffer) check that the wrapped socket counts the TLS records:tlsSocket.bytesReadis 6,raw.bytesReadis greater.src/all 9 tests of the file pass. With main'ssrc/(4b5862f) thean onread bufferrow fails withExpected: > 6, Received: 0.Background
tls.connect({ socket })andnew tls.TLSSocket(socket, { isServer: true })run TLS over an existingnet.Socket. Bun's nativeupgradeTLSturns the TCP handle into a[raw, tls]pair over one fd. The wrapped socket keepsraw, which sees the ciphertext.TLSWrapconsumes it. Sosocket.bytesReadgoes on from its value before the upgrade.onread: { buffer, callback }makes a socket read intobufferand callcallback(nread, buffer).Notes
Why a separate PR. #42313 had a CI run in progress and no human review yet. A push there restarts the run. If the tests are wanted inside #42313, cherry-pick a6cbad9.
After #42313 merges I rebase this branch on main, so that it holds the one test commit only.
Why the
onreadrow is 0 on main. Thedatahandler that theonreadoption installs never added tobytesRead(#42304 was the JS fix for that). #42265 then added an early return for the wrapped socket to that handler, before the deliver loop. #42313 counts on the handle before any JS handler runs, so the row passes there.Probe numbers (
tls.connect({ socket: raw }), the server sends"banner"):raw.bytesRead, noonreadraw.bytesRead,rawhasonreadonreadcallbackSTARTTLS probe (plaintext
greeting\n,go,ok\n, then TLS in both directions). Clientraw.bytesRead: 12 before, 12 right after the upgrade, at the end 3027 in node and 2821 in bun. Server: 2, 2, then 1720 in node and 1588 in bun. Bun 1.4.3, main and #42313 print the same numbers for this probe. Node and bun differ in the size of the handshake, so the tests compare against the plaintext count and not against a fixed number.