Pipe compilation uses a temporary znode with QM_ASSIGN to remove
references. Assert compilation wants to look at the operand AST and
convert it to a string. However the original AST is lost due to the
temporary znode. To solve this we either have to handle this specially
in pipe compilation [1], or store the AST anyway somehow.
Special casing this either way is not worth the complexity in my
opinion, especially as it looks like a dynamic call anyway due to the
FCC syntax.

[1] Prototype (incomplete) at
    https://gist.github.com/nielsdos/50dc71718639c3af05db84a4dea6eb71
    shows this is not worthwhile in my opinion.

Closes GH-18965.

Co-authored-by: Ilija Tovilo <ilija.tovilo@me.com>
This commit is contained in:
Niels Dossche 2025-06-27 23:36:33 +02:00
parent f11ea2ae13
commit 8712f4bf19
No known key found for this signature in database
GPG key ID: B8A8AD166DF0E2E5
4 changed files with 57 additions and 1 deletions

3
NEWS
View file

@ -2,6 +2,9 @@ PHP NEWS
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
?? ??? ????, PHP 8.5.0alpha2
- Core:
. Fix OSS-Fuzz #427814452 (pipe compilation fails with assert). (nielsdos)
- DOM:
. Make cloning DOM node lists, maps, and collections fail. (nielsdos)

View file

@ -0,0 +1,12 @@
--TEST--
GH-18965: ASSERT_CHECK avoids pipe lhs free
--INI--
zend.assertions=0
--FILE--
<?php
namespace Foo;
range(0, 10) |> assert(...);
echo "No leak\n";
?>
--EXPECT--
No leak

View file

@ -0,0 +1,26 @@
--TEST--
OSS-Fuzz #427814452
--FILE--
<?php
try {
false |> assert(...);
} catch (\AssertionError $e) {
echo $e::class, ": '", $e->getMessage(), "'\n";
}
try {
0 |> "assert"(...);
} catch (\AssertionError $e) {
echo $e::class, ": '", $e->getMessage(), "'\n";
}
try {
false |> ("a"."ssert")(...);
} catch (\AssertionError $e) {
echo $e::class, ": '", $e->getMessage(), "'\n";
}
?>
--EXPECT--
AssertionError: ''
AssertionError: ''
AssertionError: ''

View file

@ -6426,6 +6426,20 @@ static bool can_match_use_jumptable(zend_ast_list *arms) {
return 1;
}
static bool zend_is_pipe_optimizable_callable_name(zend_ast *ast)
{
if (ast->kind == ZEND_AST_ZVAL && Z_TYPE_P(zend_ast_get_zval(ast)) == IS_STRING) {
/* Assert compilation adds a message operand, but this is incompatible with the
* pipe optimization that uses a temporary znode for the reference elimination.
* Therefore, disable the optimization for assert.
* Note that "assert" as a name is always treated as fully qualified. */
zend_string *str = zend_ast_get_str(ast);
return !zend_string_equals_literal_ci(str, "assert");
}
return true;
}
static void zend_compile_pipe(znode *result, zend_ast *ast)
{
zend_ast *operand_ast = ast->child[0];
@ -6453,7 +6467,8 @@ static void zend_compile_pipe(znode *result, zend_ast *ast)
/* Turn $foo |> bar(...) into bar($foo). */
if (callable_ast->kind == ZEND_AST_CALL
&& callable_ast->child[1]->kind == ZEND_AST_CALLABLE_CONVERT) {
&& callable_ast->child[1]->kind == ZEND_AST_CALLABLE_CONVERT
&& zend_is_pipe_optimizable_callable_name(callable_ast->child[0])) {
fcall_ast = zend_ast_create(ZEND_AST_CALL,
callable_ast->child[0], arg_list_ast);
/* Turn $foo |> bar::baz(...) into bar::baz($foo). */