From 862420bf942b496e752e8a8d1f06224d40f90be0 Mon Sep 17 00:00:00 2001 From: Roman Pronskiy Date: Wed, 20 May 2026 23:56:08 +0200 Subject: [PATCH] Fix GH-9812: memory leak on repeated include with file_cache_only=1 With opcache.file_cache_only=1 every include allocates a fresh arena in zend_file_cache_script_load() and it leaks. Looks like zend_arena_release() only runs when cache_it is set, which is the SHM path. CG(arena) only resets at request shutdown, so I think memory just keeps growing. My attempt: per-request hashtable in ZCG, resolved path -> zend_persistent_script. First include fills it, next ones should reuse the arena allocation. Freeing it in accel_post_deactivate seems to work since it runs before zend_arena_destroy and shutdown_memory_manager, so entries and buckets should still be alive. Fixed GH-9812 Closes GH-9812 --- ext/opcache/ZendAccelerator.c | 42 ++++++++++++++++++++++---- ext/opcache/ZendAccelerator.h | 2 ++ ext/opcache/tests/gh9812.inc | 2 ++ ext/opcache/tests/gh9812.phpt | 55 +++++++++++++++++++++++++++++++++++ 4 files changed, 95 insertions(+), 6 deletions(-) create mode 100644 ext/opcache/tests/gh9812.inc create mode 100644 ext/opcache/tests/gh9812.phpt diff --git a/ext/opcache/ZendAccelerator.c b/ext/opcache/ZendAccelerator.c index 3d005b3835a7..36559bf2720f 100644 --- a/ext/opcache/ZendAccelerator.c +++ b/ext/opcache/ZendAccelerator.c @@ -1900,7 +1900,7 @@ static zend_persistent_script *opcache_compile_file(zend_file_handle *file_handl static zend_op_array *file_cache_compile_file(zend_file_handle *file_handle, int type) { - zend_persistent_script *persistent_script; + zend_persistent_script *persistent_script = NULL; zend_op_array *op_array = NULL; bool from_memory; /* if the script we've got is stored in SHM */ @@ -1923,11 +1923,35 @@ static zend_op_array *file_cache_compile_file(zend_file_handle *file_handle, int } } - HANDLE_BLOCK_INTERRUPTIONS(); - SHM_UNPROTECT(); - persistent_script = zend_file_cache_script_load(file_handle); - SHM_PROTECT(); - HANDLE_UNBLOCK_INTERRUPTIONS(); + /* In file_cache_only mode each call to zend_file_cache_script_load() + * allocates a fresh per-request arena buffer that is never released + * (see GH-9812). Reuse a previously loaded script for the same + * resolved path so subsequent includes do not grow CG(arena). */ + if (file_cache_only && file_handle->opened_path) { + if (UNEXPECTED(!ZCG(file_cache_only_scripts).nTableSize)) { + zend_hash_init(&ZCG(file_cache_only_scripts), 8, NULL, NULL, 0); + } + persistent_script = zend_hash_find_ptr( + &ZCG(file_cache_only_scripts), file_handle->opened_path); + if (persistent_script && ZCG(accel_directives).validate_timestamps + && zend_get_file_handle_timestamp(file_handle, NULL) != persistent_script->timestamp) { + zend_hash_del(&ZCG(file_cache_only_scripts), file_handle->opened_path); + persistent_script = NULL; + } + } + + if (!persistent_script) { + HANDLE_BLOCK_INTERRUPTIONS(); + SHM_UNPROTECT(); + persistent_script = zend_file_cache_script_load(file_handle); + SHM_PROTECT(); + HANDLE_UNBLOCK_INTERRUPTIONS(); + + if (persistent_script && file_cache_only && file_handle->opened_path) { + zend_hash_add_new_ptr(&ZCG(file_cache_only_scripts), + file_handle->opened_path, persistent_script); + } + } if (persistent_script) { /* see bug #15471 (old BTS) */ if (persistent_script->script.filename) { @@ -2820,6 +2844,12 @@ zend_result accel_post_deactivate(void) ZCG(cwd) = NULL; } + if (ZCG(file_cache_only_scripts).nTableSize) { + /* See GH-9812 */ + zend_hash_destroy(&ZCG(file_cache_only_scripts)); + memset(&ZCG(file_cache_only_scripts), 0, sizeof(HashTable)); + } + if (!ZCG(enabled) || !accel_startup_ok) { return SUCCESS; } diff --git a/ext/opcache/ZendAccelerator.h b/ext/opcache/ZendAccelerator.h index 91642e288d31..f179a9c9e2f4 100644 --- a/ext/opcache/ZendAccelerator.h +++ b/ext/opcache/ZendAccelerator.h @@ -227,6 +227,8 @@ typedef struct _zend_accel_globals { zend_persistent_script *cache_persistent_script; /* preallocated buffer for keys */ zend_string *key; + /* per-process cache to avoid leaks on repeated includes when opcache.file_cache_only=1. */ + HashTable file_cache_only_scripts; } zend_accel_globals; typedef struct _zend_string_table { diff --git a/ext/opcache/tests/gh9812.inc b/ext/opcache/tests/gh9812.inc new file mode 100644 index 000000000000..6fe5badb600f --- /dev/null +++ b/ext/opcache/tests/gh9812.inc @@ -0,0 +1,2 @@ + +--INI-- +opcache.enable=1 +opcache.enable_cli=1 +opcache.jit=disable +opcache.file_cache="{PWD}/gh9812_cache" +opcache.file_cache_only=1 +opcache.file_update_protection=0 +--EXTENSIONS-- +opcache +--FILE-- + +--CLEAN-- +isDir()) { + @rmdir($fileinfo->getRealPath()); + } else { + @unlink($fileinfo->getRealPath()); + } + } + @rmdir($dir); +} +removeDirRecursive(__DIR__ . '/gh9812_cache'); +?> +--EXPECT-- +stable