Commit 39f98f768a2 for php.net
commit 39f98f768a2c9dda25e52f8d9e5240551e14beb7
Author: Nicolas Grekas <nicolas.grekas@gmail.com>
Date: Sun Sep 20 17:00:32 2026 +0200
ext/standard: Fix Io\Poll timeouts longer than INT_MAX milliseconds
php_poll_timespec_to_ms() cast seconds * 1000 to an int, so a wait() of more than
about 24 days wrapped around: five years came out as 48ms on the poll backend.
The value is capped now. epoll is only affected where it falls back to
epoll_wait(), since epoll_pwait2() takes the timespec as is.
diff --git a/ext/standard/tests/poll/poll_wait_timeout_overflow.phpt b/ext/standard/tests/poll/poll_wait_timeout_overflow.phpt
new file mode 100644
index 00000000000..5009d3edd8c
--- /dev/null
+++ b/ext/standard/tests/poll/poll_wait_timeout_overflow.phpt
@@ -0,0 +1,29 @@
+--TEST--
+Io\Poll: a timeout that overflows an int of milliseconds keeps waiting
+--SKIPIF--
+<?php
+if (!Io\Poll\Backend::Poll->isAvailable()) {
+ die("skip poll backend not available\n");
+}
+if (PHP_OS_FAMILY === 'Windows') {
+ die("skip POSIX only\n");
+}
+?>
+--FILE--
+<?php
+$process = proc_open([PHP_BINARY, '-r', 'usleep(300000); echo "ready";'], [1 => ['pipe', 'w']], $pipes);
+
+$poll_ctx = new Io\Poll\Context(Io\Poll\Backend::Poll);
+$watcher = $poll_ctx->add(new StreamPollHandle($pipes[1]), [Io\Poll\Event::Read]);
+
+// 158913790 seconds is about 5 years, and wraps around to 48ms as an int of milliseconds
+$events = $poll_ctx->wait(Time\Duration::fromSeconds(158913790));
+
+var_dump(count($events), $events[0] === $watcher);
+
+fclose($pipes[1]);
+proc_close($process);
+?>
+--EXPECT--
+int(1)
+bool(true)
diff --git a/main/poll/php_poll_internal.h b/main/poll/php_poll_internal.h
index 95152131439..21b85575409 100644
--- a/main/poll/php_poll_internal.h
+++ b/main/poll/php_poll_internal.h
@@ -164,6 +164,12 @@ static inline int php_poll_timespec_to_ms(const struct timespec *timeout)
return -1;
}
+ /* Cap rather than wrap around: a timeout that long is as good as indefinite,
+ * where truncating it to an int made poll() return after a few milliseconds */
+ if (timeout->tv_sec >= (INT_MAX - 1000) / 1000) {
+ return INT_MAX;
+ }
+
int ms = (int) (timeout->tv_sec * 1000);
/* Round nanoseconds up to the next millisecond to avoid premature return */
if (timeout->tv_nsec > 0) {