From eb1d45bd425a16f4ca891648051e5f2b660429aa Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Tue, 20 Jan 2026 15:48:01 +0100 Subject: [PATCH 1/8] Restore opcache_invalidate() fuzzer behavior The zend_exception_save() and zend_exception_restore() functions were removed in GH-20256. However, the fuzzer expects to be able to call opcache_invalidate() even if there was an exception thrown. I'm not sure why exactly, but let's restore the previous behavior. Closes OSS-Fuzz #476466130 --- sapi/fuzzer/fuzzer-execute-common.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sapi/fuzzer/fuzzer-execute-common.h b/sapi/fuzzer/fuzzer-execute-common.h index ef2ff4ee79bab..81b38df40da0e 100644 --- a/sapi/fuzzer/fuzzer-execute-common.h +++ b/sapi/fuzzer/fuzzer-execute-common.h @@ -134,6 +134,8 @@ ZEND_ATTRIBUTE_UNUSED static void create_file(void) { ZEND_ATTRIBUTE_UNUSED static void opcache_invalidate(void) { steps_left = MAX_STEPS; + zend_object *exception = EG(exception); + EG(exception) = NULL; zval retval, args[2]; zend_function *fn = zend_hash_str_find_ptr(CG(function_table), ZEND_STRL("opcache_invalidate")); ZEND_ASSERT(fn != NULL); @@ -144,4 +146,5 @@ ZEND_ATTRIBUTE_UNUSED static void opcache_invalidate(void) { ZEND_ASSERT(Z_TYPE(retval) == IS_TRUE); zval_ptr_dtor(&args[0]); zval_ptr_dtor(&retval); + EG(exception) = exception; } From f38f74521b61f4a7e74d0124f557fa1d898bf0e2 Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Mon, 19 Jan 2026 15:47:03 +0100 Subject: [PATCH 2/8] Fix lazy proxy bailing __clone assertion When __clone of the underlying object fails with a bailout, ZEND_ASSERT(res == SUCCESS) in zend_lazy_object_del_info() will fail because the info has not been registered yet. Only copy OBJ_EXTRA_FLAGS once the info has been successfully registered. Fixes GH-20905 Closes GH-20975 --- NEWS | 1 + Zend/tests/lazy_objects/gh20905.phpt | 22 ++++++++++++++++++++++ Zend/zend_lazy_objects.c | 3 +-- 3 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 Zend/tests/lazy_objects/gh20905.phpt diff --git a/NEWS b/NEWS index 06ebe601478e3..80b69c11cf88d 100644 --- a/NEWS +++ b/NEWS @@ -14,6 +14,7 @@ PHP NEWS backing value). (ilutov) . Fix OSS-Fuzz #438780145 (Nested finally with repeated return type check may uaf). (ilutov) + . Fixed bug GH-20905 (Lazy proxy bailing __clone assertion). (ilutov) - Date: . Update timelib to 2022.16. (Derick) diff --git a/Zend/tests/lazy_objects/gh20905.phpt b/Zend/tests/lazy_objects/gh20905.phpt new file mode 100644 index 0000000000000..48a1360c0a949 --- /dev/null +++ b/Zend/tests/lazy_objects/gh20905.phpt @@ -0,0 +1,22 @@ +--TEST-- +GH-20905: Lazy proxy bailing __clone assertion +--FILE-- +newLazyProxy(fn() => new A); + +?> +--EXPECTF-- +Fatal error: Cannot redeclare function f() (previously declared in %s:%d) in %s on line %d diff --git a/Zend/zend_lazy_objects.c b/Zend/zend_lazy_objects.c index d1b950160e1cc..bf76f6e88fe7d 100644 --- a/Zend/zend_lazy_objects.c +++ b/Zend/zend_lazy_objects.c @@ -744,12 +744,11 @@ zend_object *zend_lazy_object_clone(zend_object *old_obj) } } - OBJ_EXTRA_FLAGS(new_proxy) = OBJ_EXTRA_FLAGS(old_obj); - zend_lazy_object_info *new_info = emalloc(sizeof(*info)); *new_info = *info; new_info->u.instance = zend_objects_clone_obj(info->u.instance); + OBJ_EXTRA_FLAGS(new_proxy) = OBJ_EXTRA_FLAGS(old_obj); zend_lazy_object_set_info(new_proxy, new_info); return new_proxy; From 6a21a41b4ad1b086e6748d6521cd04d3518e4dd2 Mon Sep 17 00:00:00 2001 From: Ilija Tovilo Date: Tue, 20 Jan 2026 18:00:58 +0100 Subject: [PATCH 3/8] [skip ci] Fix missing test attribution --- Zend/tests/lazy_objects/gh20905.phpt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Zend/tests/lazy_objects/gh20905.phpt b/Zend/tests/lazy_objects/gh20905.phpt index 48a1360c0a949..318b44e6b82a7 100644 --- a/Zend/tests/lazy_objects/gh20905.phpt +++ b/Zend/tests/lazy_objects/gh20905.phpt @@ -1,5 +1,7 @@ --TEST-- GH-20905: Lazy proxy bailing __clone assertion +--CREDITS-- +Viet Hoang Luu (@vi3tL0u1s) --FILE-- Date: Sat, 10 Jan 2026 17:42:01 +0100 Subject: [PATCH 4/8] Fix GH-20890: Segfault in zval_undefined_cv with non-simple property hook with minimal tracing JIT This is similar to f6c2e40a11 but for minimal JIT + tracing JIT. Most of the times the tracing JIT shouldn't rely on going to the VM, but in some cases, like in minimal JIT, it can and then it hits the same bug. Closes GH-20897. --- NEWS | 4 ++++ ext/opcache/jit/zend_jit_trace.c | 8 +++++++ ext/opcache/tests/jit/gh20890.phpt | 37 ++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+) create mode 100644 ext/opcache/tests/jit/gh20890.phpt diff --git a/NEWS b/NEWS index 80b69c11cf88d..4a2dbd1524998 100644 --- a/NEWS +++ b/NEWS @@ -25,6 +25,10 @@ PHP NEWS . Fixed bug GH-20836 (Stack overflow in mb_convert_variables with recursive array references). (alexandre-daubois) +- Opcache: + . Fixed bug GH-20890 (Segfault in zval_undefined_cv with non-simple + property hook with minimal tracing JIT). (ndossche) + - Phar: . Fixed bug GH-20882 (buildFromIterator breaks with missing base directory). (ndossche) diff --git a/ext/opcache/jit/zend_jit_trace.c b/ext/opcache/jit/zend_jit_trace.c index 9d2de55e29499..41393f721cfcf 100644 --- a/ext/opcache/jit/zend_jit_trace.c +++ b/ext/opcache/jit/zend_jit_trace.c @@ -328,6 +328,14 @@ static int zend_jit_trace_may_exit(const zend_op_array *op_array, const zend_op // TODO: recompilation may change target ??? return 0; #endif + case ZEND_FETCH_OBJ_R: + if (opline->op2_type == IS_CONST) { + const zend_class_entry *ce = opline->op1_type == IS_UNUSED ? op_array->scope : NULL; + if (!ce || !(ce->ce_flags & ZEND_ACC_FINAL) || ce->num_hooked_props > 0) { + return 1; + } + } + break; case ZEND_RETURN_BY_REF: case ZEND_RETURN: /* return */ diff --git a/ext/opcache/tests/jit/gh20890.phpt b/ext/opcache/tests/jit/gh20890.phpt new file mode 100644 index 0000000000000..c375c379fcc72 --- /dev/null +++ b/ext/opcache/tests/jit/gh20890.phpt @@ -0,0 +1,37 @@ +--TEST-- +GH-20890 (Segfault in zval_undefined_cv with non-simple property hook with minimal tracing JIT) +--CREDITS-- +Moonster8282 +--EXTENSIONS-- +opcache +--INI-- +opcache.jit=1251 +--FILE-- +readCount++; + return $this->readCount * 2; + } + } +} + +function hook_hot_path($obj, $iterations) { + $sum = 0; + for ($i = 0; $i < $iterations; $i++) { + $sum += $obj->computed; + } + return $sum; +} + +echo "Testing property hook in hot path...\n"; +$obj = new HookJIT(); +$result = hook_hot_path($obj, 100); +echo "Result: $result\n"; +?> +--EXPECT-- +Testing property hook in hot path... +Result: 10100 From 32c02455312ed03cfa726ecffa982977ca6e954f Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+ndossche@users.noreply.github.com> Date: Tue, 20 Jan 2026 21:05:26 +0100 Subject: [PATCH 5/8] Revert "Fix GH-20890: Segfault in zval_undefined_cv with non-simple property hook with minimal tracing JIT" This reverts commit 57c62eb2b34385a9c2ab4e7ab802c4a361483ba1. --- NEWS | 4 ---- ext/opcache/jit/zend_jit_trace.c | 8 ------- ext/opcache/tests/jit/gh20890.phpt | 37 ------------------------------ 3 files changed, 49 deletions(-) delete mode 100644 ext/opcache/tests/jit/gh20890.phpt diff --git a/NEWS b/NEWS index 4a2dbd1524998..80b69c11cf88d 100644 --- a/NEWS +++ b/NEWS @@ -25,10 +25,6 @@ PHP NEWS . Fixed bug GH-20836 (Stack overflow in mb_convert_variables with recursive array references). (alexandre-daubois) -- Opcache: - . Fixed bug GH-20890 (Segfault in zval_undefined_cv with non-simple - property hook with minimal tracing JIT). (ndossche) - - Phar: . Fixed bug GH-20882 (buildFromIterator breaks with missing base directory). (ndossche) diff --git a/ext/opcache/jit/zend_jit_trace.c b/ext/opcache/jit/zend_jit_trace.c index 41393f721cfcf..9d2de55e29499 100644 --- a/ext/opcache/jit/zend_jit_trace.c +++ b/ext/opcache/jit/zend_jit_trace.c @@ -328,14 +328,6 @@ static int zend_jit_trace_may_exit(const zend_op_array *op_array, const zend_op // TODO: recompilation may change target ??? return 0; #endif - case ZEND_FETCH_OBJ_R: - if (opline->op2_type == IS_CONST) { - const zend_class_entry *ce = opline->op1_type == IS_UNUSED ? op_array->scope : NULL; - if (!ce || !(ce->ce_flags & ZEND_ACC_FINAL) || ce->num_hooked_props > 0) { - return 1; - } - } - break; case ZEND_RETURN_BY_REF: case ZEND_RETURN: /* return */ diff --git a/ext/opcache/tests/jit/gh20890.phpt b/ext/opcache/tests/jit/gh20890.phpt deleted file mode 100644 index c375c379fcc72..0000000000000 --- a/ext/opcache/tests/jit/gh20890.phpt +++ /dev/null @@ -1,37 +0,0 @@ ---TEST-- -GH-20890 (Segfault in zval_undefined_cv with non-simple property hook with minimal tracing JIT) ---CREDITS-- -Moonster8282 ---EXTENSIONS-- -opcache ---INI-- -opcache.jit=1251 ---FILE-- -readCount++; - return $this->readCount * 2; - } - } -} - -function hook_hot_path($obj, $iterations) { - $sum = 0; - for ($i = 0; $i < $iterations; $i++) { - $sum += $obj->computed; - } - return $sum; -} - -echo "Testing property hook in hot path...\n"; -$obj = new HookJIT(); -$result = hook_hot_path($obj, 100); -echo "Result: $result\n"; -?> ---EXPECT-- -Testing property hook in hot path... -Result: 10100 From f957571d6600861481bbf0560deaba2926dcdc8e Mon Sep 17 00:00:00 2001 From: ndossche Date: Tue, 20 Jan 2026 14:11:39 +0100 Subject: [PATCH 6/8] Remove dead code from zend_test internal execute handler The internal execute handler is used for internal functions, so by definition it cannot be a user function. --- ext/zend_test/observer.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ext/zend_test/observer.c b/ext/zend_test/observer.c index 31052ec830f73..a6b43912a14f1 100644 --- a/ext/zend_test/observer.c +++ b/ext/zend_test/observer.c @@ -288,14 +288,14 @@ static void (*zend_test_prev_execute_internal)(zend_execute_data *execute_data, static void zend_test_execute_internal(zend_execute_data *execute_data, zval *return_value) { zend_function *fbc = execute_data->func; + ZEND_ASSERT(!ZEND_USER_CODE(fbc->type)); + if (fbc->common.function_name) { if (fbc->common.scope) { php_printf("%*s\n", 2 * ZT_G(observer_nesting_depth), "", ZSTR_VAL(fbc->common.scope->name), ZSTR_VAL(fbc->common.function_name)); } else { php_printf("%*s\n", 2 * ZT_G(observer_nesting_depth), "", ZSTR_VAL(fbc->common.function_name)); } - } else if (ZEND_USER_CODE(fbc->type)) { - php_printf("%*s\n", 2 * ZT_G(observer_nesting_depth), "", ZSTR_VAL(fbc->op_array.filename)); } if (zend_test_prev_execute_internal) { From 8776c7ead1758850aa3d1c9fcfd00062262dd8ee Mon Sep 17 00:00:00 2001 From: ndossche Date: Tue, 20 Jan 2026 14:13:33 +0100 Subject: [PATCH 7/8] Extend zend_test internal function handler to also print leave (with return info if requested) and exception state --- Zend/tests/attributes/nodiscard/007.phpt | 2 ++ ext/zend_test/observer.c | 15 +++++++++++++++ ext/zend_test/tests/execute_internal.phpt | 6 +++++- 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/Zend/tests/attributes/nodiscard/007.phpt b/Zend/tests/attributes/nodiscard/007.phpt index 1b72de8c22a06..de4604e15e48d 100644 --- a/Zend/tests/attributes/nodiscard/007.phpt +++ b/Zend/tests/attributes/nodiscard/007.phpt @@ -12,6 +12,8 @@ zend_test_nodiscard(); ?> --EXPECTF-- + Warning: The return value of function zend_test_nodiscard() should either be used or intentionally ignored by casting it as (void), custom message in %s on line %d + diff --git a/ext/zend_test/observer.c b/ext/zend_test/observer.c index a6b43912a14f1..9c6bf6b674c6c 100644 --- a/ext/zend_test/observer.c +++ b/ext/zend_test/observer.c @@ -303,6 +303,21 @@ static void zend_test_execute_internal(zend_execute_data *execute_data, zval *re } else { fbc->internal_function.handler(execute_data, return_value); } + + if (fbc->common.function_name) { + if (EG(exception)) { + php_printf("%*s\n", 2 * ZT_G(observer_nesting_depth), "", ZSTR_VAL(EG(exception)->ce->name)); + } + + smart_str retval_info = {0}; + get_retval_info(return_value, &retval_info); + if (fbc->common.scope) { + php_printf("%*s\n", 2 * ZT_G(observer_nesting_depth), "", ZSTR_VAL(fbc->common.scope->name), ZSTR_VAL(fbc->common.function_name), retval_info.s ? ZSTR_VAL(retval_info.s) : ""); + } else { + php_printf("%*s\n", 2 * ZT_G(observer_nesting_depth), "", ZSTR_VAL(fbc->common.function_name), retval_info.s ? ZSTR_VAL(retval_info.s) : ""); + } + smart_str_free(&retval_info); + } } static ZEND_INI_MH(zend_test_observer_OnUpdateCommaList) diff --git a/ext/zend_test/tests/execute_internal.phpt b/ext/zend_test/tests/execute_internal.phpt index ce6cb851aed60..3983e6f2b8d31 100644 --- a/ext/zend_test/tests/execute_internal.phpt +++ b/ext/zend_test/tests/execute_internal.phpt @@ -4,6 +4,7 @@ Test zend_execute_internal being called zend_test --INI-- zend_test.observer.execute_internal=1 +zend_test.observer.show_return_value=1 --FILE-- 0 ? [1, 2, 3] : []); ?> ---EXPECT-- +--EXPECTF-- + + int(6) + From 6a4d0d9456bf872286722ad5f043ad4928e92c9a Mon Sep 17 00:00:00 2001 From: ndossche Date: Tue, 20 Jan 2026 15:45:53 +0100 Subject: [PATCH 8/8] Do nesting for internal execute hook --- ext/zend_test/observer.c | 4 ++++ ext/zend_test/tests/execute_internal.phpt | 12 ++++++++++++ 2 files changed, 16 insertions(+) diff --git a/ext/zend_test/observer.c b/ext/zend_test/observer.c index 9c6bf6b674c6c..348996b7ac132 100644 --- a/ext/zend_test/observer.c +++ b/ext/zend_test/observer.c @@ -298,12 +298,16 @@ static void zend_test_execute_internal(zend_execute_data *execute_data, zval *re } } + ZT_G(observer_nesting_depth)++; + if (zend_test_prev_execute_internal) { zend_test_prev_execute_internal(execute_data, return_value); } else { fbc->internal_function.handler(execute_data, return_value); } + ZT_G(observer_nesting_depth)--; + if (fbc->common.function_name) { if (EG(exception)) { php_printf("%*s\n", 2 * ZT_G(observer_nesting_depth), "", ZSTR_VAL(EG(exception)->ce->name)); diff --git a/ext/zend_test/tests/execute_internal.phpt b/ext/zend_test/tests/execute_internal.phpt index 3983e6f2b8d31..6c7fc75775919 100644 --- a/ext/zend_test/tests/execute_internal.phpt +++ b/ext/zend_test/tests/execute_internal.phpt @@ -14,6 +14,8 @@ function f($a) { f(time() > 0 ? [1, 2, 3] : []); +array_map("count", [[], [1, 2]]); + ?> --EXPECTF-- @@ -23,3 +25,13 @@ f(time() > 0 ? [1, 2, 3] : []); int(6) + + + + + +