Commit f2fbe476d3b for php

commit f2fbe476d3bffd794864262403abc482cccf9de6
Author: Nicolas Grekas <nicolas.grekas@gmail.com>
Date:   Sun Nov 1 20:07:39 2026 +0100

    Fix the WSAPoll backend of Io\Poll accepting handles that are not sockets

    WSAPoll() can only poll sockets, but the backend accepted any fd. A file
    or a pipe such as STDOUT was then dropped by wait() without being
    reported. Adding such a handle now fails with ERR_NOSUPPORT, as epoll
    does for regular files.

    Closes GH-25135.

diff --git a/NEWS b/NEWS
index 7510b0e3005..ce22cecb3c8 100644
--- a/NEWS
+++ b/NEWS
@@ -200,6 +200,9 @@ PHP                                                                        NEWS
 - Standard:
   . Fixed bug GH-25086 (Io\Poll kqueue backend leaks memory on every
     Watcher::modifyEvents() call). (Lazizbek Ergashev, Jakub Zelenka)
+  . Fixed Io\Poll WSAPoll backend accepting handles that are not sockets, which
+    were then silently dropped by wait(). Adding them now fails with
+    ERROR_NOSUPPORT. (Nicolas Grekas)
   . Fixed memory leaks in checkdnsrr() and dns_check_record() on Windows.
     (Ilia Alshanetsky)
   . Fixed __unserialize() and __wakeup() being called for objects with a missing
diff --git a/ext/standard/tests/poll/poll_stream_add_unpollable_wsapoll.phpt b/ext/standard/tests/poll/poll_stream_add_unpollable_wsapoll.phpt
new file mode 100644
index 00000000000..ac4d2f89ac2
--- /dev/null
+++ b/ext/standard/tests/poll/poll_stream_add_unpollable_wsapoll.phpt
@@ -0,0 +1,45 @@
+--TEST--
+Io\Poll: WSAPoll reports a handle that is not a socket as unsupported
+--SKIPIF--
+<?php
+if (!Io\Poll\Backend::WSAPoll->isAvailable()) {
+    die("skip WSAPoll backend required\n");
+}
+if (!function_exists('proc_open')) {
+    die("skip proc_open required\n");
+}
+?>
+--FILE--
+<?php
+require_once __DIR__ . '/poll.inc';
+
+$ctx = new Io\Poll\Context(Io\Poll\Backend::WSAPoll);
+
+// WSAPoll() only polls sockets, while files and pipes give a CRT fd
+$proc = proc_open([PHP_BINARY, '-n', '-r', 'echo "done";'], [1 => ['pipe', 'w']], $pipes);
+foreach (['regular file' => fopen(__FILE__, 'r'), 'pipe' => $pipes[1]] as $label => $stream) {
+    try {
+        $ctx->add(new StreamPollHandle($stream), [Io\Poll\Event::Read]);
+        echo "$label: added\n";
+    } catch (Io\Poll\FailedHandleAddException $e) {
+        printf("%s: %s, code %s\n", $label, $e->getMessage(),
+            Io\Poll\FailedPollOperationException::ERROR_NOSUPPORT === $e->getCode() ? 'ERROR_NOSUPPORT' : $e->getCode());
+    }
+    fclose($stream);
+}
+proc_close($proc);
+
+// a socket is still added and reported
+[$r, $w] = pt_new_socket_pair();
+$watcher = $ctx->add(new StreamPollHandle($r), [Io\Poll\Event::Read]);
+echo "socket: added\n";
+fwrite($w, "ping");
+$events = $ctx->wait(Time\Duration::fromSeconds(5));
+var_dump(count($events), $events[0] === $watcher);
+?>
+--EXPECT--
+regular file: Failed to add handle, code ERROR_NOSUPPORT
+pipe: Failed to add handle, code ERROR_NOSUPPORT
+socket: added
+int(1)
+bool(true)
diff --git a/main/poll/poll_backend_wsapoll.c b/main/poll/poll_backend_wsapoll.c
index 101a7cfe0e4..c89a02bfdd2 100644
--- a/main/poll/poll_backend_wsapoll.c
+++ b/main/poll/poll_backend_wsapoll.c
@@ -120,6 +120,15 @@ static zend_result wsapoll_backend_add(php_poll_ctx *ctx, int fd, uint32_t event
 		return FAILURE;
 	}

+	/* WSAPoll() can only poll sockets, so reject files and pipes. */
+	int type;
+	int type_len = sizeof(type);
+	if (getsockopt((SOCKET) fd, SOL_SOCKET, SO_TYPE, (char *) &type, &type_len) != 0
+			&& WSAGetLastError() == WSAENOTSOCK) {
+		php_poll_set_error(ctx, PHP_POLL_ERR_NOSUPPORT);
+		return FAILURE;
+	}
+
 	php_poll_fd_entry *entry = php_poll_fd_table_get_new(backend_data->fd_table, fd);
 	if (!entry) {
 		php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM);