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--;