Commit b3cbd55a69f for php.net

commit b3cbd55a69f957dc830b01141dafcef86205d493
Author: Marc Bennewitz <marc@mabe.berlin>
Date:   Wed Aug 26 09:01:50 2026 +0200

    ext/standard: Io\Poll\Context::wait() max events range check

    Reject a $maxEvents value greater than INT_MAX instead of truncating it,
    which prevents an integer overflow where SIZEOF_INT < SIZEOF_ZEND_LONG.

    Close GH-23468

diff --git a/NEWS b/NEWS
index 871c1c9adde..df78bf6abe8 100644
--- a/NEWS
+++ b/NEWS
@@ -30,6 +30,8 @@ PHP                                                                        NEWS
   . Fixed an out-of-bounds read when following a redirect response with an
     empty Location header. (iliaal)
   . Fixed read buffer compaction in php_stream_filter_flush(). (crystarm)
+  . Io\Poll\Context::wait() now rejects a $maxEvents value greater than
+    INT_MAX instead of truncating it. (marc-mabe)


 27 Aug 2026, PHP 8.6.0beta2
diff --git a/ext/standard/io_poll.c b/ext/standard/io_poll.c
index f57813874d5..a8a0563627f 100644
--- a/ext/standard/io_poll.c
+++ b/ext/standard/io_poll.c
@@ -804,12 +804,15 @@ PHP_METHOD(Io_Poll_Context, wait)
 		if (max_events <= 0) {
 			max_events = 64;
 		}
-	} else if (max_events <= 0) {
+	} else if (UNEXPECTED(max_events <= 0)) {
 		zend_argument_value_error(2, "must be greater than 0");
 		RETURN_THROWS();
+	} else if (ZEND_LONG_INT_OVFL(max_events)) {
+		zend_argument_value_error(2, "must be less than or equal to %d", INT_MAX);
+		RETURN_THROWS();
 	}

-	php_poll_event *events = safe_emalloc(max_events, sizeof(*events), 0);
+	php_poll_event *events = safe_emalloc((size_t) max_events, sizeof(*events), 0);
 	int num_events = php_poll_wait(intern->ctx, events, (int) max_events, timeout ? &timeout_ts : NULL);

 	if (num_events < 0) {
diff --git a/ext/standard/tests/poll/poll_ctx_wait.phpt b/ext/standard/tests/poll/poll_ctx_wait_error.phpt
similarity index 58%
rename from ext/standard/tests/poll/poll_ctx_wait.phpt
rename to ext/standard/tests/poll/poll_ctx_wait_error.phpt
index 5080c1421fd..c514884c3c6 100644
--- a/ext/standard/tests/poll/poll_ctx_wait.phpt
+++ b/ext/standard/tests/poll/poll_ctx_wait_error.phpt
@@ -12,13 +12,27 @@
     echo $e::class, ': ', $e->getMessage(), PHP_EOL;
 }

+try {
+    $poll_ctx->wait(maxEvents: PHP_INT_MIN);
+} catch (Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+}
+
 try {
     $poll_ctx->wait(maxEvents: -1);
 } catch (Throwable $e) {
     echo $e::class, ': ', $e->getMessage(), PHP_EOL;
 }

+try {
+    $poll_ctx->wait(maxEvents: 0);
+} catch (Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+}
+
 ?>
 --EXPECT--
 ValueError: Io\Poll\Context::wait(): Argument #1 ($timeout) must not be negative
 ValueError: Io\Poll\Context::wait(): Argument #2 ($maxEvents) must be greater than 0
+ValueError: Io\Poll\Context::wait(): Argument #2 ($maxEvents) must be greater than 0
+ValueError: Io\Poll\Context::wait(): Argument #2 ($maxEvents) must be greater than 0
diff --git a/ext/standard/tests/poll/poll_ctx_wait_error_int64.phpt b/ext/standard/tests/poll/poll_ctx_wait_error_int64.phpt
new file mode 100644
index 00000000000..3d881050e65
--- /dev/null
+++ b/ext/standard/tests/poll/poll_ctx_wait_error_int64.phpt
@@ -0,0 +1,23 @@
+--TEST--
+Io\Poll\Context::wait(): Parameter validation upper limit
+--SKIPIF--
+<?php
+if (PHP_INT_SIZE <= 4) {
+    die("skip this test is for > 32bit platforms only");
+}
+?>
+--FILE--
+<?php
+require_once __DIR__ . '/poll.inc';
+
+$poll_ctx = new Io\Poll\Context();
+
+try {
+    $poll_ctx->wait(maxEvents: PHP_INT_MAX);
+} catch (Throwable $e) {
+    echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+}
+
+?>
+--EXPECT--
+ValueError: Io\Poll\Context::wait(): Argument #2 ($maxEvents) must be less than or equal to 2147483647