[php-src] master: Fix GH-23979: Nullsafe operator must not flush delayed oplines of an enclosing function

From: Date: Thu, 08 Oct 2026 17:54:26 +0000
Subject: [php-src] master: Fix GH-23979: Nullsafe operator must not flush delayed oplines of an enclosing function
Groups: php.cvs 
Request: Send a blank email to php-cvs+get-142099@lists.php.net to get a copy of this message
Author: ndossche (ndossche) Date: 2026-10-08T19:53:34+02:00 Commit: https://github.com/php/php-src/commit/39867196284c492f4d44bf2cfdf6812f6899117c Raw diff: https://github.com/php/php-src/commit/39867196284c492f4d44bf2cfdf6812f6899117c.diff 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. Changed paths: A Zend/tests/nullsafe_operator/gh23979.phpt M NEWS M Zend/zend_compile.c Diff: diff --git a/NEWS b/NEWS index 94ac095afc91..4d15d50f6436 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 000000000000..ba5e78feb1a2 --- /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 42c136d6bf36..300cd0ca061c 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;

« previous php.cvs (#142099) next »