From 587f709675b9418623228e2c1480d6ce93ec6090 Mon Sep 17 00:00:00 2001 From: Jorg Sowa Date: Sat, 7 Nov 2026 22:57:39 +0100 Subject: [PATCH 1/2] JIT: Fix FETCH_DIM_IS ignoring leading-numeric string offsets 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. --- Zend/Optimizer/zend_inference.c | 4 +- ext/opcache/jit/zend_jit_helpers.c | 28 ++++++++++--- .../jit/fetch_dim_is_string_offset_001.phpt | 39 +++++++++++++++++++ .../jit/fetch_dim_is_string_offset_002.phpt | 35 +++++++++++++++++ .../jit/fetch_dim_is_string_offset_003.phpt | 35 +++++++++++++++++ ext/opcache/tests/opt/dce_023.phpt | 34 ++++++++++++++++ 6 files changed, 168 insertions(+), 7 deletions(-) create mode 100644 ext/opcache/tests/jit/fetch_dim_is_string_offset_001.phpt create mode 100644 ext/opcache/tests/jit/fetch_dim_is_string_offset_002.phpt create mode 100644 ext/opcache/tests/jit/fetch_dim_is_string_offset_003.phpt create mode 100644 ext/opcache/tests/opt/dce_023.phpt diff --git a/Zend/Optimizer/zend_inference.c b/Zend/Optimizer/zend_inference.c index 93e69e566366..f6c8c479af47 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 b52f2cc83636..d8607b1abd95 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 000000000000..314d8c2c709c --- /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-- + +--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 000000000000..a63789c51f89 --- /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-- +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 000000000000..e2daad71de05 --- /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-- +getMessage(), "\n"; +} +?> +--EXPECT-- +Exception: Illegal string offset "1x" diff --git a/ext/opcache/tests/opt/dce_023.phpt b/ext/opcache/tests/opt/dce_023.phpt new file mode 100644 index 000000000000..9dae69d920db --- /dev/null +++ b/ext/opcache/tests/opt/dce_023.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-- +getMessage(), "\n"; + } +} +?> +--EXPECT-- +Exception: Illegal string offset "1x" +Exception: Illegal string offset "1x" From 0cf874c0eef3a6465cded985e5abfff3f6ed0f2a Mon Sep 17 00:00:00 2001 From: Jorg Sowa Date: Mon, 9 Nov 2026 19:01:23 +0100 Subject: [PATCH 2/2] Add test for string release during FETCH_DIM_IS offset warning --- .../jit/fetch_dim_is_string_offset_004.phpt | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 ext/opcache/tests/jit/fetch_dim_is_string_offset_004.phpt 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 000000000000..a210dbaedfb5 --- /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-- + +--EXPECT-- +Warning: Illegal string offset "1x" +string(7) "default"