From c73eceef4217060e39705ecce2411562c229bfb5 Mon Sep 17 00:00:00 2001 From: Jorg Sowa Date: Sun, 8 Nov 2026 19:58:57 +0100 Subject: [PATCH 1/2] Fix use-after-free when a string offset warning releases the string 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. --- NEWS | 5 +++ ...ring_offset_read_warning_frees_string.phpt | 25 +++++++++++++++ Zend/zend_execute.c | 10 ++++++ ext/opcache/jit/zend_jit_helpers.c | 9 ++++++ ...etch_dim_is_undef_offset_frees_string.phpt | 31 +++++++++++++++++++ 5 files changed, 80 insertions(+) create mode 100644 Zend/tests/offsets/string_offset_read_warning_frees_string.phpt create mode 100644 ext/opcache/tests/jit/fetch_dim_is_undef_offset_frees_string.phpt diff --git a/NEWS b/NEWS index a63b79ed1302..cce5d48b36c1 100644 --- a/NEWS +++ b/NEWS @@ -18,6 +18,8 @@ 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 on read. (Jorg Sowa) - Curl: . Fixed curl_reset() retaining CURLOPT_PRIVATE after reset. (lacatoire) @@ -44,6 +46,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 offset variable warning is raised under isset()/??. + (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 000000000000..6914142fff58 --- /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-- + +--EXPECT-- +Illegal string offset "1x" +NULL diff --git a/Zend/zend_execute.c b/Zend/zend_execute.c index 45c8b8fe2823..e49a6b120c26 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 d8607b1abd95..0579f1d2592d 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 000000000000..e08f50fbdf06 --- /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-- + +--EXPECT-- +Undefined variable $undef +string(7) "default" From 01893c644bafb2873bf4aa956f32836bac0912f4 Mon Sep 17 00:00:00 2001 From: Jorg Sowa Date: Mon, 9 Nov 2026 22:34:42 +0100 Subject: [PATCH 2/2] Clarify use-after-free NEWS entries [skip ci] --- NEWS | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/NEWS b/NEWS index cce5d48b36c1..04f6aec330c2 100644 --- a/NEWS +++ b/NEWS @@ -19,7 +19,8 @@ PHP NEWS . 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 on read. (Jorg Sowa) + "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) @@ -47,8 +48,8 @@ PHP NEWS . 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 offset variable warning is raised under isset()/??. - (Jorg Sowa) + 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,