Skip to content

ext/async: fix UDP req type-confusion in IO dispose backstop#157

Merged
EdmondDantes merged 1 commit into
mainfrom
windows-reactor-fixes
Jun 5, 2026
Merged

ext/async: fix UDP req type-confusion in IO dispose backstop#157
EdmondDantes merged 1 commit into
mainfrom
windows-reactor-fixes

Conversation

@EdmondDantes
Copy link
Copy Markdown
Contributor

Summary

Fixes a latent type-confusion crash in the libuv reactor's IO-dispose backstop when an in-flight request is a UDP recv req.

Root cause

libuv_io_event_dispose's leftover-request sweep cast io->active_req to async_io_req_t and read req->base.dispose. But dispose() sits at a different struct offset for zend_async_udp_req_t (after a 4-byte flags) than for zend_async_io_req_t (after an 8-byte free_cb). For a UDP recv req this read the request's sockaddr bytes as a function pointer → access violation (0xC0000005). The old comment ("dispose at the same offset for both") was false; the path never worked for UDP.

Fix

  • Branch on io->base.type == ZEND_ASYNC_IO_TYPE_UDP and dispose UDP reqs through the correct layout.
  • Document the libuv_io_close ownership contract: a multishot recv req (persistent callback, no awaiter) is owned and freed by the consumer that submitted it; freeing it in io_close would double-free.

libuv_io_close / udp_recv_cb behaviour is otherwise unchanged.

Context

Surfaced on Windows while fixing the true-async/server HTTP/3 listener UDP recv-request leak (paired server PR: true-async/server#86). The server-side consumer-frees fix closes the leak on its own; this reactor fix ensures the dispose backstop can never crash on a UDP req.

Verification

Built into php.exe (Debug_TS) and exercised via the server h3 suite on Windows: no crash, h3 14 passed / 0 failed / 22 skipped, and the paired leak fix verified clean (Zend MM: before 2 leaks → after 0).

libuv_io_event_dispose's leftover-request sweep cast io->active_req to
async_io_req_t and called req->base.dispose. But dispose() sits at a
different struct offset for zend_async_udp_req_t (after a 4-byte flags)
than for zend_async_io_req_t (after an 8-byte free_cb), so for a UDP
recv req this read the request's sockaddr bytes as a function pointer
-> access violation. Branch on io->base.type and dispose UDP reqs
through the correct layout.

Also document the libuv_io_close ownership contract: a multishot recv
req (persistent callback, no awaiter) is owned and freed by the consumer
that submitted it (e.g. the HTTP/3 listener on teardown); freeing it in
io_close would double-free.

Surfaced on Windows while fixing the true-async/server HTTP/3 listener
UDP recv-request leak.
@codecov
Copy link
Copy Markdown

codecov Bot commented Jun 5, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@EdmondDantes EdmondDantes added this to the TrueAsync 0.8.0 milestone Jun 5, 2026
@EdmondDantes EdmondDantes added the bug Something isn't working label Jun 5, 2026
@EdmondDantes EdmondDantes merged commit 9833dad into main Jun 5, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant