Commit 0b8612d06df for php

commit 0b8612d06df9faafd060666889ff4cc7ce15e7ec
Author: Jorg Sowa <jorg.sowa@gmail.com>
Date:   Sun Nov 8 16:54:02 2026 +0100

    Optimizer: null array keys may throw (GH-24654)

    Null keys emit a deprecation since PHP 8.5, which an error handler can
    turn into an exception. Add MAY_BE_NULL to MAY_BE_THROWING_KEY.

    Co-authored-by: Nora Dossche <7771979+ndossche@users.noreply.github.com>

    Fixes GH-25269
    Closes GH-24654

diff --git a/NEWS b/NEWS
index e1b2dad5fcd..2ce93fa3739 100644
--- a/NEWS
+++ b/NEWS
@@ -38,6 +38,8 @@ PHP                                                                        NEWS
     (Ilia Alshanetsky)
   . Fixed bug GH-25195 (smm_shared_globals left dangling without an
     SHM backend). (Lazizbek Ergashev)
+  . Fixed bug GH-25269 (JIT: misaligned zval passed to
+    ZEND_ADD_ARRAY_ELEMENT handler). (Jorg Sowa, ndossche)

 - SOAP:
   . Fixed bug GH-25267 (NULL dereference in to_xml_any() with integer key).
diff --git a/Zend/Optimizer/zend_inference.c b/Zend/Optimizer/zend_inference.c
index 9aa6b2a46a7..2f14ef8e288 100644
--- a/Zend/Optimizer/zend_inference.c
+++ b/Zend/Optimizer/zend_inference.c
@@ -4935,7 +4935,7 @@ ZEND_API zend_result zend_ssa_inference(zend_arena **arena, const zend_op_array
 /* }}} */

 /* Key types whose conversion to an array offset may emit a diagnostic */
-#define MAY_BE_THROWING_KEY (MAY_BE_DOUBLE|MAY_BE_ARRAY|MAY_BE_OBJECT|MAY_BE_RESOURCE)
+#define MAY_BE_THROWING_KEY (MAY_BE_NULL|MAY_BE_DOUBLE|MAY_BE_ARRAY|MAY_BE_OBJECT|MAY_BE_RESOURCE)

 ZEND_API bool zend_may_throw_ex(const zend_op *opline, const zend_ssa_op *ssa_op, const zend_op_array *op_array, const zend_ssa *ssa, uint32_t t1, uint32_t t2)
 {
@@ -5273,7 +5273,7 @@ ZEND_API bool zend_may_throw_ex(const zend_op *opline, const zend_ssa_op *ssa_op
 		case ZEND_INIT_ARRAY:
 			return (opline->op2_type != IS_UNUSED) && (t2 & MAY_BE_THROWING_KEY);
 		case ZEND_ADD_ARRAY_ELEMENT:
-			return (opline->op2_type == IS_UNUSED) || (t2 & (MAY_BE_NULL|MAY_BE_THROWING_KEY));
+			return (opline->op2_type == IS_UNUSED) || (t2 & MAY_BE_THROWING_KEY);
 		case ZEND_STRLEN:
 			return (t1 & MAY_BE_ANY) != MAY_BE_STRING;
 		case ZEND_COUNT:
diff --git a/ext/opcache/tests/jit/add_array_element_null_key.phpt b/ext/opcache/tests/jit/add_array_element_null_key.phpt
new file mode 100644
index 00000000000..3b535d93fc6
--- /dev/null
+++ b/ext/opcache/tests/jit/add_array_element_null_key.phpt
@@ -0,0 +1,27 @@
+--TEST--
+JIT ADD_ARRAY_ELEMENT: null key conversion may throw
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.jit=function
+opcache.file_update_protection=0
+--EXTENSIONS--
+opcache
+--FILE--
+<?php
+set_error_handler(function ($errno, $errstr) {
+    throw new Exception($errstr);
+});
+
+function test(null $k) {
+    var_dump([0, $k => 1]);
+}
+
+try {
+    test(null);
+} catch (Exception $e) {
+    echo $e::class, ": ", $e->getMessage(), "\n";
+}
+?>
+--EXPECT--
+Exception: Using null as an array offset is deprecated, use an empty string instead
diff --git a/ext/opcache/tests/jit/array_key_exists_null_key.phpt b/ext/opcache/tests/jit/array_key_exists_null_key.phpt
new file mode 100644
index 00000000000..4f0b2f25e6a
--- /dev/null
+++ b/ext/opcache/tests/jit/array_key_exists_null_key.phpt
@@ -0,0 +1,27 @@
+--TEST--
+JIT ARRAY_KEY_EXISTS: null key conversion may throw
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.jit=function
+opcache.file_update_protection=0
+--EXTENSIONS--
+opcache
+--FILE--
+<?php
+set_error_handler(function ($errno, $errstr) {
+    throw new Exception($errstr);
+});
+
+function test(null $k) {
+    var_dump(array_key_exists($k, []));
+}
+
+try {
+    test(null);
+} catch (Exception $e) {
+    echo $e::class, ": ", $e->getMessage(), "\n";
+}
+?>
+--EXPECT--
+Exception: Using null as the key parameter for array_key_exists() is deprecated, use an empty string instead
diff --git a/ext/opcache/tests/jit/assign_dim_null_key.phpt b/ext/opcache/tests/jit/assign_dim_null_key.phpt
new file mode 100644
index 00000000000..bb255df6a6c
--- /dev/null
+++ b/ext/opcache/tests/jit/assign_dim_null_key.phpt
@@ -0,0 +1,28 @@
+--TEST--
+JIT ASSIGN_DIM: null key conversion may throw
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.jit=function
+opcache.file_update_protection=0
+--EXTENSIONS--
+opcache
+--FILE--
+<?php
+set_error_handler(function ($errno, $errstr) {
+    throw new Exception($errstr);
+});
+
+function test(null $k) {
+    $a = [];
+    var_dump($a[$k] = 1);
+}
+
+try {
+    test(null);
+} catch (Exception $e) {
+    echo $e::class, ": ", $e->getMessage(), "\n";
+}
+?>
+--EXPECT--
+Exception: Using null as an array offset is deprecated, use an empty string instead
diff --git a/ext/opcache/tests/jit/fetch_dim_is_null_key.phpt b/ext/opcache/tests/jit/fetch_dim_is_null_key.phpt
new file mode 100644
index 00000000000..2a09da67e17
--- /dev/null
+++ b/ext/opcache/tests/jit/fetch_dim_is_null_key.phpt
@@ -0,0 +1,28 @@
+--TEST--
+JIT FETCH_DIM_IS: null key conversion may throw
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.jit=function
+opcache.file_update_protection=0
+--EXTENSIONS--
+opcache
+--FILE--
+<?php
+set_error_handler(function ($errno, $errstr) {
+    throw new Exception($errstr);
+});
+
+function test(null $k) {
+    $a = [];
+    var_dump($a[$k] ?? 0);
+}
+
+try {
+    test(null);
+} catch (Exception $e) {
+    echo $e::class, ": ", $e->getMessage(), "\n";
+}
+?>
+--EXPECT--
+Exception: Using null as an array offset is deprecated, use an empty string instead
diff --git a/ext/opcache/tests/jit/fetch_dim_w_null_key.phpt b/ext/opcache/tests/jit/fetch_dim_w_null_key.phpt
new file mode 100644
index 00000000000..3ec27c5893a
--- /dev/null
+++ b/ext/opcache/tests/jit/fetch_dim_w_null_key.phpt
@@ -0,0 +1,37 @@
+--TEST--
+JIT FETCH_DIM_W: null key conversion may throw
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.jit=function
+opcache.file_update_protection=0
+--EXTENSIONS--
+opcache
+--FILE--
+<?php
+set_error_handler(function ($errno, $errstr) {
+    throw new Exception($errstr);
+});
+
+class C {
+    function __destruct() {
+        echo "destruct\n";
+    }
+}
+
+function g($x, &$y) {}
+
+function test(null $k) {
+    $c = new C;
+    g($c, $a[$k]);
+}
+
+try {
+    test(null);
+} catch (Exception $e) {
+    echo $e::class, ": ", $e->getMessage(), "\n";
+}
+?>
+--EXPECT--
+destruct
+Exception: Using null as an array offset is deprecated, use an empty string instead
diff --git a/ext/opcache/tests/jit/gh25269.phpt b/ext/opcache/tests/jit/gh25269.phpt
new file mode 100644
index 00000000000..9a260491945
--- /dev/null
+++ b/ext/opcache/tests/jit/gh25269.phpt
@@ -0,0 +1,27 @@
+--TEST--
+GH-25269 (JIT: misaligned zval passed to ZEND_ADD_ARRAY_ELEMENT handler)
+--CREDITS--
+YuanchengJiang
+--EXTENSIONS--
+opcache
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.jit_buffer_size=64M
+opcache.jit=function
+--FILE--
+<?php
+set_error_handler(function($errno, $errstr) {
+    throw new ErrorException($errstr);
+});
+function test() {
+    return [null => 1, "a" => 2, "b" => 3];
+}
+try {
+    test();
+} catch (ErrorException $e) {
+    echo $e->getMessage(), "\n";
+}
+?>
+--EXPECT--
+Using null as an array offset is deprecated, use an empty string instead
diff --git a/ext/opcache/tests/jit/isset_isempty_dim_obj_null_key.phpt b/ext/opcache/tests/jit/isset_isempty_dim_obj_null_key.phpt
new file mode 100644
index 00000000000..2b84e5a4ad6
--- /dev/null
+++ b/ext/opcache/tests/jit/isset_isempty_dim_obj_null_key.phpt
@@ -0,0 +1,28 @@
+--TEST--
+JIT ISSET_ISEMPTY_DIM_OBJ: null key conversion may throw
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.jit=function
+opcache.file_update_protection=0
+--EXTENSIONS--
+opcache
+--FILE--
+<?php
+set_error_handler(function ($errno, $errstr) {
+    throw new Exception($errstr);
+});
+
+function test(null $k) {
+    $a = [];
+    var_dump(isset($a[$k]));
+}
+
+try {
+    test(null);
+} catch (Exception $e) {
+    echo $e::class, ": ", $e->getMessage(), "\n";
+}
+?>
+--EXPECT--
+Exception: Using null as an array offset is deprecated, use an empty string instead
diff --git a/ext/opcache/tests/opt/dce_023.phpt b/ext/opcache/tests/opt/dce_023.phpt
new file mode 100644
index 00000000000..ffb4f3ba846
--- /dev/null
+++ b/ext/opcache/tests/opt/dce_023.phpt
@@ -0,0 +1,27 @@
+--TEST--
+DCE must not remove ASSIGN_DIM with a null key
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.optimization_level=-1
+opcache.file_update_protection=0
+--EXTENSIONS--
+opcache
+--FILE--
+<?php
+set_error_handler(function ($errno, $errstr) {
+    throw new Exception($errstr);
+});
+
+function test(null $k) {
+    $a[$k] = 1;
+}
+
+try {
+    test(null);
+} catch (Exception $e) {
+    echo $e::class, ": ", $e->getMessage(), "\n";
+}
+?>
+--EXPECT--
+Exception: Using null as an array offset is deprecated, use an empty string instead
diff --git a/ext/opcache/tests/opt/dce_024.phpt b/ext/opcache/tests/opt/dce_024.phpt
new file mode 100644
index 00000000000..5db1400c995
--- /dev/null
+++ b/ext/opcache/tests/opt/dce_024.phpt
@@ -0,0 +1,27 @@
+--TEST--
+DCE must not remove INIT_ARRAY with a null key
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.optimization_level=-1
+opcache.file_update_protection=0
+--EXTENSIONS--
+opcache
+--FILE--
+<?php
+set_error_handler(function ($errno, $errstr) {
+    throw new Exception($errstr);
+});
+
+function test(null $k) {
+    $a = [$k => 1];
+}
+
+try {
+    test(null);
+} catch (Exception $e) {
+    echo $e::class, ": ", $e->getMessage(), "\n";
+}
+?>
+--EXPECT--
+Exception: Using null as an array offset is deprecated, use an empty string instead
diff --git a/ext/opcache/tests/opt/dce_025.phpt b/ext/opcache/tests/opt/dce_025.phpt
new file mode 100644
index 00000000000..16d31607644
--- /dev/null
+++ b/ext/opcache/tests/opt/dce_025.phpt
@@ -0,0 +1,27 @@
+--TEST--
+DCE must not remove ARRAY_KEY_EXISTS with a null key
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.optimization_level=-1
+opcache.file_update_protection=0
+--EXTENSIONS--
+opcache
+--FILE--
+<?php
+set_error_handler(function ($errno, $errstr) {
+    throw new Exception($errstr);
+});
+
+function test(null $k) {
+    array_key_exists($k, []);
+}
+
+try {
+    test(null);
+} catch (Exception $e) {
+    echo $e::class, ": ", $e->getMessage(), "\n";
+}
+?>
+--EXPECT--
+Exception: Using null as the key parameter for array_key_exists() is deprecated, use an empty string instead
diff --git a/ext/opcache/tests/opt/dce_026.phpt b/ext/opcache/tests/opt/dce_026.phpt
new file mode 100644
index 00000000000..b582063f7b2
--- /dev/null
+++ b/ext/opcache/tests/opt/dce_026.phpt
@@ -0,0 +1,28 @@
+--TEST--
+DCE must not remove ISSET_ISEMPTY_DIM_OBJ with a null key
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.optimization_level=-1
+opcache.file_update_protection=0
+--EXTENSIONS--
+opcache
+--FILE--
+<?php
+set_error_handler(function ($errno, $errstr) {
+    throw new Exception($errstr);
+});
+
+function test(null $k) {
+    $a = [];
+    isset($a[$k]);
+}
+
+try {
+    test(null);
+} catch (Exception $e) {
+    echo $e::class, ": ", $e->getMessage(), "\n";
+}
+?>
+--EXPECT--
+Exception: Using null as an array offset is deprecated, use an empty string instead
diff --git a/ext/opcache/tests/opt/dce_027.phpt b/ext/opcache/tests/opt/dce_027.phpt
new file mode 100644
index 00000000000..a9613621540
--- /dev/null
+++ b/ext/opcache/tests/opt/dce_027.phpt
@@ -0,0 +1,28 @@
+--TEST--
+DCE must not remove FETCH_DIM_IS with a null key
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.optimization_level=-1
+opcache.file_update_protection=0
+--EXTENSIONS--
+opcache
+--FILE--
+<?php
+set_error_handler(function ($errno, $errstr) {
+    throw new Exception($errstr);
+});
+
+function test(null $k) {
+    $a = [];
+    $a[$k] ?? 0;
+}
+
+try {
+    test(null);
+} catch (Exception $e) {
+    echo $e::class, ": ", $e->getMessage(), "\n";
+}
+?>
+--EXPECT--
+Exception: Using null as an array offset is deprecated, use an empty string instead