Repository navigation
Conversation
SSL_read() returns at most one TLS record per call, and ClearOut() emitted every record separately: one buffer allocation and one call into JavaScript, followed by a nextTick drain, per record. Peers that send small records (4 KiB from ingress-nginx's default ssl_buffer_size, or about 1 KiB with dynamic record sizing) made consumers pay those costs once per record instead of once per read. When more input is already buffered after a record has been decrypted, decrypt the following records into the same buffer, up to the 16 KiB of a full record, and emit them as one chunk. Chunks therefore never exceed the size a single record could already produce. Larger chunks were tried and did not pack into the 64 KiB stream read slabs; the extra external memory triggered major GCs that cost more than the saved calls. An SSL_read() failure that ends such a batch is examined before the batch is emitted, because SSL_get_error() and the error queue have to be read before calling into JavaScript. It is reported after the data it follows, as before. 'data' chunks therefore no longer correspond to TLS records, which test-tls-max-send-fragment relied on to check the sender's fragment size. It now checks the record sizes on the wire instead. Assisted-by: Opus 5.5 Signed-off-by: Robert Nagy <ronagy@icloud.com>
Collaborator
|
Review requested:
|
Assisted-by: OpenAI Codex Signed-off-by: Robert Nagy <ronagy@icloud.com>
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.
Flush buffered application data before reading a visible protocol record. This lets a preceding data handler disable TLS 1.2 renegotiation before the following ClientHello is processed. Assisted-by: OpenAI Codex Signed-off-by: Robert Nagy <ronagy@icloud.com>
Flush preceding cleartext before session, keylog and handshake callbacks. Extend the TLS throughput benchmark with record-size and protocol selectors, and cover coalesced TLS 1.3 KeyUpdate ordering. Assisted-by: OpenAI Codex Signed-off-by: Robert Nagy <ronagy@icloud.com>
Signed-off-by: Robert Nagy <ronagy@icloud.com>
Signed-off-by: Robert Nagy <ronagy@icloud.com>
ronag
marked this pull request as ready for review
October 13, 2026 09:48
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #66698 +/- ##
==========================================
- Coverage 92.82% 90.46% -2.37%
==========================================
Files 422 791 +369
Lines 194189 277293 +83104
Branches 30039 53326 +23287
==========================================
+ Hits 180257 250844 +70587
- Misses 13607 16835 +3228
- Partials 325 9614 +9289
🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


TLSWrap::ClearOut()currently emits one JavaScript read per TLS record. Peers that send small records cause allocation, callback and downstream processing costs once per record, even when several records arrived together.This change decrypts records that are already buffered into a 64 KiB output buffer before emitting a read. It does not wait for additional network input. The cap increases from 16 KiB to 64 KiB, allowing several full-sized records or a burst of small records to share a read. This also increases the temporary stack buffer by 48 KiB and changes allocation sizes and stream chunk boundaries.
SSL errors and close notifications are inspected before entering JavaScript and reported after preceding data. Accumulated data is flushed before visible control records and before handshake, session and key-log callbacks, including TLS 1.3 encrypted control records. Reentrant writes and shutdown retain the existing SSL-library guards. Destruction requested from a read callback while OpenSSL is on the stack is deferred; subsequent output and callbacks stop immediately, and a strong reference keeps the wrapper alive until cleanup.
Tests cover coalescing, the 64 KiB chunk limit, complete delivery through a small
onreadbuffer, clean EOF, corrupted records, TLS 1.2 renegotiation, TLS 1.3 KeyUpdate ordering, reentrant writes and synchronous SSL destruction.test-tls-max-send-fragmentnow checks record sizes on the wire, since readable chunks can contain multiple records.The existing
benchmark/tls/throughput-s2c.jsbenchmark gains TLS-version and record-size selectors: 1 KiB, 4 KiB and 16 KiB records under TLS 1.2 and TLS 1.3. Application write size and receive-buffer configuration remain independently selectable.Validation
c883ccf3b249033e417368366a6f6c16f185bc5a; candidate:f09b719c804aac271d8ee82f0332316ad647055f../configureand GCC 14.2.0 on Linux x64; Nodev27.0.0-pre, bundled OpenSSL 3.5.9.git diff --checkpass._destroySSL()from the preceding data callback.net.onreadcases.make -j16 test PARALLEL_ARGS="-j16 --timeout=120 -p tap"target passes: 7,518 JavaScript/native-addon TAP cases, with expected platform skips, plus 275 C++ tests and the tool tests. The TLS benchmark smoke test also passes. The container runs tests as a non-root user with a subreaper and standard privileged-port permissions; fixture ownership was corrected after reproducing environment-only failures on the base.Before/after benchmark
AMD EPYC 9355P, Debian 13, Docker limited to 16 CPUs and 32 GiB. Both benchmark processes and their children were pinned to the same CPU, with a 256 MiB JavaScript heap. Eight runs per binary and configuration, three seconds each, using 16 MiB Buffer writes and the default read allocation path. Values are mean ± sample standard deviation in MiB/s.
Node's comparison analyzer reports all six differences significant at 5% after Holm–Bonferroni correction. The TLS 1.3 full-record regression is a tradeoff of the candidate. These measurements compare the complete patch against the base; they do not isolate the effect of the 64 KiB cap from coalescing and callback handling.
Separate
--trace-gcruns used the same workload and limits, three repetitions per case. With 1 KiB records, peak process RSS rose from 116–130 MiB to 175–186 MiB. Major collections rose from zero to 12–20 per three-second run, with 13–26 ms of summed major-GC pause time. With 4 KiB records, peak RSS rose from 133–157 MiB to 170–182 MiB, while major collection counts generally fell. With 16 KiB records, peak RSS remained roughly 171–179 MiB and collection counts varied. These whole-process measurements include both client and server, and the candidate transfers more data in several cases; they are not a per-socket allocation measurement or a claim that GC pressure falls.