unwrap() Client-side pre-check in SSLEngine to fix BUFFER_UNDERFLOW#352
unwrap() Client-side pre-check in SSLEngine to fix BUFFER_UNDERFLOW#352rlm2002 wants to merge 2 commits intowolfSSL:masterfrom
Conversation
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
This PR adjusts SSLEngine.unwrap() handshake behavior to better comply with the JSSE spec on BUFFER_UNDERFLOW, and updates a regression test to match the expected bytesConsumed() semantics.
Changes:
- Added a client-mode pre-check for partial TLS records during handshake to return
BUFFER_UNDERFLOWwithbytesConsumed() == 0. - Guarded post-handshake/close-status logic to avoid running when the handshake underflow pre-check triggers.
- Updated the handshake regression test to only fail when
BUFFER_UNDERFLOWis returned withbytesConsumed() > 0.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| src/java/com/wolfssl/provider/jsse/WolfSSLEngine.java | Adds client-side TLS record header pre-check to drive spec-compliant BUFFER_UNDERFLOW behavior during handshake. |
| src/test/com/wolfssl/provider/jsse/test/WolfSSLEngineTest.java | Adjusts regression test assertions to align with JSSE “no data consumed on underflow” requirement. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
8322b35 to
e593e63
Compare
BUFFER_UNDERFLOW encountered edit comment
e593e63 to
34e923d
Compare
|
retest this please Jenkins |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| prevSessionTicketCount = this.sessionTicketCount; | ||
| } | ||
|
|
||
| boolean hsUnderflow = false; |
There was a problem hiding this comment.
When hsUnderflow is detected, the method sets status = BUFFER_UNDERFLOW but (from the shown hunk) does not update the handshake status to reflect “need more inbound data”. If the returned SSLEngineResult ends up carrying the prior HandshakeStatus (notably NEED_WRAP in this surrounding branch), callers that prioritize HandshakeStatus may incorrectly try to wrap() instead of reading more network data. Consider explicitly ensuring the result’s HandshakeStatus is NEED_UNWRAP (or otherwise consistent with BUFFER_UNDERFLOW) in the hsUnderflow path.
| if (this.getUseClientMode() && | ||
| inRemaining > 0 && | ||
| (this.ssl.dtls() == 0)) { | ||
| synchronized (netDataLock) { | ||
| int pos = in.position(); | ||
| if (inRemaining < TLS_RECORD_HEADER_LEN) { | ||
| hsUnderflow = true; | ||
| } else { | ||
| int recLen = | ||
| ((in.get(pos + TLS_RECORD_LEN_HI_OFF) | ||
| & 0xFF) << 8) | | ||
| (in.get(pos + TLS_RECORD_LEN_LO_OFF) | ||
| & 0xFF); | ||
| if (recLen <= WolfSSL.MAX_RECORD_SIZE | ||
| && inRemaining < | ||
| TLS_RECORD_HEADER_LEN + recLen) { | ||
| hsUnderflow = true; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| if (hsUnderflow) { | ||
| status = SSLEngineResult.Status.BUFFER_UNDERFLOW; | ||
| } else { | ||
| WolfSSLDebug.log(getClass(), WolfSSLDebug.INFO, | ||
| () -> "starting or continuing handshake"); | ||
| ret = DoHandshake(false); | ||
| } |
There was a problem hiding this comment.
When hsUnderflow is detected, the method sets status = BUFFER_UNDERFLOW but (from the shown hunk) does not update the handshake status to reflect “need more inbound data”. If the returned SSLEngineResult ends up carrying the prior HandshakeStatus (notably NEED_WRAP in this surrounding branch), callers that prioritize HandshakeStatus may incorrectly try to wrap() instead of reading more network data. Consider explicitly ensuring the result’s HandshakeStatus is NEED_UNWRAP (or otherwise consistent with BUFFER_UNDERFLOW) in the hsUnderflow path.
| if (!hsUnderflow) { | ||
| if (outBoundOpen == false || this.closeNotifySent || | ||
| this.closeNotifyReceived) { | ||
| /* Mark SSLEngine status as CLOSED */ | ||
| status = SSLEngineResult.Status.CLOSED; | ||
| /* Handshake has finished and SSLEngine is closed, | ||
| * release, global JNI verify callback pointer */ | ||
| this.engineHelper.unsetVerifyCallback(); | ||
| } | ||
|
|
There was a problem hiding this comment.
Gating the CLOSED-state propagation behind if (!hsUnderflow) can return BUFFER_UNDERFLOW even when the engine is already logically closed (outBoundOpen == false, closeNotifySent, or closeNotifyReceived). CLOSED typically should take precedence over underflow to avoid prompting callers to read more data on a closed engine. Consider checking and applying the CLOSED state before the underflow early-return path (or applying it regardless of hsUnderflow).
| if (!hsUnderflow) { | |
| if (outBoundOpen == false || this.closeNotifySent || | |
| this.closeNotifyReceived) { | |
| /* Mark SSLEngine status as CLOSED */ | |
| status = SSLEngineResult.Status.CLOSED; | |
| /* Handshake has finished and SSLEngine is closed, | |
| * release, global JNI verify callback pointer */ | |
| this.engineHelper.unsetVerifyCallback(); | |
| } | |
| if (outBoundOpen == false || this.closeNotifySent || | |
| this.closeNotifyReceived) { | |
| /* Mark SSLEngine status as CLOSED */ | |
| status = SSLEngineResult.Status.CLOSED; | |
| /* Handshake has finished and SSLEngine is closed, | |
| * release, global JNI verify callback pointer */ | |
| this.engineHelper.unsetVerifyCallback(); | |
| } | |
| if (!hsUnderflow) { |
| if (recLen <= WolfSSL.MAX_RECORD_SIZE | ||
| && inRemaining < | ||
| TLS_RECORD_HEADER_LEN + recLen) { | ||
| bufferUnderflow = true; | ||
| } |
There was a problem hiding this comment.
The “partial TLS record” detection logic (read header, compute recLen, compare against TLS_RECORD_HEADER_LEN + recLen, and cap with MAX_RECORD_SIZE) now appears in multiple places (hsUnderflow and bufferUnderflow). This duplication increases the risk of future drift (e.g., changing the cap/conditions in one place but not the other). Consider extracting a small private helper (e.g., isPartialTlsRecord(ByteBuffer in, int inRemaining)) and reusing it for both branches.
| result = client.unwrap(firstHalf, cliAppBuf); | ||
|
|
||
| /* BUFFER_UNDERFLOW must not be returned when input was | ||
| * consumed - regression for inRemaining == 0 guard fix */ | ||
| * consumed - regression for inRemaining == 0 guard fix. | ||
| * If BUFFER_UNDERFLOW is returned, bytesConsumed() must be 0 | ||
| * per the JSSE spec ("No data was consumed"). */ | ||
| if (result.getStatus() == | ||
| SSLEngineResult.Status.BUFFER_UNDERFLOW) { | ||
| SSLEngineResult.Status.BUFFER_UNDERFLOW && | ||
| result.bytesConsumed() > 0) { |
There was a problem hiding this comment.
This test now permits BUFFER_UNDERFLOW as long as bytesConsumed() == 0, but it doesn’t assert the newly introduced behavior that specifically motivated the change in WolfSSLEngine.unwrap() (client-mode underflow on a partial TLS record header/record before calling into wolfSSL). Consider adding a targeted test case that feeds (a) <5 bytes (partial header) and (b) a full header with an incomplete payload, and asserts Status.BUFFER_UNDERFLOW with bytesConsumed() == 0 for both.
| if (result.getStatus() != SSLEngineResult.Status.BUFFER_UNDERFLOW | ||
| && result.bytesConsumed() == 0) { |
There was a problem hiding this comment.
This test now permits BUFFER_UNDERFLOW as long as bytesConsumed() == 0, but it doesn’t assert the newly introduced behavior that specifically motivated the change in WolfSSLEngine.unwrap() (client-mode underflow on a partial TLS record header/record before calling into wolfSSL). Consider adding a targeted test case that feeds (a) <5 bytes (partial header) and (b) a full header with an incomplete payload, and asserts Status.BUFFER_UNDERFLOW with bytesConsumed() == 0 for both.
Adds a client-mode pre-check in unwrap() that inspects the TLS record header before calling into wolfSSL, returning BUFFER_UNDERFLOW with bytesConsumed() == 0 when the buffer holds a partial record: "no data was consumed" true on underflow.
Also corrects the
testHandshakeUnwrapConsumedNotBufferUnderflowregression test: rather than rejecting allBUFFER_UNDERFLOWresults, now fails ifBUFFER_UNDERFLOWis returned whenbytesConsumed() > 0.