Repository navigation
fixed blocked write racing with close, reset or abort of the output stream - #103
Merged
ptrd merged 1 commit intoNov 7, 2026
Conversation
…tream A write waiting for send buffer space was not woken up by close(). When the sender drained the buffer, the write completed and appended its data after the end of stream marker. If the frame ending at the marker went out as the final frame, flow control was stopped for the stream, and sending the data that followed threw a NullPointerException in FlowControl.getFlowControlLimit on the sender thread, which aborted the connection; otherwise the marker was taken as data and the FIN was never sent. Now close() marks the send buffer closed and wakes up the waiting writer, which fails with an IOException. Data is added to the buffer under the buffer lock, so it can never end up after the marker. A writer that had passed the stream state check but had not yet registered itself as blocked writer was missed by abort() and reset(): after an abort it waited forever, after a reset its write completed. The send buffer now remembers that writers are interrupted and checks this after registering, so such a write fails as well. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ptrd
changed the base branch from
master
to
write-after-close-and-abort-interrupt
November 7, 2026 09:50
Owner
|
Good catch. Thanks for the nice PR and explanations. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A write that was waiting for send buffer space was not woken up by
close(). When the sender drained the buffer, the write completed and appended its data after the end of stream marker. If the frame ending at the marker was sent as the final frame,finalFrameSentstopped flow control for the stream, and sending the leftover data threw aNullPointerExceptioninFlowControl.getFlowControlLimiton the sender thread, aborting the whole connection. Otherwise the marker was taken as stream data and the FIN was never sent. We observed the NPE from a server application that closes a stream while another thread is blocked writing to it.abort()andreset()could also miss a writer that had passed the stream state check but had not yet registered itself as the blocked writer. After an abort, such a writer waited forever; after a reset, its write completed even though the stream was reset.Fix:
SendBuffer.close()marks the buffer closed under the buffer lock and wakes a waiting writer, which then fails with anIOException("output stream already closed"). Data is always added under the buffer lock, so it can never end up after the marker.interruptBlockedWriter()now also sets a flag that a writer checks after registering itself and before adding data, so a reset or abort that races the registration still fails the write.restart()(used when 0-RTT data is rejected) reopens the buffer.Tests:
QuicStreamImplTestcovers a writer blocked on a full buffer while the stream is closed (this test reproduces the NPE before the fix), and abort or reset arriving while the writer is between the state check and its registration (forced by holding the buffer lock).SendBufferTestcovers close while waiting, write after close, and an interrupt before the writer starts waiting.EarlyDataStreamTestcovers rejected 0-RTT data that included the FIN.Not covered: a write racing
resetReliable()can still land after the reliable reset completes, becauseresetis set before the writer is interrupted; that is a separate ordering change.