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"