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)
} elseif (!(CG(compiler_options) &ZEND_COMPILE_IGNORE_USER_FUNCTIONS)){
if (zend_execute_ex==execute_ex) {
if (!(fbc->common.fn_flags& (ZEND_ACC_DEPRECATED|no_discard))) {
returnZEND_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, 0EXECUTE_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, 1EXECUTE_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 {
protectedfunction &getData(): array {
$x = [];
return$x;
}
publicfunctionprocess(): array {
if ($data = &$this->getData() && !isset($data['key'])) {
// unreachable
}
return$data;
}
}
class Child extends Base {
protectedfunction &getData(): array {
static$x = ['value' => 42];
return$x;
}
}
$child = newChild();
$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))) {
returnZEND_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) !=0EXECUTE_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