Skip to content

Commit 862420b

Browse files
committed
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
1 parent f51c8d5 commit 862420b

4 files changed

Lines changed: 95 additions & 6 deletions

File tree

ext/opcache/ZendAccelerator.c

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1900,7 +1900,7 @@ static zend_persistent_script *opcache_compile_file(zend_file_handle *file_handl
19001900

19011901
static zend_op_array *file_cache_compile_file(zend_file_handle *file_handle, int type)
19021902
{
1903-
zend_persistent_script *persistent_script;
1903+
zend_persistent_script *persistent_script = NULL;
19041904
zend_op_array *op_array = NULL;
19051905
bool from_memory; /* if the script we've got is stored in SHM */
19061906

@@ -1923,11 +1923,35 @@ static zend_op_array *file_cache_compile_file(zend_file_handle *file_handle, int
19231923
}
19241924
}
19251925

1926-
HANDLE_BLOCK_INTERRUPTIONS();
1927-
SHM_UNPROTECT();
1928-
persistent_script = zend_file_cache_script_load(file_handle);
1929-
SHM_PROTECT();
1930-
HANDLE_UNBLOCK_INTERRUPTIONS();
1926+
/* In file_cache_only mode each call to zend_file_cache_script_load()
1927+
* allocates a fresh per-request arena buffer that is never released
1928+
* (see GH-9812). Reuse a previously loaded script for the same
1929+
* resolved path so subsequent includes do not grow CG(arena). */
1930+
if (file_cache_only && file_handle->opened_path) {
1931+
if (UNEXPECTED(!ZCG(file_cache_only_scripts).nTableSize)) {
1932+
zend_hash_init(&ZCG(file_cache_only_scripts), 8, NULL, NULL, 0);
1933+
}
1934+
persistent_script = zend_hash_find_ptr(
1935+
&ZCG(file_cache_only_scripts), file_handle->opened_path);
1936+
if (persistent_script && ZCG(accel_directives).validate_timestamps
1937+
&& zend_get_file_handle_timestamp(file_handle, NULL) != persistent_script->timestamp) {
1938+
zend_hash_del(&ZCG(file_cache_only_scripts), file_handle->opened_path);
1939+
persistent_script = NULL;
1940+
}
1941+
}
1942+
1943+
if (!persistent_script) {
1944+
HANDLE_BLOCK_INTERRUPTIONS();
1945+
SHM_UNPROTECT();
1946+
persistent_script = zend_file_cache_script_load(file_handle);
1947+
SHM_PROTECT();
1948+
HANDLE_UNBLOCK_INTERRUPTIONS();
1949+
1950+
if (persistent_script && file_cache_only && file_handle->opened_path) {
1951+
zend_hash_add_new_ptr(&ZCG(file_cache_only_scripts),
1952+
file_handle->opened_path, persistent_script);
1953+
}
1954+
}
19311955
if (persistent_script) {
19321956
/* see bug #15471 (old BTS) */
19331957
if (persistent_script->script.filename) {
@@ -2820,6 +2844,12 @@ zend_result accel_post_deactivate(void)
28202844
ZCG(cwd) = NULL;
28212845
}
28222846

2847+
if (ZCG(file_cache_only_scripts).nTableSize) {
2848+
/* See GH-9812 */
2849+
zend_hash_destroy(&ZCG(file_cache_only_scripts));
2850+
memset(&ZCG(file_cache_only_scripts), 0, sizeof(HashTable));
2851+
}
2852+
28232853
if (!ZCG(enabled) || !accel_startup_ok) {
28242854
return SUCCESS;
28252855
}

ext/opcache/ZendAccelerator.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,8 @@ typedef struct _zend_accel_globals {
227227
zend_persistent_script *cache_persistent_script;
228228
/* preallocated buffer for keys */
229229
zend_string *key;
230+
/* per-process cache to avoid leaks on repeated includes when opcache.file_cache_only=1. */
231+
HashTable file_cache_only_scripts;
230232
} zend_accel_globals;
231233

232234
typedef struct _zend_string_table {

ext/opcache/tests/gh9812.inc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<?php
2+
return;

ext/opcache/tests/gh9812.phpt

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
--TEST--
2+
GH-9812: memory usage grows on repeated include with opcache.file_cache_only=1
3+
--SKIPIF--
4+
<?php
5+
@mkdir(__DIR__ . '/gh9812_cache', 0777, true);
6+
?>
7+
--INI--
8+
opcache.enable=1
9+
opcache.enable_cli=1
10+
opcache.jit=disable
11+
opcache.file_cache="{PWD}/gh9812_cache"
12+
opcache.file_cache_only=1
13+
opcache.file_update_protection=0
14+
--EXTENSIONS--
15+
opcache
16+
--FILE--
17+
<?php
18+
19+
$file = __DIR__ . '/gh9812.inc';
20+
21+
// Warm-up: write the script into the file cache
22+
require $file;
23+
require $file;
24+
25+
$baseline = memory_get_usage();
26+
for ($i = 0; $i < 200; $i++) {
27+
require $file;
28+
}
29+
$delta = memory_get_usage() - $baseline;
30+
31+
// Allow a small headroom for VM/JIT.
32+
echo $delta < 4096 ? "stable\n" : "leaking: $delta bytes\n";
33+
34+
?>
35+
--CLEAN--
36+
<?php
37+
function removeDirRecursive($dir) {
38+
if (!is_dir($dir)) return;
39+
$iterator = new RecursiveIteratorIterator(
40+
new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS),
41+
RecursiveIteratorIterator::CHILD_FIRST
42+
);
43+
foreach ($iterator as $fileinfo) {
44+
if ($fileinfo->isDir()) {
45+
@rmdir($fileinfo->getRealPath());
46+
} else {
47+
@unlink($fileinfo->getRealPath());
48+
}
49+
}
50+
@rmdir($dir);
51+
}
52+
removeDirRecursive(__DIR__ . '/gh9812_cache');
53+
?>
54+
--EXPECT--
55+
stable

0 commit comments

Comments
 (0)