[php-src] Issue #20913: OPcache folds FETCH_CONSTANT across later DECLARE_CONST when zend_execute_internal is set
[email protected] (KaseyJenkins)
| Newsgroups | php.bugs |
|---|---|
| Message-ID | <[email protected]> |
Issue: https://github.com/php/php-src/issues/20913
Author: KaseyJenkins
### 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](https://github.com/php/php-src/blob/PHP-8.5.1/Zend/zend_compile.c#L3917) 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](https://github.com/php/php-src/blob/PHP-8.5.1/Zend/Optimizer/pass1.c#L160) 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](https://github.com/php/php-src/blob/PHP-8.5.1/ext/opcache/tests/bug66251.phpt)).
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
```plain
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
```
### Operating System
_No response_