Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
39 changes: 39 additions & 0 deletions Zend/tests/pipe_operator/pipe_ns_frameless_const.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
--TEST--
Piping a constant into a namespaced frameless call must not release it twice
--FILE--
<?php

/* In 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. The
* pipe operator passes its left hand side as an already compiled ZEND_AST_ZNODE,
* so both compilations have to keep the constant alive. trim(), strtolower(),
* dirname() and implode() all have frameless handlers. */

namespace Frameless {
var_dump(' string literal ' |> 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"
22 changes: 22 additions & 0 deletions Zend/zend_compile.c
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand All @@ -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;
Expand Down
Loading