Commit e8714b0f386 for php
commit e8714b0f38676abb910e800eff343668459d35b7
Author: Jorg Sowa <jorg.sowa@gmail.com>
Date: Mon Nov 9 21:58:48 2026 +0100
JIT: Fix FETCH_DIM_IS ignoring leading-numeric string offsets (GH-25298)
For $str["1x"] ?? $default, the VM warns "Illegal string offset" and
uses the leading number, while the JIT helper returned null silently.
zend_may_throw_ex() did not count this warning either, so DCE removed
the inner fetch of an unused nested isset()/empty() and the JIT skipped
saving the call chain around it.
Closes GH-25298
diff --git a/NEWS b/NEWS
index facc4a1d003..a63b79ed130 100644
--- a/NEWS
+++ b/NEWS
@@ -42,6 +42,8 @@ PHP NEWS
conversion in JIT). (Jorg Sowa)
. Fixed bug GH-25037 (Function JIT ENTRY live var assertion).
(Ilia Alshanetsky)
+ . Fixed JIT ignoring leading-numeric string offsets in isset(), empty() and
+ ??. (Jorg Sowa)
- PDO_Firebird:
. Fixed stack buffer overflow with long connection user names, passwords,
diff --git a/Zend/Optimizer/zend_inference.c b/Zend/Optimizer/zend_inference.c
index 93e69e56636..f6c8c479af4 100644
--- a/Zend/Optimizer/zend_inference.c
+++ b/Zend/Optimizer/zend_inference.c
@@ -5299,7 +5299,9 @@ ZEND_API bool zend_may_throw_ex(const zend_op *opline, const zend_ssa_op *ssa_op
case ZEND_ISSET_ISEMPTY_DIM_OBJ:
return (t1 & MAY_BE_OBJECT) || (t2 & MAY_BE_THROWING_KEY);
case ZEND_FETCH_DIM_IS:
- return (t1 & MAY_BE_OBJECT) || (t2 & MAY_BE_THROWING_KEY);
+ return (t1 & MAY_BE_OBJECT) || (t2 & MAY_BE_THROWING_KEY)
+ /* "Illegal string offset" warning for leading-numeric string offsets */
+ || ((t1 & MAY_BE_STRING) && (t2 & MAY_BE_STRING));
case ZEND_CAST:
switch (opline->extended_value) {
case IS_LONG:
diff --git a/ext/opcache/jit/zend_jit_helpers.c b/ext/opcache/jit/zend_jit_helpers.c
index b52f2cc8363..d8607b1abd9 100644
--- a/ext/opcache/jit/zend_jit_helpers.c
+++ b/ext/opcache/jit/zend_jit_helpers.c
@@ -1112,17 +1112,35 @@ static void ZEND_FASTCALL zend_jit_fetch_dim_str_is_helper(zend_string *str, zva
switch (Z_TYPE_P(dim)) {
/* case IS_LONG: */
case IS_STRING:
- if (IS_LONG == is_numeric_string(Z_STRVAL_P(dim), Z_STRLEN_P(dim), NULL, NULL, false)) {
- break;
+ {
+ bool trailing_data = false;
+ /* Allow errors to warn on leading-numeric strings, like the VM */
+ if (IS_LONG != is_numeric_string_ex(Z_STRVAL_P(dim), Z_STRLEN_P(dim), &offset,
+ NULL, /* allow errors */ true, NULL, &trailing_data)) {
+ ZVAL_NULL(result);
+ return;
}
- ZVAL_NULL(result);
- return;
+ if (UNEXPECTED(trailing_data)) {
+ if (!(GC_FLAGS(str) & IS_STR_INTERNED)) {
+ GC_ADDREF(str);
+ }
+ zend_error(E_WARNING, "Illegal string offset \"%s\"", Z_STRVAL_P(dim));
+ if (!(GC_FLAGS(str) & IS_STR_INTERNED) && UNEXPECTED(GC_DELREF(str) == 0)) {
+ zend_string_efree(str);
+ ZVAL_NULL(result);
+ return;
+ }
+ }
+ break;
+ }
case IS_UNDEF:
zend_jit_undefined_op_helper(EG(current_execute_data)->opline->op2.var);
+ ZEND_FALLTHROUGH;
case IS_DOUBLE:
case IS_NULL:
case IS_FALSE:
case IS_TRUE:
+ offset = zval_get_long_func(dim, /* is_strict */ false);
break;
case IS_REFERENCE:
dim = Z_REFVAL_P(dim);
@@ -1134,8 +1152,6 @@ static void ZEND_FASTCALL zend_jit_fetch_dim_str_is_helper(zend_string *str, zva
ZVAL_NULL(result);
return;
}
-
- offset = zval_get_long_func(dim, /* is_strict */ false);
} else {
offset = Z_LVAL_P(dim);
}
diff --git a/ext/opcache/tests/jit/fetch_dim_is_string_offset_001.phpt b/ext/opcache/tests/jit/fetch_dim_is_string_offset_001.phpt
new file mode 100644
index 00000000000..314d8c2c709
--- /dev/null
+++ b/ext/opcache/tests/jit/fetch_dim_is_string_offset_001.phpt
@@ -0,0 +1,39 @@
+--TEST--
+JIT FETCH_DIM_IS: string offsets given as strings behave like the VM
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.jit=function
+opcache.file_update_protection=0
+--EXTENSIONS--
+opcache
+--FILE--
+<?php
+function test(string $s, string $k) {
+ return $s[$k] ?? "default";
+}
+
+foreach (["1", "-1", "1 ", " 1", "1x", "-1x", "9x", "x", "", "1.5", "1.5x", "99999999999999999999"] as $k) {
+ echo var_export($k, true), ": ";
+ var_dump(test("abc", $k));
+}
+?>
+--EXPECTF--
+'1': string(1) "b"
+'-1': string(1) "c"
+'1 ': string(1) "b"
+' 1': string(1) "b"
+'1x':
+Warning: Illegal string offset "1x" in %s on line %d
+string(1) "b"
+'-1x':
+Warning: Illegal string offset "-1x" in %s on line %d
+string(1) "c"
+'9x':
+Warning: Illegal string offset "9x" in %s on line %d
+string(7) "default"
+'x': string(7) "default"
+'': string(7) "default"
+'1.5': string(7) "default"
+'1.5x': string(7) "default"
+'99999999999999999999': string(7) "default"
diff --git a/ext/opcache/tests/jit/fetch_dim_is_string_offset_002.phpt b/ext/opcache/tests/jit/fetch_dim_is_string_offset_002.phpt
new file mode 100644
index 00000000000..a63789c51f8
--- /dev/null
+++ b/ext/opcache/tests/jit/fetch_dim_is_string_offset_002.phpt
@@ -0,0 +1,35 @@
+--TEST--
+JIT FETCH_DIM_IS: leading-numeric string offset 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 make(int $n) {
+ return str_repeat("abc", $n);
+}
+
+function consume($v) {
+ echo "unreachable\n";
+}
+
+function test(string $s) {
+ consume($s["1x"] ?? "default");
+}
+
+try {
+ test(make(2));
+} catch (Exception $e) {
+ echo $e::class, ": ", $e->getMessage(), "\n";
+}
+?>
+--EXPECT--
+Exception: Illegal string offset "1x"
diff --git a/ext/opcache/tests/jit/fetch_dim_is_string_offset_003.phpt b/ext/opcache/tests/jit/fetch_dim_is_string_offset_003.phpt
new file mode 100644
index 00000000000..e2daad71de0
--- /dev/null
+++ b/ext/opcache/tests/jit/fetch_dim_is_string_offset_003.phpt
@@ -0,0 +1,35 @@
+--TEST--
+JIT FETCH_DIM_IS: leading-numeric string offset may throw between call arguments
+--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 make(int $n) {
+ return str_repeat("abc", $n);
+}
+
+function consume($a, $b) {
+ echo "unreachable\n";
+}
+
+function test(string $s, string $t) {
+ consume($t, isset($s["1x"][0]));
+}
+
+try {
+ test(make(2), make(3));
+} catch (Exception $e) {
+ echo $e::class, ": ", $e->getMessage(), "\n";
+}
+?>
+--EXPECT--
+Exception: Illegal string offset "1x"
diff --git a/ext/opcache/tests/jit/fetch_dim_is_string_offset_004.phpt b/ext/opcache/tests/jit/fetch_dim_is_string_offset_004.phpt
new file mode 100644
index 00000000000..a210dbaedfb
--- /dev/null
+++ b/ext/opcache/tests/jit/fetch_dim_is_string_offset_004.phpt
@@ -0,0 +1,31 @@
+--TEST--
+JIT FETCH_DIM_IS: leading-numeric string offset warning may release the string
+--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) {
+ echo "Warning: $errstr\n";
+ $GLOBALS['s'] = null;
+});
+
+function make(int $n) {
+ return str_repeat("abc", $n);
+}
+
+function test() {
+ global $s;
+ return $s["1x"] ?? "default";
+}
+
+$s = make(2);
+var_dump(test());
+?>
+--EXPECT--
+Warning: Illegal string offset "1x"
+string(7) "default"
diff --git a/ext/opcache/tests/opt/dce_028.phpt b/ext/opcache/tests/opt/dce_028.phpt
new file mode 100644
index 00000000000..9dae69d920d
--- /dev/null
+++ b/ext/opcache/tests/opt/dce_028.phpt
@@ -0,0 +1,34 @@
+--TEST--
+DCE must not remove FETCH_DIM_IS of a nested isset()/empty() with a leading-numeric string offset
+--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_isset(string $s) {
+ $x = isset($s["1x"][0]);
+}
+
+function test_empty(string $s) {
+ $x = empty($s["1x"][0]);
+}
+
+foreach (['test_isset', 'test_empty'] as $f) {
+ try {
+ $f("abc");
+ } catch (Exception $e) {
+ echo $e::class, ": ", $e->getMessage(), "\n";
+ }
+}
+?>
+--EXPECT--
+Exception: Illegal string offset "1x"
+Exception: Illegal string offset "1x"