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 3 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. |
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.