From e974407f3171ff81eb217d2a44c589c438be3b0b Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Wed, 18 Mar 2026 17:33:41 -0400 Subject: [PATCH] Fix GH-18714: opcache_compile_file() breaks class hoisting opcache_compile_file() sets ZEND_COMPILE_WITHOUT_EXECUTION which prevented simple parentless classes from being linked during compilation. When the cached script was later loaded via require, opcache's delayed early binding couldn't resolve the unlinked class, causing "Class not found" errors for classes used before their declaration. Two changes: 1. zend_compile.c: Allow linking simple classes (no parent, no interfaces, no traits) even with ZEND_COMPILE_WITHOUT_EXECUTION. This is purely structural with no execution side effects. The change is scoped to non-preload compilation to avoid interfering with preloading's own class linking pipeline. 2. zend_accelerator_module.c: Clean up classes/functions registered by zend_accel_load_script() after opcache_compile_file() returns. The function should only cache, not pollute the runtime tables. Cleanup is skipped during preloading where registrations must persist. Closes GH-18714 --- Zend/zend_compile.c | 55 ++++++++++++++++----------- ext/opcache/tests/gh18714.phpt | 28 ++++++++++++++ ext/opcache/zend_accelerator_module.c | 30 +++++++++++++++ 3 files changed, 90 insertions(+), 23 deletions(-) create mode 100644 ext/opcache/tests/gh18714.phpt diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index ee83ee75ff6ea..a36b80a9794c3 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -9654,35 +9654,44 @@ static void zend_compile_class_decl(znode *result, const zend_ast *ast, bool top /* See zend_link_hooked_object_iter(). */ && !ce->num_hooked_props #endif - && !(CG(compiler_options) & ZEND_COMPILE_WITHOUT_EXECUTION)) { - if (toplevel) { - if (extends_ast) { - zend_class_entry *parent_ce = zend_lookup_class_ex( - ce->parent_name, NULL, ZEND_FETCH_CLASS_NO_AUTOLOAD); - - if (parent_ce - && !zend_compile_ignore_class(parent_ce, ce->info.user.filename)) { - if (zend_try_early_bind(ce, parent_ce, lcname, NULL)) { - zend_string_release(lcname); - return; + ) { + if (!(CG(compiler_options) & ZEND_COMPILE_WITHOUT_EXECUTION)) { + if (toplevel) { + if (extends_ast) { + zend_class_entry *parent_ce = zend_lookup_class_ex( + ce->parent_name, NULL, ZEND_FETCH_CLASS_NO_AUTOLOAD); + + if (parent_ce + && !zend_compile_ignore_class(parent_ce, ce->info.user.filename)) { + if (zend_try_early_bind(ce, parent_ce, lcname, NULL)) { + zend_string_release(lcname); + return; + } } + } else if (EXPECTED(zend_hash_add_ptr(CG(class_table), lcname, ce) != NULL)) { + zend_string_release(lcname); + zend_build_properties_info_table(ce); + zend_inheritance_check_override(ce); + ce->ce_flags |= ZEND_ACC_LINKED; + zend_observer_class_linked_notify(ce, lcname); + return; + } else { + goto link_unbound; } - } else if (EXPECTED(zend_hash_add_ptr(CG(class_table), lcname, ce) != NULL)) { - zend_string_release(lcname); + } else if (!extends_ast) { +link_unbound: + /* Link unbound simple class */ zend_build_properties_info_table(ce); zend_inheritance_check_override(ce); ce->ce_flags |= ZEND_ACC_LINKED; - zend_observer_class_linked_notify(ce, lcname); - return; - } else { - goto link_unbound; } - } else if (!extends_ast) { -link_unbound: - /* Link unbound simple class */ - zend_build_properties_info_table(ce); - zend_inheritance_check_override(ce); - ce->ce_flags |= ZEND_ACC_LINKED; + } else if (!extends_ast + && !(CG(compiler_options) & ZEND_COMPILE_PRELOAD)) { + /* When compiling without execution (opcache_compile_file), + * link simple classes without parents so opcache can + * early-bind them when loaded from cache. Skip during + * preloading which has its own linking pipeline. */ + goto link_unbound; } } diff --git a/ext/opcache/tests/gh18714.phpt b/ext/opcache/tests/gh18714.phpt new file mode 100644 index 0000000000000..b79afa8c09986 --- /dev/null +++ b/ext/opcache/tests/gh18714.phpt @@ -0,0 +1,28 @@ +--TEST-- +GH-18714 (opcache_compile_file() breaks class hoisting) +--EXTENSIONS-- +opcache +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +--FILE-- + +--CLEAN-- + +--EXPECT-- +HelloWorld diff --git a/ext/opcache/zend_accelerator_module.c b/ext/opcache/zend_accelerator_module.c index 1eaa183f9df5b..2e9c126e526e9 100644 --- a/ext/opcache/zend_accelerator_module.c +++ b/ext/opcache/zend_accelerator_module.c @@ -961,6 +961,23 @@ ZEND_FUNCTION(opcache_jit_blacklist) #endif } +/* Remove hash table entries appended after orig_count without calling the + * destructor, since these point to SHM-backed data we don't own. */ +static void accel_rollback_hash(HashTable *ht, uint32_t orig_count) +{ + dtor_func_t orig_dtor = ht->pDestructor; + ht->pDestructor = NULL; + while (ht->nNumUsed > orig_count) { + Bucket *p = &ht->arData[ht->nNumUsed - 1]; + if (EXPECTED(Z_TYPE(p->val) != IS_UNDEF)) { + zend_hash_del_bucket(ht, p); + } else { + ht->nNumUsed--; + } + } + ht->pDestructor = orig_dtor; +} + ZEND_FUNCTION(opcache_compile_file) { zend_string *script_name; @@ -984,6 +1001,11 @@ ZEND_FUNCTION(opcache_compile_file) orig_compiler_options = CG(compiler_options); CG(compiler_options) |= ZEND_COMPILE_WITHOUT_EXECUTION; + /* Save class/function table state so we can undo the side effects + * of zend_accel_load_script() called by persistent_compile_file(). */ + uint32_t orig_class_count = EG(class_table)->nNumUsed; + uint32_t orig_function_count = EG(function_table)->nNumUsed; + if (CG(compiler_options) & ZEND_COMPILE_PRELOAD) { /* During preloading, a failure in opcache_compile_file() should result in an overall * preloading failure. Otherwise we may include partially compiled files in the preload @@ -1001,6 +1023,14 @@ ZEND_FUNCTION(opcache_compile_file) CG(compiler_options) = orig_compiler_options; if(op_array != NULL) { + /* Undo classes/functions registered by zend_accel_load_script(). + * opcache_compile_file() should only cache without side effects. + * Skip during preloading: preload needs the registrations to persist. */ + if (!(orig_compiler_options & ZEND_COMPILE_PRELOAD)) { + accel_rollback_hash(EG(class_table), orig_class_count); + accel_rollback_hash(EG(function_table), orig_function_count); + } + destroy_op_array(op_array); efree(op_array); RETVAL_TRUE;