From ff5f50e6e8cf95caf7add971a8ce39dfff35af6d Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 10 Jun 2026 18:20:44 +0200 Subject: [PATCH] Serialize closures declared in constant expressions --- NEWS | 5 + UPGRADING | 11 + .../serialization_allowed_classes.phpt | 47 ++ .../serialization_basic.phpt | 84 +++ .../serialization_graph.phpt | 39 ++ .../serialization_inheritance.phpt | 50 ++ .../serialization_invalid_payload.phpt | 67 +++ .../serialization_misc.phpt | 61 ++ .../serialization_named.phpt | 95 +++ .../serialization_nested.phpt | 66 +++ .../serialization_not_allowed.phpt | 86 +++ Zend/zend_ast.c | 6 + Zend/zend_closures.c | 559 +++++++++++++++++- Zend/zend_closures.h | 11 + Zend/zend_closures.stub.php | 7 +- Zend/zend_closures_arginfo.h | 22 +- Zend/zend_compile.c | 1 + Zend/zend_compile.h | 10 +- ext/reflection/php_reflection.c | 50 ++ ext/reflection/php_reflection.stub.php | 4 + ext/reflection/php_reflection_arginfo.h | 18 +- ext/reflection/php_reflection_decl.h | 8 +- .../ReflectionFunction_getConstExprId.phpt | 58 ++ 23 files changed, 1346 insertions(+), 19 deletions(-) create mode 100644 Zend/tests/closures/closure_const_expr/serialization_allowed_classes.phpt create mode 100644 Zend/tests/closures/closure_const_expr/serialization_basic.phpt create mode 100644 Zend/tests/closures/closure_const_expr/serialization_graph.phpt create mode 100644 Zend/tests/closures/closure_const_expr/serialization_inheritance.phpt create mode 100644 Zend/tests/closures/closure_const_expr/serialization_invalid_payload.phpt create mode 100644 Zend/tests/closures/closure_const_expr/serialization_misc.phpt create mode 100644 Zend/tests/closures/closure_const_expr/serialization_named.phpt create mode 100644 Zend/tests/closures/closure_const_expr/serialization_nested.phpt create mode 100644 Zend/tests/closures/closure_const_expr/serialization_not_allowed.phpt create mode 100644 ext/reflection/tests/ReflectionFunction_getConstExprId.phpt diff --git a/NEWS b/NEWS index 4fbc7e89eb11..adeaf98c29bb 100644 --- a/NEWS +++ b/NEWS @@ -5,6 +5,11 @@ PHP NEWS - Core: . Added first-class callable cache to share instances for the duration of the request. (ilutov) + . Closures declared in constant expressions (anonymous closures and + first-class callables) can now be serialized as references to their + declaration site. Added Closure::fromConstExpr(), + ReflectionFunction::getConstExprId() and getConstExprClass(). + (nicolas-grekas) . It is now possible to use reference assign on WeakMap without the key needing to be present beforehand. (ndossche) . Added `clamp()`. (kylekatarnls, thinkverse) diff --git a/UPGRADING b/UPGRADING index 2ec6ce92aa52..a64b1375dcc7 100644 --- a/UPGRADING +++ b/UPGRADING @@ -181,6 +181,17 @@ PHP 8.6 UPGRADE NOTES needing to be present beforehand. . It is now possible to define the `__debugInfo()` magic method on enums. RFC: https://wiki.php.net/rfc/debugable-enums + . Closures declared in constant expressions of a class member (anonymous + closures and first-class callables, of any visibility, in attribute + arguments and parameter default values) can now be serialized and + unserialized. The payload contains no code: it is a reference to the + declaration site (class name, deterministic per-class id, start line), + resolved against the loaded class. Closures created at runtime, bound to + an object, rebound to another scope, or declared in class constant + values, property defaults or outside a class still refuse to serialize. + The unserialize() allowed_classes filter applies to Closure as to any + other class. Added Closure::fromConstExpr($class, $id) plus + ReflectionFunction::getConstExprId()/getConstExprClass() for exporters. - Fileinfo: . finfo_file() now works with remote streams. diff --git a/Zend/tests/closures/closure_const_expr/serialization_allowed_classes.phpt b/Zend/tests/closures/closure_const_expr/serialization_allowed_classes.phpt new file mode 100644 index 000000000000..a3300a8a3d1a --- /dev/null +++ b/Zend/tests/closures/closure_const_expr/serialization_allowed_classes.phpt @@ -0,0 +1,47 @@ +--TEST-- +Serializable closures are gated by the unserialize() allowed_classes filter +--FILE-- +getAttributes(); +$payloads = [ + 'anonymous' => serialize($attrs[0]->getArguments()[0]), + 'fcc site' => serialize($attrs[1]->getArguments()[0]), +]; + +// The recommended safe-unserialize practice (allowed_classes => false) blocks +// every Closure payload: it becomes __PHP_Incomplete_Class and __unserialize() +// is never invoked, exactly like any other object-injection gadget. +foreach ($payloads as $name => $payload) { + $r = unserialize($payload, ['allowed_classes' => false]); + var_dump($name, $r instanceof __PHP_Incomplete_Class); +} + +// A list that does not contain Closure also blocks it. +$r = unserialize($payloads['fcc site'], ['allowed_classes' => ['stdClass']]); +var_dump($r instanceof __PHP_Incomplete_Class); + +// Closure must be explicitly opted in. +$r = unserialize($payloads['fcc site'], ['allowed_classes' => ['Closure']]); +var_dump($r instanceof Closure, $r('test')); + +?> +--EXPECT-- +string(9) "anonymous" +bool(true) +string(8) "fcc site" +bool(true) +bool(true) +bool(true) +int(4) diff --git a/Zend/tests/closures/closure_const_expr/serialization_basic.phpt b/Zend/tests/closures/closure_const_expr/serialization_basic.phpt new file mode 100644 index 000000000000..b8ccaf3d9e4d --- /dev/null +++ b/Zend/tests/closures/closure_const_expr/serialization_basic.phpt @@ -0,0 +1,84 @@ +--TEST-- +Closures in constant expressions are serializable as declaration-site references +--FILE-- + (new ReflectionClass(Demo::class))->getAttributes()[0]->getArguments()[0], + 'const' => (new ReflectionClassConstant(Demo::class, 'FOO'))->getAttributes()[0]->getArguments()[0], + 'prop-1' => (new ReflectionProperty(Demo::class, 'name'))->getAttributes()[0]->getArguments()['cb'][0], + 'prop-2' => (new ReflectionProperty(Demo::class, 'name'))->getAttributes()[0]->getArguments()['cb'][1], + 'method' => (new ReflectionMethod(Demo::class, 'm'))->getAttributes()[0]->getArguments()[0], + 'param' => (new ReflectionParameter([Demo::class, 'm'], 'x'))->getAttributes()[0]->getArguments()[0], + 'case' => (new ReflectionClassConstant(E::class, 'X'))->getAttributes()[0]->getArguments()[0], +]; + +foreach ($closures as $expected => $closure) { + $r = new ReflectionFunction($closure); + $id = $r->getConstExprId(); + $scope = $r->getClosureScopeClass()->name; + + $unserialized = unserialize(serialize($closure)); + $recreated = Closure::fromConstExpr($scope, $id); + + var_dump($expected === $closure() && $expected === $unserialized() && $expected === $recreated()); +} + +// Ids are assigned in canonical walk order: class attributes first, then +// constants, then properties, then methods (including parameters). +$ids = array_map( + static fn ($c) => (new ReflectionFunction($c))->getConstExprId(), + $closures +); +var_dump($ids); + +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +array(7) { + ["class"]=> + int(0) + ["const"]=> + int(1) + ["prop-1"]=> + int(2) + ["prop-2"]=> + int(3) + ["method"]=> + int(4) + ["param"]=> + int(5) + ["case"]=> + int(0) +} diff --git a/Zend/tests/closures/closure_const_expr/serialization_graph.phpt b/Zend/tests/closures/closure_const_expr/serialization_graph.phpt new file mode 100644 index 000000000000..d57e5ca567c0 --- /dev/null +++ b/Zend/tests/closures/closure_const_expr/serialization_graph.phpt @@ -0,0 +1,39 @@ +--TEST-- +Const-expr closures inside serialized object graphs +--FILE-- +c)(), "\n"; + } +} + +$h = new Holder(); +$h->c = (new ReflectionProperty(Demo::class, 'p'))->getAttributes()[0]->getArguments()[0]; + +$u = unserialize(serialize([$h, $h->c, [$h->c]])); + +echo "after: ", ($u[1])(), "\n"; +// Shared instances are preserved within the graph. +var_dump($u[0]->c === $u[1], $u[1] === $u[2][0]); + +?> +--EXPECT-- +wakeup sees: ok +after: ok +bool(true) +bool(true) diff --git a/Zend/tests/closures/closure_const_expr/serialization_inheritance.phpt b/Zend/tests/closures/closure_const_expr/serialization_inheritance.phpt new file mode 100644 index 000000000000..ff3b3ed6683c --- /dev/null +++ b/Zend/tests/closures/closure_const_expr/serialization_inheritance.phpt @@ -0,0 +1,50 @@ +--TEST-- +Serialization of const-expr closures with inheritance and traits +--FILE-- +getAttributes()[0]->getArguments()[0]; +var_dump((new ReflectionFunction($c))->getClosureScopeClass()->name); +$payload = serialize($c); +var_dump(str_contains($payload, '"Base"')); +var_dump(unserialize($payload)()); + +// Attribute on a parameter of a trait method: the copied method is scoped +// to the using class and the reference resolves through it. +$c = (new ReflectionParameter([UsesTrait::class, 'm'], 'x'))->getAttributes()[0]->getArguments()[0]; +var_dump((new ReflectionFunction($c))->getClosureScopeClass()->name); +$u = unserialize(serialize($c)); +var_dump($u()); + +?> +--EXPECT-- +string(4) "Base" +bool(true) +string(4) "base" +string(9) "UsesTrait" +string(5) "trait" diff --git a/Zend/tests/closures/closure_const_expr/serialization_invalid_payload.phpt b/Zend/tests/closures/closure_const_expr/serialization_invalid_payload.phpt new file mode 100644 index 000000000000..7660328435be --- /dev/null +++ b/Zend/tests/closures/closure_const_expr/serialization_invalid_payload.phpt @@ -0,0 +1,67 @@ +--TEST-- +Unserializing invalid or stale Closure declaration-site references +--FILE-- +getAttributes()[0]->getArguments()[0]; +$r = new ReflectionFunction($closure); +$id = $r->getConstExprId(); +$line = $r->getStartLine(); + +$mk = static fn (string $class, int $id, int $line) => sprintf( + 'O:7:"Closure":3:{s:5:"class";s:%d:"%s";s:2:"id";i:%d;s:4:"line";i:%d;}', + strlen($class), $class, $id, $line +); + +// Sanity check: a valid reference works. +var_dump(unserialize($mk('Demo', $id, $line))()); + +$payloads = [ + 'empty data' => 'O:7:"Closure":0:{}', + 'missing keys' => 'O:7:"Closure":1:{s:5:"class";s:4:"Demo";}', + 'wrong types' => 'O:7:"Closure":3:{s:5:"class";s:4:"Demo";s:2:"id";s:1:"0";s:4:"line";i:1;}', + 'unknown class' => $mk('NoSuchClass', $id, $line), + 'internal class' => $mk('stdClass', $id, $line), + 'unknown id' => $mk('Demo', 999, $line), + 'negative id' => $mk('Demo', -1, $line), + 'stale line' => $mk('Demo', $id, $line + 1), +]; + +foreach ($payloads as $name => $payload) { + try { + unserialize($payload); + echo "$name: unserialized!?\n"; + } catch (Exception $e) { + echo "$name: {$e->getMessage()}\n"; + } +} + +// __unserialize() cannot be used to reinitialize a live closure. +try { + $closure->__unserialize(['class' => 'Demo', 'id' => $id, 'line' => $line]); +} catch (Exception $e) { + echo $e->getMessage(), "\n"; +} + +?> +--EXPECT-- +string(2) "ok" +empty data: Invalid serialization data for Closure object +missing keys: Invalid serialization data for Closure object +wrong types: Invalid serialization data for Closure object +unknown class: Invalid serialization data for Closure object (cannot load class "NoSuchClass") +internal class: Invalid serialization data for Closure object (cannot load class "stdClass") +unknown id: Invalid serialization data for Closure object (constant-expression closure 999 of class Demo not found) +negative id: Invalid serialization data for Closure object (constant-expression closure -1 of class Demo not found) +stale line: Invalid serialization data for Closure object (constant-expression closure 0 of class Demo not found) +Cannot unserialize an already initialized Closure diff --git a/Zend/tests/closures/closure_const_expr/serialization_misc.phpt b/Zend/tests/closures/closure_const_expr/serialization_misc.phpt new file mode 100644 index 000000000000..60818108b069 --- /dev/null +++ b/Zend/tests/closures/closure_const_expr/serialization_misc.phpt @@ -0,0 +1,61 @@ +--TEST-- +Misc behaviors of serializable const-expr closures: static vars, identity, scope binding, fromConstExpr() errors +--FILE-- +getAttributes(); +$counter = $attributes[0]->getArguments()[0]; +$scoped = $attributes[1]->getArguments()[0]; + +// A reference does not carry the state of static variables: unserializing +// produces the closure as if it was freshly evaluated. +var_dump($counter(), $counter()); +$u = unserialize(serialize($counter)); +var_dump($u()); + +// Unserialized closures are new instances. +var_dump($u === $counter); + +// Scope binding is restored: private members of the class are accessible. +var_dump(unserialize(serialize($scoped))()); + +// fromConstExpr() error cases +try { + Closure::fromConstExpr('NoSuchClass', 0); +} catch (Error $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} +try { + Closure::fromConstExpr('stdClass', 0); +} catch (ValueError $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} +try { + Closure::fromConstExpr('Demo', 999); +} catch (ValueError $e) { + echo get_class($e), ': ', $e->getMessage(), "\n"; +} + +?> +--EXPECT-- +int(1) +int(2) +int(1) +bool(false) +string(6) "secret" +Error: Class "NoSuchClass" not found +ValueError: Closure::fromConstExpr(): Argument #2 ($id) does not refer to a constant-expression closure of class stdClass +ValueError: Closure::fromConstExpr(): Argument #2 ($id) does not refer to a constant-expression closure of class Demo diff --git a/Zend/tests/closures/closure_const_expr/serialization_named.phpt b/Zend/tests/closures/closure_const_expr/serialization_named.phpt new file mode 100644 index 000000000000..51056621e89d --- /dev/null +++ b/Zend/tests/closures/closure_const_expr/serialization_named.phpt @@ -0,0 +1,95 @@ +--TEST-- +First-class callable references in constant expressions serialize as declaration sites +--FILE-- +getAttributes(); +foreach ($attrs as $i => $attr) { + $closure = $attr->getArguments()[0]; + $payload = serialize($closure); + $u = unserialize($payload); + $args = $i === 4 ? ['abcd'] : []; + // The payload references the declaring class, ids are stable, and the + // recreated closure behaves identically (including static:: binding). + var_dump( + str_contains($payload, '"Demo"') + && $u(...$args) === $closure(...$args) + && serialize($u) === $payload + ); +} + +// parent:: and self:: forms resolve their distinct static:: bindings. +var_dump(unserialize(serialize($attrs[1]->getArguments()[0]))()); +var_dump(unserialize(serialize($attrs[2]->getArguments()[0]))()); + +// Runtime-created named closures are not declaration sites and refuse, +// even when an identical reference exists in an attribute. +foreach ([strlen(...), Validators::check(...), Closure::fromCallable('strlen')] as $closure) { + try { + serialize($closure); + echo "serialized!?\n"; + } catch (Exception $e) { + echo $e->getMessage(), "\n"; + } +} + +// The name-based payload format does not exist: only site references resolve. +foreach ([ + 'O:7:"Closure":1:{s:8:"function";s:6:"strlen";}', + 'O:7:"Closure":2:{s:5:"class";s:10:"Validators";s:6:"method";s:5:"check";}', +] as $payload) { + try { + unserialize($payload); + echo "unserialized!?\n"; + } catch (Exception $e) { + echo $e->getMessage(), "\n"; + } +} + +} +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +string(9) "prot-Demo" +string(9) "prot-Base" +Serialization of 'Closure' is not allowed +Serialization of 'Closure' is not allowed +Serialization of 'Closure' is not allowed +Invalid serialization data for Closure object +Invalid serialization data for Closure object diff --git a/Zend/tests/closures/closure_const_expr/serialization_nested.phpt b/Zend/tests/closures/closure_const_expr/serialization_nested.phpt new file mode 100644 index 000000000000..9f400137c812 --- /dev/null +++ b/Zend/tests/closures/closure_const_expr/serialization_nested.phpt @@ -0,0 +1,66 @@ +--TEST-- +Serialization of const-expr closures in parameter defaults, hooks and nested functions +--FILE-- + $this->v; + } + + public function withDefault($cb = static function () { return 'default'; }) { + return $cb; + } + + public function makeClosure() { + return #[A(static function () { return 'inner'; })] static function () {}; + } +} + +class Nested { + #[A(static function ( + #[A(static function () { return 'deep'; })] $x = null, + ) { return 'outer'; })] + public int $p = 0; +} + +$roundtrip = static function (Closure $c) { + $u = unserialize(serialize($c)); + var_dump($u(), (new ReflectionFunction($c))->getConstExprId()); +}; + +// Attribute on a property hook +$hook = (new ReflectionProperty(Demo::class, 'v'))->getHook(PropertyHookType::Get); +$roundtrip($hook->getAttributes()[0]->getArguments()[0]); + +// Parameter default value +$roundtrip((new Demo)->withDefault()); + +// Attribute on a runtime closure declared in a method body +$runtime = (new Demo)->makeClosure(); +$roundtrip((new ReflectionFunction($runtime))->getAttributes()[0]->getArguments()[0]); + +// Const-expr closure nested in the parameter attribute of another one +$outer = (new ReflectionProperty(Nested::class, 'p'))->getAttributes()[0]->getArguments()[0]; +$inner = (new ReflectionFunction($outer))->getParameters()[0]->getAttributes()[0]->getArguments()[0]; +$roundtrip($outer); +$roundtrip($inner); + +?> +--EXPECT-- +string(8) "get-hook" +int(0) +string(7) "default" +int(1) +string(5) "inner" +int(2) +string(5) "outer" +int(0) +string(4) "deep" +int(1) diff --git a/Zend/tests/closures/closure_const_expr/serialization_not_allowed.phpt b/Zend/tests/closures/closure_const_expr/serialization_not_allowed.phpt new file mode 100644 index 000000000000..fbcef956cf1c --- /dev/null +++ b/Zend/tests/closures/closure_const_expr/serialization_not_allowed.phpt @@ -0,0 +1,86 @@ +--TEST-- +Closures that are not addressable by a declaration-site reference still refuse to serialize +--FILE-- +getAttributes()[0]->getArguments()[0]; + +$cases = [ + 'runtime closure' => function () {}, + 'static runtime closure' => static function () {}, + 'arrow function' => fn () => 1, + 'runtime function callable' => strlen(...), + 'runtime method callable' => Demo::privateFcc(...), + 'bound method callable' => (new Demo)->boundMethod(...), + 'private method callable' => Demo::privateFcc(), + '__callStatic trampoline' => Demo::someUndefinedMethod(...), + 'class constant value' => Demo::BAD, + 'property default' => (new Demo)->bad, + 'rebound scope' => Closure::bind($attrClosure, null, A::class), + 'free function attribute' => (new ReflectionFunction('freeFunction'))->getAttributes()[0]->getArguments()[0], + 'anonymous class attribute' => (new ReflectionClass($anon))->getAttributes()[0]->getArguments()[0], +]; + +foreach ($cases as $name => $closure) { + try { + serialize($closure); + echo "$name: serialized!?\n"; + } catch (Exception $e) { + echo "$name: {$e->getMessage()}\n"; + } + var_dump((new ReflectionFunction($closure))->getConstExprId()); +} + +?> +--EXPECT-- +runtime closure: Serialization of 'Closure' is not allowed +NULL +static runtime closure: Serialization of 'Closure' is not allowed +NULL +arrow function: Serialization of 'Closure' is not allowed +NULL +runtime function callable: Serialization of 'Closure' is not allowed +NULL +runtime method callable: Serialization of 'Closure' is not allowed +NULL +bound method callable: Serialization of 'Closure' is not allowed +NULL +private method callable: Serialization of 'Closure' is not allowed +NULL +__callStatic trampoline: Serialization of 'Closure' is not allowed +NULL +class constant value: Serialization of 'Closure' is not allowed +NULL +property default: Serialization of 'Closure' is not allowed +NULL +rebound scope: Serialization of 'Closure' is not allowed +NULL +free function attribute: Serialization of 'Closure' is not allowed +NULL +anonymous class attribute: Serialization of 'Closure' is not allowed +NULL diff --git a/Zend/zend_ast.c b/Zend/zend_ast.c index a7e26711cd17..4788a7b69b3d 100644 --- a/Zend/zend_ast.c +++ b/Zend/zend_ast.c @@ -1244,6 +1244,12 @@ static zend_result ZEND_FASTCALL zend_ast_evaluate_inner( zend_create_fake_closure(result, fptr, fptr->common.scope, called_scope, NULL); + if (scope) { + /* Remember the declaration site, so that the closure can be + * serialized as a reference to it. */ + zend_closure_mark_as_constexpr_fcc(result, scope); + } + return SUCCESS; } case ZEND_AST_OP_ARRAY: diff --git a/Zend/zend_closures.c b/Zend/zend_closures.c index 840d2dbe32e1..b46a309303e7 100644 --- a/Zend/zend_closures.c +++ b/Zend/zend_closures.c @@ -19,8 +19,11 @@ #include "zend.h" #include "zend_API.h" +#include "zend_ast.h" +#include "zend_attributes.h" #include "zend_closures.h" #include "zend_exceptions.h" +#include "zend_execute.h" #include "zend_interfaces.h" #include "zend_objects.h" #include "zend_objects_API.h" @@ -33,6 +36,10 @@ typedef struct _zend_closure { zval this_ptr; zend_class_entry *called_scope; zif_handler orig_internal_handler; + /* The class whose constant expressions declared this closure, when it was + * created by evaluating a first-class callable in one of them + * (ZEND_ACC2_CONSTEXPR_FCC). */ + zend_class_entry *constexpr_site; } zend_closure; /* non-static since it needs to be referenced */ @@ -40,6 +47,8 @@ ZEND_API zend_class_entry *zend_ce_closure; static zend_object_handlers closure_handlers; static zend_result zend_closure_get_closure(zend_object *obj, zend_class_entry **ce_ptr, zend_function **fptr_ptr, zend_object **obj_ptr, bool check_only); +static ZEND_COLD ZEND_NAMED_FUNCTION(zend_closure_uninitialized_handler); +static void zend_closure_init_ex(zend_closure *closure, zend_function *func, zend_class_entry *scope, zend_class_entry *called_scope, zval *this_ptr, bool is_fake); ZEND_METHOD(Closure, __invoke) /* {{{ */ { @@ -448,6 +457,524 @@ ZEND_METHOD(Closure, getCurrent) RETURN_OBJ_COPY(obj); } +/* {{{ Closures declared in constant expressions + * + * Closures declared in constant expressions are static, capture no variables + * and are fully described by their compiled op_array. This makes them + * addressable by (class, id), where the id is the position of the closure in + * a canonical walk over all constant expression ASTs that are reachable from + * the class and that are not subject to in-place evaluation: attribute + * arguments and parameter default values, including those of nested + * functions. Class constant values and property default values are evaluated + * (and their AST freed) in place, so closures declared there are not + * addressable this way. + * + * The walk visits candidates in a deterministic order that only depends on + * the compiled class, not on runtime evaluation state, so ids computed by one + * process can be resolved by another process running the same code. + */ + +typedef struct { + uint32_t next_id; + /* When by_id is true, search for target_id. Otherwise search for the + * op_array of an anonymous closure identified by its opcodes, or for a + * first-class callable reference matching the fcc closure. */ + bool by_id; + uint32_t target_id; + const zend_op *opcodes; + const zend_closure *fcc; + /* The class being walked; used to resolve self/parent references. */ + zend_class_entry *site_class; + /* The found site: either a ZEND_AST_OP_ARRAY node, or a ZEND_AST_CALL / + * ZEND_AST_STATIC_CALL node whose argument list is a first-class callable + * placeholder. */ + zend_ast *found; + uint32_t found_id; +} zend_constexpr_closure_walk; + +static bool zend_constexpr_closure_walk_op_array(zend_constexpr_closure_walk *w, zend_op_array *op_array); + +/* Whether the named function resolved from a first-class callable site is the + * one the closure was created from. */ +static bool zend_closure_func_matches(const zend_closure *closure, const zend_function *resolved) +{ + if (resolved->type != closure->func.type) { + return false; + } + if (resolved->type == ZEND_USER_FUNCTION) { + return resolved->op_array.opcodes == closure->func.op_array.opcodes; + } + /* The handler of internal fake closures is wrapped, the original one is + * saved in orig_internal_handler. This also rejects __call/__callstatic + * trampolines, whose handler is not the resolved function. */ + return resolved->internal_function.handler == closure->orig_internal_handler; +} + +/* Whether the first-class callable declared by the given site resolves to the + * function the closure was created from. Resolution is done by name and + * pointer comparisons only: no autoloading, no user code, no exceptions. */ +static bool zend_constexpr_fcc_matches(const zend_constexpr_closure_walk *w, const zend_ast *ast) +{ + const zend_closure *closure = w->fcc; + const zend_function *resolved = NULL; + zend_string *lcname; + + if (!closure) { + return false; + } + + if (ast->kind == ZEND_AST_STATIC_CALL) { + const zend_ast *class_ast = ast->child[0]; + zend_class_entry *ref_ce; + + if (!closure->func.common.scope || !closure->called_scope) { + return false; + } + + switch (class_ast->attr >> ZEND_CONST_EXPR_NEW_FETCH_TYPE_SHIFT) { + case ZEND_FETCH_CLASS_SELF: + ref_ce = w->site_class; + break; + case ZEND_FETCH_CLASS_PARENT: + ref_ce = w->site_class->parent; + break; + default: + ref_ce = zend_string_equals_ci(zend_ast_get_str((zend_ast *) class_ast), closure->called_scope->name) + ? closure->called_scope : NULL; + break; + } + if (!ref_ce || ref_ce != closure->called_scope) { + return false; + } + + lcname = zend_string_tolower(zend_ast_get_str(ast->child[1])); + resolved = zend_hash_find_ptr(&ref_ce->function_table, lcname); + zend_string_release_ex(lcname, 0); + } else { + ZEND_ASSERT(ast->kind == ZEND_AST_CALL); + + if (closure->func.common.scope || closure->called_scope) { + return false; + } + + lcname = zend_string_tolower(zend_ast_get_str(ast->child[0])); + resolved = zend_hash_find_ptr(EG(function_table), lcname); + if (!resolved && ast->child[0]->attr != ZEND_NAME_FQ) { + const char *backslash = zend_memrchr(ZSTR_VAL(lcname), '\\', ZSTR_LEN(lcname)); + if (backslash) { + resolved = zend_hash_str_find_ptr(EG(function_table), + backslash + 1, ZSTR_LEN(lcname) - (backslash - ZSTR_VAL(lcname) + 1)); + } + } + zend_string_release_ex(lcname, 0); + } + + return resolved && zend_closure_func_matches(closure, resolved); +} + +static bool zend_constexpr_closure_visit_op_array(zend_constexpr_closure_walk *w, zend_ast *ast) +{ + zend_op_array *op_array = zend_ast_get_op_array(ast)->op_array; + uint32_t id = w->next_id++; + + if (w->by_id ? w->target_id == id : (w->opcodes && w->opcodes == op_array->opcodes)) { + w->found = ast; + w->found_id = id; + return true; + } + + /* The closure may itself declare closures in its attributes or in its + * parameter default values. */ + return zend_constexpr_closure_walk_op_array(w, op_array); +} + +static bool zend_constexpr_closure_walk_ast(zend_constexpr_closure_walk *w, zend_ast *ast) +{ + if (!ast) { + return false; + } + + switch (ast->kind) { + case ZEND_AST_OP_ARRAY: + return zend_constexpr_closure_visit_op_array(w, ast); + case ZEND_AST_ZVAL: + case ZEND_AST_CONSTANT: + return false; + case ZEND_AST_CALL: + case ZEND_AST_STATIC_CALL: { + zend_ast *args = ast->kind == ZEND_AST_CALL ? ast->child[1] : ast->child[2]; + + /* In constant expressions, calls only exist in their first-class + * callable form: each one is a closure declaration site. */ + if (args && args->kind == ZEND_AST_CALLABLE_CONVERT) { + uint32_t id = w->next_id++; + + if (w->by_id ? w->target_id == id : zend_constexpr_fcc_matches(w, ast)) { + w->found = ast; + w->found_id = id; + return true; + } + return false; + } + break; + } + case ZEND_AST_CALLABLE_CONVERT: + /* Visited through its enclosing call node. */ + return false; + default: + break; + } + + if (zend_ast_is_list(ast)) { + zend_ast_list *list = zend_ast_get_list(ast); + for (uint32_t i = 0; i < list->children; i++) { + if (zend_constexpr_closure_walk_ast(w, list->child[i])) { + return true; + } + } + return false; + } + + if (zend_ast_is_decl(ast)) { + /* Closure declarations in constant expressions are compiled to + * ZEND_AST_OP_ARRAY nodes, so declarations cannot appear here. */ + ZEND_UNREACHABLE(); + return false; + } + + uint32_t children = zend_ast_get_num_children(ast); + for (uint32_t i = 0; i < children; i++) { + if (zend_constexpr_closure_walk_ast(w, ast->child[i])) { + return true; + } + } + return false; +} + +static bool zend_constexpr_closure_walk_zval(zend_constexpr_closure_walk *w, zval *zv) +{ + if (Z_TYPE_P(zv) == IS_CONSTANT_AST) { + return zend_constexpr_closure_walk_ast(w, Z_ASTVAL_P(zv)); + } + return false; +} + +static bool zend_constexpr_closure_walk_attributes(zend_constexpr_closure_walk *w, HashTable *attributes) +{ + zend_attribute *attr; + + if (!attributes) { + return false; + } + + ZEND_HASH_FOREACH_PTR(attributes, attr) { + for (uint32_t i = 0; i < attr->argc; i++) { + if (zend_constexpr_closure_walk_zval(w, &attr->args[i].value)) { + return true; + } + } + } ZEND_HASH_FOREACH_END(); + + return false; +} + +static bool zend_constexpr_closure_walk_op_array(zend_constexpr_closure_walk *w, zend_op_array *op_array) +{ + if (op_array->type != ZEND_USER_FUNCTION) { + return false; + } + + if (zend_constexpr_closure_walk_attributes(w, op_array->attributes)) { + return true; + } + + /* Parameter default values are IS_CONSTANT_AST literals used by RECV_INIT. */ + for (uint32_t i = 0; i < op_array->last_literal; i++) { + if (zend_constexpr_closure_walk_zval(w, &op_array->literals[i])) { + return true; + } + } + + for (uint32_t i = 0; i < op_array->num_dynamic_func_defs; i++) { + if (zend_constexpr_closure_walk_op_array(w, op_array->dynamic_func_defs[i])) { + return true; + } + } + + return false; +} + +static bool zend_constexpr_closure_walk_class(zend_constexpr_closure_walk *w, zend_class_entry *ce) +{ + zend_class_constant *c; + zend_property_info *prop_info; + zend_function *func; + + if (ce->type != ZEND_USER_CLASS) { + return false; + } + + if (zend_constexpr_closure_walk_attributes(w, ce->attributes)) { + return true; + } + + ZEND_HASH_MAP_FOREACH_PTR(&ce->constants_table, c) { + if (zend_constexpr_closure_walk_attributes(w, c->attributes)) { + return true; + } + } ZEND_HASH_FOREACH_END(); + + ZEND_HASH_MAP_FOREACH_PTR(&ce->properties_info, prop_info) { + if (zend_constexpr_closure_walk_attributes(w, prop_info->attributes)) { + return true; + } + if (prop_info->hooks) { + for (uint32_t i = 0; i < ZEND_PROPERTY_HOOK_COUNT; i++) { + if (prop_info->hooks[i] + && zend_constexpr_closure_walk_op_array(w, &prop_info->hooks[i]->op_array)) { + return true; + } + } + } + } ZEND_HASH_FOREACH_END(); + + ZEND_HASH_MAP_FOREACH_PTR(&ce->function_table, func) { + if (zend_constexpr_closure_walk_op_array(w, &func->op_array)) { + return true; + } + } ZEND_HASH_FOREACH_END(); + + return false; +} + +ZEND_API zend_ast *zend_constexpr_closure_site_by_id(zend_class_entry *ce, zend_long id) +{ + zend_constexpr_closure_walk w; + + if (id < 0 || (zend_ulong) id > UINT32_MAX || ce->type != ZEND_USER_CLASS) { + return NULL; + } + + memset(&w, 0, sizeof(w)); + w.by_id = true; + w.target_id = (uint32_t) id; + w.site_class = ce; + + if (!zend_constexpr_closure_walk_class(&w, ce)) { + return NULL; + } + + return w.found; +} + +static uint32_t zend_constexpr_closure_site_lineno(const zend_ast *site) +{ + if (site->kind == ZEND_AST_OP_ARRAY) { + return zend_ast_get_op_array((zend_ast *) site)->op_array->line_start; + } + return zend_ast_get_lineno((zend_ast *) site); +} + +ZEND_API zend_result zend_constexpr_closure_ref(zend_object *closure_obj, zend_class_entry **ce, uint32_t *id, uint32_t *lineno) +{ + const zend_closure *closure = (const zend_closure *) closure_obj; + zend_constexpr_closure_walk w; + zend_class_entry *site_class; + + memset(&w, 0, sizeof(w)); + + if (closure->func.type == ZEND_USER_FUNCTION + && (closure->func.common.fn_flags2 & ZEND_ACC2_CONSTEXPR_CLOSURE) + && !(closure->func.common.fn_flags & ZEND_ACC_FAKE_CLOSURE)) { + /* Anonymous closure declared in a constant expression: identified by + * its op_array, found in the class it is scoped to. */ + site_class = closure->func.common.scope; + w.opcodes = closure->func.op_array.opcodes; + } else if ((closure->func.common.fn_flags2 & ZEND_ACC2_CONSTEXPR_FCC) + && (closure->func.common.fn_flags & ZEND_ACC_FAKE_CLOSURE) + && Z_TYPE(closure->this_ptr) == IS_UNDEF) { + /* First-class callable evaluated in a constant expression: identified + * by its target, found in the class that declared the reference. */ + site_class = closure->constexpr_site; + w.fcc = closure; + } else { + return FAILURE; + } + + if (!site_class || site_class->type != ZEND_USER_CLASS || (site_class->ce_flags & ZEND_ACC_ANON_CLASS)) { + return FAILURE; + } + + w.site_class = site_class; + + if (!zend_constexpr_closure_walk_class(&w, site_class)) { + return FAILURE; + } + + *ce = site_class; + *id = w.found_id; + if (lineno) { + *lineno = zend_constexpr_closure_site_lineno(w.found); + } + return SUCCESS; +} + +ZEND_API void zend_closure_mark_as_constexpr_fcc(zval *closure_zv, zend_class_entry *site_class) +{ + zend_closure *closure = (zend_closure *) Z_OBJ_P(closure_zv); + + ZEND_ASSERT(closure->func.common.fn_flags & ZEND_ACC_FAKE_CLOSURE); + closure->func.common.fn_flags2 |= ZEND_ACC2_CONSTEXPR_FCC; + closure->constexpr_site = site_class; +} +/* }}} */ + +/* {{{ Serialize a closure as a reference to the declaration site of the + constant expression that declared it: either an anonymous closure + declaration, or a first-class callable reference */ +ZEND_METHOD(Closure, __serialize) +{ + zend_class_entry *ce; + uint32_t id, lineno; + + ZEND_PARSE_PARAMETERS_NONE(); + + if (zend_constexpr_closure_ref(Z_OBJ_P(ZEND_THIS), &ce, &id, &lineno) == FAILURE) { + zend_throw_exception(NULL, "Serialization of 'Closure' is not allowed", 0); + RETURN_THROWS(); + } + + array_init(return_value); + add_assoc_str(return_value, "class", zend_string_copy(ce->name)); + add_assoc_long(return_value, "id", id); + add_assoc_long(return_value, "line", lineno); +} +/* }}} */ + +/* {{{ Recreate a closure from a reference to its declaration site */ +ZEND_METHOD(Closure, __unserialize) +{ + zend_closure *closure = (zend_closure *) Z_OBJ_P(ZEND_THIS); + HashTable *data; + zval *z_class, *z_id, *z_line; + zend_class_entry *ce; + zend_ast *site; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_ARRAY_HT(data) + ZEND_PARSE_PARAMETERS_END(); + + if (closure->func.type != ZEND_INTERNAL_FUNCTION + || closure->func.internal_function.handler != zend_closure_uninitialized_handler) { + zend_throw_exception(NULL, "Cannot unserialize an already initialized Closure", 0); + RETURN_THROWS(); + } + + z_class = zend_hash_str_find(data, ZEND_STRL("class")); + z_id = zend_hash_str_find(data, ZEND_STRL("id")); + z_line = zend_hash_str_find(data, ZEND_STRL("line")); + if (z_class) { + ZVAL_DEREF(z_class); + } + if (z_id) { + ZVAL_DEREF(z_id); + } + if (z_line) { + ZVAL_DEREF(z_line); + } + if (!z_class || Z_TYPE_P(z_class) != IS_STRING + || !z_id || Z_TYPE_P(z_id) != IS_LONG + || !z_line || Z_TYPE_P(z_line) != IS_LONG) { + zend_throw_exception(NULL, "Invalid serialization data for Closure object", 0); + RETURN_THROWS(); + } + + ce = zend_lookup_class(Z_STR_P(z_class)); + if (!ce || ce->type != ZEND_USER_CLASS) { + if (!EG(exception)) { + zend_throw_exception_ex(NULL, 0, + "Invalid serialization data for Closure object (cannot load class \"%s\")", + Z_STRVAL_P(z_class)); + } + RETURN_THROWS(); + } + + site = zend_constexpr_closure_site_by_id(ce, Z_LVAL_P(z_id)); + if (!site || (zend_long) zend_constexpr_closure_site_lineno(site) != Z_LVAL_P(z_line)) { + zend_throw_exception_ex(NULL, 0, + "Invalid serialization data for Closure object (constant-expression closure " ZEND_LONG_FMT " of class %s not found)", + Z_LVAL_P(z_id), ZSTR_VAL(ce->name)); + RETURN_THROWS(); + } + + if (site->kind == ZEND_AST_OP_ARRAY) { + zend_closure_init_ex(closure, + (zend_function *) zend_ast_get_op_array(site)->op_array, ce, ce, NULL, /* is_fake */ false); + } else { + /* First-class callable site: re-evaluate it like attribute evaluation + * would, including its visibility checks against the site class. */ + zval tmp; + zend_closure *tmp_closure; + + if (zend_ast_evaluate(&tmp, site, ce) == FAILURE) { + if (!EG(exception)) { + zend_throw_exception(NULL, "Invalid serialization data for Closure object", 0); + } + RETURN_THROWS(); + } + + ZEND_ASSERT(Z_TYPE(tmp) == IS_OBJECT && Z_OBJCE(tmp) == zend_ce_closure); + tmp_closure = (zend_closure *) Z_OBJ(tmp); + + zend_closure_init_ex(closure, &tmp_closure->func, + tmp_closure->func.common.scope, tmp_closure->called_scope, NULL, /* is_fake */ true); + closure->func.common.fn_flags |= ZEND_ACC_FAKE_CLOSURE; + closure->constexpr_site = tmp_closure->constexpr_site; + GC_ADD_FLAGS(&closure->std, GC_NOT_COLLECTABLE); + + zval_ptr_dtor(&tmp); + } +} +/* }}} */ + +/* {{{ Recreate a closure declared in a constant expression of the given class */ +ZEND_METHOD(Closure, fromConstExpr) +{ + zend_string *class_name; + zend_long id; + zend_class_entry *ce; + zend_ast *site; + + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_STR(class_name) + Z_PARAM_LONG(id) + ZEND_PARSE_PARAMETERS_END(); + + ce = zend_lookup_class(class_name); + if (!ce) { + if (!EG(exception)) { + zend_throw_error(NULL, "Class \"%s\" not found", ZSTR_VAL(class_name)); + } + RETURN_THROWS(); + } + + site = zend_constexpr_closure_site_by_id(ce, id); + if (!site) { + zend_value_error("Closure::fromConstExpr(): Argument #2 ($id) does not refer to a constant-expression closure of class %s", ZSTR_VAL(ce->name)); + RETURN_THROWS(); + } + + if (site->kind == ZEND_AST_OP_ARRAY) { + zend_create_closure(return_value, (zend_function *) zend_ast_get_op_array(site)->op_array, ce, ce, NULL); + } else if (zend_ast_evaluate(return_value, site, ce) == FAILURE) { + if (!EG(exception)) { + zend_value_error("Closure::fromConstExpr(): Argument #2 ($id) does not refer to a constant-expression closure of class %s", ZSTR_VAL(ce->name)); + } + RETURN_THROWS(); + } +} +/* }}} */ + static ZEND_COLD zend_function *zend_closure_get_constructor(zend_object *object) /* {{{ */ { zend_throw_error(NULL, "Instantiation of class Closure is not allowed"); @@ -570,6 +1097,12 @@ static void zend_closure_free_storage(zend_object *object) /* {{{ */ } /* }}} */ +static ZEND_COLD ZEND_NAMED_FUNCTION(zend_closure_uninitialized_handler) /* {{{ */ +{ + zend_throw_error(NULL, "Cannot call an uninitialized Closure"); +} +/* }}} */ + static zend_object *zend_closure_new(zend_class_entry *class_type) /* {{{ */ { zend_closure *closure; @@ -579,6 +1112,17 @@ static zend_object *zend_closure_new(zend_class_entry *class_type) /* {{{ */ zend_object_std_init(&closure->std, class_type); + /* Initialize the function as a safe placeholder. Until the closure is + * actually initialized through zend_closure_init_ex() it may be observed + * by userland code (e.g. while delayed __unserialize() calls are still + * pending) and must not crash any object handler. The placeholder mimics + * a fake internal closure with a handler that always throws. */ + closure->func.type = ZEND_INTERNAL_FUNCTION; + closure->func.common.fn_flags = ZEND_ACC_PUBLIC | ZEND_ACC_CLOSURE | ZEND_ACC_FAKE_CLOSURE; + closure->func.common.function_name = ZSTR_EMPTY_ALLOC(); + closure->func.internal_function.handler = zend_closure_uninitialized_handler; + closure->orig_internal_handler = zend_closure_uninitialized_handler; + return (zend_object*)closure; } /* }}} */ @@ -590,6 +1134,7 @@ static zend_object *zend_closure_clone(zend_object *zobject) /* {{{ */ zend_create_closure(&result, &closure->func, closure->func.common.scope, closure->called_scope, &closure->this_ptr); + ((zend_closure *) Z_OBJ(result))->constexpr_site = closure->constexpr_site; return Z_OBJ(result); } /* }}} */ @@ -757,15 +1302,10 @@ static ZEND_NAMED_FUNCTION(zend_closure_internal_handler) /* {{{ */ } /* }}} */ -static void zend_create_closure_ex(zval *res, zend_function *func, zend_class_entry *scope, zend_class_entry *called_scope, zval *this_ptr, bool is_fake) /* {{{ */ +static void zend_closure_init_ex(zend_closure *closure, zend_function *func, zend_class_entry *scope, zend_class_entry *called_scope, zval *this_ptr, bool is_fake) /* {{{ */ { - zend_closure *closure; void *ptr; - object_init_ex(res, zend_ce_closure); - - closure = (zend_closure *)Z_OBJ_P(res); - if ((scope == NULL) && this_ptr && (Z_TYPE_P(this_ptr) != IS_UNDEF)) { /* use dummy scope if we're binding an object without specifying a scope */ /* maybe it would be better to create one for this purpose */ @@ -859,6 +1399,13 @@ static void zend_create_closure_ex(zval *res, zend_function *func, zend_class_en } /* }}} */ +static void zend_create_closure_ex(zval *res, zend_function *func, zend_class_entry *scope, zend_class_entry *called_scope, zval *this_ptr, bool is_fake) /* {{{ */ +{ + object_init_ex(res, zend_ce_closure); + zend_closure_init_ex((zend_closure *) Z_OBJ_P(res), func, scope, called_scope, this_ptr, is_fake); +} +/* }}} */ + ZEND_API void zend_create_closure(zval *res, zend_function *func, zend_class_entry *scope, zend_class_entry *called_scope, zval *this_ptr) { zend_create_closure_ex(res, func, scope, called_scope, this_ptr, diff --git a/Zend/zend_closures.h b/Zend/zend_closures.h index 2ff4934f2a3a..0b97af21f723 100644 --- a/Zend/zend_closures.h +++ b/Zend/zend_closures.h @@ -40,6 +40,17 @@ ZEND_API zend_function *zend_get_closure_invoke_method(zend_object *obj); ZEND_API const zend_function *zend_get_closure_method_def(zend_object *obj); ZEND_API zval* zend_get_closure_this_ptr(zval *obj); +/* Closures declared in constant expressions (anonymous declarations as well + * as first-class callable references) are addressable by a canonical + * per-class id (see zend_closures.c for the definition of the walk). + * zend_constexpr_closure_site_by_id() returns the declaration site for an id: + * either a ZEND_AST_OP_ARRAY node or a ZEND_AST_CALL/ZEND_AST_STATIC_CALL + * node in first-class callable form. zend_constexpr_closure_ref() returns the + * reference for a Closure object, when it has one. */ +ZEND_API zend_ast *zend_constexpr_closure_site_by_id(zend_class_entry *ce, zend_long id); +ZEND_API zend_result zend_constexpr_closure_ref(zend_object *closure_obj, zend_class_entry **ce, uint32_t *id, uint32_t *lineno); +ZEND_API void zend_closure_mark_as_constexpr_fcc(zval *closure_zv, zend_class_entry *site_class); + END_EXTERN_C() #endif diff --git a/Zend/zend_closures.stub.php b/Zend/zend_closures.stub.php index 46b51617eef9..527d4f82e75b 100644 --- a/Zend/zend_closures.stub.php +++ b/Zend/zend_closures.stub.php @@ -4,7 +4,6 @@ /** * @strict-properties - * @not-serializable */ final class Closure { @@ -22,5 +21,11 @@ public function call(object $newThis, mixed ...$args): mixed {} public static function fromCallable(callable $callback): Closure {} + public static function fromConstExpr(string $class, int $id): Closure {} + public static function getCurrent(): Closure {} + + public function __serialize(): array {} + + public function __unserialize(array $data): void {} } diff --git a/Zend/zend_closures_arginfo.h b/Zend/zend_closures_arginfo.h index 5bc983a97c2c..a9c46e4b0f02 100644 --- a/Zend/zend_closures_arginfo.h +++ b/Zend/zend_closures_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit zend_closures.stub.php instead. - * Stub hash: e0626e52adb2d38dad1140c1a28cc7774cc84500 */ + * Stub hash: 6bae83b479dd62881fe60643e4692e0218d436a5 */ ZEND_BEGIN_ARG_INFO_EX(arginfo_class_Closure___construct, 0, 0, 0) ZEND_END_ARG_INFO() @@ -24,15 +24,30 @@ ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Closure_fromCallable, 0, 1, ZEND_ARG_TYPE_INFO(0, callback, IS_CALLABLE, 0) ZEND_END_ARG_INFO() +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Closure_fromConstExpr, 0, 2, Closure, 0) + ZEND_ARG_TYPE_INFO(0, class, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, id, IS_LONG, 0) +ZEND_END_ARG_INFO() + ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Closure_getCurrent, 0, 0, Closure, 0) ZEND_END_ARG_INFO() +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Closure___serialize, 0, 0, IS_ARRAY, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Closure___unserialize, 0, 1, IS_VOID, 0) + ZEND_ARG_TYPE_INFO(0, data, IS_ARRAY, 0) +ZEND_END_ARG_INFO() + ZEND_METHOD(Closure, __construct); ZEND_METHOD(Closure, bind); ZEND_METHOD(Closure, bindTo); ZEND_METHOD(Closure, call); ZEND_METHOD(Closure, fromCallable); +ZEND_METHOD(Closure, fromConstExpr); ZEND_METHOD(Closure, getCurrent); +ZEND_METHOD(Closure, __serialize); +ZEND_METHOD(Closure, __unserialize); static const zend_function_entry class_Closure_methods[] = { ZEND_ME(Closure, __construct, arginfo_class_Closure___construct, ZEND_ACC_PRIVATE) @@ -40,7 +55,10 @@ static const zend_function_entry class_Closure_methods[] = { ZEND_ME(Closure, bindTo, arginfo_class_Closure_bindTo, ZEND_ACC_PUBLIC) ZEND_ME(Closure, call, arginfo_class_Closure_call, ZEND_ACC_PUBLIC) ZEND_ME(Closure, fromCallable, arginfo_class_Closure_fromCallable, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC) + ZEND_ME(Closure, fromConstExpr, arginfo_class_Closure_fromConstExpr, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC) ZEND_ME(Closure, getCurrent, arginfo_class_Closure_getCurrent, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC) + ZEND_ME(Closure, __serialize, arginfo_class_Closure___serialize, ZEND_ACC_PUBLIC) + ZEND_ME(Closure, __unserialize, arginfo_class_Closure___unserialize, ZEND_ACC_PUBLIC) ZEND_FE_END }; @@ -49,7 +67,7 @@ static zend_class_entry *register_class_Closure(void) zend_class_entry ce, *class_entry; INIT_CLASS_ENTRY(ce, "Closure", class_Closure_methods); - class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL|ZEND_ACC_NO_DYNAMIC_PROPERTIES|ZEND_ACC_NOT_SERIALIZABLE); + class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL|ZEND_ACC_NO_DYNAMIC_PROPERTIES); return class_entry; } diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 105f99d24171..87e497246da7 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -11788,6 +11788,7 @@ static void zend_compile_const_expr_closure(zend_ast **ast_ptr) znode node; zend_op_array *op = zend_compile_func_decl(&node, (zend_ast*)closure_ast, FUNC_DECL_LEVEL_CONSTEXPR); + op->fn_flags2 |= ZEND_ACC2_CONSTEXPR_CLOSURE; zend_ast_destroy(*ast_ptr); *ast_ptr = zend_ast_create_op_array(op); diff --git a/Zend/zend_compile.h b/Zend/zend_compile.h index 0e31332c97f0..7df202da60d3 100644 --- a/Zend/zend_compile.h +++ b/Zend/zend_compile.h @@ -412,11 +412,19 @@ typedef struct _zend_oparray_context { /* op_array uses strict mode types | | | */ #define ZEND_ACC_STRICT_TYPES (1U << 31) /* | X | | */ /* | | | */ -/* Function Flags 2 (fn_flags2) (unused: 1-31) | | | */ +/* Function Flags 2 (fn_flags2) (unused: 3-31) | | | */ /* ============================ | | | */ /* | | | */ /* Function forbids dynamic calls | | | */ #define ZEND_ACC2_FORBID_DYN_CALLS (1 << 0) /* | X | | */ +/* | | | */ +/* Closure was declared in a constant expression | | | */ +#define ZEND_ACC2_CONSTEXPR_CLOSURE (1 << 1) /* | X | | */ +/* | | | */ +/* Fake closure was created by evaluating a first-class | | | */ +/* callable in a constant expression (set on the | | | */ +/* closure's own function copy, never on the original) | | | */ +#define ZEND_ACC2_CONSTEXPR_FCC (1 << 2) /* | X | | */ #define ZEND_ACC_PPP_MASK (ZEND_ACC_PUBLIC | ZEND_ACC_PROTECTED | ZEND_ACC_PRIVATE) #define ZEND_ACC_PPP_SET_MASK (ZEND_ACC_PUBLIC_SET | ZEND_ACC_PROTECTED_SET | ZEND_ACC_PRIVATE_SET) diff --git a/ext/reflection/php_reflection.c b/ext/reflection/php_reflection.c index af565ed53a32..52b7b906258d 100644 --- a/ext/reflection/php_reflection.c +++ b/ext/reflection/php_reflection.c @@ -1963,6 +1963,56 @@ ZEND_METHOD(ReflectionFunction, isAnonymous) } /* }}} */ +/* {{{ Returns the declaration-site id of a closure declared in a constant + expression (an anonymous closure or a first-class callable reference), + for use with Closure::fromConstExpr(), or null */ +ZEND_METHOD(ReflectionFunction, getConstExprId) +{ + reflection_object *intern; + zend_function *fptr; + zend_class_entry *ce; + uint32_t id; + + ZEND_PARSE_PARAMETERS_NONE(); + + GET_REFLECTION_OBJECT_PTR(fptr); + (void) fptr; + + if (Z_ISUNDEF(intern->obj) + || Z_OBJCE(intern->obj) != zend_ce_closure + || zend_constexpr_closure_ref(Z_OBJ(intern->obj), &ce, &id, NULL) == FAILURE) { + RETURN_NULL(); + } + + RETURN_LONG(id); +} +/* }}} */ + +/* {{{ Returns the class whose constant expressions declared this closure, + for use with Closure::fromConstExpr() together with getConstExprId(), + or null */ +ZEND_METHOD(ReflectionFunction, getConstExprClass) +{ + reflection_object *intern; + zend_function *fptr; + zend_class_entry *ce; + uint32_t id; + + ZEND_PARSE_PARAMETERS_NONE(); + + GET_REFLECTION_OBJECT_PTR(fptr); + (void) fptr; + + if (Z_ISUNDEF(intern->obj) + || Z_OBJCE(intern->obj) != zend_ce_closure + || zend_constexpr_closure_ref(Z_OBJ(intern->obj), &ce, &id, NULL) == FAILURE) { + RETURN_NULL(); + } + + RETURN_STR_COPY(ce->name); +} +/* }}} */ + /* {{{ Returns whether this function has been disabled or not */ ZEND_METHOD(ReflectionFunction, isDisabled) { diff --git a/ext/reflection/php_reflection.stub.php b/ext/reflection/php_reflection.stub.php index dd605100f8ba..af6b43f3b4af 100644 --- a/ext/reflection/php_reflection.stub.php +++ b/ext/reflection/php_reflection.stub.php @@ -128,6 +128,10 @@ public function __toString(): string {} public function isAnonymous(): bool {} + public function getConstExprId(): ?int {} + + public function getConstExprClass(): ?string {} + /** * @tentative-return-type */ diff --git a/ext/reflection/php_reflection_arginfo.h b/ext/reflection/php_reflection_arginfo.h index 65571f38d43c..5e986aaa1c4b 100644 --- a/ext/reflection/php_reflection_arginfo.h +++ b/ext/reflection/php_reflection_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit php_reflection.stub.php instead. - * Stub hash: c80946cc8c8215bb6527e09bb71b3a97a76a6a98 + * Stub hash: 3b5292eac2a57e3c46caaba3d93b26cb876894b5 * Has decl header: yes */ ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_Reflection_getModifierNames, 0, 1, IS_ARRAY, 0) @@ -96,6 +96,12 @@ ZEND_END_ARG_INFO() #define arginfo_class_ReflectionFunction_isAnonymous arginfo_class_ReflectionFunctionAbstract_hasTentativeReturnType +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_ReflectionFunction_getConstExprId, 0, 0, IS_LONG, 1) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_ReflectionFunction_getConstExprClass, 0, 0, IS_STRING, 1) +ZEND_END_ARG_INFO() + #define arginfo_class_ReflectionFunction_isDisabled arginfo_class_ReflectionFunctionAbstract_inNamespace ZEND_BEGIN_ARG_WITH_TENTATIVE_RETURN_TYPE_INFO_EX(arginfo_class_ReflectionFunction_invoke, 0, 0, IS_MIXED, 0) @@ -698,11 +704,9 @@ ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_ReflectionFiber_getFiber, 0, 0, Fiber, 0) ZEND_END_ARG_INFO() -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_ReflectionFiber_getExecutingFile, 0, 0, IS_STRING, 1) -ZEND_END_ARG_INFO() +#define arginfo_class_ReflectionFiber_getExecutingFile arginfo_class_ReflectionFunction_getConstExprClass -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_ReflectionFiber_getExecutingLine, 0, 0, IS_LONG, 1) -ZEND_END_ARG_INFO() +#define arginfo_class_ReflectionFiber_getExecutingLine arginfo_class_ReflectionFunction_getConstExprId ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_ReflectionFiber_getCallable, 0, 0, IS_CALLABLE, 0) ZEND_END_ARG_INFO() @@ -771,6 +775,8 @@ ZEND_METHOD(ReflectionFunctionAbstract, getAttributes); ZEND_METHOD(ReflectionFunction, __construct); ZEND_METHOD(ReflectionFunction, __toString); ZEND_METHOD(ReflectionFunction, isAnonymous); +ZEND_METHOD(ReflectionFunction, getConstExprId); +ZEND_METHOD(ReflectionFunction, getConstExprClass); ZEND_METHOD(ReflectionFunction, isDisabled); ZEND_METHOD(ReflectionFunction, invoke); ZEND_METHOD(ReflectionFunction, invokeArgs); @@ -1055,6 +1061,8 @@ static const zend_function_entry class_ReflectionFunction_methods[] = { ZEND_ME(ReflectionFunction, __construct, arginfo_class_ReflectionFunction___construct, ZEND_ACC_PUBLIC) ZEND_ME(ReflectionFunction, __toString, arginfo_class_ReflectionFunction___toString, ZEND_ACC_PUBLIC) ZEND_ME(ReflectionFunction, isAnonymous, arginfo_class_ReflectionFunction_isAnonymous, ZEND_ACC_PUBLIC) + ZEND_ME(ReflectionFunction, getConstExprId, arginfo_class_ReflectionFunction_getConstExprId, ZEND_ACC_PUBLIC) + ZEND_ME(ReflectionFunction, getConstExprClass, arginfo_class_ReflectionFunction_getConstExprClass, ZEND_ACC_PUBLIC) ZEND_ME(ReflectionFunction, isDisabled, arginfo_class_ReflectionFunction_isDisabled, ZEND_ACC_PUBLIC|ZEND_ACC_DEPRECATED) ZEND_ME(ReflectionFunction, invoke, arginfo_class_ReflectionFunction_invoke, ZEND_ACC_PUBLIC) ZEND_ME(ReflectionFunction, invokeArgs, arginfo_class_ReflectionFunction_invokeArgs, ZEND_ACC_PUBLIC) diff --git a/ext/reflection/php_reflection_decl.h b/ext/reflection/php_reflection_decl.h index a87e1635419b..f56e63826114 100644 --- a/ext/reflection/php_reflection_decl.h +++ b/ext/reflection/php_reflection_decl.h @@ -1,12 +1,12 @@ /* This is a generated file, edit php_reflection.stub.php instead. - * Stub hash: c80946cc8c8215bb6527e09bb71b3a97a76a6a98 */ + * Stub hash: 3b5292eac2a57e3c46caaba3d93b26cb876894b5 */ -#ifndef ZEND_PHP_REFLECTION_DECL_c80946cc8c8215bb6527e09bb71b3a97a76a6a98_H -#define ZEND_PHP_REFLECTION_DECL_c80946cc8c8215bb6527e09bb71b3a97a76a6a98_H +#ifndef ZEND_PHP_REFLECTION_DECL_3b5292eac2a57e3c46caaba3d93b26cb876894b5_H +#define ZEND_PHP_REFLECTION_DECL_3b5292eac2a57e3c46caaba3d93b26cb876894b5_H typedef enum zend_enum_PropertyHookType { ZEND_ENUM_PropertyHookType_Get = 1, ZEND_ENUM_PropertyHookType_Set = 2, } zend_enum_PropertyHookType; -#endif /* ZEND_PHP_REFLECTION_DECL_c80946cc8c8215bb6527e09bb71b3a97a76a6a98_H */ +#endif /* ZEND_PHP_REFLECTION_DECL_3b5292eac2a57e3c46caaba3d93b26cb876894b5_H */ diff --git a/ext/reflection/tests/ReflectionFunction_getConstExprId.phpt b/ext/reflection/tests/ReflectionFunction_getConstExprId.phpt new file mode 100644 index 000000000000..b89eff350365 --- /dev/null +++ b/ext/reflection/tests/ReflectionFunction_getConstExprId.phpt @@ -0,0 +1,58 @@ +--TEST-- +ReflectionFunction::getConstExprId() and getConstExprClass() +--FILE-- +getAttributes(); + +// Anonymous closure declared in a constant expression +$r = new ReflectionFunction($attrs[0]->getArguments()[0]); +var_dump($r->getConstExprId(), $r->getConstExprClass()); + +$recreated = Closure::fromConstExpr($r->getConstExprClass(), $r->getConstExprId()); +var_dump($recreated()); + +// First-class callable reference declared in a constant expression: the +// const-expr class is the declaring site, not the callable's scope. +$r = new ReflectionFunction($attrs[1]->getArguments()[0]); +var_dump($r->getConstExprId(), $r->getConstExprClass(), $r->getClosureScopeClass()->name); + +$recreated = Closure::fromConstExpr($r->getConstExprClass(), $r->getConstExprId()); +var_dump($recreated()); + +// Null for anything that is not declared in a constant expression +var_dump((new ReflectionFunction(function () {}))->getConstExprId()); +var_dump((new ReflectionFunction(strlen(...)))->getConstExprId()); +var_dump((new ReflectionFunction(Validators::check(...)))->getConstExprId()); +var_dump((new ReflectionFunction('strlen'))->getConstExprId()); +var_dump((new ReflectionFunction('strlen'))->getConstExprClass()); + +?> +--EXPECT-- +int(0) +string(4) "Demo" +string(2) "ok" +int(1) +string(4) "Demo" +string(10) "Validators" +string(7) "checked" +NULL +NULL +NULL +NULL +NULL