Commit 47ae17db183 for php

commit 47ae17db1837e346efcd0a5b1c005e1dfdcf5bfa
Author: Jorg Sowa <jorg.sowa@gmail.com>
Date:   Sat Nov 7 16:29:08 2026 +0100

    Optimizer: array keys whose conversion may throw

    Float keys emit a deprecation and resource keys a warning, which an error
    handler can turn into an exception. Share one key mask in
    zend_may_throw_ex() so ASSIGN_DIM, INIT_ARRAY, ADD_ARRAY_ELEMENT,
    FETCH_DIM_W/FETCH_LIST_W, ARRAY_KEY_EXISTS, FETCH_DIM_IS and
    ISSET_ISEMPTY_DIM_OBJ agree.

    Fixes GH-16396
    Closes GH-24653

diff --git a/NEWS b/NEWS
index 26abaf2f03a..2d3e9d00d30 100644
--- a/NEWS
+++ b/NEWS
@@ -22,6 +22,8 @@ PHP                                                                        NEWS
   . Fixed bug GH-25164 (Function JIT loop calling a user function runs once
     or never terminates). (Ilia Alshanetsky)
   . Fixed memory leak in JIT ASSIGN_DIM when the assignment fails. (Jorg Sowa)
+  . Fixed bug GH-16396 (Failed to invoke user error handler during index
+    conversion in JIT). (Jorg Sowa)

 - Streams:
   . Fixed bug GH-22063 (Stream filter chain UAF on self-removal during
diff --git a/Zend/Optimizer/zend_inference.c b/Zend/Optimizer/zend_inference.c
index 5a8bfed10cf..93e69e56636 100644
--- a/Zend/Optimizer/zend_inference.c
+++ b/Zend/Optimizer/zend_inference.c
@@ -4934,6 +4934,9 @@ 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)
+
 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)
 {
 	if (opline->op1_type == IS_CV) {
@@ -5213,7 +5216,7 @@ ZEND_API bool zend_may_throw_ex(const zend_op *opline, const zend_ssa_op *ssa_op
 				return 1;
 			}
 			return (t1 & (MAY_BE_OBJECT|MAY_BE_RESOURCE|MAY_BE_TRUE|MAY_BE_FALSE|MAY_BE_STRING|MAY_BE_LONG|MAY_BE_DOUBLE)) || opline->op2_type == IS_UNUSED ||
-				(t2 & (MAY_BE_UNDEF|MAY_BE_ARRAY|MAY_BE_OBJECT|MAY_BE_RESOURCE));
+				(t2 & (MAY_BE_UNDEF|MAY_BE_THROWING_KEY));
 		case ZEND_ASSIGN_OBJ:
 			if (t1 & (MAY_BE_ANY-MAY_BE_OBJECT)) {
 				return 1;
@@ -5265,9 +5268,9 @@ ZEND_API bool zend_may_throw_ex(const zend_op *opline, const zend_ssa_op *ssa_op
 		case ZEND_ROPE_END:
 			return t2 & (MAY_BE_ARRAY|MAY_BE_OBJECT);
 		case ZEND_INIT_ARRAY:
-			return (opline->op2_type != IS_UNUSED) && (t2 & (MAY_BE_ARRAY|MAY_BE_OBJECT|MAY_BE_RESOURCE));
+			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_ARRAY|MAY_BE_OBJECT|MAY_BE_RESOURCE));
+			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:
@@ -5294,9 +5297,9 @@ ZEND_API bool zend_may_throw_ex(const zend_op *opline, const zend_ssa_op *ssa_op
 		case ZEND_FETCH_IS:
 			return (t2 & (MAY_BE_ARRAY|MAY_BE_OBJECT));
 		case ZEND_ISSET_ISEMPTY_DIM_OBJ:
-			return (t1 & MAY_BE_OBJECT) || (t2 & (MAY_BE_DOUBLE|MAY_BE_ARRAY|MAY_BE_OBJECT));
+			return (t1 & MAY_BE_OBJECT) || (t2 & MAY_BE_THROWING_KEY);
 		case ZEND_FETCH_DIM_IS:
-			return (t1 & MAY_BE_OBJECT) || (t2 & (MAY_BE_DOUBLE|MAY_BE_ARRAY|MAY_BE_OBJECT|MAY_BE_RESOURCE));
+			return (t1 & MAY_BE_OBJECT) || (t2 & MAY_BE_THROWING_KEY);
 		case ZEND_CAST:
 			switch (opline->extended_value) {
 				case IS_LONG:
@@ -5317,7 +5320,7 @@ ZEND_API bool zend_may_throw_ex(const zend_op *opline, const zend_ssa_op *ssa_op
 			if ((t2 & MAY_BE_ANY) != MAY_BE_ARRAY) {
 				return 1;
 			}
-			if ((t1 & (MAY_BE_ARRAY|MAY_BE_OBJECT|MAY_BE_RESOURCE))) {
+			if (t1 & MAY_BE_THROWING_KEY) {
 				return 1;
 			}
 			return 0;
@@ -5342,7 +5345,7 @@ ZEND_API bool zend_may_throw_ex(const zend_op *opline, const zend_ssa_op *ssa_op
 			if (t1 & (MAY_BE_FALSE|MAY_BE_TRUE|MAY_BE_LONG|MAY_BE_DOUBLE|MAY_BE_STRING|MAY_BE_OBJECT|MAY_BE_RESOURCE|MAY_BE_REF)) {
 				return 1;
 			}
-			if (t2 & (MAY_BE_RESOURCE|MAY_BE_ARRAY|MAY_BE_OBJECT)) {
+			if (t2 & MAY_BE_THROWING_KEY) {
 				return 1;
 			}
 			if (opline->op2_type == IS_UNUSED) {
diff --git a/ext/opcache/tests/jit/add_array_element_float_key.phpt b/ext/opcache/tests/jit/add_array_element_float_key.phpt
new file mode 100644
index 00000000000..8d6d4efd2b3
--- /dev/null
+++ b/ext/opcache/tests/jit/add_array_element_float_key.phpt
@@ -0,0 +1,27 @@
+--TEST--
+JIT ADD_ARRAY_ELEMENT: float 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(float $k) {
+    var_dump([0, $k => 1]);
+}
+
+try {
+    test(1.5);
+} catch (Exception $e) {
+    echo $e::class, ": ", $e->getMessage(), "\n";
+}
+?>
+--EXPECT--
+Exception: Implicit conversion from float 1.5 to int loses precision
diff --git a/ext/opcache/tests/jit/array_key_exists_float_key.phpt b/ext/opcache/tests/jit/array_key_exists_float_key.phpt
new file mode 100644
index 00000000000..e1c9b62ac23
--- /dev/null
+++ b/ext/opcache/tests/jit/array_key_exists_float_key.phpt
@@ -0,0 +1,27 @@
+--TEST--
+JIT ARRAY_KEY_EXISTS: float 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(float $k) {
+    var_dump(array_key_exists($k, []));
+}
+
+try {
+    test(1.5);
+} catch (Exception $e) {
+    echo $e::class, ": ", $e->getMessage(), "\n";
+}
+?>
+--EXPECT--
+Exception: Implicit conversion from float 1.5 to int loses precision
diff --git a/ext/opcache/tests/jit/assign_dim_float_key.phpt b/ext/opcache/tests/jit/assign_dim_float_key.phpt
new file mode 100644
index 00000000000..9ebd78c63b0
--- /dev/null
+++ b/ext/opcache/tests/jit/assign_dim_float_key.phpt
@@ -0,0 +1,30 @@
+--TEST--
+JIT ASSIGN_DIM: float key conversion may throw
+--CREDITS--
+YuanchengJiang
+--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(float $k) {
+    $a = [];
+    var_dump($a[$k] = 1);
+}
+
+try {
+    test(1.5);
+} catch (Exception $e) {
+    echo $e::class, ": ", $e->getMessage(), "\n";
+}
+?>
+--EXPECT--
+Exception: Implicit conversion from float 1.5 to int loses precision
diff --git a/ext/opcache/tests/jit/fetch_dim_w_float_key.phpt b/ext/opcache/tests/jit/fetch_dim_w_float_key.phpt
new file mode 100644
index 00000000000..03058e91665
--- /dev/null
+++ b/ext/opcache/tests/jit/fetch_dim_w_float_key.phpt
@@ -0,0 +1,37 @@
+--TEST--
+JIT FETCH_DIM_W: float 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(float $k) {
+    $c = new C;
+    g($c, $a[$k]);
+}
+
+try {
+    test(1.5);
+} catch (Exception $e) {
+    echo $e::class, ": ", $e->getMessage(), "\n";
+}
+?>
+--EXPECT--
+destruct
+Exception: Implicit conversion from float 1.5 to int loses precision
diff --git a/ext/opcache/tests/jit/init_array_float_key.phpt b/ext/opcache/tests/jit/init_array_float_key.phpt
new file mode 100644
index 00000000000..315c7f08181
--- /dev/null
+++ b/ext/opcache/tests/jit/init_array_float_key.phpt
@@ -0,0 +1,27 @@
+--TEST--
+JIT INIT_ARRAY: float 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(float $k) {
+    var_dump([$k => 1]);
+}
+
+try {
+    test(1.5);
+} catch (Exception $e) {
+    echo $e::class, ": ", $e->getMessage(), "\n";
+}
+?>
+--EXPECT--
+Exception: Implicit conversion from float 1.5 to int loses precision
diff --git a/ext/opcache/tests/jit/isset_isempty_dim_obj_resource_key.phpt b/ext/opcache/tests/jit/isset_isempty_dim_obj_resource_key.phpt
new file mode 100644
index 00000000000..cb5ccd6b040
--- /dev/null
+++ b/ext/opcache/tests/jit/isset_isempty_dim_obj_resource_key.phpt
@@ -0,0 +1,29 @@
+--TEST--
+JIT ISSET_ISEMPTY_DIM_OBJ: resource 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() {
+    $k = fopen("php://memory", "r");
+    $a = [];
+    var_dump(isset($a[$k]));
+}
+
+try {
+    test();
+} catch (Exception $e) {
+    echo $e::class, ": ", $e->getMessage(), "\n";
+}
+?>
+--EXPECTF--
+Exception: Resource ID#%d used as offset, casting to integer (%d)
diff --git a/ext/opcache/tests/opt/dce_018.phpt b/ext/opcache/tests/opt/dce_018.phpt
new file mode 100644
index 00000000000..946b581b02d
--- /dev/null
+++ b/ext/opcache/tests/opt/dce_018.phpt
@@ -0,0 +1,29 @@
+--TEST--
+DCE must not remove ASSIGN_DIM with a float key
+--CREDITS--
+YuanchengJiang
+--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(float $k) {
+    $a[$k] = 1;
+}
+
+try {
+    test(1.5);
+} catch (Exception $e) {
+    echo $e::class, ": ", $e->getMessage(), "\n";
+}
+?>
+--EXPECT--
+Exception: Implicit conversion from float 1.5 to int loses precision
diff --git a/ext/opcache/tests/opt/dce_019.phpt b/ext/opcache/tests/opt/dce_019.phpt
new file mode 100644
index 00000000000..3996185c84f
--- /dev/null
+++ b/ext/opcache/tests/opt/dce_019.phpt
@@ -0,0 +1,27 @@
+--TEST--
+DCE must not remove INIT_ARRAY with a float 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(float $k) {
+    $a = [$k => 1];
+}
+
+try {
+    test(1.5);
+} catch (Exception $e) {
+    echo $e::class, ": ", $e->getMessage(), "\n";
+}
+?>
+--EXPECT--
+Exception: Implicit conversion from float 1.5 to int loses precision
diff --git a/ext/opcache/tests/opt/dce_020.phpt b/ext/opcache/tests/opt/dce_020.phpt
new file mode 100644
index 00000000000..722c40506fb
--- /dev/null
+++ b/ext/opcache/tests/opt/dce_020.phpt
@@ -0,0 +1,27 @@
+--TEST--
+DCE must not remove ADD_ARRAY_ELEMENT with a float 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(float $k) {
+    $a = [0, $k => 1];
+}
+
+try {
+    test(1.5);
+} catch (Exception $e) {
+    echo $e::class, ": ", $e->getMessage(), "\n";
+}
+?>
+--EXPECT--
+Exception: Implicit conversion from float 1.5 to int loses precision
diff --git a/ext/opcache/tests/opt/dce_021.phpt b/ext/opcache/tests/opt/dce_021.phpt
new file mode 100644
index 00000000000..6cf0fd49779
--- /dev/null
+++ b/ext/opcache/tests/opt/dce_021.phpt
@@ -0,0 +1,27 @@
+--TEST--
+DCE must not remove ARRAY_KEY_EXISTS with a float 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(float $k) {
+    array_key_exists($k, []);
+}
+
+try {
+    test(1.5);
+} catch (Exception $e) {
+    echo $e::class, ": ", $e->getMessage(), "\n";
+}
+?>
+--EXPECT--
+Exception: Implicit conversion from float 1.5 to int loses precision
diff --git a/ext/opcache/tests/opt/dce_022.phpt b/ext/opcache/tests/opt/dce_022.phpt
new file mode 100644
index 00000000000..d2e0ac1baf4
--- /dev/null
+++ b/ext/opcache/tests/opt/dce_022.phpt
@@ -0,0 +1,29 @@
+--TEST--
+DCE must not remove ISSET_ISEMPTY_DIM_OBJ with a resource 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() {
+    $k = fopen("php://memory", "r");
+    $a = [];
+    isset($a[$k]);
+}
+
+try {
+    test();
+} catch (Exception $e) {
+    echo $e::class, ": ", $e->getMessage(), "\n";
+}
+?>
+--EXPECTF--
+Exception: Resource ID#%d used as offset, casting to integer (%d)