Fix NegotiateStream stale read buffer on mid-frame read failure#128067
Fix NegotiateStream stale read buffer on mid-frame read failure#128067rzikm wants to merge 4 commits into
Conversation
ReadAsync was assigning _readBufferCount = readBytes (announced frame size) before the body read completed. If the body read failed (e.g. connection close), _readBufferCount stayed non-zero pointing at a freshly-allocated zero-filled buffer, and the next Read call returned that stale data instead of propagating EOF/error. Defer assignment of _readBufferOffset/_readBufferCount until after decryption succeeds. UnwrapInPlace also writes those fields via out-params even on failure, so we use locals and only commit to the fields after the success check. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Tagging subscribers to this area: @dotnet/ncl, @bartonjs, @vcsjones |
There was a problem hiding this comment.
Pull request overview
This PR fixes a NegotiateStream read-path state bug where an exception during the framed-body read/decrypt could leave internal buffer state inconsistent, allowing subsequent reads to return stale/undecrypted data rather than properly surfacing EOF/error.
Changes:
- Delay committing
_readBufferOffset/_readBufferCountuntil after the framed body is fully read and successfully decrypted. - Refactor the decrypt/unwrap block to use local
decryptedOffset/decryptedCountand only publish them to fields on success. - Add a functional regression test that forces an EOF mid-frame and verifies the next read doesn’t return stale buffered bytes.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| src/libraries/System.Net.Security/src/System/Net/Security/NegotiateStream.cs | Defers internal read-buffer state updates until the read+decrypt succeeds to avoid stale-buffer reads after failures. |
| src/libraries/System.Net.Security/tests/FunctionalTests/NegotiateStreamStreamToStreamTest.cs | Adds a regression test covering mid-frame EOF to ensure subsequent reads don’t return stale data. |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
would we ever go back after throwing? For negotiate and SslStream I would feel the state would be undefined after throwing. And if for example peer closes the socket I see no point of ever trying again. |
With ProtectionLevel.None, NegotiateStream.Read forwards directly to the inner stream and never populates _readBufferCount, so the mid-frame stale-buffer scenario the test asserts on does not apply. The _NotEncrypted test class variants currently fail the IOException assertion on Windows because the fake frame header is returned verbatim as the read payload. Bail out after authentication when neither encryption nor signing is in effect. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Normally no, but this bug has been flagged by an AI scan tool and it seemd as a simple fix |
Fixes a bug in NegotiateStream.ReadAsync where a failed body read could cause subsequent
Readcalls to return stale (zero-filled) buffer contents instead of propagating EOF/error.Problem
In
ReadAsync, after reading the 4-byte frame header, the code was assigning_readBufferCount = readBytes(the announced frame size) before actually reading the body:If
ReadAllAsyncthrew (e.g. the peer closed the connection mid-frame),_readBufferCountwas left non-zero, pointing at a freshly-allocated zero-filled buffer.NegotiateStreamdoes not set any sticky failure flag, so a subsequentReadcall would hit the cached-buffer branch (if (_readBufferCount != 0)) and silently return up toreadBytesbytes of zeros — the caller has no way to tell the difference between a graceful zero-byte payload and a corrupted stream.Fix
_readBufferOffset/_readBufferCountuntil after decryption succeeds.decryptedOffset/decryptedCount) and only commit them to the fields after the success check.UnwrapInPlacewrites its out-params even on failure, and the original code threw without resetting them.Test
Added
NegotiateStream_StreamToStream_ReadFailsMidFrame_DoesNotReturnStaleBufferOnNextReadto the abstractNegotiateStreamStreamToStreamTestclass so all concrete variants (Async_Array, Async_Memory, Async_Memory_NotEncrypted, BeginEnd, Sync, Sync_NotEncrypted) exercise it. The test:IOException.Verified the test fails without the fix (
Expected: 0, Actual: 100) and passes with it.Test is Windows-only (matches the rest of the class), as NTLM loopback works there without explicit credentials/SPNs.
Note
This PR description was drafted by GitHub Copilot.