Commit 7e5029bd37a for php.net

commit 7e5029bd37a3d63758a29d71500075e0d23f1a6d
Author: Nicolas Grekas <nicolas.grekas@gmail.com>
Date:   Mon Sep 21 17:16:21 2026 +0200

    Map epoll_ctl() errors through php_poll_errno_to_error() (#23805)

    The epoll backend reported everything but EEXIST and ENOENT as ERR_SYSTEM, so
    a caller could not tell that epoll refuses the handle from a real failure. The
    kqueue backend already maps errno, this makes epoll do the same, with EPERM
    reported as ERR_NOSUPPORT: that is what epoll_ctl() returns for files that do
    not implement polling, such as regular files and /dev/null.

diff --git a/ext/standard/tests/poll/poll_stream_add_unpollable.phpt b/ext/standard/tests/poll/poll_stream_add_unpollable.phpt
new file mode 100644
index 00000000000..f38618e622d
--- /dev/null
+++ b/ext/standard/tests/poll/poll_stream_add_unpollable.phpt
@@ -0,0 +1,36 @@
+--TEST--
+Io\Poll: epoll reports a handle it cannot watch as unsupported
+--SKIPIF--
+<?php
+require_once __DIR__ . '/poll.inc';
+if ('Epoll' !== pt_new_stream_poll()->getBackend()->name) {
+    die("skip requires the epoll backend\n");
+}
+?>
+--FILE--
+<?php
+require_once __DIR__ . '/poll.inc';
+
+$ctx = pt_new_stream_poll();
+
+// epoll_ctl() rejects files that do not implement polling
+foreach (['regular file' => fopen(__FILE__, 'r'), '/dev/null' => fopen('/dev/null', 'r')] 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);
+}
+
+// a pollable handle still works
+[$r, $w] = pt_new_socket_pair();
+$ctx->add(new StreamPollHandle($r), [Io\Poll\Event::Read]);
+echo "socket: added\n";
+?>
+--EXPECT--
+regular file: Failed to add handle, code ERROR_NOSUPPORT
+/dev/null: Failed to add handle, code ERROR_NOSUPPORT
+socket: added
diff --git a/main/poll/poll_backend_epoll.c b/main/poll/poll_backend_epoll.c
index 394b0dc5244..1cc05b3005e 100644
--- a/main/poll/poll_backend_epoll.c
+++ b/main/poll/poll_backend_epoll.c
@@ -125,7 +125,13 @@ static zend_result epoll_backend_add(php_poll_ctx *ctx, int fd, uint32_t events,
 	ev.data.ptr = data;

 	if (epoll_ctl(backend_data->epoll_fd, EPOLL_CTL_ADD, fd, &ev) == -1) {
-		php_poll_set_error(ctx, (errno == EEXIST) ? PHP_POLL_ERR_EXISTS : PHP_POLL_ERR_SYSTEM);
+		/* EPERM means the target file does not implement polling, which is the case
+		 * for regular files and for character devices such as /dev/null. */
+		if (errno == EPERM) {
+			php_poll_set_error(ctx, PHP_POLL_ERR_NOSUPPORT);
+		} else {
+			php_poll_set_current_errno_error(ctx);
+		}
 		return FAILURE;
 	}
 	backend_data->fd_count++;
@@ -142,7 +148,7 @@ static zend_result epoll_backend_modify(php_poll_ctx *ctx, int fd, uint32_t even
 	ev.data.ptr = data;

 	if (epoll_ctl(backend_data->epoll_fd, EPOLL_CTL_MOD, fd, &ev) == -1) {
-		php_poll_set_error(ctx, (errno == ENOENT) ? PHP_POLL_ERR_NOTFOUND : PHP_POLL_ERR_SYSTEM);
+		php_poll_set_current_errno_error(ctx);
 		return FAILURE;
 	}

@@ -154,7 +160,7 @@ static zend_result epoll_backend_remove(php_poll_ctx *ctx, int fd)
 	epoll_backend_data_t *backend_data = (epoll_backend_data_t *) ctx->backend_data;

 	if (epoll_ctl(backend_data->epoll_fd, EPOLL_CTL_DEL, fd, NULL) == -1) {
-		php_poll_set_error(ctx, (errno == ENOENT) ? PHP_POLL_ERR_NOTFOUND : PHP_POLL_ERR_SYSTEM);
+		php_poll_set_current_errno_error(ctx);
 		return FAILURE;
 	}
 	backend_data->fd_count--;