Skip to content

test(tls): deduplicate and deflake the getPeerCertificate() RSS leak test - #34163

Merged
Jarred-Sumner merged 4 commits into
mainfrom
farm/9f80927e/tls-peercert-leak-dedupe
Jul 15, 2026
Merged

test(tls): deduplicate and deflake the getPeerCertificate() RSS leak test#34163
Jarred-Sumner merged 4 commits into
mainfrom
farm/9f80927e/tls-peercert-leak-dedupe

Conversation

@robobun

@robobun robobun commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

Problem

test/js/node/tls/node-tls-cert.test.ts went red on :darwin: 26 aarch64 in build 72915:

error: expect(received).toBeLessThan(expected)

Expected: < 12582912
Received: 21839872

✗ server-side getPeerCertificate() should not leak

The same assertion also flaked in node-tls-getpeercert-leak.test.ts on :darwin: 14 x64 in build 72900 (21008384 bytes, passed on retry).

Cause

The server-side getPeerCertificate() should not leak test exists twice: #29881 landed it as the standalone node-tls-getpeercert-leak.test.ts, and #29916 (merged a few hours later) appended an identical copy to node-tls-cert.test.ts. Both copies baseline RSS after four 5k-call warmup rounds and assert less than 12MB of growth afterwards.

On the macOS tart runners the allocator's high-water mark can climb by ~21MB over that window with no leak present, while a probe on darwin-test-arm64-3 with the current canary shows RSS flat within ~1MB once it has had a couple more rounds to settle, and the full test file passes 8/8 on that box. The copy inside node-tls-cert.test.ts is the worse of the two because it runs after 30 other TLS tests in the same process; in build 72915 the standalone file passed on the same commit in the same lane.

There is no regression in the underlying SSL_get_peer_certificate/computeRaw paths: the growth is the same for 5 and 10 measurement rounds, which rules out the per-call leak the test guards against.

Fix

The unpatched BIO/X509 leak is ~800 bytes per call; over the 75k calls now measured that is ~60MB of growth, so the regression the test exists to catch remains well above the new threshold, while the ~21MB noise ceiling observed on the macOS runners sits comfortably below it.

Verification

$ USE_SYSTEM_BUN=1 bun test test/js/node/tls/node-tls-getpeercert-leak.test.ts
(pass) server-side getPeerCertificate() should not leak [5150.55ms]
 1 pass  0 fail

$ USE_SYSTEM_BUN=1 bun test test/js/node/tls/node-tls-cert.test.ts
 27 pass  3 todo  0 fail

$ bun bd test test/js/node/tls/node-tls-getpeercert-leak.test.ts
(skip) server-side getPeerCertificate() should not leak

Ran the full node-tls-cert.test.ts eight times on darwin-test-arm64-3 (macOS 26) against canary cc0c1e835; 8/8 pass.


[stamp-90s] gate passed · iteration 8 · 2 files touched

passes on PR (with fix)
Test-only change.

Debug/ASAN (expected pass):
$ bun bd test 'test/js/node/tls/node-tls-cert.test.ts' 'test/js/node/tls/node-tls-getpeercert-leak.test.ts'
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test test/js/node/tls/node-tls-cert.test.ts test/js/node/tls/node-tls-getpeercert-leak.test.ts
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
bun test v1.4.0 (81bb980bb)

test/js/node/tls/node-tls-getpeercert-leak.test.ts:
(skip) server-side getPeerCertificate() should not leak

test/js/node/tls/node-tls-cert.test.ts:
(pass) complete cert chains sent to peer. [576.13ms]
(pass) complete cert chains sent to peer, but without requesting client's cert. [162.21ms]
(todo) Request cert from TLS1.2 client that doesn't have one.
(pass) Typical configuration error, incomplete cert chains sent, we have to know the peer's subordinate CAs in order to verify the peer. [107.76ms]
(pass) Typical configuration error, incomplete cert chains sent, we have to know the peer's subordinate CAs in order to verify the peer. But using multi-PEM [102.96ms]
(pass) Typical configuration error, incomplete cert chains sent, we have to know the peer's subordinate CAs in order to verify the peer. But using multi-PEM in an array [97.10ms]
(pass) Fail to complete server's chain [83.14ms]
(pass) Fail to complete client's chain. [103.92ms]
(pass) rejects an unverifiable client certificate by default when requestCert is true [247.07ms]
(pass) explicit rejectUnauthorized: false still admits an unverified client certificate [79.04ms]
(pass) Fail to find CA for server. [151.43ms]
(pass) Server sent their CA, but CA cannot be trusted if it is not locally known. [69.40ms]
(pass) Server sent their CA, wrongly, but its OK since we know the CA locally. [83.05ms]
(todo) Confirm client support for "BEGIN TRUSTED CERTIFICATE".
(todo) Confirm server support for "BEGIN TRUSTED CERTIFICATE".
(pass) Confirm client support for 
... (truncated)
Exit: 0
diff hotspot
test/js/node/tls/node-tls-cert.test.ts             | 150 ++++++---------------
 test/js/node/tls/node-tls-getpeercert-leak.test.ts | 127 +++++++++--------
 2 files changed, 107 insertions(+), 170 deletions(-)

gate history · 3 passed · 1 rejected · iteration 8

evidence per changed file
file                                                reads  edits  tests
test/js/node/tls/node-tls-cert.test.ts                  5      6      0
test/js/node/tls/node-tls-getpeercert-leak.test.ts      2      2      0

…test

The server-side getPeerCertificate() leak test exists in two places: the
standalone node-tls-getpeercert-leak.test.ts added by #29881 and an
identical copy appended to node-tls-cert.test.ts by #29916 the same day.
The second copy runs after 30 other TLS tests in the same process and
its 12MB RSS threshold has been flaking on the macOS tart runners
(21.8MB observed in build 72915 on darwin-26-aarch64, 21.0MB on
darwin-14-x64 in build 72900), while the standalone file passes on the
same commit in its own process.

Remove the duplicate from node-tls-cert.test.ts and leave a pointer to
the standalone file. Bring the standalone file in line with the
refinements the duplicate had picked up (skipIf(isDebug) so the ASAN
quarantine cannot mask the signal), lengthen the warmup from 4 to 8
rounds so the allocator's high-water mark settles before the baseline
is taken, lengthen the measured window to 15 rounds, and raise the
release threshold from 12MB to 32MB (40MB under ASAN). The unpatched
BIO/X509 leak would now produce ~60MB of growth over the measured
window, so the regression the test guards against remains well above
the threshold.
@robobun

robobun commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 5:08 PM PT - Jul 14th, 2026

@robobun, your commit 81bb980 is still building in Build #73035, but has 2 failures so far (All Failures):

The two NODE_EXTRA_CA_CERTS negative tests spawned 3 and 2 debug-build
child processes sequentially; at ~1.8s per debug+ASAN child the 3-spawn
case exceeds the 5s default timeout. Spawn them concurrently and drain
stderr alongside exited so the test stays under 2s on debug builds.
@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

The TLS certificate tests now run selected NODE_EXTRA_CA_CERTS cases concurrently. The server-side getPeerCertificate() RSS leak test is isolated in a standalone file, skipped for debug builds, and retuned with updated workloads, rounds, and thresholds.

TLS certificate and leak testing

Layer / File(s) Summary
Parallelize certificate validation tests
test/js/node/tls/node-tls-cert.test.ts
Invalid and mixed NODE_EXTRA_CA_CERTS cases spawn concurrently while retaining stderr and exit-code assertions.
Isolate and gate the leak test
test/js/node/tls/node-tls-cert.test.ts, test/js/node/tls/node-tls-getpeercert-leak.test.ts
The embedded leak test is removed, imports are simplified, and the standalone test is skipped in debug builds.
Retune TLS leak measurement
test/js/node/tls/node-tls-getpeercert-leak.test.ts
Warmup and measurement rounds increase, perRound is fixed at 5,000, and the RSS threshold is updated.

Possibly related PRs

  • oven-sh/bun#29916: Changes native getPeerCertificate() certificate cleanup in the same leak area.
  • oven-sh/bun#33963: Changes server-side getPeerCertificate() implementation details related to this leak coverage.

Suggested reviewers: jarred-sumner

🚥 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 states the main change: deduplicating and deflaking the TLS getPeerCertificate RSS leak test.
Description check ✅ Passed The description covers the problem, cause, fix, and verification, satisfying the template's intent despite different headings.

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

@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-getpeercert-leak.test.ts`:
- Around line 21-27: Update the skip condition for the “server-side
getPeerCertificate() should not leak” test to gate specifically on the ASAN
build indicator rather than isDebug. Preserve execution in plain debug builds
while continuing to skip ASAN-instrumented debug builds whose quarantine
invalidates the RSS leak threshold.
🪄 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: c003e542-d59d-4a2e-8a2b-5291a308d2d1

📥 Commits

Reviewing files that changed from the base of the PR and between cc0c1e8 and aba7143.

📒 Files selected for processing (2)
  • test/js/node/tls/node-tls-cert.test.ts
  • test/js/node/tls/node-tls-getpeercert-leak.test.ts

Comment thread test/js/node/tls/node-tls-getpeercert-leak.test.ts

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

LGTM — test-only deflake with well-justified threshold math.

What was reviewed:

  • Duplicate leak test removal: the deleted copy in node-tls-cert.test.ts was already skipIf(isDebug), so porting that gate to the standalone file is not a net coverage loss (re: CodeRabbit's note).
  • Threshold retune: 32MB sits above the observed ~21MB macOS noise and well below the ~60MB+ the guarded leak would produce over 15×5k rounds.
  • Concurrent NODE_EXTRA_CA_CERTS spawn refactor: await using, concurrent stderr/exit drain, and stderr-before-exitCode ordering all match repo conventions.
Extended reasoning...

Overview

Test-only PR touching two files under test/js/node/tls/. It (1) removes a duplicated RSS leak test from node-tls-cert.test.ts in favor of the standalone node-tls-getpeercert-leak.test.ts, (2) retunes the standalone copy with skipIf(isDebug), longer warmup (4→8 rounds), longer measurement window (10→15 rounds), and a higher threshold (12MB→32MB, 40MB under ASAN), and (3) refactors two NODE_EXTRA_CA_CERTS fixture-spawn tests to run their subprocess variants concurrently.

Security risks

None. No production code is modified; the tests spawn local fixtures against a 127.0.0.1 TLS server on port 0 and read repo-local PEM fixtures.

Level of scrutiny

Low. This is a CI deflake of a memory-growth heuristic test. The key question is whether the raised threshold could mask the regression the test guards against, and the PR description does the math: the unpatched BIO/X509 leak is ~800 bytes/call, which over 75k measured iterations is ≥60MB — comfortably above the new 32MB bar, while the observed benign macOS allocator noise (~21MB) sits below it. The skipIf(isDebug) gate is not new behavior: it was already on the now-removed duplicate (via #31155) and is simply being ported to the surviving copy, so CodeRabbit's concern about lost debug coverage doesn't represent a regression from main.

Other factors

The concurrent-spawn refactor follows the repo's own test guidance: await using proc, Promise.all([stderr.text(), exited]) to avoid pipe deadlock, and asserting stderr content before exit code. Assertions are unchanged. The PR description includes 8/8 pass verification on the flaking macOS runner and both bun bd and USE_SYSTEM_BUN=1 runs. No prior human review comments to address.

@robobun

robobun commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator Author

CI status: the two changed files (node-tls-cert.test.ts, node-tls-getpeercert-leak.test.ts) passed on every lane across build 72937 and build 73004, including both :darwin: 26 aarch64 shards where the original failure was reported, both :darwin: 14 aarch64 shards, and both :darwin: 14 x64 shards.

Remaining red is unrelated to this diff:

  • test-net-connect-memleak.js on alpine x64 / x64-baseline, tagged [pre-existing] (also red in 72937)
  • no-orphans.test.ts timed out in the perl daemon-reaping case on one darwin-14-aarch64 shard
  • the rest are retried-green flakes (ci:errors tags them [flaky])

Ready for review.

Matches the sibling tests in this file which use plain const proc; the
map callback already awaits proc.exited so no disposal guard is needed.

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

LGTM — test-only deflake with well-justified threshold math.

What was reviewed:

  • Duplicate leak test removal: confirmed the standalone file retains identical coverage; skipIf(isDebug) is carried forward from the removed copy (not a new coverage reduction).
  • Threshold retune: 32MB sits between the ~21MB observed macOS noise floor and the ~60MB leak signal at 75k calls, so the guarded regression stays detectable.
  • Parallelized NODE_EXTRA_CA_CERTS spawns: same assertions, now stderr-before-exitCode; concurrent clients against one tls.createServer is fine.
  • The isDebug vs isASAN gating question raised in-thread was checked and resolved — release ASAN lane still runs.
Extended reasoning...

Overview

Test-only change touching two files under test/js/node/tls/. It (1) deletes a duplicate copy of the getPeerCertificate() RSS leak test from node-tls-cert.test.ts (the same test already lives in node-tls-getpeercert-leak.test.ts), (2) retunes the surviving standalone copy with longer warmup (4→8 rounds), longer measurement window (10→15 rounds), skipIf(isDebug), and a raised threshold (12→32MB, 16→40MB under release ASAN), and (3) parallelizes two NODE_EXTRA_CA_CERTS subprocess loops via Promise.all while reordering assertions to stderr-before-exitCode. Unused isASAN/isDebug imports are dropped from the file that no longer needs them.

Security risks

None. No production code is touched. The changes are to test infrastructure only — RSS thresholds, warmup counts, and spawn concurrency in test files.

Level of scrutiny

Low-to-moderate. The main question for a deflake PR that raises a threshold is whether the test can still catch the regression it guards. The PR description does the arithmetic explicitly: the unpatched BIO/X509 leak is ~800 bytes/call, and the new 15-round × 5000-call window is 75k calls ≈ 60MB of leak growth, well above the new 32MB (40MB ASAN) threshold, which in turn is well above the ~21MB benign noise ceiling observed on the macOS runners. That margin is comfortable in both directions. The skipIf(isDebug) gate is not new — it was already applied to the now-removed duplicate in #31155 and is simply being carried to the surviving copy; the release ASAN lane (isDebug=false, isASAN=true) continues to run with its own threshold branch.

Other factors

  • CI build 72937 shows both changed files passing on every lane, including the darwin-26-aarch64 and darwin-14-x64 shards where the flake was originally observed.
  • The one inline review concern (CodeRabbit's isASAN vs isDebug gating suggestion) was answered in-thread and withdrawn; I independently agree skipIf(isDebug) is correct here since gating on isASAN would drop the release ASAN lane.
  • The concurrent-spawn refactor is mechanical: each child still gets its own Bun.spawn, stderr and exit are collected via Promise.all, and the assertion loop preserves the same toContain/toBe checks. Multiple TLS clients hitting one tls.createServer concurrently is standard.
  • No CODEOWNERS cover these paths. The bug-hunting system found no issues.

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

LGTM — test-only deflake with well-reasoned threshold math.

Checked that the raised 32MB/40MB threshold still sits below the ~60MB leak signal at the new 75k-call window, so the regression guard remains effective.
Verified the removed duplicate in node-tls-cert.test.ts was byte-equivalent coverage; skipIf(isDebug) is ported from the duplicate (not new coverage loss) and the release-ASAN lane still runs.
The concurrent NODE_EXTRA_CA_CERTS spawns share one server safely (each child is an independent tls.connect) and assertions are preserved with stderr now checked before exitCode.

Extended reasoning...

Overview

Test-only change touching two files under test/js/node/tls/. It (1) deletes a duplicate copy of the getPeerCertificate() RSS leak test from node-tls-cert.test.ts, (2) retunes the surviving copy in node-tls-getpeercert-leak.test.ts (8 warmup rounds, 15 measured rounds, 32MB/40MB threshold, skipIf(isDebug)), and (3) converts two sequential for-loop subprocess spawns in the NODE_EXTRA_CA_CERTS tests to Promise.all concurrent spawns. No runtime source is touched.

Security risks

None. Test code only; no changes to TLS implementation, cert validation, or any shipped code path.

Level of scrutiny

Low-to-moderate. The main risk with a leak-test threshold raise is silently defeating the regression guard. The PR description does the arithmetic: the original BIO/X509 leak is ~800 bytes/call, and 15 rounds × 5000 calls = 75k calls → ~60MB growth if the leak returns, comfortably above the new 32MB (release) / 40MB (release-ASAN) thresholds while clearing the ~21MB macOS allocator noise observed in builds 72900/72915. The skipIf(isDebug) gate is not new — it was already applied to the now-removed duplicate in #31155 and is being ported to the surviving copy; the CodeRabbit thread on isDebug vs isASAN was resolved with a correct explanation (release-ASAN lane must keep running, and it does). The concurrent-spawn refactor is mechanical: same env, same fixture, same assertions, now with stderr asserted before exit code per repo convention.

Other factors

  • CI: author reports both changed files green across builds 72937 and 73004 including all macOS lanes where the flake originated; remaining red is pre-existing/unrelated.
  • The isDebug ? 2_500 : 5_000 branch in perRound is now dead (test skips on debug), so simplifying to 5_000 is correct.
  • Removed isASAN/isDebug imports from node-tls-cert.test.ts match the deleted usage.
  • No prior human review comments outstanding; the one bot comment was withdrawn.

@Jarred-Sumner
Jarred-Sumner merged commit 1e35118 into main Jul 15, 2026
74 of 77 checks passed
@Jarred-Sumner
Jarred-Sumner deleted the farm/9f80927e/tls-peercert-leak-dedupe branch July 15, 2026 00:12
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.

2 participants