Description
Summary
The DO_UCALL opcode handler hardcodes return_reference=0 when calling
i_init_func_execute_data(), while DO_FCALL passes 1. When the optimizer
(in zend_get_call_op()) converts DO_FCALL to DO_UCALL for a user function
that returns by reference, the ASSIGN_REF that consumes the result gets an
incorrectly initialized return value, producing "Invalid opcode" errors or segfaults.
Affected: PHP 8.6.0-dev (the DO_UCALL opcode was introduced in 2015, but
zend_get_call_op() in the optimizer only recently started converting method calls
to DO_UCALL). The bug reproduces on PHP 8.5 as well with the same optimizer behavior.
Root Cause
Two files are involved:
1. Zend/zend_compile.c — zend_get_call_op() (line ~3989)
} else if (!(CG(compiler_options) & ZEND_COMPILE_IGNORE_USER_FUNCTIONS)){
if (zend_execute_ex == execute_ex) {
if (!(fbc->common.fn_flags & (ZEND_ACC_DEPRECATED|no_discard))) {
return ZEND_DO_UCALL; // BUG: ignores ZEND_ACC_RETURN_REFERENCE
}
}
}
Returns ZEND_DO_UCALL without checking ZEND_ACC_RETURN_REFERENCE.
2. Zend/zend_vm_def.h — ZEND_DO_UCALL handler (line ~4210)
i_init_func_execute_data(&fbc->op_array, ret, 0 EXECUTE_DATA_CC);
// ^ always 0
The third argument (return_reference) is hardcoded to 0. Compare with
ZEND_DO_FCALL (line ~4371):
i_init_func_execute_data(&fbc->op_array, ret, 1 EXECUTE_DATA_CC);
// ^ passes 1
Self-Contained Reproducer (no dependencies)
<?php
// php -d opcache.enable_cli=1 reproducer.php
// Expected: array(1) { ["value"]=> int(42) }
// Actual: PHP Fatal error: Invalid opcode 43/4/0.
class Base {
protected function &getData(): array {
$x = [];
return $x;
}
public function process(): array {
if ($data = &$this->getData() && !isset($data['key'])) {
// unreachable
}
return $data;
}
}
class Child extends Base {
protected function &getData(): array {
static $x = ['value' => 42];
return $x;
}
}
$child = new Child();
$result = $child->process();
var_dump($result);
Verified Behavior
| PHP |
OPcache |
Result |
| 8.6.0-dev |
enabled (default) |
Fatal error: Invalid opcode 43/4/0 |
| 8.6.0-dev |
opcache.enable_cli=0 |
OK |
| 8.6.0-dev |
opcache.optimization_level=0x7FFEBFEF (no pass 5) |
OK |
| 8.5.x |
enabled |
OK |
Opcode Diff
Before optimization (DO_FCALL):
INIT_METHOD_CALL 0 THIS string("getData")
V6 = DO_FCALL
V5 = ASSIGN_REF (function) CV2($batch) V6
After optimization (DO_UCALL):
INIT_METHOD_CALL 0 THIS string("getData")
V6 = DO_UCALL <-- broken: return_reference=0
V5 = ASSIGN_REF (function) CV2($batch) V6
Suggested Fixes
Option A: Fix zend_get_call_op() — don't use DO_UCALL for ref-returning functions
// In zend_get_call_op():
if (!(fbc->common.fn_flags & (ZEND_ACC_DEPRECATED|no_discard|ZEND_ACC_RETURN_REFERENCE))) {
return ZEND_DO_UCALL;
}
Option B: Fix DO_UCALL handler — honor return_reference flag
// In ZEND_DO_UCALL handler:
i_init_func_execute_data(
&fbc->op_array, ret,
(fbc->common.fn_flags & ZEND_ACC_RETURN_REFERENCE) != 0
EXECUTE_DATA_CC
);
Option A is simpler and has no runtime cost. Option B is more correct long-term.
Proposed Test
func_call_ref_return_overridden.phpt.txt
PHP Version
PHP 8.6.0-dev (cli) (built: Apr 9 2026 16:11:38) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.6.0-dev, Copyright (c) Zend Technologies
with Zend OPcache v8.6.0-dev, Copyright (c), by Zend Technologies
Operating System
ubuntu:resolute
Description
Summary
The
DO_UCALLopcode handler hardcodesreturn_reference=0when callingi_init_func_execute_data(), whileDO_FCALLpasses1. When the optimizer(in
zend_get_call_op()) convertsDO_FCALLtoDO_UCALLfor a user functionthat returns by reference, the
ASSIGN_REFthat consumes the result gets anincorrectly initialized return value, producing "Invalid opcode" errors or segfaults.
Affected: PHP 8.6.0-dev (the
DO_UCALLopcode was introduced in 2015, butzend_get_call_op()in the optimizer only recently started converting method callsto
DO_UCALL). The bug reproduces on PHP 8.5 as well with the same optimizer behavior.Root Cause
Two files are involved:
1.
Zend/zend_compile.c—zend_get_call_op()(line ~3989)Returns
ZEND_DO_UCALLwithout checkingZEND_ACC_RETURN_REFERENCE.2.
Zend/zend_vm_def.h—ZEND_DO_UCALLhandler (line ~4210)The third argument (
return_reference) is hardcoded to0. Compare withZEND_DO_FCALL(line ~4371):Self-Contained Reproducer (no dependencies)
Verified Behavior
Fatal error: Invalid opcode 43/4/0opcache.enable_cli=0opcache.optimization_level=0x7FFEBFEF(no pass 5)Opcode Diff
Before optimization (
DO_FCALL):After optimization (
DO_UCALL):Suggested Fixes
Option A: Fix
zend_get_call_op()— don't use DO_UCALL for ref-returning functionsOption B: Fix
DO_UCALLhandler — honor return_reference flagOption A is simpler and has no runtime cost. Option B is more correct long-term.
Proposed Test
func_call_ref_return_overridden.phpt.txt
PHP Version
Operating System
ubuntu:resolute