Skip to content

PHP 8.6 OPcache: DO_UCALL breaks reference returns (segfault / invalid opcode) #21691

Description

@andypost

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.czend_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.hZEND_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

PHPOPcacheResult
8.6.0-devenabled (default)Fatal error: Invalid opcode 43/4/0
8.6.0-devopcache.enable_cli=0OK
8.6.0-devopcache.optimization_level=0x7FFEBFEF (no pass 5)OK
8.5.xenabledOK

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions