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)

} 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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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