Skip to content

tls: coalesce buffered records into one read - #66698

Merged
nodejs-github-bot merged 3 commits into
nodejs:mainfrom
ronag:codex/tls-coalesce-buffered-records
Oct 21, 2026
Merged

nodejs-github-bot merged 3 commits into
nodejs:mainfrom
ronag:codex/tls-coalesce-buffered-records

Conversation

@ronag

@ronag ronag commented Oct 13, 2026 •

Copy link
Copy Markdown
Member

SSL_read() returns at most one TLS record per call, and TLSWrap::ClearOut() emits a separate read for each one. Records can be up to 16 KiB, but plenty of servers send smaller ones: nginx defaults ssl_buffer_size to 4 KiB, and servers that do dynamic record sizing start out at around 1 KiB. Every record then pays for its own buffer allocation and call into JS.

With this change ClearOut() keeps calling SSL_read() into the same 16 KiB buffer while the next record in enc_in_ is application data. It never waits for more input, it only merges records that have already arrived, and reads never get bigger than one full record already makes them.

Some notes:

  • Batching stops in front of any other record type, and in front of application data records that are short enough to be empty, since SSL_read() skips empty records and goes straight on to the next one. In TLS 1.2 that means handshake and alert records are still processed after the preceding data has been emitted, so a 'data' handler can e.g. still call disableRenegotiation() before a renegotiation that arrived in the same read. test-tls-coalesce-renegotiation covers that, and fails if the record type check is removed.
  • When SSL_read() fails in the middle of a batch, SSL_get_error() and the error are still read right away, as before. The batched data is emitted next, then the error or EOF.
  • TLS 1.3 encrypts post-handshake messages, so a NewSessionTicket or KeyUpdate can't be told apart from data by its record header. If one arrives together with data, 'session' or 'keylog' can now be emitted before the 'data' that came before it. There is no documented ordering between those events and 'data'. Keeping it would mean emitting data from inside OpenSSL callbacks, with the reentrancy problems that brings, so I didn't.
  • 'data' chunks no longer line up with records, so test-tls-max-send-fragment now checks the record sizes on the wire through a small proxy.
  • I also tried a 64 KiB buffer, which lets full 16 KiB records merge too. It was only 2-5 points faster than 16 KiB in the runs below, and a paused stream then buffers up to one extra 64 KiB chunk past its high water mark (pummel/test-https-no-reader fails), so I kept 16 KiB.

benchmark/tls/throughput-s2c.js gets a recordlen option, which sets the server's setMaxSendFragment().

Results on an AMD EPYC 9355P (Linux, Docker, 16 CPUs on one NUMA node, benchmark pinned to one CPU), 10 runs each, MiB/s:

recordlen recvbuflen before after
1024 0 667 ± 10 945 ± 7 +41.6% ***
4096 0 1456 ± 41 1833 ± 17 +25.9% ***
16384 0 2271 ± 63 2235 ± 66 -1.6%
1024 65536 913 ± 14 1083 ± 15 +18.6% ***
4096 65536 2011 ± 16 2127 ± 13 +5.8% ***
16384 65536 2826 ± 10 2816 ± 11 -0.4%
$ node benchmark/compare.js --old ./node-base --new ./node-new --runs 10 \
    --filter throughput-s2c --set CPUSET=21 --set dur=3 --set type=buf \
    --set sendchunklen=16777216 --set recvbufgenfn=false \
    --set recvbuflen=0 --set recvbuflen=65536 --analyze tls

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Review requested:

  • @nodejs/crypto

@nodejs-github-bot nodejs-github-bot added c++ Issues and PRs that require attention from people who are familiar with C++. crypto Issues and PRs related to the crypto subsystem. needs-ci PRs that need a full CI run. labels Oct 13, 2026
@ronag
ronag requested a balanced review from Copilot October 13, 2026 07:29

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Additional SSL reads can let renegotiation callbacks overtake preceding application data.

1 open finding
What changed in this PR

Coalesces buffered TLS records into 16 KiB reads to reduce per-record overhead.

Changes:

  • Batches decrypted records while preserving EOF and TLS error handling.
  • Adds coalescing, small-buffer, EOF, and corruption tests.
  • Updates fragment-size validation to inspect wire records.
File Description
src/​crypto/​crypto_tls.cc Implements record coalescing and deferred error emission.
src/​crypto/​crypto_tls.h Declares extracted TLS error helpers.
test/​parallel/​test-tls-coalesce-records.js Adds coalescing regressions.
test/​parallel/​test-tls-max-send-fragment.js Validates fragment sizes on the wire.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/crypto/crypto_tls.cc Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

TLS 1.3 protocol records can trigger callbacks before preceding application data is emitted.

1 open finding
1 resolved since last review

🧠 Review effort: Balanced

Comment thread src/crypto/crypto_tls.cc Outdated
@ronag
ronag requested review from jasnell and mcollina and a balanced review from Copilot October 13, 2026 08:33

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The implementation raises the read cap to 64 KiB despite the stated requirement to preserve 16 KiB behavior.

1 open finding
2 resolved since last review

🧠 Review effort: Balanced

Comment thread src/crypto/crypto_tls.h Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

A data callback can synchronously destroy the SSL object while an OpenSSL callback is still using it.

2 open findings

🧠 Review effort: Balanced

Comment thread src/crypto/crypto_tls.cc Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Native TLS callback and lifetime changes require human review and completion of the stated runtime validation.

1 open finding
1 resolved since last review

🧠 Review effort: Balanced

@ronag
ronag marked this pull request as ready for review October 13, 2026 09:48
@codecov

codecov Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 85.18519% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.50%. Comparing base (c883ccf) to head (6f8a340).
⚠️ Report is 134 commits behind head on main.

Files with missing lines Patch % Lines
src/crypto/crypto_tls.cc 85.18% 2 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #66698      +/-   ##
==========================================
- Coverage   92.82%   90.50%   -2.33%     
==========================================
  Files         422      797     +375     
  Lines      194189   278211   +84022     
  Branches    30039    53450   +23411     
==========================================
+ Hits       180257   251787   +71530     
- Misses      13607    16797    +3190     
- Partials      325     9627    +9302     
Files with missing lines Coverage Δ
src/crypto/crypto_tls.cc 78.40% <85.18%> (ø)

... and 533 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@ronag
ronag marked this pull request as draft October 17, 2026 07:29
@ronag
ronag force-pushed the codex/tls-coalesce-buffered-records branch 2 times, most recently from ab55637 to 9f3bab3 Compare October 17, 2026 07:57
@ronag
ronag requested a balanced review from Copilot October 17, 2026 07:57

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

An unresolved empty-record ordering issue and changes to TLS callback behavior require human review and CI validation.

1 open issue
1 resolved since last review

Review effort: Balanced

Comment thread src/crypto/crypto_tls.cc Outdated
@ronag
ronag force-pushed the codex/tls-coalesce-buffered-records branch from 9f3bab3 to 435181a Compare October 17, 2026 08:06
@ronag ronag added the request-ci Add this label to start a Jenkins CI on a PR. Only starts once the PR has an approving review. label Oct 18, 2026
@github-actions github-actions Bot removed the request-ci Add this label to start a Jenkins CI on a PR. Only starts once the PR has an approving review. label Oct 18, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Comment thread src/crypto/crypto_tls.cc
Comment thread src/crypto/crypto_tls.cc Outdated
@ronag
ronag marked this pull request as draft October 19, 2026 13:29
Log the captured SSL error when emitting buffered plaintext destroys the
SSL context before onerror can run. Clarify that an incomplete next
record can return WANT_READ and still flush the accumulated plaintext.

Signed-off-by: Robert Nagy <ronagy@icloud.com>
Assisted-by: Codex

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Native TLS read-loop and callback-ordering changes require final human review and CI verification.

No open issues 🎉

Review effort: Balanced

@ronag
ronag requested review from anonrig and bnoordhuis October 19, 2026 13:38
@ronag ronag added the request-ci Add this label to start a Jenkins CI on a PR. Only starts once the PR has an approving review. label Oct 19, 2026
@github-actions github-actions Bot removed the request-ci Add this label to start a Jenkins CI on a PR. Only starts once the PR has an approving review. label Oct 19, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@ronag
ronag marked this pull request as ready for review October 19, 2026 13:59
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@ronag ronag added the commit-queue PRs queued for automated landing through the Commit Queue. label Oct 21, 2026
@nodejs-github-bot nodejs-github-bot added commit-queue-failed PRs whose Commit Queue landing failed and need manual intervention before retrying. and removed commit-queue PRs queued for automated landing through the Commit Queue. labels Oct 21, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Commit Queue failed

This pull request has multiple commits, but no landing policy was selected.

Add commit-queue-squash PRs the Commit Queue should land as one squashed commit. to land it as one commit, or commit-queue-rebase PRs the Commit Queue should land as multiple self-contained commits. to land the commits separately.

The pull request was removed from the Commit Queue and labeled commit-queue-failed PRs whose Commit Queue landing failed and need manual intervention before retrying. . After resolving the failure, remove that label and add commit-queue PRs queued for automated landing through the Commit Queue. to retry.

Full Commit Queue output
�[36m⠋�[39m Loading data for nodejs/node/pull/66698
�[36m⠋�[39m Loading data for nodejs/node/pull/66698
�[36m⠋�[39m Getting collaborator contacts from README of nodejs/node
�[36m⠋�[39m Getting PR from nodejs/node/pull/66698
�[36m⠋�[39m Getting reviews from nodejs/node/pull/66698
�[36m⠋�[39m Getting comments from nodejs/node/pull/66698
�[36m⠋�[39m Getting commits from nodejs/node/pull/66698
✔  Done loading data for nodejs/node/pull/66698
----------------------------------- PR info ------------------------------------
Title      tls: coalesce buffered records into one read (#66698)
Author     Robert Nagy <ronagy@icloud.com> (@ronag)
Branch     ronag:codex/tls-coalesce-buffered-records -> nodejs:main
Labels     crypto, c++, needs-ci, commit-queue
Commits    3
 - tls: coalesce buffered records into one read
 - test: fix TLS tests for coalesced records
 - tls: retain debug output when buffered reads destroy SSL
Committers 1
 - Robert Nagy <ronagy@icloud.com>
PR-URL: https://github.com/nodejs/node/pull/66698
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
------------------------------ Generated metadata ------------------------------
PR-URL: https://github.com/nodejs/node/pull/66698
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
--------------------------------------------------------------------------------
   ℹ  This PR was created on Tue, 13 Oct 2026 07:29:15 GMT
   ✔  Approvals: 2
   ✔  - Yagiz Nizipli (@anonrig) (TSC): https://github.com/nodejs/node/pull/66698#pullrequestreview-5572396658
   ✔  - Ben Noordhuis (@bnoordhuis): https://github.com/nodejs/node/pull/66698#pullrequestreview-5590395478
   ✔  Last GitHub CI successful
   ℹ  Last Full PR CI on 2026-10-20T11:35:47Z: https://ci.nodejs.org/job/node-test-pull-request/78507/
�[36m⠙�[39m Querying data for job/node-test-pull-request/78507/
�[36m⠙�[39m Querying data for job/node-test-pull-request/78507/
�[36m⠙�[39m Querying API for job/node-test-pull-request/78507/
✔  Build data downloaded
   ✔  Last Jenkins CI successful
--------------------------------------------------------------------------------
   ✔  No git cherry-pick in progress
   ✔  No git am in progress
   ✔  No git rebase in progress
--------------------------------------------------------------------------------
�[36m⠹�[39m Bringing origin/main up to date...
�[36m⠹�[39m Bringing origin/main up to date...
From https://github.com/nodejs/node
 * branch                  main       -> FETCH_HEAD
   b85dd8021c..8c345413c1  main       -> origin/main
✔  origin/main is now up-to-date
main is out of sync with origin/main. Mismatched commits:
 - a370966282 dns: validate resolve inputs for null bytes
 - 9731991b3c tls: fix ip normalization regression
 - 9ccbc81ece tls: reject non-canonical IP addresses
 - c5f5cb5b96 zlib: avoid caching invalid ZIP local headers
 - 5faa1497e0 src: fix the fs permission tree node on wildcard nodes
 - 6b88896652 permission: check permission model in realpathSync
 - 8c44e38dc6 crypto: reset random caches across snapshots
 - 228a568600 deps: brotli: cherry-pick 0ed4268a4401abcb44720963
 - 8c345413c1 src: expose `BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH`
--------------------------------------------------------------------------------
HEAD is now at 8c345413c1 src: expose `BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH`
   ✔  Reset to origin/main
�[36m⠸�[39m Downloading patch for 66698
�[36m⠸�[39m Downloading patch for 66698
From https://github.com/nodejs/node
 * branch                  refs/pull/66698/merge -> FETCH_HEAD
✔  Fetched commits as ef632266e573..6f8a340acc32
--------------------------------------------------------------------------------
Auto-merging src/crypto/crypto_tls.cc
[main 878632c789] tls: coalesce buffered records into one read
 Author: Robert Nagy <ronagy@icloud.com>
 Date: Sat Oct 17 09:16:36 2026 +0200
 5 files changed, 302 insertions(+), 29 deletions(-)
 create mode 100644 test/parallel/test-tls-coalesce-records.js
 create mode 100644 test/parallel/test-tls-coalesce-renegotiation.js
[main e8571570cb] test: fix TLS tests for coalesced records
 Author: Robert Nagy <ronagy@icloud.com>
 Date: Sun Oct 18 09:23:59 2026 +0200
 2 files changed, 13 insertions(+), 4 deletions(-)
Auto-merging src/crypto/crypto_tls.cc
[main 9420023c17] tls: retain debug output when buffered reads destroy SSL
 Author: Robert Nagy <ronagy@icloud.com>
 Date: Mon Oct 19 15:30:21 2026 +0200
 1 file changed, 7 insertions(+), 1 deletion(-)
   ✔  Patches applied
There are 3 commits in the PR. Attempting autorebase.
(node:488) [DEP0190] DeprecationWarning: Passing args to a child process with shell option true can lead to security vulnerabilities, as the arguments are not escaped, only concatenated.
(Use `node --trace-deprecation ...` to show where the warning was created)
Rebasing (2/6)
Executing: git node land --amend --yes
--------------------------------- New Message ----------------------------------
tls: coalesce buffered records into one read

SSL_read() returns at most one TLS record, and ClearOut() emitted a
separate read for each one. Peers that use small records, such as
nginx with its default 4 KiB ssl_buffer_size or servers doing dynamic
record sizing, made every record pay for its own buffer allocation
and call into JavaScript.

While the next buffered record is application data, keep reading into
the same 16 KiB buffer before emitting it, so reads never get larger
than one full record already makes them. Batching stops before any
other record type, so that e.g. a TLS 1.2 renegotiation is still
processed after the data in front of it has been emitted. It also
stops before records short enough to be empty, because SSL_read()
skips empty records and carries on with the next one. If SSL_read()
fails in the middle of a batch, the error is captured first and
reported after the data.

test-tls-max-send-fragment relied on 'data' chunks matching records,
so it now checks the record sizes on the wire.

Signed-off-by: Robert Nagy <ronagy@icloud.com>
Assisted-by: claude:opus-5.5
PR-URL: https://github.com/nodejs/node/pull/66698
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
--------------------------------------------------------------------------------
[detached HEAD 7b3ecb9362] tls: coalesce buffered records into one read
 Author: Robert Nagy <ronagy@icloud.com>
 Date: Sat Oct 17 09:16:36 2026 +0200
 5 files changed, 302 insertions(+), 29 deletions(-)
 create mode 100644 test/parallel/test-tls-coalesce-records.js
 create mode 100644 test/parallel/test-tls-coalesce-renegotiation.js
Rebasing (3/6)
Rebasing (4/6)
Executing: git node land --amend --yes
--------------------------------- New Message ----------------------------------
test: fix TLS tests for coalesced records

test-double-tls-client expected the head and the inner ClientHello in
separate 'data' chunks. Now that buffered records are read together,
both can arrive in one chunk, so put back whatever follows the head
before handing the socket to the inner server.

BoringSSL reports the corrupted record in test-tls-coalesce-records as
ERR_SSL_BAD_DECRYPT.

Signed-off-by: Robert Nagy <ronagy@icloud.com>
Assisted-by: claude:opus-5.5
PR-URL: https://github.com/nodejs/node/pull/66698
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
--------------------------------------------------------------------------------
[detached HEAD 13470cde60] test: fix TLS tests for coalesced records
 Author: Robert Nagy <ronagy@icloud.com>
 Date: Sun Oct 18 09:23:59 2026 +0200
 2 files changed, 13 insertions(+), 4 deletions(-)
Rebasing (5/6)
Rebasing (6/6)
Executing: git node land --amend --yes
--------------------------------- New Message ----------------------------------
tls: retain debug output when buffered reads destroy SSL

Log the captured SSL error when emitting buffered plaintext destroys the
SSL context before onerror can run. Clarify that an incomplete next
record can return WANT_READ and still flush the accumulated plaintext.

Signed-off-by: Robert Nagy <ronagy@icloud.com>
Assisted-by: Codex
PR-URL: https://github.com/nodejs/node/pull/66698
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
--------------------------------------------------------------------------------
[detached HEAD 2455658738] tls: retain debug output when buffered reads destroy SSL
 Author: Robert Nagy <ronagy@icloud.com>
 Date: Mon Oct 19 15:30:21 2026 +0200
 1 file changed, 7 insertions(+), 1 deletion(-)
Successfully rebased and updated refs/heads/main.
--------------------------------------------------------------------------------
   ℹ  Add `commit-queue-squash` label to land the PR as one commit, or `commit-queue-rebase` to land as separate commits.

View workflow run

@ronag ronag added commit-queue-squash PRs the Commit Queue should land as one squashed commit. commit-queue PRs queued for automated landing through the Commit Queue. and removed commit-queue-failed PRs whose Commit Queue landing failed and need manual intervention before retrying. labels Oct 21, 2026
@nodejs-github-bot
nodejs-github-bot merged commit c7974d9 into nodejs:main Oct 21, 2026
100 checks passed
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Landed in c7974d9

@nodejs-github-bot nodejs-github-bot removed the commit-queue PRs queued for automated landing through the Commit Queue. label Oct 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

c++ Issues and PRs that require attention from people who are familiar with C++. commit-queue-squash PRs the Commit Queue should land as one squashed commit. crypto Issues and PRs related to the crypto subsystem. needs-ci PRs that need a full CI run.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants