[php-src] Issue #21691: PHP 8.6 OPcache: DO_UCALL breaks reference returns (segfault / invalid opcode)
[email protected] (andypost)
| Newsgroups | php.bugs |
|---|---|
| Message-ID | <[email protected]> |
Issue: https://github.com/php/php-src/issues/21691
Author: 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.c` — `zend_get_call_op()` (line ~3989)
```c
} 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)
```c
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):
```c
i_init_func_execute_data(&fbc->op_array, ret, 1 EXECUTE_DATA_CC);
// ^ passes 1
```
## Self-Contained Reproducer (no dependencies)
```php
<?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
```c
// 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
```c
// 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](https://github.com/user-attachments/files/26609313/func_call_ref_return_overridden.phpt.txt)
### PHP Version
```plain
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