Skip to content

fix(http3): dispose multishot UDP recv req on listener teardown#86

Merged
EdmondDantes merged 1 commit into
mainfrom
h3-udp-recv-leak
Jun 5, 2026
Merged

fix(http3): dispose multishot UDP recv req on listener teardown#86
EdmondDantes merged 1 commit into
mainfrom
h3-udp-recv-leak

Conversation

@EdmondDantes
Copy link
Copy Markdown
Contributor

Summary

Fixes a memory leak in the HTTP/3 listener: the multishot UDP recv request submitted by http3_listener was never freed on teardown, leaking the request struct + its 2 KiB recv buffer on every listener destroy.

Root cause

http3_listener_destroy did ZEND_ASYNC_IO_CLOSE(io) + event.dispose(io) but never disposed the recv req it submitted via ZEND_ASYNC_UDP_RECVFROM. ZEND_ASYNC_IO_CLOSE only detaches io->active_req (its await-handoff path assumes a parked coroutine frees the req), and the listener's recv callback only counts datagrams — so nothing freed the req.

Fix

Capture recv_req before close, then recv_req->dispose(recv_req) after ZEND_ASYNC_IO_CLOSE:

  • close clears the reactor's reference first → no use-after-free;
  • the typed zend_async_udp_req_t pointer frees through the correct layout.

This follows the reactor's documented ownership contract (a multishot recv req is owned by the consumer that submitted it) and matches how http_connection already disposes its own multishot read req (src/core/http_connection.c).

Verification (Windows, Debug_TS)

  • Before: === Total 2 memory leaks detected === — 320 B req + 2048 B buffer (from libuv_udp_recvfrom).
  • After: 0 leaks, datagrams_received=3.
  • tests/phpt/server/h3: 14 passed / 0 failed / 22 skipped (skips are platform-only: POSIX-only h3client / objdump n/a on Windows).

Related

Pairs with the true-async/php-async PR (branch windows-reactor-fixes, commit ed2730d), which fixes a latent type-confusion in the reactor's dispose backstop so the UDP path can never crash. That fix is independent — this PR closes the leak on its own — but both come from the same investigation.

http3_listener_destroy closed the UDP io and disposed the event but
never freed the multishot recv request it submitted via
ZEND_ASYNC_UDP_RECVFROM. ZEND_ASYNC_IO_CLOSE only detaches
io->active_req (its await-handoff assumes a parked coroutine frees the
req) and the recv callback merely counts datagrams, so the req struct
plus its 2 KiB recv buffer leaked on every listener teardown.

Capture recv_req before close and dispose it after: close clears the
reactor's reference first, so there is no use-after-free, and the typed
zend_async_udp_req_t pointer frees through the correct layout. This
matches the reactor's documented ownership contract (a multishot recv
req is owned by the consumer that submitted it) and how http_connection
already disposes its own multishot read req.

Verified on Windows (Debug_TS): before = 2 Zend MM leaks (320 B req +
2048 B buf); after = 0 leaks, h3 suite green.

Pairs with true-async/php-async#windows-reactor-fixes (ed2730d), which
fixes a latent type-confusion in the reactor's dispose backstop so the
UDP path can never crash.
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Jun 5, 2026

Coverage

Total lines: 81.42% → 81.20% (-0.22 pp)

File Baseline Current Δ Touched
src/http3/http3_callbacks.c 80.43% 81.02% +0.59 pp
src/http3/http3_listener.c 74.34% 75.70% +1.36 pp

@EdmondDantes EdmondDantes merged commit 980558f into main Jun 5, 2026
8 checks passed
@EdmondDantes EdmondDantes deleted the h3-udp-recv-leak branch June 5, 2026 08:26
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.

1 participant