Commit 39867196284 for php

commit 39867196284c492f4d44bf2cfdf6812f6899117c
Author: ndossche <7771979+ndossche@users.noreply.github.com>
Date:   Tue Sep 29 12:18:26 2026 +0200

    Fix GH-23979: Nullsafe operator must not flush delayed oplines of an enclosing function

    The fetch of `${$name}` is delayed until the whole expr has been
    compiled. But the closure in the brackets is compiled meanwhile, and
    uses the same delayed-opline stack. We havet o bound the loop.

    Closes GH-23984.

diff --git a/NEWS b/NEWS
index 94ac095afc9..4d15d50f643 100644
--- a/NEWS
+++ b/NEWS
@@ -5,6 +5,8 @@ PHP                                                                        NEWS
 - Core:
   . Fixed memory leak when an exception already contains the pending exception
     in its previous chain. (Edmond)
+  . Fixed bug GH-23979 (Nullsafe operator must not flush delayed oplines of an
+    enclosing function). (ndossche)

 - DOM:
   . Fixed bug GH-23352 (UAF reading an attribute value node retained across
diff --git a/Zend/tests/nullsafe_operator/gh23979.phpt b/Zend/tests/nullsafe_operator/gh23979.phpt
new file mode 100644
index 00000000000..ba5e78feb1a
--- /dev/null
+++ b/Zend/tests/nullsafe_operator/gh23979.phpt
@@ -0,0 +1,16 @@
+--TEST--
+GH-23979 (Nullsafe operator must not flush delayed oplines of an enclosing function)
+--FILE--
+<?php
+function test($name) {
+    $arr = ['foo' => 'bar'];
+    return ${$name}[(function () {
+        return A . B?->prop;
+    })()];
+}
+const A = 'foo';
+const B = null;
+var_dump(test('arr'));
+?>
+--EXPECT--
+string(3) "bar"
diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c
index 42c136d6bf3..300cd0ca061 100644
--- a/Zend/zend_compile.c
+++ b/Zend/zend_compile.c
@@ -2466,10 +2466,15 @@ static inline zend_op *zend_delayed_emit_op(znode *result, uint8_t opcode, znode
 }
 /* }}} */

-static inline uint32_t zend_delayed_compile_begin(void) /* {{{ */
+static zend_always_inline uint32_t zend_delayed_oplines_stack_size(void)
 {
 	return zend_stack_count(&CG(delayed_oplines_stack));
 }
+
+static inline uint32_t zend_delayed_compile_begin(void) /* {{{ */
+{
+	return zend_delayed_oplines_stack_size();
+}
 /* }}} */

 static zend_op *zend_delayed_compile_end(uint32_t offset) /* {{{ */
@@ -3155,6 +3160,7 @@ static zend_op *zend_delayed_compile_prop(znode *result, zend_ast *ast, uint32_t
 		/* We will throw if $this doesn't exist, so there's no need to emit a JMP_NULL
 		 * check for a nullsafe access. */
 	} else {
+		uint32_t offset = zend_delayed_oplines_stack_size();
 		zend_short_circuiting_mark_inner(obj_ast);
 		opline = zend_delayed_compile_var(&obj_node, obj_ast, type, 0);
 		if (opline && (opline->opcode == ZEND_FETCH_DIM_W
@@ -3170,10 +3176,11 @@ static zend_op *zend_delayed_compile_prop(znode *result, zend_ast *ast, uint32_t
 				/* Flush delayed oplines */
 				zend_op *opline = NULL, *oplines = zend_stack_base(&CG(delayed_oplines_stack));
 				uint32_t var = obj_node.u.op.var;
-				uint32_t count = zend_stack_count(&CG(delayed_oplines_stack));
+				uint32_t count = zend_delayed_oplines_stack_size();
 				uint32_t i = count;

-				while (i > 0 && oplines[i-1].result_type == IS_TMP_VAR && oplines[i-1].result.var == var) {
+				/* Only consider the oplines delayed while compiling obj_ast. */
+				while (i > offset && oplines[i-1].result_type == IS_TMP_VAR && oplines[i-1].result.var == var) {
 					i--;
 					if (oplines[i].op1_type == IS_TMP_VAR) {
 						var = oplines[i].op1.var;