fix(win): sendfile to a socket via TransmitFile, not uv_fs_sendfile#158
Merged
Conversation
ZEND_ASYNC_IO_SENDFILE is documented as "Windows TransmitFile" but the implementation routed every platform through uv_fs_sendfile, whose Windows backend writes the destination with CRT _write(). A TCP destination is a Winsock SOCKET, not a CRT file descriptor — three distinct Windows descriptor namespaces (HANDLE / SOCKET / CRT fd) that POSIX collapses into one int. So uv_fs_sendfile(out->crt_fd, ...) works on Linux and writes nowhere on Windows: response headers (uv_write) arrived, the file body vanished. Any static file above the 64 KiB slurp threshold served an empty/garbage body over plaintext HTTP/1 and HTTP/2. Resolve the descriptor confusion at the reactor layer: libuv_io_sendfile dispatches socket destinations to the real TransmitFile primitive — the destination socket from descriptor.socket, the source as a Win32 HANDLE via _get_osfhandle(crt_fd), never the conflated crt_fd-as-socket value. It runs synchronously on a libuv threadpool worker (uv_queue_work), exactly as uv_fs_sendfile runs its own copy loop off-loop, so completion still arrives on the loop thread through the unchanged sendfile_complete() notify path. The POSIX path is untouched (new code entirely under #ifdef PHP_WIN32). Verified on Windows: true-async/server static/011-static-precompressed- cache-uaf (96 KiB precompressed sidecar over plain TCP) goes red->green; full static suite 19 pass / 1 fail (unrelated h2 test) / 4 skip, no regressions in the large-file TLS / h2 paths.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
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.
Problem
ZEND_ASYNC_IO_SENDFILEis documented as "WindowsTransmitFile" (CHANGELOG, API entry), but the implementation routed every platform throughuv_fs_sendfile, whose Windows backend writes the destination via CRT_write().A TCP destination is a Winsock
SOCKET, not a CRT file descriptor — three distinct Windows descriptor namespaces (HANDLE/SOCKET/ CRT fd) that POSIX collapses into oneint. Souv_fs_sendfile(out->crt_fd, …)works on Linux and writes nowhere on Windows: response headers (uv_write) arrived, the file body vanished. Any static file above the 64 KiB slurp threshold served an empty/garbage body over plaintext HTTP/1 and HTTP/2.The root is a descriptor confusion:
async_io_t.crt_fdis named/used as a CRT fd, but for socket IOs it holds the raw WinsockSOCKET(libuv_reactor.csocket-create + accept paths setcrt_fd = socket).libuv_io_sendfiletype-checks only the source (must be FILE) and then blindly feedsout_io->crt_fdas the destination.Fix
Resolve it at the reactor layer (not via a server-side read+write kludge):
libuv_io_sendfiledispatches socket destinations to the realTransmitFileprimitive — the Win32 file→socket "sendfile":descriptor.socket(the correct field, notcrt_fd)HANDLEvia_get_osfhandle(in_io->crt_fd)uv_queue_work), exactly asuv_fs_sendfileruns its own copy loop off-loop, so completion still arrives on the loop thread through the unchangedsendfile_complete()notify pathTransmitFileresolved lazily viaWSAIoctl(SIO_GET_EXTENSION_FUNCTION_POINTER)— nomswsock.liblink addedThe POSIX path is untouched — all new code is under
#ifdef PHP_WIN32.Verification (Windows, Debug_TS)
true-async/serverstatic/011-static-precompressed-cache-uaf(96 KiB precompressed sidecar over plain TCP): red → greenstatic/suite: 19 pass / 1 fail / 4 skip — the one fail (012-static-h2) is an unrelated h2 issue, the large-file TLS (007,013) and h2 paths are unaffected