Repository navigation
fix(tls): preserve BUFFER_OVERFLOW when array-destination unwrap drains nothing - #64
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #64 +/- ##
============================================
+ Coverage 86.82% 86.91% +0.09%
- Complexity 616 622 +6
============================================
Files 60 60
Lines 16979 17013 +34
Branches 490 491 +1
============================================
+ Hits 14742 14787 +45
+ Misses 1913 1898 -15
- Partials 324 328 +4
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. |
Merging this PR will degrade performance by 10.46%
|
| Mode | Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|---|
| ❌ | Simulation | retain-drop[1048576] |
300.8 ns | 354.9 ns | -15.26% |
| ❌ | Simulation | retain-drop[4096] |
300.8 ns | 354.9 ns | -15.26% |
| ❌ | Simulation | retain-drop[64] |
300.8 ns | 354.9 ns | -15.26% |
| ❌ | Simulation | retain-drop[65536] |
300.8 ns | 354.9 ns | -15.26% |
| ⚡ | Simulation | overwrite[64] |
520.2 ns | 466.1 ns | +11.62% |
Tip
Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.
Comparing detail/bug-fix/fix-tls-preserve-buffer-overflow-when-array-destin-32d64e (c99e998) with main (26fb123)
Footnotes
-
65 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩
Detail bug report: View on Detail
Change
NativeSslEngine.unwrap(ByteBuffer, ByteBuffer[], int, int)reportedBUFFER_UNDERFLOW(0 consumed / 0 produced) while a complete, decryptable record was pending — a contract violation that stalls any caller using the standard array-destinationSSLEngine.unwrapAPI with a first destination smaller than the pending record's plaintext.Root cause (Java layer, in the array-destination
unwrapdrain loop): when the first destination is too small, the native engine returnsBUFFER_OVERFLOWfrom a pre-check (record too large for the destination, nothing decrypted/queued). The Java loop then issued a zero-length-source "drain" call to retrieve queued plaintext — but in the pre-check case nothing was queued, so that drain correctly returnedBUFFER_UNDERFLOW, and the loop overwrote the genuineBUFFER_OVERFLOWwith it. A compliant caller then waits for more inbound bytes that can never help, retrying indefinitely.The native pre-check path (in
crates/bemo/src/tls/engine.rs) is valid engine behavior; the contract violation is in the Java loop masking it.Fix (
packages/netty/.../NativeSslEngine.java): the drain loop now distinguishes the two overflow causes:produced(drain) > 0), queued plaintext from a prior decryption is adopted — the existing, correct path is unchanged.When every destination is too small, the final status stays
BUFFER_OVERFLOW(neverBUFFER_UNDERFLOW). This also makes progress into later destinations instead of merely re-reporting the overflow.Affected layers: JVM (
NativeSslEngine, the publicSSLEnginesurface shipped to consumers). No ABI change, no native behavior change — the native pre-check overflow and empty-source underflow are part of the existing engine contract.Closes #57
Validation
Bug introduced in
d99f693; the drain loop has been byte-identical since, with no test coverage on the array-destinationunwrap(the repo and Netty'sSslHandleronly exercise the 2-arg variant). Added regression coverage at both layers, then ran the full gate.New regression tests (versioned):
tls::engine::tests::engine_pre_check_overflow_queues_no_plaintext_and_empty_drain_underflows(crates/bemo/src/tls/engine.rs) — pins the native contract the Java loop depends on: a too-small destination returnsBufferOverflow(consumed 0, produced 0, destination untouched); an empty-source drain returnsBufferUnderflow0/0; the still-pending record then decrypts into a large destination withOk(consumed = record length, produced = plaintext length).NativeSslEngineTest.arrayUnwrapOverflow(tests/transport/java/NativeSslEngineTest.java) — drives a real TLS 1.3 handshake (no sockets) and theunwrap(ByteBuffer, ByteBuffer[], int, int)public API: a single 100-byte destination correctly reportsBUFFER_OVERFLOW0/0 with the record left on the wire (matching the 2-arg unwrap); a{100-byte, 64 KB}destination pair returnsOK, consuming the full record and delivering 10,000 plaintext bytes into the large destination, leaving the small one untouched.Commands run (Linux x86_64):
cargo nextest run -p bemo --lib --tests --locked— alltls::engine::tests::*pass, including the new test.cargo nextest run -p bemo --locked --test tls --test tls_abi --test engine— 23/23 pass.cargo clippy --workspace --all-targets --locked -- -D warningsandcargo fmt --package bemo --check— clean.make build-jvm— production classes compile (-Xlint:all -Werror --release 22).make checkJava path (tools/check_java.py) — Error Prone + NullAway (JSpecify mode) + Checker Framework Regex/Formatter pass.google-java-format --dry-run --set-exit-if-changedon both Java files — clean.make test-jvm—NativeSslEngineTest(incl. the new contract),NativeSslPolicyTest, and JSSE-interop cases all pass.NativeSslInteropTest's OpenSSL CLI cases fail in this sandbox because the JDK cannotposix_spawnsubprocesses (EACCES, error 13); re-running that contract with-Djdk.lang.Process.launchMechanism=FORK(the JDK's legacy fork mechanism) makes all its cases pass, including both OpenSSLs_client/s_serverinterop — i.e. the failure is environmental, not the fix./usr/bin/opensslitself runs fine from the shell.make test-native-image— passes after the same JDKposix_spawnworkaround is applied tonative-image(it spawnsgcc/ccfor toolchain checks and linking).CapiTlsChannelTest(which callsNativeSslEngineTest.verify+NativeSslPolicyTest.verify+NativeSslInteropTest.verify, including the newarrayUnwrapOverflowtest) builds ahead-of-time and passes, along with the OpenSSL interop cases.make test-asanandmake test-tsan— all TLS tests pass under both sanitizers (the retry path that re-enters the native engine with the full source shows no ASAN or TSAN finding).make test-miri— passes (the new Rust test is correctly#[cfg_attr(miri, ignore)]since it drives a rustls handshake, matching the existing engine-test convention).make bench-smokeandmake bench-tls-records— TLS correctness and record-encryption throughput pass; thebemoprovider is on par with theopenssl/tcnative reference, so no throughput regression.Two unrelated tests fail in the workspace-wide, ASAN, and TSAN runs (
persistent_keepalive_reuses_receive_registration_and_fixed_send_state,arm_receive_failure_leaves_socket_dormant); both are pre-existing on the clean baseline (verified by re-running on the unmodified tree) and live indriver/http io_uringcode, untouched by this change.Automatic Fixes PRs can be configured here.