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)