From 8b59ae62d49ffdc8ffaed7b3fb758e8cc0428c55 Mon Sep 17 00:00:00 2001 From: Edmond <1571649+EdmondDantes@users.noreply.github.com> Date: Fri, 5 Jun 2026 11:14:36 +0300 Subject: [PATCH] fix(http3): dispose multishot UDP recv req on listener teardown 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. --- src/http3/http3_listener.c | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/http3/http3_listener.c b/src/http3/http3_listener.c index 82f3160..3143cac 100644 --- a/src/http3/http3_listener.c +++ b/src/http3/http3_listener.c @@ -1449,10 +1449,24 @@ void http3_listener_destroy(http3_listener_t *listener) #endif if (listener->udp_io != NULL) { zend_async_io_t *io = listener->udp_io; + zend_async_udp_req_t *recv_req = listener->recv_req; listener->udp_io = NULL; listener->recv_cb = NULL; listener->recv_req = NULL; ZEND_ASYNC_IO_CLOSE(io); + + /* Dispose the multishot recv req we submitted. ZEND_ASYNC_IO_CLOSE + * only detaches io->active_req (its await-handoff path assumes a + * parked coroutine frees it), and our recv callback merely counts + * datagrams — neither frees the req. Without this the req struct + + * 2 KiB recv buffer (plus any error exception) leak on every listener + * teardown. Dispose AFTER close: close clears the reactor's reference + * so there is no use-after-free, and the typed recv_req pointer frees + * through the correct zend_async_udp_req_t layout. */ + if (recv_req != NULL && recv_req->dispose != NULL) { + recv_req->dispose(recv_req); + } + io->event.dispose(&io->event); }