Fix SslStream detection of exactly-5-byte TLS frames - #132694
Conversation
EnsureFullTlsFrameAsync only recalculated the frame size once more than HeaderSize (5) bytes were buffered. A TLS record that is exactly the 5-byte header with a zero-length payload (e.g. 16 03 01 00 00) never triggered the recalculation, leaving frameSize at UnknownTlsFrameLength and causing the read loop to wait indefinitely for data that would never arrive. Recalculate as soon as at least HeaderSize bytes are available. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
Azure Pipelines: Successfully started running 4 pipeline(s). 12 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
There was a problem hiding this comment.
Pull request overview
Fixes a TLS record buffering edge case in SslStream where a record that is exactly the 5-byte TLS header (including a valid zero-length payload) could fail to trigger frame-size recalculation and cause EnsureFullTlsFrameAsync to keep waiting for more data.
Changes:
- Recomputes TLS frame size as soon as
_buffer.EncryptedLengthreachesTlsFrameHelper.HeaderSize(uses>=instead of>). - Replaces the
int.MaxValuesentinel check with the existingUnknownTlsFrameLengthconstant for consistency.
|
can we add some unit tests? e.g. artificially constructed case. |
Injects a raw 5-byte zero-length TLS record after the handshake and asserts SslStream treats it as a complete frame instead of waiting indefinitely for a sixth byte. Verified the test hangs (fails) against the unfixed code and passes with the fix. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
Added a regression test in It completes a real handshake, then injects a raw, exactly-5-byte TLS record with a zero-length payload ( Verified both directions locally: against the unfixed code the read hangs and the test fails at the 60s timeout ("SslStream hung waiting for more data"); with the fix it passes in <1s. Note This comment was generated by GitHub Copilot. |
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7fc8a916-3685-4b82-8150-6c18e8f5ee18
A zero-length application_data record only fails to decrypt (and thus surfaces the framing behavior) under TLS 1.3. Under TLS 1.2 it is a legal empty fragment that decrypts to zero bytes and is skipped, so the fixed code reads again and the test hung on TLS 1.2-only CI platforms. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7fc8a916-3685-4b82-8150-6c18e8f5ee18
|
The regression test hung on TLS 1.2-only CI legs (macOS, Mono, older Windows images). Root cause: the test injects a raw, exactly-5-byte zero-length Pinned the test to TLS 1.3 ( Note This comment was generated by GitHub Copilot. |
The osx-arm64 Debug CoreCLR_AllSubsets build leg was cancelled due to an Azure DevOps worker timeout (unrelated to this test-only change). Empty commit to re-run the pipeline. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7fc8a916-3685-4b82-8150-6c18e8f5ee18
|
/ba-g Test failures are unrelated. |
Summary
SslStream.EnsureFullTlsFrameAsyncloops until a complete TLS record is buffered. When it starts with fewer thanHeaderSize(5) bytes,frameSizeisUnknownTlsFrameLengthand the code recalculates the real size once more data arrives. However, the recalculation guard used a strict comparison:A TLS record that is exactly the 5-byte header with a zero-length payload (e.g.
16 03 01 00 00) never satisfiesEncryptedLength > 5, soframeSizestays atUnknownTlsFrameLengthand the read loop keeps waiting for data that never comes.Fix
Recalculate the frame size as soon as at least
HeaderSizebytes are available (>=instead of>). Also replaced theint.MaxValueliteral with theUnknownTlsFrameLengthconstant it aliases, for consistency with the surrounding code.Testing
Added
SslStreamFramingTests.Read_ExactlyFiveByteTlsRecord_DetectedAsCompleteFrame, which completes a real handshake and then injects a raw, exactly-5-byte zero-length TLS record on the next read, assertingSslStreamdetects a complete frame rather than hanging for a sixth byte. A real encrypted record is never exactly 5 bytes (AEAD overhead), so the record is injected artificially. Verified locally that the test hangs and fails at the 60s timeout against the unfixed code, and passes in <1s with the fix.Note
This PR was authored by GitHub Copilot.