Repository navigation
tls: coalesce buffered records into one read - #66698
Conversation
|
Review requested:
|
There was a problem hiding this comment.
🟡 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.
Codecov Report❌ Patch coverage is
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
🚀 New features to boost your workflow:
|
ab55637 to
9f3bab3
Compare
9f3bab3 to
435181a
Compare
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
Commit Queue failedThis pull request has multiple commits, but no landing policy was selected. Add
commit-queue-squash
The pull request was removed from the Commit Queue and labeled
commit-queue-failed
Full Commit Queue output |
|
Landed in c7974d9 |


SSL_read()returns at most one TLS record per call, andTLSWrap::ClearOut()emits a separate read for each one. Records can be up to 16 KiB, but plenty of servers send smaller ones: nginx defaultsssl_buffer_sizeto 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 callingSSL_read()into the same 16 KiB buffer while the next record inenc_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:
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 calldisableRenegotiation()before a renegotiation that arrived in the same read.test-tls-coalesce-renegotiationcovers that, and fails if the record type check is removed.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.'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, sotest-tls-max-send-fragmentnow checks the record sizes on the wire through a small proxy.pummel/test-https-no-readerfails), so I kept 16 KiB.benchmark/tls/throughput-s2c.jsgets arecordlenoption, which sets the server'ssetMaxSendFragment().Results on an AMD EPYC 9355P (Linux, Docker, 16 CPUs on one NUMA node, benchmark pinned to one CPU), 10 runs each, MiB/s: