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"