[php-src] master: Fix GH-22490: NULL deref for a pipe on the lhs of ??= (#22494)

Ilia Alshanetsky via GitHub <[email protected]>
Newsgroups gmane.comp.php.cvs.general
Message-ID <[email protected]>
Author: Ilia Alshanetsky (iliaal)
Committer: GitHub (web-flow)
Pusher: iliaal
Date: 2026-06-28T10:17:11-04:00

Commit: https://github.com/php/php-src/commit/2db458f44470b16222d9a8f7ad17ce368969cb80
Raw diff: https://github.com/php/php-src/commit/2db458f44470b16222d9a8f7ad17ce368969cb80.diff

Fix GH-22490: NULL deref for a pipe on the lhs of ??= (#22494)

zend_compile_var_inner did not route ZEND_AST_PIPE through
zend_compile_memoized_expr, so a pipe on the left-hand side of ??= was
recompiled during the FETCH pass. zend_compile_pipe synthesizes a fresh
call node on each pass, so the memoized-result lookup keyed by that node
returned NULL and was dereferenced. Memoize the pipe like the other call
kinds zend_is_call() already recognizes.

Fixes GH-22490

Changed paths:
  A  Zend/tests/pipe_operator/gh22490.phpt
  M  Zend/zend_compile.c


Diff:

diff --git a/Zend/tests/pipe_operator/gh22490.phpt b/Zend/tests/pipe_operator/gh22490.phpt
new file mode 100644
index 000000000000..c801d938c275
--- /dev/null
+++ b/Zend/tests/pipe_operator/gh22490.phpt
@@ -0,0 +1,60 @@
+--TEST--
+GH-22490: Null pointer dereference compiling a pipe on the lhs of ??=
+--FILE--
+<?php
+$calls = 0;
+function unset_prop($x) {
+    global $calls;
+    $calls++;
+    return new stdClass;
+}
+function set_prop($x) {
+    global $calls;
+    $calls++;
+    $o = new stdClass;
+    $o->p = 7;
+    return $o;
+}
+
+echo "property unset, assigns and returns default: ";
+var_dump((1 |> unset_prop(...))->p ??= 99);
+echo "pipe evaluated once: ";
+var_dump($calls);
+
+$calls = 0;
+echo "property set, returns existing value: ";
+var_dump((1 |> set_prop(...))->p ??= 99);
+echo "pipe evaluated once: ";
+var_dump($calls);
+
+function new_array($x) {
+    global $calls;
+    $calls++;
+    return [];
+}
+
+$calls = 0;
+echo "dim target on a pipe result: ";
+var_dump((1 |> new_array(...))[0] ??= 42);
+echo "pipe evaluated once: ";
+var_dump($calls);
+
+function identity($x) {
+    return $x;
+}
+
+$calls = 0;
+echo "nested pipe on the lhs: ";
+var_dump((1 |> identity(...) |> unset_prop(...))->p ??= 5);
+echo "pipe evaluated once: ";
+var_dump($calls);
+?>
+--EXPECT--
+property unset, assigns and returns default: int(99)
+pipe evaluated once: int(1)
+property set, returns existing value: int(7)
+pipe evaluated once: int(1)
+dim target on a pipe result: int(42)
+pipe evaluated once: int(1)
+nested pipe on the lhs: int(5)
+pipe evaluated once: int(1)
diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c
index 7e9f7ceac8db..9f8ebad8ab29 100644
--- a/Zend/zend_compile.c
+++ b/Zend/zend_compile.c
@@ -12270,6 +12270,7 @@ static zend_op *zend_compile_var_inner(znode *result, zend_ast *ast, uint32_t ty
 			case ZEND_AST_METHOD_CALL:
 			case ZEND_AST_NULLSAFE_METHOD_CALL:
 			case ZEND_AST_STATIC_CALL:
+			case ZEND_AST_PIPE:
 				zend_compile_memoized_expr(result, ast, BP_VAR_W);
 				/* This might not actually produce an opcode, e.g. for expressions evaluated at comptime. */
 				return NULL;
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.