You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Gate closures over named callables behind an allow_named_closures option
A by-name closure payload (a first-class callable over a named function
or method) lets deepclone_from_array() mint a Closure over any function or
method of that name, with no visibility or staticness restriction and
including internal functions such as system() -- a stronger primitive than
anything unserialize() exposes, and a one-step gadget when paired with an
object whose __wakeup/__unserialize/destructor invokes a stored callback.
Both deepclone_to_array() and deepclone_from_array() gain a third parameter,
bool $allow_named_closures = false, that both ends must enable: with it off
(the default) to_array refuses to encode such a closure, and from_array
rejects any payload carrying a by-name closure marker before instantiating
anything.
So the attribute-cache use case does not regress, first-class callables over
a method of their own declaring class declared in a constant expression (e.g.
#[When(self::isStrict(...))]) are now encoded as a declaration-site reference
(mask LONG 1), like anonymous const-expr closures, instead of by name. They
resolve only to a closure the named class itself declares and round-trip
without the opt-in. References whose declaring class cannot be derived from
the closure (cross-class or global-function callables, inherited methods,
runtime-created callables) keep the by-name form and need the opt-in.
allowed_classes still gates Closure for both forms.
/* Const-expr declaration-site reference. This covers anonymous static
1793
+
* closures and first-class callables over a method of their own
1794
+
* declaring class (e.g. #[When(self::isStrict(...))]). It is attempted
1795
+
* before the by-name encoding so that such closures serialize as a
1796
+
* declaration-site reference — resolvable only to what the class
1797
+
* itself declares — and therefore round-trip without requiring the
1798
+
* allow_named_closures opt-in. The allow-list is checked first so that
1799
+
* disallowing Closure is reported before any const-expr of the scope
1800
+
* class is evaluated. */
1801
+
if (func&&func->type==ZEND_USER_FUNCTION&&func->common.scope) {
1802
+
zval*this_ptr=zend_get_closure_this_ptr(src);
1803
+
if (!this_ptr||Z_TYPE_P(this_ptr) !=IS_OBJECT) {
1804
+
if (!dc_class_allowed(ctx->allowed_ht, zend_ce_closure->name)) {
1805
+
zend_value_error("deepclone_to_array(): class \"Closure\" is not allowed");
1806
+
return;
1807
+
}
1808
+
zvalpayload;
1809
+
ZVAL_UNDEF(&payload);
1810
+
if (dc_cexpr_locate(func, &payload)) {
1811
+
ZVAL_COPY_VALUE(dst, &payload);
1812
+
DC_MASK_CONSTEXPR_CLOSURE(mask_dst);
1813
+
goto handle_value;
1814
+
}
1815
+
if (UNEXPECTED(EG(exception))) {
1816
+
return;
1817
+
}
1818
+
}
1819
+
}
1820
+
#endif
1821
+
1822
+
/* Named closure: a first-class callable that is not addressable as a
1823
+
* declaration-site reference (one created at runtime, or whose target
1824
+
* lives outside its declaring class, or over an internal/global
1825
+
* function). Encoding it stores the callable by name, which lets
1826
+
* deepclone_from_array() mint a Closure over any function or method of
1827
+
* that name — including internal functions such as system(). It is
1828
+
* therefore gated behind the allow_named_closures opt-in, which both
1829
+
* ends must enable. */
1788
1830
if (func&& (func->common.fn_flags&ZEND_ACC_FAKE_CLOSURE)) {
1831
+
if (!ctx->allow_named_closures) {
1832
+
zend_value_error("deepclone_to_array(): serializing a closure over the named callable \"%s\" requires enabling the allow_named_closures option", ZSTR_VAL(func->common.function_name));
1833
+
return;
1834
+
}
1789
1835
if (!dc_class_allowed(ctx->allowed_ht, zend_ce_closure->name)) {
1790
1836
zend_value_error("deepclone_to_array(): class \"Closure\" is not allowed");
0 commit comments