Repository navigation
nghttp2: a response given after the peer reset its stream attaches nothing, so its body no longer leaks and a streamed writer hears the peer gone - #501
Open
MDA2AV wants to merge 2 commits into
Conversation
…thing, so its body no longer leaks and a streamed writer hears the peer gone A peer that resets a stream after its request ended and before the handler answers (a browser cancelling a slow request) leaves nothing on the managed side to mark. ih2_submit_response then copied the response's body into an ih2_stream for a stream nghttp2 had already freed; the HEADERS were dropped at send with no callback, so the copy stayed until the connection closed. A streamed writer created after such a reset wrote into its own orphaned ih2_stream, never heard the reset, and past 64 KiB its flush waited until the connection was disposed (#496). Both submit functions now return 0 without attaching anything when nghttp2_session_find_stream finds no stream. The streamed writer's first write then finds none, which SendStreamedData already reads as that stream's end. Both runtimes' libraries are rebuilt from this source.
…ders, so a flush with nothing staged reports the peer gone too ih2_submit_response_stream returns NGHTTP2_ERR_STREAM_CLOSED for a gone stream, which only that case can return: a response past the shim's 64 headers returns INVALID_ARGUMENT and still fails the connection. The test proves the connection serves on, and the leak's bound is well clear of what the rest of the process mallocs.
This branch has not been deployed
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.
What was wrong
ih2_submit_responsethen copied the response's body into anih2_streamand callednghttp2_submit_response, which accepts any positive stream id. The drain dropped the HEADERS as STREAM_CLOSED with no callback, soon_stream_closenever freed thatih2_stream: the body stayed until the connection closed.ih2_submit_response_stream, and everyih2_stream_writethen succeeded into it. Past 64 KiB of unframed bytes its flush parked, and nothing woke it until the connection was disposed.Details are in #496.
The fix
Both submit functions attach nothing when
nghttp2_session_find_streamfinds no stream.ih2_submit_responsereturns 0, with nothing queued and nothing kept. An error here would fail the connection, for one stream's end.ih2_submit_response_streamreturnsNGHTTP2_ERR_STREAM_CLOSED.SendStreamedHeadersreads that as "this stream is gone" and the writer marks itself gone, so its flush returnsIsCompleted, with or without bytes staged.Native libraries
runtimes/linux-x64andruntimes/linux-arm64are rebuilt withscripts/build-nghttp2-native.shat its pinned nghttp2 ref: x64 on the host, arm64 with theaarch64-linux-gnucross toolchain in an Ubuntu 24.04 container. Both still need nothing past GLIBC 2.17.#495 rebuilds the same libraries. Whichever of the two merges second has to merge main and rerun the build script, rather than take either side's binaries.
Behaviour
IsCompletedat its first flush. Before, a flush with nothing staged reported success, and a body past 64 KiB made its flush wait until the connection closed.API
None.
Tests
New in the Unit suite (
Nghttp2ResetResponseTests.cs), over the strict test transport:nghttp2: a buffered response to a stream its peer reset while the handler ran is not keptmallinfo2) may grow under 6 MiB across the other 200. They are the whole process's, so the bound sits well clear of the JIT's; the leak is 12.8 MiB.nghttp2: a streamed response to a stream reset in the same read as its end hears the peer gone at a flush of 131072 B, and of 0 BIsCompleted, whether it staged 128 KiB, past the 64 KiB a flush may run ahead of nghttp2, or nothing.Against main's library (the tests only):
Each streamed test was also run against a mutation of the code it guards and failed there: the writer ignoring the result of its headers (the 0 B flush), and a closed stream failing the connection (both).
With the fix: Unit: 148 passed, E2E: 453, Chaos: 83, Http: 95. The shim compiles with no warnings under
-Wall -Wextra -Werror=implicit-function-declaration; the lookup is nghttp2's publicnghttp2_session_find_stream, which finds nothing oncenghttp2_session_close_streamhas destroyed the stream.Bench
Each response gains one stream lookup in C (
nghttp2_session_find_stream). Measured with the same setup as #486:h2load -c 32 -m 16 -t 1pinned to CPU 2.Http2/Nghttp2BufferedHttp2/Nghttp2Response(8 chunks of 1 KiB)Both are inside the noise, and the per-round ranges overlap: buffered 918-948 ns per request for the branch, 897-954 for main and 881-947 for the control. If the lookup costs anything, it is about 1%.
Measured before the streamed function's result changed for a stream already gone. That changes only that case, not the path of a live stream, so it was not measured again.
Fixes #496.