php / php/php-src

OPcache folds FETCH_CONSTANT across later DECLARE_CONST when zend_execute_internal is set

Open
#20,913 1 comment 0 reactions 1 assignee View on GitHub

@arnaud-lb is already working on this.

Since Jan 13, 2026.

Bug Extension: opcache Status: Needs Triage
Dominant language
C
Stars
40.4k
Forks
8.1k
Avg merge
2d 13h
Merged PRs (30d)
96

Description

Description

OPcache folds FETCH_CONSTANT across later DECLARE_CONST when zend_execute_internal is set, changing program semantics.

When an extension sets zend_execute_internal (even to a no-op), zend_get_call_op chooses the generic ZEND_DO_FCALL for INIT_FCALL_BY_NAME. This appears to affect OPcache pass 1: in a script that calls a function before a constant is declared, OPcache replaces ZEND_FETCH_CONSTANT inside that function with the later-declared literal, changing behavior from a fatal error to printing the constant value.

Reproducer

INI:

opcache.enable=1
opcache.enable_cli=1
opcache.optimization_level=-1
opcache.opt_debug_level=0x10000

Code:

<?php
printf("A=%s\n", getA());
const A = "hello";
function getA() { return A; }

Without any module: the script fatals (Undefined constant "A"), and getA keeps FETCH_CONSTANT (see bug66251).
With a module that only does:

orig_execute_internal = zend_execute_internal;
zend_execute_internal = our_own_execute_internal;

the optimizer turns getA into RETURN "hello" and the script prints A=hello.

Question
Is this behavior expected when an extension hooks zend_execute_internal?

Output (for reference).

No zend_execute_internal (before and after optimizer):

$_main:
     ; (lines=8, args=0, vars=0, tmps=2)
     ; (before optimizer)
     ; /bin/bug66251.php:1-6
     ; return  [] RANGE[0..0]
0000 INIT_FCALL 2 112 string("printf")
0001 SEND_VAL string("A=%s\n") 1
0002 INIT_FCALL_BY_NAME 0 string("getA")
0003 V0 = DO_FCALL_BY_NAME
0004 SEND_VAR V0 2
0005 DO_ICALL
0006 DECLARE_CONST string("A") string("hello")
0007 RETURN int(1)

getA:
     ; (lines=3, args=0, vars=0, tmps=1)
     ; (before optimizer)
     ; /bin/bug66251.php:4-4
     ; return  [] RANGE[0..0]
0000 T0 = FETCH_CONSTANT string("A")
0001 RETURN T0
0002 RETURN null

$_main:
     ; (lines=8, args=0, vars=0, tmps=1)
     ; (after optimizer)
     ; /bin/bug66251.php:1-6
0000 INIT_FCALL 2 112 string("printf")
0001 SEND_VAL string("A=%s\n") 1
0002 INIT_FCALL 0 96 string("geta")
0003 V0 = DO_UCALL
0004 SEND_VAR V0 2
0005 DO_ICALL
0006 DECLARE_CONST string("A") string("hello")
0007 RETURN int(1)

getA:
     ; (lines=2, args=0, vars=0, tmps=1)
     ; (after optimizer)
     ; /bin/bug66251.php:4-4
0000 T0 = FETCH_CONSTANT string("A")
0001 RETURN T0

zend_execute_internal is set (before and after optimizer):

$_main:
     ; (lines=8, args=0, vars=0, tmps=2)
     ; (before optimizer)
     ; /bin/bug66251.php:1-6
     ; return  [] RANGE[0..0]
0000 INIT_FCALL 2 112 string("printf")
0001 SEND_VAL string("A=%s\n") 1
0002 INIT_FCALL_BY_NAME 0 string("getA")
0003 V0 = DO_FCALL
0004 SEND_VAR V0 2
0005 DO_FCALL
0006 DECLARE_CONST string("A") string("hello")
0007 RETURN int(1)

getA:
     ; (lines=3, args=0, vars=0, tmps=1)
     ; (before optimizer)
     ; /bin/bug66251.php:4-4
     ; return  [] RANGE[0..0]
0000 T0 = FETCH_CONSTANT string("A")
0001 RETURN T0
0002 RETURN null

$_main:
     ; (lines=8, args=0, vars=0, tmps=1)
     ; (after optimizer)
     ; /bin/bug66251.php:1-6
0000 INIT_FCALL 2 112 string("printf")
0001 SEND_VAL string("A=%s\n") 1
0002 INIT_FCALL 0 80 string("geta")
0003 V0 = DO_UCALL
0004 SEND_VAR V0 2
0005 DO_FCALL
0006 DECLARE_CONST string("A") string("hello")
0007 RETURN int(1)

getA:
     ; (lines=1, args=0, vars=0, tmps=0)
     ; (after optimizer)
     ; /bin/bug66251.php:4-4
0000 RETURN string("hello")
PHP Version
PHP 8.5.1 (cli) (built: Dec 17 2025 08:47:15) (ZTS)
Copyright (c) The PHP Group
Zend Engine v4.5.1, Copyright (c) Zend Technologies
    with Zend OPcache v8.5.1, Copyright (c), by Zend Technologies

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.