From 0580b99d7c67682c4db3f2b58c94b9ce83cf54cb Mon Sep 17 00:00:00 2001 From: Edmond <1571649+EdmondDantes@users.noreply.github.com> Date: Thu, 4 Jun 2026 12:56:37 +0300 Subject: [PATCH 1/5] fix(listener): don't request SO_REUSEPORT on Windows (#82) TCP bind failed on Windows with 'Async\AsyncException: Failed to bind to :: operation not supported on socket'. The listener setup derived 'use REUSEPORT' as !http_server_use_shared_listen_fd(). That boolean models two platform camps (kernel-LB REUSEPORT vs shared-fd dup), but Windows is neither: Winsock has no SO_REUSEPORT and libuv's uv_tcp_bind() returns UV_ENOTSUP when UV_TCP_REUSEPORT is set. Since use_shared_listen_fd() is false on Windows (no POSIX dup), !false wrongly requested REUSEPORT, so every TCP bind failed. Add http_server_use_reuseport() as an explicit capability predicate: false on Windows, !use_shared_listen_fd() on POSIX. Single-listener Windows now binds directly; POSIX behaviour is unchanged. Verified locally on Windows: tests/phpt/server/h1 binds and serves (21/22 pass). --- src/http_server_class.c | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/src/http_server_class.c b/src/http_server_class.c index 87b09b2..157c206 100644 --- a/src/http_server_class.c +++ b/src/http_server_class.c @@ -100,6 +100,26 @@ static bool http_server_use_shared_listen_fd(void) #endif } +/* Whether to ask the reactor for SO_REUSEPORT on each listener. This is a + * kernel capability, NOT the logical inverse of the shared-fd strategy — + * conflating the two is what broke Windows (issue #82). Three platform camps: + * - Linux/FreeBSD: load-balanced REUSEPORT, each worker binds itself. + * - macOS/other BSD: no LB REUSEPORT, so the shared-fd dup model instead. + * - Windows: NEITHER. Winsock has no SO_REUSEPORT; libuv's uv_tcp_bind() + * returns UV_ENOTSUP ("operation not supported on socket") if + * UV_TCP_REUSEPORT is set, so it must never be requested. A single + * listener (workers=1, the default) then just binds directly. + * On POSIX the answer is still !use_shared_listen_fd(); Windows is the third + * case a lone boolean cannot express, hence its own carve-out here. */ +static bool http_server_use_reuseport(void) +{ +#ifdef PHP_WIN32 + return false; +#else + return !http_server_use_shared_listen_fd(); +#endif +} + /* Max listeners */ #define MAX_LISTENERS 16 @@ -2548,11 +2568,14 @@ ZEND_METHOD(TrueAsync_HttpServer, start) unsigned int listen_flags = 0; zend_async_listen_event_t *listen_event = NULL; - /* Two strategies for a worker pool sharing host:port. REUSEPORT: + /* Strategies for a worker pool sharing host:port. REUSEPORT: * each worker binds independently, the kernel load-balances * (Linux/FreeBSD). Shared fd: the parent bound once and each - * worker adopts a dup (macOS/Windows, no LB REUSEPORT). */ - if (!http_server_use_shared_listen_fd()) { + * worker adopts a dup (macOS/other BSD, no LB REUSEPORT). + * Windows has neither — uv_tcp_bind() rejects UV_TCP_REUSEPORT + * with ENOTSUP and there is no POSIX dup to share — so it takes + * the plain-bind fall-through below (single listener only). */ + if (http_server_use_reuseport()) { listen_flags |= ZEND_ASYNC_LISTEN_F_REUSEPORT; } #ifndef PHP_WIN32 From f39e97b6b9c2fbb28c5588a1a63aae3dfb6c39e5 Mon Sep 17 00:00:00 2001 From: Edmond <1571649+EdmondDantes@users.noreply.github.com> Date: Thu, 4 Jun 2026 13:05:42 +0300 Subject: [PATCH 2/5] fix(static): accept Windows absolute paths in StaticHandler root Root validation tested only for a leading '/', so every Windows path (C:\...) was rejected and StaticHandler was unusable on Windows. Use IS_ABSOLUTE_PATH (leading '/' on POSIX; drive-letter / UNC on Windows); POSIX behaviour unchanged. Found while running the server phpt suite on Windows for #82; unblocks ~8 static tests + core/033. --- src/static/static_handler_class.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/static/static_handler_class.c b/src/static/static_handler_class.c index 93b9824..7cd810a 100644 --- a/src/static/static_handler_class.c +++ b/src/static/static_handler_class.c @@ -117,7 +117,10 @@ static zend_string *canonicalise_root_directory(const zend_string *path) return NULL; } - if (ZSTR_VAL(path)[0] != '/') { + /* Cross-platform absolute-path check: leading '/' on POSIX, drive-letter + * (C:\) or UNC (\\) on Windows. The old `[0] != '/'` test rejected every + * valid Windows path, making StaticHandler unusable there. */ + if (!IS_ABSOLUTE_PATH(ZSTR_VAL(path), ZSTR_LEN(path))) { zend_throw_exception(http_server_invalid_argument_exception_ce, "StaticHandler root directory must be an absolute path", 0); return NULL; From bbccb1b06cb6b4790ae279081516d7976f7fad14 Mon Sep 17 00:00:00 2001 From: Edmond <1571649+EdmondDantes@users.noreply.github.com> Date: Thu, 4 Jun 2026 13:58:08 +0300 Subject: [PATCH 3/5] test(static): skip POSIX-only symlink-policy tests on Windows (documented) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 009 (OwnerMatch, uid-based) and 021 (REJECT via open(O_NOFOLLOW)) exercise symlink enforcement that is POSIX-specific: O_NOFOLLOW does not exist on Windows and symlink() needs privilege there. SKIPIF records WHY and flags the Windows reparse-point enforcement as a tracked gap — explicitly not a silent mute. A real Windows reparse-point reject is a separate follow-up. --- .../phpt/server/static/009-static-symlink-owner.phpt | 9 +++++++++ .../static/021-static-symlink-reject-nofollow.phpt | 11 +++++++++++ 2 files changed, 20 insertions(+) diff --git a/tests/phpt/server/static/009-static-symlink-owner.phpt b/tests/phpt/server/static/009-static-symlink-owner.phpt index 187b679..d27cbca 100644 --- a/tests/phpt/server/static/009-static-symlink-owner.phpt +++ b/tests/phpt/server/static/009-static-symlink-owner.phpt @@ -3,6 +3,15 @@ StaticHandler: SymlinkPolicy::OwnerMatch follows owner-equal links (issue #13 § --EXTENSIONS-- true_async_server true_async +--SKIPIF-- + --FILE-- --FILE-- Date: Thu, 4 Jun 2026 14:22:31 +0300 Subject: [PATCH 4/5] fix(static): open send_file engine bodies with O_BINARY on Windows The async send_file engine opened files with O_RDONLY|O_CLOEXEC only. On Windows that is text mode: CRLF translation and a 0x1A byte treated as EOF corrupt/truncate binary bodies (precompressed .br/.gz, byte ranges, images). Add O_BINARY, mirroring open_for_policy in http_static_safety.c. No-op on POSIX (O_BINARY is 0/undefined). --- src/send_file.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/send_file.c b/src/send_file.c index dac6728..4d371f3 100644 --- a/src/send_file.c +++ b/src/send_file.c @@ -714,6 +714,12 @@ send_file_result_t send_file(struct http_request_t *request, zend_object *respon /* === Synchronous open(2) ========================================= */ int open_flags = O_RDONLY | O_CLOEXEC; +#ifdef O_BINARY + /* Windows opens in text mode by default: CRLF translation and a 0x1A + * byte treated as EOF would corrupt/truncate binary bodies (precompressed + * .br/.gz, ranges, images). Mirrors open_for_policy in http_static_safety.c. */ + open_flags |= O_BINARY; +#endif #ifdef O_NOFOLLOW /* REJECT mount: reject a final-component symlink atomically (ELOOP). */ if (state->cfg.reject_symlinks) { From b8538c24c16410a80cc35a81d1f048a6260be91a Mon Sep 17 00:00:00 2001 From: Edmond <1571649+EdmondDantes@users.noreply.github.com> Date: Thu, 4 Jun 2026 14:22:31 +0300 Subject: [PATCH 5/5] docs(changelog): record Windows portability fixes REUSEPORT bind (#82), StaticHandler absolute-path acceptance, and the send_file O_BINARY fix. --- CHANGELOG.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b631d78..f8d001c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- **Windows: TCP listeners now bind.** The server failed to start on Windows + with `Async\AsyncException: Failed to bind to :: operation not + supported on socket`. The listener requested `SO_REUSEPORT`, which libuv's + `uv_tcp_bind()` rejects with `UV_ENOTSUP` on Windows (Winsock has no + `SO_REUSEPORT`). REUSEPORT is now treated as a platform capability and never + requested on Windows; the default single-listener server binds directly. No + change on Linux/BSD/macOS (#82). +- **Windows: `StaticHandler` accepts native absolute paths.** Root-directory + validation only accepted a leading `/`, rejecting every Windows path + (`C:\...`) and making `StaticHandler` unusable there. It now uses + `IS_ABSOLUTE_PATH` (drive-letter / UNC on Windows, leading `/` on POSIX). +- **Windows: static file bodies are served binary-clean.** The `send_file` + engine opened files without `O_BINARY`, so Windows text-mode translation + could corrupt or truncate binary bodies (precompressed `.br`/`.gz`, byte + ranges, images). It now opens with `O_BINARY`, matching the policy open path. + ## [0.7.2] - 2026-06-02 ### Added