diff --git a/test/js/node/tls/node-tls-cert.test.ts b/test/js/node/tls/node-tls-cert.test.ts index 46a66ffebfde..189fc4c0af00 100644 --- a/test/js/node/tls/node-tls-cert.test.ts +++ b/test/js/node/tls/node-tls-cert.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "bun:test"; import { once } from "events"; import { readFileSync } from "fs"; -import { bunEnv, bunExe, invalidTls, isASAN, isDebug, tmpdirSync } from "harness"; +import { bunEnv, bunExe, invalidTls, tmpdirSync } from "harness"; import type { AddressInfo } from "node:net"; import type { Server, TLSSocket } from "node:tls"; import { join } from "path"; @@ -628,22 +628,27 @@ it("tls.connect should ignore invalid NODE_EXTRA_CA_CERTS", async () => { passphrase: "123123123", }); - for (const invalid of ["not-exist.pem", "", " "]) { - const proc = Bun.spawn({ - env: { - ...bunEnv, - SERVER_PORT: server.address.port.toString(), - NODE_EXTRA_CA_CERTS: invalid, - }, - stderr: "pipe", - stdout: "inherit", - stdin: "inherit", - cmd: [bunExe(), join(import.meta.dir, "node-tls-cert-extra-ca.fixture.js")], - }); + const results = await Promise.all( + ["not-exist.pem", "", " "].map(async invalid => { + const proc = Bun.spawn({ + env: { + ...bunEnv, + SERVER_PORT: server.address.port.toString(), + NODE_EXTRA_CA_CERTS: invalid, + }, + stderr: "pipe", + stdout: "inherit", + stdin: "inherit", + cmd: [bunExe(), join(import.meta.dir, "node-tls-cert-extra-ca.fixture.js")], + }); + const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]); + return { invalid, stderr, exitCode }; + }), + ); - expect(await proc.exited).toBe(1); - const stderr = await proc.stderr.text(); + for (const { stderr, exitCode } of results) { expect(stderr).toContain("UNABLE_TO_GET_ISSUER_CERT_LOCALLY"); + expect(exitCode).toBe(1); } }); @@ -660,22 +665,27 @@ it("tls.connect should ignore NODE_EXTRA_CA_CERTS if it contains invalid cert", passphrase: "123123123", }); - for (const invalid of [mixedValidAndInvalidCertsBundlePath, mixedInvalidAndValidCertsBundlePath]) { - const proc = Bun.spawn({ - env: { - ...bunEnv, - SERVER_PORT: server.address.port.toString(), - NODE_EXTRA_CA_CERTS: invalid, - }, - stderr: "pipe", - stdout: "inherit", - stdin: "inherit", - cmd: [bunExe(), join(import.meta.dir, "node-tls-cert-extra-ca.fixture.js")], - }); + const results = await Promise.all( + [mixedValidAndInvalidCertsBundlePath, mixedInvalidAndValidCertsBundlePath].map(async invalid => { + const proc = Bun.spawn({ + env: { + ...bunEnv, + SERVER_PORT: server.address.port.toString(), + NODE_EXTRA_CA_CERTS: invalid, + }, + stderr: "pipe", + stdout: "inherit", + stdin: "inherit", + cmd: [bunExe(), join(import.meta.dir, "node-tls-cert-extra-ca.fixture.js")], + }); + const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]); + return { invalid, stderr, exitCode }; + }), + ); - expect(await proc.exited).toBe(1); - const stderr = await proc.stderr.text(); + for (const { stderr, exitCode } of results) { expect(stderr).toContain("ignoring extra certs"); + expect(exitCode).toBe(1); } }); describe("tls ciphers should work", () => { @@ -719,85 +729,3 @@ describe("tls ciphers should work", () => { ); }); }); - -// A local `bun bd` debug build is ASAN-instrumented but not named `bun-asan`; -// ASAN's default 256MB quarantine retains every freed allocation, so RSS grows -// by the total allocation churn regardless of leaks and the threshold below -// cannot distinguish a leak from the quarantine. Skip every debug build - -// since isASAN learned that local debug builds are ASAN-instrumented too, -// debug+ASAN is exactly the un-measurable combination (quarantine + redzones -// inflate RSS unboundedly for this access pattern); CI's release ASAN lane -// (isDebug false) keeps running with its own threshold. -it.skipIf(isDebug)( - "server-side getPeerCertificate() should not leak", - async () => { - // Guards against the SSL_get_peer_certificate X509 ref leak and the - // computeRaw BIO leak on the server getPeerCertificate() path. - const { promise: serverSocketPromise, resolve: onServerSocket } = Promise.withResolvers(); - const server = tls.createServer( - { - key: serverTls.key, - cert: serverTls.cert, - ca: [clientTls.ca], - requestCert: true, - rejectUnauthorized: false, - }, - socket => onServerSocket(socket), - ); - await once(server.listen(0, "127.0.0.1"), "listening"); - - const client = tls.connect({ - host: "127.0.0.1", - port: (server.address() as AddressInfo).port, - key: clientTls.key, - cert: clientTls.cert, - ca: [serverTls.ca], - checkServerIdentity, - }); - await once(client, "secureConnect"); - - const serverSocket = await serverSocketPromise; - try { - // Make sure the client actually sent a cert so we exercise the - // SSL_get_peer_certificate path rather than falling through to the - // cert-chain branch. - const first = serverSocket.getPeerCertificate(); - expect(first).toBeDefined(); - expect(first?.subject).toBeDefined(); - - function spin(n: number) { - for (let i = 0; i < n; i++) { - serverSocket.getPeerCertificate(); - serverSocket.getPeerCertificate(false); - } - Bun.gc(true); - Bun.gc(true); - } - - // Run in fixed-size rounds with a GC after each so the steady-state - // heap footprint stays bounded. The first few rounds grow the heap - // regardless of leaks, so take the baseline after warmup. - const perRound = isDebug ? 2_500 : 5_000; - for (let round = 0; round < 4; round++) spin(perRound); - const baseline = process.memoryUsage.rss(); - - for (let round = 0; round < 5; round++) spin(perRound); - const after = process.memoryUsage.rss(); - const growth = after - baseline; - - // Unpatched, the BIO leak alone is ~800 bytes/call → ~20MB over the - // 25k abbreviated calls here (~10MB for 12.5k in debug). Leave slack for - // allocator/ASAN noise but stay well below that. Both calls in the loop - // build the full leaf-certificate object (getPeerCertificate(false) used - // to return {}), so the debug budget covers 2x the constructions. Local - // debug (non-`bun-asan`) builds skip this test entirely - see skipIf above. - const threshold = 1024 * 1024 * (isDebug ? 20 : isASAN ? 16 : 12); - expect(growth).toBeLessThan(threshold); - } finally { - client.end(); - serverSocket.end(); - server.close(); - } - }, - 180_000, -); diff --git a/test/js/node/tls/node-tls-getpeercert-leak.test.ts b/test/js/node/tls/node-tls-getpeercert-leak.test.ts index 5db1164b72e9..11bbdfda7238 100644 --- a/test/js/node/tls/node-tls-getpeercert-leak.test.ts +++ b/test/js/node/tls/node-tls-getpeercert-leak.test.ts @@ -18,69 +18,78 @@ const serverTls = { ca: readFileSync(join(import.meta.dir, "fixtures", "ca2-cert.pem"), "utf8"), }; -it("server-side getPeerCertificate() should not leak", async () => { - // Guards against the SSL_get_peer_certificate X509 ref leak and the - // computeRaw BIO leak on the server getPeerCertificate() path. - const { promise: serverSocketPromise, resolve: onServerSocket } = Promise.withResolvers(); - const server = tls.createServer( - { - key: serverTls.key, - cert: serverTls.cert, - ca: [clientTls.ca], - requestCert: true, - rejectUnauthorized: false, - }, - socket => onServerSocket(socket), - ); - await once(server.listen(0, "127.0.0.1"), "listening"); +// Debug builds are ASAN-instrumented; ASAN's default 256MB quarantine retains +// every freed allocation, so RSS grows with total allocation churn regardless +// of leaks and the threshold below cannot distinguish a leak from the +// quarantine. CI's release ASAN lane (isDebug false) still runs. +it.skipIf(isDebug)( + "server-side getPeerCertificate() should not leak", + async () => { + // Guards against the SSL_get_peer_certificate X509 ref leak and the + // computeRaw BIO leak on the server getPeerCertificate() path. + const { promise: serverSocketPromise, resolve: onServerSocket } = Promise.withResolvers(); + const server = tls.createServer( + { + key: serverTls.key, + cert: serverTls.cert, + ca: [clientTls.ca], + requestCert: true, + rejectUnauthorized: false, + }, + socket => onServerSocket(socket), + ); + await once(server.listen(0, "127.0.0.1"), "listening"); - const client = tls.connect({ - host: "127.0.0.1", - port: (server.address() as AddressInfo).port, - key: clientTls.key, - cert: clientTls.cert, - ca: [serverTls.ca], - checkServerIdentity: () => undefined, - }); - await once(client, "secureConnect"); + const client = tls.connect({ + host: "127.0.0.1", + port: (server.address() as AddressInfo).port, + key: clientTls.key, + cert: clientTls.cert, + ca: [serverTls.ca], + checkServerIdentity: () => undefined, + }); + await once(client, "secureConnect"); - const serverSocket = await serverSocketPromise; - try { - // Make sure the client actually sent a cert so we exercise the - // SSL_get_peer_certificate path rather than falling through to the - // cert-chain branch. - const first = serverSocket.getPeerCertificate(); - expect(first).toBeDefined(); - expect(first?.subject).toBeDefined(); + const serverSocket = await serverSocketPromise; + try { + // Make sure the client actually sent a cert so we exercise the + // SSL_get_peer_certificate path rather than falling through to the + // cert-chain branch. + const first = serverSocket.getPeerCertificate(); + expect(first).toBeDefined(); + expect(first?.subject).toBeDefined(); - function spin(n: number) { - for (let i = 0; i < n; i++) { - serverSocket.getPeerCertificate(); - serverSocket.getPeerCertificate(false); + function spin(n: number) { + for (let i = 0; i < n; i++) { + serverSocket.getPeerCertificate(); + serverSocket.getPeerCertificate(false); + } + Bun.gc(true); + Bun.gc(true); } - Bun.gc(true); - Bun.gc(true); - } - // Run in fixed-size rounds with a GC after each so the steady-state - // heap footprint stays bounded. The first few rounds grow the heap - // regardless of leaks, so take the baseline after warmup. - const perRound = isDebug ? 2_500 : 5_000; - for (let round = 0; round < 4; round++) spin(perRound); - const baseline = process.memoryUsage.rss(); + // Run in fixed-size rounds with a GC after each so the steady-state + // heap footprint stays bounded. The first few rounds grow the heap + // regardless of leaks, so take the baseline after warmup. + const perRound = 5_000; + for (let round = 0; round < 8; round++) spin(perRound); + const baseline = process.memoryUsage.rss(); - for (let round = 0; round < 10; round++) spin(perRound); - const after = process.memoryUsage.rss(); - const growth = after - baseline; + for (let round = 0; round < 15; round++) spin(perRound); + const after = process.memoryUsage.rss(); + const growth = after - baseline; - // Unpatched, the BIO leak alone is ~800 bytes/call → ~40MB over the - // 50k abbreviated calls here (~20MB for 25k in debug). Leave slack for - // allocator/ASAN noise but stay well below that. - const threshold = 1024 * 1024 * (isDebug ? 10 : isASAN ? 16 : 12); - expect(growth).toBeLessThan(threshold); - } finally { - client.end(); - serverSocket.end(); - server.close(); - } -}, 180_000); + // Unpatched, the BIO leak alone is ~800 bytes/call → ~60MB over the + // 75k abbreviated calls measured here. macOS CI VMs have shown ~21MB of + // benign RSS growth over the same window with no leak present, so the + // threshold is set well above that noise floor but well below the leak. + const threshold = 1024 * 1024 * (isASAN ? 40 : 32); + expect(growth).toBeLessThan(threshold); + } finally { + client.end(); + serverSocket.end(); + server.close(); + } + }, + 180_000, +);