Skip to content

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

@andypost

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)

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

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