Skip to content

fix(win): sendfile to a socket via TransmitFile, not uv_fs_sendfile#158

Merged
EdmondDantes merged 1 commit into
mainfrom
windows-sendfile-transmitfile
Jun 6, 2026
Merged

fix(win): sendfile to a socket via TransmitFile, not uv_fs_sendfile#158
EdmondDantes merged 1 commit into
mainfrom
windows-sendfile-transmitfile

Conversation

@EdmondDantes
Copy link
Copy Markdown
Contributor

Problem

ZEND_ASYNC_IO_SENDFILE is documented as "Windows TransmitFile" (CHANGELOG, API entry), but the implementation routed every platform through uv_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 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.

The root is a descriptor confusion: async_io_t.crt_fd is named/used as a CRT fd, but for socket IOs it holds the raw Winsock SOCKET (libuv_reactor.c socket-create + accept paths set crt_fd = socket). libuv_io_sendfile type-checks only the source (must be FILE) and then blindly feeds out_io->crt_fd as the destination.

Fix

Resolve it at the reactor layer (not via a server-side read+write kludge): libuv_io_sendfile dispatches socket destinations to the real TransmitFile primitive — the Win32 file→socket "sendfile":

  • destination socket from descriptor.socket (the correct field, not crt_fd)
  • source as a Win32 HANDLE via _get_osfhandle(in_io->crt_fd)
  • 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
  • TransmitFile resolved lazily via WSAIoctl(SIO_GET_EXTENSION_FUNCTION_POINTER) — no mswsock.lib link added

The POSIX path is untouched — all new code is under #ifdef PHP_WIN32.

Verification (Windows, Debug_TS)

  • true-async/server static/011-static-precompressed-cache-uaf (96 KiB precompressed sidecar over plain TCP): red → green
  • full static/ 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
  • POSIX unchanged by construction (compiled out)

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
Copy link
Copy Markdown

codecov Bot commented Jun 6, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@EdmondDantes EdmondDantes merged commit bb9de86 into main Jun 6, 2026
14 of 15 checks passed
@EdmondDantes EdmondDantes deleted the windows-sendfile-transmitfile branch June 6, 2026 10:50
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