Commit ec5a1e001db for php.net
commit ec5a1e001db8455d89bc22d38d9dfd5bd17c0041
Author: Ilija Tovilo <ilija.tovilo@me.com>
Date: Mon Feb 23 22:25:10 2026 +0100
Fix preloaded constant erroneously propagated to file-cached script
Since GH-15021 preloaded constants are propagated to compiled scripts. This is
problematic for file cache, which assumes all referenced zvals are either
persistently allocated or local to the current script. However, preloaded
constants live in shm as immutable, but not persistent.
To solve this, we'd need to duplicate propagated constants in the optimizer when
file cache is used. This is error prone given it needs to happen in many places.
It's debatable whether constant propagation is even correct in this case, as
running the preloaded script on a restart isn't guaranteed to produce the same
result.
Hence, avoid the issue for now by just not relying on preloaded symbols when
file cache is used.
Fixes GH-21052
Closes GH-21281
diff --git a/NEWS b/NEWS
index 28187bd8590..b23bd68b838 100644
--- a/NEWS
+++ b/NEWS
@@ -39,6 +39,8 @@ PHP NEWS
(Petr Sumbera)
. Fixed bug GH-21227 (Borked SCCP of array containing partial object).
(ilutov)
+ . Fixed bug GH-21052 (Preloaded constant erroneously propagated to file-cached
+ script). (ilutov)
- OpenSSL:
. Fix a bunch of leaks and error propagation. (ndossche)
diff --git a/Zend/Optimizer/zend_optimizer.c b/Zend/Optimizer/zend_optimizer.c
index 25f16d252a8..9017572118e 100644
--- a/Zend/Optimizer/zend_optimizer.c
+++ b/Zend/Optimizer/zend_optimizer.c
@@ -799,6 +799,9 @@ static bool zend_optimizer_ignore_class(zval *ce_zv, zend_string *filename)
zend_class_entry *ce = Z_PTR_P(ce_zv);
if (ce->ce_flags & ZEND_ACC_PRELOADED) {
+ if (CG(compiler_options) & ZEND_COMPILE_WITH_FILE_CACHE) {
+ return true;
+ }
Bucket *ce_bucket = (Bucket*)((uintptr_t)ce_zv - XtOffsetOf(Bucket, val));
size_t offset = ce_bucket - EG(class_table)->arData;
if (offset < EG(persistent_classes_count)) {
@@ -817,6 +820,9 @@ static bool zend_optimizer_ignore_function(zval *fbc_zv, zend_string *filename)
return false;
} else if (fbc->type == ZEND_USER_FUNCTION) {
if (fbc->op_array.fn_flags & ZEND_ACC_PRELOADED) {
+ if (CG(compiler_options) & ZEND_COMPILE_WITH_FILE_CACHE) {
+ return true;
+ }
Bucket *fbc_bucket = (Bucket*)((uintptr_t)fbc_zv - XtOffsetOf(Bucket, val));
size_t offset = fbc_bucket - EG(function_table)->arData;
if (offset < EG(persistent_functions_count)) {
diff --git a/ext/opcache/tests/gh21052.phpt b/ext/opcache/tests/gh21052.phpt
new file mode 100644
index 00000000000..d9d0430b9ee
--- /dev/null
+++ b/ext/opcache/tests/gh21052.phpt
@@ -0,0 +1,32 @@
+--TEST--
+GH-21052: Preloaded constant erroneously propagated to file-cached script
+--CREDITS--
+Grummfy
+--EXTENSIONS--
+opcache
+--INI--
+opcache.enable=1
+opcache.enable_cli=1
+opcache.file_cache="{TMP}"
+opcache.preload={PWD}/gh21052_a.inc
+--SKIPIF--
+<?php
+if (PHP_OS_FAMILY == 'Windows') die('skip Preloading is not supported on Windows');
+?>
+--FILE--
+<?php
+require __DIR__ . '/gh21052_b.inc';
+?>
+--EXPECT--
+array(1) {
+ [0]=>
+ string(3) "foo"
+}
+array(1) {
+ [0]=>
+ string(3) "foo"
+}
+array(1) {
+ [0]=>
+ string(3) "foo"
+}
diff --git a/ext/opcache/tests/gh21052_a.inc b/ext/opcache/tests/gh21052_a.inc
new file mode 100644
index 00000000000..f9614369798
--- /dev/null
+++ b/ext/opcache/tests/gh21052_a.inc
@@ -0,0 +1,13 @@
+<?php
+
+class A {
+ const C = ['foo'];
+
+ public static function test() {
+ return ['foo'];
+ }
+}
+
+function test() {
+ return ['foo'];
+}
diff --git a/ext/opcache/tests/gh21052_b.inc b/ext/opcache/tests/gh21052_b.inc
new file mode 100644
index 00000000000..cb64924ed5a
--- /dev/null
+++ b/ext/opcache/tests/gh21052_b.inc
@@ -0,0 +1,5 @@
+<?php
+
+var_dump(A::C);
+var_dump(A::test());
+var_dump(test());