Skip to content

mod_http3: fix a session allocator race, a stuck graceful drain and a blocked-stream spin - #947

Merged
jfclere merged 6 commits into
apache:trunkfrom
xl32:h3-stability-fixes
Nov 12, 2026
Merged

jfclere merged 6 commits into
apache:trunkfrom
xl32:h3-stability-fixes

Conversation

@xl32

@xl32 xl32 commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Three small mod_http3 stability fixes. Each is its own commit with its own changes-entries file.

1. Lock the session allocator: workers and the event thread share it
Each QUIC connection gets an APR allocator owned by its session pool. Worker threads allocate from stream subpools of that pool while the event thread allocates from the session itself, and the allocator has no mutex. Under concurrent requests on one connection this corrupts the heap and crashes the child. The fix gives the allocator a mutex (apr_allocator_mutex_set), as mod_http2 does for its per-connection allocators. The mutex is only taken when a pool needs a new block, not on each allocation.

2. Advance pending handshakes while draining
If a handshake was still pending when the child began to drain (graceful restart or stop), the event loop no longer advanced it, so it never completed or timed out. The old child kept its connection count and held the UDP port, so the graceful restart never finished. A pending handshake counts as an MPM connection, so progress_pending_handshakes() now also runs while draining.

3. Block a write-blocked stream again each time nghttp3 offers it
When the QUIC send buffer of a stream is full, flush_nghttp3() called nghttp3_conn_block_stream() only the first time (guarded by write_blocked). Once nghttp3_conn_resume_stream() puts the stream back in the schedule, nghttp3 offers it again, so while a client keeps its flow-control window shut the event thread spun on it. The stream is now blocked again every time it is offered. The write_blocked bookkeeping is unchanged.

Tested: built httpd trunk with --enable-http3=shared on EL9 (OpenSSL 4.0.3 with QUIC, nghttp3 1.19), no new warnings. HTTP/3 GET returns 200 with the right body in 8 of 8 start/request/stop cycles.

xl32 added 3 commits November 5, 2026 01:31
Worker threads allocate from stream subpools while the event thread
allocates from the session pool. All of them use one allocator, which
had no mutex, so concurrent allocations corrupted the heap and crashed
the child under load. Set a mutex on the allocator, as mod_http2 does.
A pending handshake counts as an MPM connection. The event thread
advanced pending handshakes only when not draining, so a handshake in
flight at a graceful restart did not finish or time out. The old child
then never reached zero connections and kept the port.
nghttp3_conn_resume_stream puts a blocked stream back in the write
queue. The module blocked the stream only on the first refusal, so
when the peer kept its window shut, nghttp3 offered the stream again
and again and the event thread used 100% CPU.
@jfclere

jfclere commented Nov 11, 2026

Copy link
Copy Markdown
Contributor

PR #993 #993 was also trying to fix the problem, could have a look?

@xl32

xl32 commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor Author

@jfclere looks like different problems addressed (analysed it only with Claude, not with my eyes):

#993 and #947 overlap but don't duplicate each other. #993 works around the symptoms in the shutdown code and in the tests; #947 fixes what most likely causes them. They merge cleanly together, and nothing in #947 has to change.

#993 (merged to trunk): h3_server.c: the port-acquire thread is detached, and shutdown polls a flag instead of
joining the thread
What it does: Moves the crash away from apr_thread_join
How it relates to #947: The crash its notes describe is "a double-free in apr_allocator_destroy() during
apr_thread_join()". That fits heap damage from the unlocked session allocator, which is #947 fix 1
(apr_allocator_mutex_set).
────────────────────────────────────────
#993 (merged to trunk): h3_session.c: frees each stream's qstream (its OpenSSL QUIC stream object) when the session
is destroyed
What it does: Fixes the control-stream leak that ASAN (the address sanitizer) reported
How it relates to #947: Separate from #947 and correct. The notes say this once caused heap corruption; that was
most likely the same allocator race.
────────────────────────────────────────
#993 (merged to trunk): test_202_graceful: known_bug when no GOAWAY shows up
What it does: Hides a flaky graceful restart
How it relates to #947: A handshake still pending when the drain starts was never advanced, so the old child never
finished. That is #947 fix 2.
────────────────────────────────────────
#993 (merged to trunk): test_014_04: known_bug for prefork children that crash or don't exit on stop
What it does: Hides the crash or hang
How it relates to #947: Likely fix 1 or fix 2.

#993 also brought one new bug into trunk. If port_acquire_thread_fn can't create its memory pool, it returns without clearing port_acquire_running, so h3_c1_child_stopping waits forever.

  • Freeing streams at session destroy: not needed. The leak Machine moon tests #993 fixes only exists with the OpenSSL QUIC backend that trunk uses. On our ngtcp2 transport, h3q_conn_free already frees every stream still attached to the connection, control streams included (mod_http3/src/quic/h3q_conn.c:617).
  • Detached port-acquire thread with a flag poll: we don't want it. We already lock the shared allocator (mod_http3/src/h3_io.c:278), which is mod_http3: fix a session allocator race, a stuck graceful drain and a blocked-stream spin #947 fix 1, so the plain apr_thread_join is correct. The poll adds a busy-wait plus the hang I found in the early-return path.
  • Tests: they came from machine-moon, and we already have them as test_004 to test_008 (concurrency, abandoned connections, graceful shutdown, priority, multiplexing).

I would take your 20-restart test_h3_014_04_stop_cleanly to xl32/mod_http3 and if you want, can add it to this pr also.

xl32 added 3 commits November 11, 2026 21:45
…ake its pool

Else h3_c1_child_stopping waits for ever.
A live session at stop also tears down its control streams, which is where the shutdown crashes were.
@xl32

xl32 commented Nov 12, 2026

Copy link
Copy Markdown
Contributor Author

@jfclere I reviewed #993 and think it is not needed when #947 gets merged (#947 fixes root causes of the problems #993 addresses)

@jfclere
jfclere merged commit 80a9294 into apache:trunk Nov 12, 2026
46 checks passed
@jfclere

jfclere commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

merged: Now I am rolling backing back the fix for #993, retesting and PR the rollback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants