Commit e23dc68f119 for php

commit e23dc68f1199bf69524fb84e3e1b0e295c0fb863
Author: lacatoire <la.catoire@gmail.com>
Date:   Sat Sep 26 18:22:54 2026 +0100

    ext/pcntl: fix an off by one bound and a parameter the parser made required

    pcntl_setcpuaffinity() refuses a cpu id equal to the cpu count but named
    that count as the inclusive upper bound, printing a zend_long with the
    unsigned format on the way. pcntl_setqos_class() declares $qos_class
    optional while its parser demanded it, leaving the declared default
    unreachable.

    Close GH-23553

diff --git a/NEWS b/NEWS
index 8d34aad3fe6..fb25e3a0b80 100644
--- a/NEWS
+++ b/NEWS
@@ -70,6 +70,10 @@ PHP                                                                        NEWS
     an exception is pending. (nicolas-grekas)
   . Fixed pcntl_signal_dispatch() dropping the signals queued behind a handler
     that throws. (nicolas-grekas)
+  . Fixed pcntl_setcpuaffinity() error message advertising the cpu count as
+    the inclusive upper bound. (lacatoire)
+  . Fixed pcntl_setqos_class() requiring its optional $qos_class argument.
+    (lacatoire)

 - PDO:
   . Fixed PDOStatement::getColumnMeta() reading out of bounds for an invalid
diff --git a/ext/pcntl/pcntl.c b/ext/pcntl/pcntl.c
index b55e1417d11..34a67028263 100644
--- a/ext/pcntl/pcntl.c
+++ b/ext/pcntl/pcntl.c
@@ -1791,7 +1791,7 @@ PHP_FUNCTION(pcntl_setcpuaffinity)
 		}

 		if (cpu < 0 || cpu >= maxcpus) {
-			zend_argument_value_error(2, "cpu id must be between 0 and " ZEND_ULONG_FMT " (" ZEND_LONG_FMT ")", maxcpus, cpu);
+			zend_argument_value_error(2, "cpu id must be between 0 and " ZEND_LONG_FMT " (" ZEND_LONG_FMT ")", maxcpus - 1, cpu);
 			PCNTL_CPU_DESTROY(mask);
 			RETURN_THROWS();
 		}
@@ -1898,13 +1898,14 @@ PHP_FUNCTION(pcntl_getqos_class)

 PHP_FUNCTION(pcntl_setqos_class)
 {
-	zval *qos_obj;
+	zval *qos_obj = NULL;

-	ZEND_PARSE_PARAMETERS_START(1, 1)
+	ZEND_PARSE_PARAMETERS_START(0, 1)
+		Z_PARAM_OPTIONAL
 		Z_PARAM_OBJECT_OF_CLASS(qos_obj, QosClass_ce)
 	ZEND_PARSE_PARAMETERS_END();

-	qos_class_t qos_class = qos_zval_to_lval(qos_obj);
+	qos_class_t qos_class = qos_obj ? qos_zval_to_lval(qos_obj) : QOS_CLASS_DEFAULT;

 	if (UNEXPECTED(pthread_set_qos_class_self_np((qos_class_t)qos_class, 0) != 0))
 	{
diff --git a/ext/pcntl/tests/pcntl_cpuaffinity_bound.phpt b/ext/pcntl/tests/pcntl_cpuaffinity_bound.phpt
new file mode 100644
index 00000000000..6b415a52c24
--- /dev/null
+++ b/ext/pcntl/tests/pcntl_cpuaffinity_bound.phpt
@@ -0,0 +1,52 @@
+--TEST--
+pcntl_setcpuaffinity(): the upper bound the error advertises is itself a valid cpu id
+--EXTENSIONS--
+pcntl
+--SKIPIF--
+<?php
+if (PHP_OS_FAMILY === 'Solaris') {
+    die("skip broken pset_create()");
+}
+if (!function_exists("pcntl_setcpuaffinity")) die("skip pcntl_setcpuaffinity is not available");
+?>
+--FILE--
+<?php
+$pid = getmypid();
+$prefix = 'pcntl_setcpuaffinity(): Argument #2 ($cpu_ids) cpu id must be between 0 and ';
+
+/* read the advertised bound out of the message itself */
+try {
+    pcntl_setcpuaffinity($pid, [PHP_INT_MAX]);
+    exit("PHP_INT_MAX was accepted as a cpu id" . PHP_EOL);
+} catch (Throwable $e) {
+    echo $e::class, "\n";
+    if (!preg_match('/must be between 0 and (\d+) \(/', $e->getMessage(), $m)) {
+        exit("unexpected message: " . $e->getMessage() . PHP_EOL);
+    }
+}
+$bound = (int) $m[1];
+
+/* Every id is range checked before any syscall runs, so pairing the advertised
+   bound with an out of range id shows which of the two the check rejects,
+   without ever changing the process affinity. */
+try {
+    pcntl_setcpuaffinity($pid, [$bound, PHP_INT_MAX]);
+} catch (Throwable $e) {
+    echo $e::class, "\n";
+    var_dump($e->getMessage() === $prefix . $bound . ' (' . PHP_INT_MAX . ')');
+}
+
+/* and the first id past the bound is rejected, naming itself */
+try {
+    pcntl_setcpuaffinity($pid, [$bound + 1]);
+} catch (Throwable $e) {
+    echo $e::class, "\n";
+    var_dump($e->getMessage() === $prefix . $bound . ' (' . ($bound + 1) . ')');
+}
+?>
+--EXPECT--
+ValueError
+ValueError
+bool(true)
+ValueError
+bool(true)
diff --git a/ext/pcntl/tests/pcntl_qosclass.phpt b/ext/pcntl/tests/pcntl_qosclass.phpt
index f8ca1a706bd..947afdb4836 100644
--- a/ext/pcntl/tests/pcntl_qosclass.phpt
+++ b/ext/pcntl/tests/pcntl_qosclass.phpt
@@ -13,7 +13,12 @@
 var_dump(Pcntl\QosClass::Default === pcntl_getqos_class());
 pcntl_setqos_class(Pcntl\QosClass::Background);
 var_dump(Pcntl\QosClass::Background == pcntl_getqos_class());
+
+/* the parameter is optional, and omitting it applies the declared default */
+pcntl_setqos_class();
+var_dump(Pcntl\QosClass::Default === pcntl_getqos_class());
 ?>
 --EXPECT--
 bool(true)
 bool(true)
+bool(true)