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;