From 2ae4846b0e48c32d9c0e7ebb8aed71fe7af4f259 Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Fri, 27 Jun 2025 23:36:33 +0200 Subject: [PATCH 1/2] Fix OSS-Fuzz #427814452 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. --- .../pipe_operator/oss_fuzz_427814452.phpt | 26 +++++++++++++++++++ Zend/zend_compile.c | 5 +++- 2 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 Zend/tests/pipe_operator/oss_fuzz_427814452.phpt diff --git a/Zend/tests/pipe_operator/oss_fuzz_427814452.phpt b/Zend/tests/pipe_operator/oss_fuzz_427814452.phpt new file mode 100644 index 0000000000000..2ecfbab6348f7 --- /dev/null +++ b/Zend/tests/pipe_operator/oss_fuzz_427814452.phpt @@ -0,0 +1,26 @@ +--TEST-- +OSS-Fuzz #427814452 +--FILE-- + 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: '' diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index f3f6d1b75aec1..053d8b21568a2 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -4356,7 +4356,10 @@ static void zend_compile_assert(znode *result, zend_ast_list *args, zend_string } opline->result.num = zend_alloc_cache_slot(); - if (args->children == 1) { + /* Skip adding a message on piped assert(...) calls, hence the ZEND_AST_ZNODE check. + * We don't have access to the original AST anyway, so we would either need to duplicate + * this logic in pipe compilation or store the AST. Neither seems worth the complexity. */ + if (args->children == 1 && args->child[0]->kind != ZEND_AST_ZNODE) { /* add "assert(condition) as assertion message */ zend_ast *arg = zend_ast_create_zval_from_str( zend_ast_export("assert(", args->child[0], ")")); From 8276f9da202f2468ae878c3bd5013af9b507b49b Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Wed, 2 Jul 2025 21:22:50 +0200 Subject: [PATCH 2/2] Switch approach to blocking the optimization Co-authored-by: Ilija Tovilo --- Zend/zend_compile.c | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 053d8b21568a2..b9c1f9e223a8a 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -4356,10 +4356,7 @@ static void zend_compile_assert(znode *result, zend_ast_list *args, zend_string } opline->result.num = zend_alloc_cache_slot(); - /* Skip adding a message on piped assert(...) calls, hence the ZEND_AST_ZNODE check. - * We don't have access to the original AST anyway, so we would either need to duplicate - * this logic in pipe compilation or store the AST. Neither seems worth the complexity. */ - if (args->children == 1 && args->child[0]->kind != ZEND_AST_ZNODE) { + if (args->children == 1) { /* add "assert(condition) as assertion message */ zend_ast *arg = zend_ast_create_zval_from_str( zend_ast_export("assert(", args->child[0], ")")); @@ -6429,6 +6426,20 @@ static bool can_match_use_jumptable(zend_ast_list *arms) { return 1; } +static bool zend_is_deopt_pipe_name(zend_ast *ast) +{ + if (ast->kind != ZEND_AST_ZVAL || Z_TYPE_P(zend_ast_get_zval(ast)) != IS_STRING) { + return false; + } + + /* 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"); +} + static void zend_compile_pipe(znode *result, zend_ast *ast) { zend_ast *operand_ast = ast->child[0]; @@ -6456,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_deopt_pipe_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). */