Commit 2db458f4447 for php.net
commit 2db458f44470b16222d9a8f7ad17ce368969cb80
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date: Sun Jun 28 10:17:11 2026 -0400
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
diff --git a/Zend/tests/pipe_operator/gh22490.phpt b/Zend/tests/pipe_operator/gh22490.phpt
new file mode 100644
index 00000000000..c801d938c27
--- /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 7e9f7ceac8d..9f8ebad8ab2 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;