Commit febae1cbfd9 for php

commit febae1cbfd98b23a41b18bea014fb8b7c9a60eda
Author: Jorg Sowa <jorg.sowa@gmail.com>
Date:   Mon Nov 9 23:04:55 2026 +0100

    Fix use-after-free when a string offset warning releases the string (GH-25329)

    The error handler may release the container string while the
    "Illegal string offset" warning (VM read) or the undefined offset
    warning (JIT FETCH_DIM_IS) is raised. Hold a reference across the
    warning, like the other string offset warnings already do.

    Closes GH-25329

diff --git a/NEWS b/NEWS
index a63b79ed130..04f6aec330c 100644
--- a/NEWS
+++ b/NEWS
@@ -18,6 +18,9 @@ PHP                                                                        NEWS
     (Lazizbek Ergashev, David Carlier)
   . Fixed bug GH-25302 (Use-after-free in zend_compile_const_decl() with
     OPcache during concurrent compilation). (Nicolas Grekas)
+  . Fixed use-after-free when the error handler releases a string while an
+    "Illegal string offset" warning is raised for its offset in a read, ?? or
+    nested isset()/empty(). (Jorg Sowa)

 - Curl:
   . Fixed curl_reset() retaining CURLOPT_PRIVATE after reset. (lacatoire)
@@ -44,6 +47,9 @@ PHP                                                                        NEWS
     (Ilia Alshanetsky)
   . Fixed JIT ignoring leading-numeric string offsets in isset(), empty() and
     ??. (Jorg Sowa)
+  . Fixed use-after-free in JIT when the error handler releases a string while
+    an "Undefined variable" warning is raised for its offset in ?? or nested
+    isset()/empty(). (Jorg Sowa)

 - PDO_Firebird:
   . Fixed stack buffer overflow with long connection user names, passwords,
diff --git a/Zend/tests/offsets/string_offset_read_warning_frees_string.phpt b/Zend/tests/offsets/string_offset_read_warning_frees_string.phpt
new file mode 100644
index 00000000000..6914142fff5
--- /dev/null
+++ b/Zend/tests/offsets/string_offset_read_warning_frees_string.phpt
@@ -0,0 +1,25 @@
+--TEST--
+Reading a leading-numeric string offset when the error handler releases the string
+--FILE--
+<?php
+set_error_handler(function ($errno, $errstr) {
+    $GLOBALS['s'] = null;
+    echo $errstr, "\n";
+});
+
+function make(int $n) {
+    return str_repeat("abc", $n);
+}
+
+function test() {
+    global $s;
+    return $s["1x"];
+}
+
+$s = make(2);
+test();
+var_dump($s);
+?>
+--EXPECT--
+Illegal string offset "1x"
+NULL
diff --git a/Zend/zend_execute.c b/Zend/zend_execute.c
index 45c8b8fe282..e49a6b120c2 100644
--- a/Zend/zend_execute.c
+++ b/Zend/zend_execute.c
@@ -2956,7 +2956,17 @@ static zend_always_inline void zend_fetch_dimension_address_read(zval *result, z
 					if (IS_LONG == is_numeric_string_ex(Z_STRVAL_P(dim), Z_STRLEN_P(dim), &offset,
 							NULL, /* allow errors */ true, NULL, &trailing_data)) {
 						if (UNEXPECTED(trailing_data)) {
+							/* The string may be destroyed while throwing the warning.
+							 * Temporarily increase the refcount to detect this situation. */
+							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;
+							}
 						}
 						goto out;
 					}
diff --git a/ext/opcache/jit/zend_jit_helpers.c b/ext/opcache/jit/zend_jit_helpers.c
index d8607b1abd9..0579f1d2592 100644
--- a/ext/opcache/jit/zend_jit_helpers.c
+++ b/ext/opcache/jit/zend_jit_helpers.c
@@ -1134,7 +1134,16 @@ static void ZEND_FASTCALL zend_jit_fetch_dim_str_is_helper(zend_string *str, zva
 				break;
 			}
 			case IS_UNDEF:
+				/* The string may be destroyed while throwing the notice */
+				if (!(GC_FLAGS(str) & IS_STR_INTERNED)) {
+					GC_ADDREF(str);
+				}
 				zend_jit_undefined_op_helper(EG(current_execute_data)->opline->op2.var);
+				if (!(GC_FLAGS(str) & IS_STR_INTERNED) && UNEXPECTED(GC_DELREF(str) == 0)) {
+					zend_string_efree(str);
+					ZVAL_NULL(result);
+					return;
+				}
 				ZEND_FALLTHROUGH;
 			case IS_DOUBLE:
 			case IS_NULL:
diff --git a/ext/opcache/tests/jit/fetch_dim_is_undef_offset_frees_string.phpt b/ext/opcache/tests/jit/fetch_dim_is_undef_offset_frees_string.phpt
new file mode 100644
index 00000000000..e08f50fbdf0
--- /dev/null
+++ b/ext/opcache/tests/jit/fetch_dim_is_undef_offset_frees_string.phpt
@@ -0,0 +1,31 @@
+--TEST--
+JIT FETCH_DIM_IS: undefined offset variable when the error handler releases 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) {
+    $GLOBALS['s'] = null;
+    echo $errstr, "\n";
+});
+
+function make(int $n) {
+    return str_repeat("abc", $n);
+}
+
+function test() {
+    global $s;
+    return $s[$undef] ?? "default";
+}
+
+$s = make(2);
+var_dump(test());
+?>
+--EXPECT--
+Undefined variable $undef
+string(7) "default"