Hi,

thanks for having a look

Le lun. 15 juin 2026 à 21:30, Tim Düsterhus <[email protected]> a écrit :

> Hi
>
> On 6/10/26 19:02, Nicolas Grekas wrote:
> > I'd like to open the discussion on a new RFC:
> > https://wiki.php.net/rfc/serializable_closures
>
> Don't forget to link the discussion thread in the RFC. For your
> convenience: https://news-web.php.net/php.internals/131198
>
> As to the RFC itself: I think it might be useful to split this into two
> RFCs, similarly to how Volker and I split the initial support for
> Closures in const-expr into support for Closures and support for first
> class callables.
>

The 8.5 split worked because closures and FCCs were separable features.
Here it's one mechanism, the FCC half alone or the anonymous-closure one
alone would be just missing its other half.
I'd keep it as one RFC.



> Specifically I believe that the proposed serialization format is fragile
> and the stated security model, which I suspect heavily influenced the
> design of the serialization format, is probably overly cautious.
>
> I don't think it is a problem to make `unserialize()` a Closure factory,
> because the created Closure is “inert”. Contrary to arbitrary object
> unserialization (which will immediately call the deserialization hooks
> and then later __destruct()), the Closure will not actually do anything
> unless it is called.
>
> If folks are able to unserialize arbitrary payloads - which is
> documented to be unsafe - they already have capabilities that are much
> more powerful than “creating Closures”.
>

Creation is inert, but the point of these payloads is to be called. Once
it's called, the only thing that matters is which callables a payload can
name.
Declared-set resolution: an attacker can swap one closure the class already
declares for another.
Unrestricted name-based: {function: "system"} with attacker-supplied args
is a call gadget shipped in the engine, against any app that unserializes
untrusted input.
"allowed_classes" exists and 8.6 tightened session defaults precisely
because we bound damage despite unserialize being documented unsafe.
So I'd keep the declared-set boundary as required defense-in-depth /
hardening.



> For first class callables specifically, there is a very obvious and
> stable identifier to use: The underlying function name.
>
> For regular Closures, I don't like how changing something unrelated
> (namely adding new Closures to a class) can change the meaning of the
> serialized data. This is not how serialization works right now: The
> value in question is in full control of its serialization format and is
> able to “gracefully” support existing serialized payloads using an old
> format within an unserialization hook (e.g. by including a version
> number field). This allows to e.g. keep serialized queue jobs working
> across deploys.
>

Agreed: first-class callables now serialize with the function name as
identifier, no ordinal involved. The id is `member@callable`, e.g.
`$billingAddress@Order::isStrict` for `#[When(self::isStrict(...))]` on
that property, `$p@strlen` for a plain function. The member prefix keeps
resolution local to one reflection element instead of scanning the whole
class on every cache read (fat classes would pay otherwise), and it gives
both closure forms the same staleness rule: a reference is valid while its
member and its declaration survive. Adding, removing or reordering anything
else in the class changes nothing; renaming the target method fails.

One catch: the idiomatic form references a private helper of the same
class, #[When(self::isStrict(...))], and Closure::fromCallable('C::priv')
from global scope throws "cannot access private method". So resolution
doesn't resolve the name directly: it checks if the named member declares
that exact reference, then evaluates the declaration in class scope, name
as address, declaration as guard.
Private keeps working, {..., "$p@system"} stays rejected because no class
declares it.



> As for the var_export() in the future scope: I think adding support for
> first class callables to `var_export()` would be a change that can just
> be done without an RFC. It might be a good first step that might already
> be helpful to your use case?
>

Not really helpful for the main use cases I gathered, which require full
compat with serialize semantics, but yeah, I agree this can be dealt with
on its own.

RFC updated!
I tried to reflect all your points in the update.

Cheers,
Nicolas

Reply via email to