Skip to content

Fix crash when a partial skips a param with a constant default#22804

Open
iliaal wants to merge 1 commit into
php:masterfrom
iliaal:fix/pfa-const-default-gap-crash
Open

Fix crash when a partial skips a param with a constant default#22804
iliaal wants to merge 1 commit into
php:masterfrom
iliaal:fix/pfa-const-default-gap-crash

Conversation

@iliaal

@iliaal iliaal commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

A partial that leaves an optional parameter unbound via a named-argument gap, where that parameter's default is a constant expression, crashes the compiler. f(a: 10, c: ?) for function f($a, $b = SOME_CONST, $c = 0) compiled the callee default into the generated forwarding call as a ZEND_AST_CONSTANT node, which zend_compile_expr_inner has no case for: ZEND_ASSERT(0) in debug, __builtin_unreachable UB in release. A plain literal default ($b = 5) is unaffected.

For a user function, this skips the unbound parameter in the forwarding call and passes the following arguments by name, so the target applies its own default at call time. That matches a normal call exactly (verified for global constants, self:: class constants, and enum cases) and materializes no value, so it is safe under opcache for defaults that cannot be stored as a literal, such as enum cases. Internal functions are unchanged: they still pass an explicit default and error when it is not introspectable.

When a partial leaves an optional parameter unbound (a positional gap from
named arguments) and its default is a constant expression, the default AST
was compiled into the forwarding call as an uncompilable ZEND_AST_CONSTANT
node: an assertion in debug builds, undefined behavior otherwise. For a
user function, skip the parameter and pass the following arguments by name
so the target applies its own default at call time; this materializes no
value and handles every default kind, including enum cases that cannot be
a literal. Internal functions still pass an explicit default and error if
it is not introspectable.
@arnaud-lb

arnaud-lb commented Jul 20, 2026

Copy link
Copy Markdown
Member

Interesting one. It looks like an edge case with ZEND_AST_CONSTANT vs ZEND_AST_CONST: The compiler knows how to compile ZEND_AST_CONST, but const exprs represent const fetches as ZEND_AST_CONSTANT, which the compiler doesn't understand.

Unfortunately, passing by name doesn't work in all cases (or requires more work), for example:

function f($a, $b = K, $c = 0, ...$args) {
    return [$a, $b, $c];
}

$p = f(a: 10, c: ?, foo: 1);

Will generate:

static function ($c) use($a, $extra_named_params) {
    return \f($a, c: $c, ...$extra_named_params);
}

which is invalid due to the use of unpacking after a named param. We would need to unpack at compile time, but we may run into other issues.

Possible alternative ways to fix this:

  • Support ZEND_AST_CONSTANT in zend_compile_expr_inner()
  • Convert ZEND_AST_CONSTANT to ZEND_AST_CONST nodes in zp_compile_forwarding_call() (recursively)
  • Remove ZEND_AST_CONSTANT to use only ZEND_AST_CONST in const exprs

I have a preference for the first one. wdyt?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants