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;