From 29c28e5351f8338c98ad6366a7712d3d3a4ac62f Mon Sep 17 00:00:00 2001 From: Mrmaxmeier Date: Mon, 24 Aug 2026 15:02:23 +0200 Subject: [PATCH] Fix use-after-free when piping a constant into a namespaced frameless call zend_compile_pipe() passes its left hand side as a ZEND_AST_ZNODE argument. Compiling that node is a plain struct copy, so every compilation hands out the same single reference to the constant it holds -- unlike ZEND_AST_ZVAL, which does a ZVAL_COPY. Inside a namespace, zend_compile_ns_call() compiles the argument list twice: once for the INIT_NS_FCALL_BY_NAME fallback and once for the frameless icall. Both compilations end in zend_add_literal() -> zval_make_interned_string(), which releases the string when interning returns a different one, so the second release frees a string the AST (or the first literal) still points at: namespace N; var_dump(' string literal ' |> trim(...)); Take one extra reference per additional compilation, so the frameless optimization can be kept for pipes. Assisted-By: Claude Opus 5 --- NEWS | 2 + .../pipe_ns_frameless_const.phpt | 39 +++++++++++++++++++ Zend/zend_compile.c | 22 +++++++++++ 3 files changed, 63 insertions(+) create mode 100644 Zend/tests/pipe_operator/pipe_ns_frameless_const.phpt diff --git a/NEWS b/NEWS index 283c90bad870..9d33165d0c95 100644 --- a/NEWS +++ b/NEWS @@ -7,6 +7,8 @@ PHP NEWS next() call on the inner generator). (iliaal) . Fixed bug GH-23301 (Nested "yield from" yields a value twice when the middle generator delegates again). (Lazizbek Ergashev) + . Fixed a use-after-free when piping a constant into a namespaced frameless + call. (Mrmaxmeier) - DOM: . Fixed a use-after-free when cloning a DOMNameSpaceNode after diff --git a/Zend/tests/pipe_operator/pipe_ns_frameless_const.phpt b/Zend/tests/pipe_operator/pipe_ns_frameless_const.phpt new file mode 100644 index 000000000000..4216f083969a --- /dev/null +++ b/Zend/tests/pipe_operator/pipe_ns_frameless_const.phpt @@ -0,0 +1,39 @@ +--TEST-- +Piping a constant into a namespaced frameless call must not release it twice +--FILE-- + trim(...)); + var_dump(<<<'NOWDOC' + nowdoc value + NOWDOC |> trim(...)); + var_dump('MiXeD CaSe' |> strtolower(...)); + var_dump(__DIR__ |> dirname(...) === \dirname(__DIR__)); + var_dump(['a', 'b', 'c'] |> implode(...)); + var_dump(' chained call ' |> trim(...) |> strlen(...)); +} + +namespace Fallback { + function strtolower(string $string): string { + return 'namespaced ' . \strtolower($string); + } + + var_dump('MiXeD CaSe' |> strtolower(...)); +} + +?> +--EXPECT-- +string(14) "string literal" +string(12) "nowdoc value" +string(10) "mixed case" +bool(true) +string(3) "abc" +int(12) +string(21) "namespaced mixed case" diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index d8d61ea979e4..b5d64d3a24c5 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -4731,6 +4731,26 @@ static uint32_t zend_compile_frameless_icall(znode *result, zend_ast_list *args, return zend_compile_frameless_icall_ex(result, args, fbc, frameless_function_info, type); } +/* The pipe operator passes its left hand side as a ZEND_AST_ZNODE argument holding + * a single reference to an already compiled value. zend_compile_ns_call() compiles + * its argument list twice and each compilation hands the constant over to + * zend_add_literal(), which may release it while interning. Take one extra reference + * per additional compilation, so neither the literals nor the AST are left with a + * dangling pointer. */ +static void zend_args_addref_const_znodes(const zend_ast_list *args) +{ + uint32_t i; + for (i = 0; i < args->children; ++i) { + zend_ast *arg = args->child[i]; + if (arg->kind == ZEND_AST_ZNODE) { + znode *node = zend_ast_get_znode(arg); + if (node->op_type == IS_CONST) { + Z_TRY_ADDREF(node->u.constant); + } + } + } +} + static void zend_compile_ns_call(znode *result, znode *name_node, zend_ast *args_ast, uint32_t lineno, uint32_t type) /* {{{ */ { int name_constants = zend_add_ns_func_name_literal(Z_STR(name_node->u.constant)); @@ -4751,6 +4771,8 @@ static void zend_compile_ns_call(znode *result, znode *name_node, zend_ast *args if (frameless_function) { frameless_function_info = find_frameless_function_info(zend_ast_get_list(args_ast), frameless_function, type); if (frameless_function_info) { + /* The argument list is compiled a second time below. */ + zend_args_addref_const_znodes(zend_ast_get_list(args_ast)); CG(context).in_jmp_frameless_branch = true; znode op1; op1.op_type = IS_CONST;