@@ -1566,6 +1566,26 @@ static zend_class_entry *dc_provenance_lookup(zend_class_entry *target_ce, zend_
15661566 return zend_lookup_class_ex (decl , NULL , ZEND_FETCH_CLASS_NO_AUTOLOAD );
15671567}
15681568
1569+ /* The class whose constant expression declares this first-class callable. On
1570+ * PHP 8.6 the engine records it (zend_constexpr_closure_ref), so it is exact
1571+ * and needs no capture; on 8.5 it comes from the ReflectionAttribute-captured
1572+ * index. Either way it feeds the same site-based (5-element) reference, which
1573+ * is interchangeable with the polyfill — unlike the engine-id form, whose fcc
1574+ * line userland cannot reproduce. */
1575+ static zend_class_entry * dc_declaring_class (zval * src , const zend_function * func )
1576+ {
1577+ #if PHP_VERSION_ID >= 80600
1578+ zend_class_entry * ce ;
1579+ uint32_t id , line ;
1580+ if (zend_constexpr_closure_ref (Z_OBJ_P (src ), & ce , & id , & line ) == SUCCESS ) {
1581+ return ce ;
1582+ }
1583+ #endif
1584+ return func -> common .function_name
1585+ ? dc_provenance_lookup (func -> common .scope , func -> common .function_name )
1586+ : NULL ;
1587+ }
1588+
15691589/* Walk a value (a getArguments() argument, or a newInstance() attribute object
15701590 * and its properties), recording every cross-class FCC against `scope`. The
15711591 * `seen` set guards cycles: getArguments() values are acyclic constant
@@ -1656,6 +1676,66 @@ static void ZEND_FASTCALL dc_attr_new_instance_wrapper(INTERNAL_FUNCTION_PARAMET
16561676}
16571677#endif /* PHP_VERSION_ID >= 80500 */
16581678
1679+ /* deepclone_from_array() counterpart for engine-id references [class, id,
1680+ * line], emitted on PHP >= 8.6: the id is the engine's canonical per-class
1681+ * const-expr closure id (see Closure::fromConstExpr()). */
1682+ static void dc_cexpr_resolve_id (HashTable * ht , HashTable * allowed_set , zval * retval )
1683+ {
1684+ zval * zclass = zend_hash_index_find (ht , 0 );
1685+ zval * zid = zend_hash_index_find (ht , 1 );
1686+ zval * zline = zend_hash_index_find (ht , 2 );
1687+ if (!zclass || !zid || !zline || zend_hash_num_elements (ht ) != 3 ) {
1688+ zend_value_error ("deepclone_from_array(): malformed payload, const-expr-closure value must have 3 elements" );
1689+ return ;
1690+ }
1691+ ZVAL_DEREF (zclass );
1692+ ZVAL_DEREF (zid );
1693+ ZVAL_DEREF (zline );
1694+ if (Z_TYPE_P (zclass ) != IS_STRING ) {
1695+ zend_value_error ("deepclone_from_array(): malformed payload, const-expr-closure class name must be of type string, %s given" , zend_zval_value_name (zclass ));
1696+ return ;
1697+ }
1698+ if (Z_TYPE_P (zline ) != IS_LONG ) {
1699+ zend_value_error ("deepclone_from_array(): malformed payload, const-expr-closure line must be of type int, %s given" , zend_zval_value_name (zline ));
1700+ return ;
1701+ }
1702+
1703+ /* Gate before zend_lookup_class(): the payload must not be able to
1704+ * autoload, let alone evaluate, classes outside the allow-list. */
1705+ if (!dc_class_allowed (allowed_set , Z_STR_P (zclass ))) {
1706+ zend_value_error ("deepclone_from_array(): class \"%s\" is not allowed" , Z_STRVAL_P (zclass ));
1707+ return ;
1708+ }
1709+
1710+ #if PHP_VERSION_ID >= 80600
1711+ zend_class_entry * ce = zend_lookup_class (Z_STR_P (zclass ));
1712+ if (!ce ) {
1713+ zend_value_error ("deepclone_from_array(): malformed payload, const-expr-closure references unknown class \"%s\"" , Z_STRVAL_P (zclass ));
1714+ return ;
1715+ }
1716+
1717+ zend_ast * site = zend_constexpr_closure_site_by_id (ce , Z_LVAL_P (zid ));
1718+ if (!site ) {
1719+ zend_value_error ("deepclone_from_array(): malformed payload, const-expr-closure references unknown closure id " ZEND_LONG_FMT " in class \"%s\"" , Z_LVAL_P (zid ), ZSTR_VAL (ce -> name ));
1720+ return ;
1721+ }
1722+ if (site -> kind != ZEND_AST_OP_ARRAY ) {
1723+ zend_value_error ("deepclone_from_array(): malformed payload, const-expr-closure references a first-class callable site" );
1724+ return ;
1725+ }
1726+
1727+ zend_op_array * op = zend_ast_get_op_array (site )-> op_array ;
1728+ if (Z_LVAL_P (zline ) != (zend_long ) op -> line_start ) {
1729+ zend_value_error ("deepclone_from_array(): stale payload, const-expr-closure moved from line " ZEND_LONG_FMT " to line %u" , Z_LVAL_P (zline ), op -> line_start );
1730+ return ;
1731+ }
1732+
1733+ zend_create_closure (retval , (zend_function * ) op , ce , ce , NULL );
1734+ #else
1735+ zend_value_error ("deepclone_from_array(): const-expr-closure payload was created on PHP 8.6 or later and cannot be resolved on PHP %s" , PHP_VERSION );
1736+ #endif
1737+ }
1738+
16591739/* deepclone_from_array() counterpart: resolve a declaration-site reference
16601740 * back to a live Closure. */
16611741static void dc_cexpr_resolve (zval * value , HashTable * allowed_set , zval * retval )
@@ -1665,6 +1745,18 @@ static void dc_cexpr_resolve(zval *value, HashTable *allowed_set, zval *retval)
16651745 return ;
16661746 }
16671747 HashTable * ht = Z_ARRVAL_P (value );
1748+
1749+ zval * zid = zend_hash_index_find (ht , 1 );
1750+ if (zid ) {
1751+ ZVAL_DEREF (zid );
1752+ }
1753+ if (zid && Z_TYPE_P (zid ) == IS_LONG ) {
1754+ /* The type of element 1 (int id vs string site) discriminates
1755+ * engine-id references from site-based ones. */
1756+ dc_cexpr_resolve_id (ht , allowed_set , retval );
1757+ return ;
1758+ }
1759+
16681760 zval * zclass = zend_hash_index_find (ht , 0 );
16691761 zval * zsite = zend_hash_index_find (ht , 1 );
16701762 zval * zattr = zend_hash_index_find (ht , 2 );
@@ -2031,6 +2123,31 @@ static void dc_copy_value(dc_ctx *ctx, zval *src, zval *dst, zval *mask_dst)
20312123 zend_value_error ("deepclone_to_array(): class \"Closure\" is not allowed" );
20322124 return ;
20332125 }
2126+ #if PHP_VERSION_ID >= 80600
2127+ /* The engine assigns a canonical per-class id to anonymous closures
2128+ * declared in attribute arguments and parameter default values; prefer
2129+ * it to the site-based reference below. First-class callables are
2130+ * excluded: their engine id resolves to an fcc site the decode side
2131+ * cannot recreate, so they keep the site-based and by-name paths.
2132+ * Closures in class constant values and property defaults have no id
2133+ * and also fall through. */
2134+ if (!(func -> common .fn_flags & ZEND_ACC_FAKE_CLOSURE )) {
2135+ zend_class_entry * site_ce ;
2136+ uint32_t cexpr_id , cexpr_line ;
2137+ if (zend_constexpr_closure_ref (Z_OBJ_P (src ), & site_ce , & cexpr_id , & cexpr_line ) == SUCCESS ) {
2138+ zval tmp ;
2139+ array_init_size (dst , 3 );
2140+ ZVAL_STR_COPY (& tmp , site_ce -> name );
2141+ zend_hash_index_add_new (Z_ARRVAL_P (dst ), 0 , & tmp );
2142+ ZVAL_LONG (& tmp , (zend_long ) cexpr_id );
2143+ zend_hash_index_add_new (Z_ARRVAL_P (dst ), 1 , & tmp );
2144+ ZVAL_LONG (& tmp , (zend_long ) cexpr_line );
2145+ zend_hash_index_add_new (Z_ARRVAL_P (dst ), 2 , & tmp );
2146+ DC_MASK_CONSTEXPR_CLOSURE (mask_dst );
2147+ goto handle_value ;
2148+ }
2149+ }
2150+ #endif
20342151 zval payload ;
20352152 ZVAL_UNDEF (& payload );
20362153 if (dc_cexpr_locate (func , & payload )) {
@@ -2046,8 +2163,8 @@ static void dc_copy_value(dc_ctx *ctx, zval *src, zval *dst, zval *mask_dst)
20462163 * scope) misses. On 8.5 there is no engine provenance; fall back
20472164 * to a declaring class captured from ReflectionAttribute, if
20482165 * any, and locate the site there. */
2049- if (DC_G ( capture_attribute_closures ) && func -> common . scope && func -> common .function_name ) {
2050- zend_class_entry * decl = dc_provenance_lookup ( func -> common . scope , func -> common . function_name );
2166+ if (func -> common .function_name ) {
2167+ zend_class_entry * decl = dc_declaring_class ( src , func );
20512168 if (decl && decl != func -> common .scope && dc_cexpr_locate_ce (func , decl , & payload )) {
20522169 ZVAL_COPY_VALUE (dst , & payload );
20532170 DC_MASK_CONSTEXPR_CLOSURE (mask_dst );
@@ -2061,15 +2178,15 @@ static void dc_copy_value(dc_ctx *ctx, zval *src, zval *dst, zval *mask_dst)
20612178 }
20622179
20632180 /* Global-function first-class callable (no scope, internal or user):
2064- * the declaring class can only come from captured provenance. Same
2181+ * the declaring class comes from the engine (8.6) or captured
2182+ * provenance (8.5). Same
20652183 * declaration-site reference and Closure gating as above; unresolved
20662184 * ones fall through to the by-name path. */
20672185 if (func && (func -> common .fn_flags & ZEND_ACC_FAKE_CLOSURE )
2068- && !func -> common .scope && func -> common .function_name
2069- && DC_G (capture_attribute_closures )) {
2186+ && !func -> common .scope && func -> common .function_name ) {
20702187 zval * this_ptr = zend_get_closure_this_ptr (src );
20712188 if (!this_ptr || Z_TYPE_P (this_ptr ) != IS_OBJECT ) {
2072- zend_class_entry * decl = dc_provenance_lookup ( NULL , func -> common . function_name );
2189+ zend_class_entry * decl = dc_declaring_class ( src , func );
20732190 if (decl ) {
20742191 if (!dc_class_allowed (ctx -> allowed_ht , zend_ce_closure -> name )) {
20752192 zend_value_error ("deepclone_to_array(): class \"Closure\" is not allowed" );
0 commit comments