Skip to content

Create named closures like Closure::fromCallable() - #60

Merged
nicolas-grekas merged 1 commit into
mainfrom
fuzz-named-closures
Sep 29, 2026
Merged

nicolas-grekas merged 1 commit into
mainfrom
fuzz-named-closures

Conversation

@nicolas-grekas

@nicolas-grekas nicolas-grekas commented Sep 29, 2026 •

Copy link
Copy Markdown
Member

Also found by the differential fuzzing of the polyfill against the extension.

deepclone_from_array() gave the closure over a method the scope of the class it looked the method up on. For an inherited method, the private properties of the parent class weren't reachable anymore:

class Base { private $secret = 'base'; function reveal() { return $this->secret; } }
class Child extends Base {}
$f = deepclone_from_array(deepclone_to_array((new Child)->reveal(...), null, true), null, true);
$f(); // Warning: Undefined property: Child::$secret

The closure now gets the scope of the class that declares the method, and is called on the class of its object or on the named class, like with Closure::fromCallable(). For static methods, deepclone_to_array() exports that called class instead of the declaring one, as the polyfill does, so that static:: resolves the same.

Named closures over a non-static method without an object, or over a method of a class that their object or class doesn't extend, are now rejected, a top-level one whose function doesn't exist throws instead of returning null, the ones over a method that __call() or __callStatic() handles are created with Closure::fromCallable(), and on PHP 8.4+ the shape of the ones lazy objects hold is checked right away, as the README says for malformed payloads.

@nicolas-grekas
nicolas-grekas force-pushed the fuzz-named-closures branch 2 times, most recently from 4209a8c to b07c146 Compare September 29, 2026 21:03
deepclone_from_array() gave the closure over a method the scope of the
class it looked the method up on: for an inherited method, the private
properties of the parent class weren't reachable anymore. The closure now
gets the scope of the class that declares the method, and is called on
the class of its object, or on the named class, like with
Closure::fromCallable(). For static methods, deepclone_to_array()
exports that called class instead of the declaring one, which static::
resolves to.

Named closures over a non-static method without an object, or over a
method of a class that their object or class doesn't extend, are
rejected instead of being created, and a top-level one whose function
doesn't exist throws instead of returning null. The ones over a method
that __call() or __callStatic() handles are created with
Closure::fromCallable(), instead of returning null too.

On PHP 8.4+, the shape of the named closures that lazy objects hold is
checked right away, like const-expr closures are against the allowed
classes, instead of when these objects are first used.
@nicolas-grekas
nicolas-grekas merged commit a41e128 into main Sep 29, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant