Skip to content

Fix SslStream detection of exactly-5-byte TLS frames - #132694

Merged
rzikm merged 5 commits into
mainfrom
rzikm/fix-tls-frame-exact-5-bytes
Aug 25, 2026
Merged

Fix SslStream detection of exactly-5-byte TLS frames#132694
rzikm merged 5 commits into
mainfrom
rzikm/fix-tls-frame-exact-5-bytes

Conversation

@rzikm

@rzikm rzikm commented Aug 24, 2026

Copy link
Copy Markdown
Member

Summary

SslStream.EnsureFullTlsFrameAsync loops until a complete TLS record is buffered. When it starts with fewer than HeaderSize (5) bytes, frameSize is UnknownTlsFrameLength and the code recalculates the real size once more data arrives. However, the recalculation guard used a strict comparison:

if (frameSize == int.MaxValue && _buffer.EncryptedLength > TlsFrameHelper.HeaderSize)

A TLS record that is exactly the 5-byte header with a zero-length payload (e.g. 16 03 01 00 00) never satisfies EncryptedLength > 5, so frameSize stays at UnknownTlsFrameLength and the read loop keeps waiting for data that never comes.

Fix

Recalculate the frame size as soon as at least HeaderSize bytes are available (>= instead of >). Also replaced the int.MaxValue literal with the UnknownTlsFrameLength constant 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, asserting SslStream detects 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.

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>
Copilot AI lite review requested due to automatic review settings August 24, 2026 12:30
@azure-pipelines

Copy link
Copy Markdown
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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.EncryptedLength reaches TlsFrameHelper.HeaderSize (uses >= instead of >).
  • Replaces the int.MaxValue sentinel check with the existing UnknownTlsFrameLength constant for consistency.

@rzikm
rzikm enabled auto-merge (squash) August 24, 2026 14:02
@wfurt

wfurt commented Aug 24, 2026

Copy link
Copy Markdown
Member

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>
Copilot AI review requested due to automatic review settings August 24, 2026 14:47
@rzikm

rzikm commented Aug 24, 2026

Copy link
Copy Markdown
Member Author

Added a regression test in SslStreamFramingTest.cs (Read_ExactlyFiveByteTlsRecord_DetectedAsCompleteFrame).

It completes a real handshake, then injects a raw, exactly-5-byte TLS record with a zero-length payload (17 03 03 00 00) on the next read and asserts SslStream treats it as a complete frame instead of waiting forever for a sixth byte. A real encrypted record is never exactly 5 bytes on the wire (AEAD tag/overhead), so the record is injected artificially.

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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 7fc8a916-3685-4b82-8150-6c18e8f5ee18
Copilot AI review requested due to automatic review settings August 24, 2026 14:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

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
Copilot AI review requested due to automatic review settings August 24, 2026 17:14
@rzikm

rzikm commented Aug 24, 2026

Copy link
Copy Markdown
Member Author

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 application_data record. Under TLS 1.3 that record can't be decrypted (no room for the AEAD tag), so once framing recognizes the complete 5-byte frame the read fails fast — which is the behavior under test. Under TLS 1.2 a zero-length record is a legal empty fragment that decrypts to zero bytes and is silently skipped, so even the fixed code loops to read the next frame and hangs when the peer withholds data.

Pinned the test to TLS 1.3 ([ConditionalFact(..., SupportsTls13)] + EnabledSslProtocols = Tls13) so its observable signal is deterministic. The product fix itself is protocol-independent and unchanged.

Note

This comment was generated by GitHub Copilot.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

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
@rzikm

rzikm commented Aug 25, 2026

Copy link
Copy Markdown
Member Author

/ba-g Test failures are unrelated.

@rzikm
rzikm merged commit 8c61077 into main Aug 25, 2026
75 of 78 checks passed
@rzikm
rzikm deleted the rzikm/fix-tls-frame-exact-5-bytes branch August 25, 2026 13:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants