From 955da0af319133ccc9e5c31a0a0cfc10ba8e064c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Wed, 14 May 2025 09:52:17 +0200 Subject: [PATCH 1/2] zend_compile: Set `op_array->scope` for Closures as compile time see php/php-src#18546 --- Zend/zend_compile.c | 6 + ext/zend_test/tests/observer_closure_04.phpt | 126 +++++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 ext/zend_test/tests/observer_closure_04.phpt diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 0669d106f15e9..85c893d30c78b 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -8306,6 +8306,12 @@ static zend_op_array *zend_compile_func_decl_ex( if (decl->kind == ZEND_AST_CLOSURE || decl->kind == ZEND_AST_ARROW_FUNC) { op_array->fn_flags |= ZEND_ACC_CLOSURE; + /* Set the closure scope at compile time as an optimization to + * prevent creating a separate runtime cache for every initialization + * of this closure. Most closures are expected not to change their + * scope in practice. + */ + op_array->scope = CG(active_class_entry); } if (is_hook) { diff --git a/ext/zend_test/tests/observer_closure_04.phpt b/ext/zend_test/tests/observer_closure_04.phpt new file mode 100644 index 0000000000000..2178bc578d4d2 --- /dev/null +++ b/ext/zend_test/tests/observer_closure_04.phpt @@ -0,0 +1,126 @@ +--TEST-- +Observer: Closures keep their runtime cache when not rebinding +--EXTENSIONS-- +zend_test +--INI-- +zend_test.observer.enabled=1 +zend_test.observer.show_output=1 +zend_test.observer.observe_all=1 +--FILE-- +getMessage(), PHP_EOL; + } + })->bindTo(null, null))(); + } +} + +$obj = new Foo(); +$obj->static(); +$obj->static(); +$obj->non_static(); +$obj->non_static(); +$obj->rebind(); +$obj->rebind(); + +$closure = function () { + echo Foo::$value, PHP_EOL; +}; + +($closure->bindTo(null, Foo::class))(); +($closure->bindTo(null, Foo::class))(); + +$boundClosure = $closure->bindTo(null, Foo::class); +$boundClosure(); +$boundClosure(); + +?> +--EXPECTF-- + + + + + + +x + + + + +x + + + + + + +x + + + + +x + + + + + + + + + <{closure:Foo::rebind():16}> +Error: + + +Cannot access private property Foo::$value + + + + + + + <{closure:Foo::rebind():16}> +Error: + +Cannot access private property Foo::$value + + + + + + +x + + + + + +x + + + + + +x + + +x + + From 0ec7095b6e72d8afeb9effc7a3e6e6746ed475e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Wed, 14 May 2025 10:21:29 +0200 Subject: [PATCH 2/2] Prevent the scope from affecting `__METHOD__` --- Zend/zend_compile.c | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 85c893d30c78b..7afa220ee08e4 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -8306,12 +8306,6 @@ static zend_op_array *zend_compile_func_decl_ex( if (decl->kind == ZEND_AST_CLOSURE || decl->kind == ZEND_AST_ARROW_FUNC) { op_array->fn_flags |= ZEND_ACC_CLOSURE; - /* Set the closure scope at compile time as an optimization to - * prevent creating a separate runtime cache for every initialization - * of this closure. Most closures are expected not to change their - * scope in practice. - */ - op_array->scope = CG(active_class_entry); } if (is_hook) { @@ -8468,6 +8462,15 @@ static zend_op_array *zend_compile_func_decl_ex( /* Pop the loop variable stack separator */ zend_stack_del_top(&CG(loop_var_stack)); + if (op_array->fn_flags & ZEND_ACC_CLOSURE) { + /* Set the closure scope at compile time as an optimization to + * prevent creating a separate runtime cache for every initialization + * of this closure. Most closures are expected not to change their + * scope in practice. + */ + op_array->scope = CG(active_class_entry); + } + if (level == FUNC_DECL_LEVEL_TOPLEVEL) { zend_observer_function_declared_notify(op_array, lcname); }