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) {