Skip to content

Commit ae2f866

Browse files
committed
add recursive session cleanup for dirname > 0
1 parent 74fad61 commit ae2f866

7 files changed

Lines changed: 259 additions & 45 deletions

ext/session/mod_files.c

Lines changed: 35 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ static zend_result ps_files_write(ps_files *data, zend_string *key, zend_string
276276
return SUCCESS;
277277
}
278278

279-
static int ps_files_cleanup_dir(const zend_string *dirname, zend_long maxlifetime)
279+
static int ps_files_cleanup_dir(const zend_string *dirname, zend_long maxlifetime, size_t remaining_depth)
280280
{
281281
DIR *dir;
282282
struct dirent *entry;
@@ -291,44 +291,56 @@ static int ps_files_cleanup_dir(const zend_string *dirname, zend_long maxlifetim
291291
return -1;
292292
}
293293

294-
time(&now);
295-
296294
if (ZSTR_LEN(dirname) >= MAXPATHLEN) {
297295
php_error_docref(NULL, E_NOTICE, "ps_files_cleanup_dir: dirname(%s) is too long", ZSTR_VAL(dirname));
298296
closedir(dir);
299297
return -1;
300298
}
301299

302-
/* Prepare buffer (dirname never changes) */
303300
memcpy(buf, ZSTR_VAL(dirname), ZSTR_LEN(dirname));
304301
buf[ZSTR_LEN(dirname)] = PHP_DIR_SEPARATOR;
305302

306-
while ((entry = readdir(dir))) {
307-
/* does the file start with our prefix? */
308-
if (!strncmp(entry->d_name, FILE_PREFIX, sizeof(FILE_PREFIX) - 1)) {
303+
if (remaining_depth == 0) {
304+
time(&now);
305+
while ((entry = readdir(dir))) {
306+
if (!strncmp(entry->d_name, FILE_PREFIX, sizeof(FILE_PREFIX) - 1)) {
307+
size_t entry_len = strlen(entry->d_name);
308+
if (entry_len + ZSTR_LEN(dirname) + 2 < MAXPATHLEN) {
309+
memcpy(buf + ZSTR_LEN(dirname) + 1, entry->d_name, entry_len);
310+
buf[ZSTR_LEN(dirname) + entry_len + 1] = '\0';
311+
if (VCWD_STAT(buf, &sbuf) == 0 &&
312+
(now - sbuf.st_mtime) > maxlifetime) {
313+
VCWD_UNLINK(buf);
314+
nrdels++;
315+
}
316+
}
317+
}
318+
}
319+
} else {
320+
while ((entry = readdir(dir))) {
321+
if (entry->d_name[0] == '.' &&
322+
(entry->d_name[1] == '\0' ||
323+
(entry->d_name[1] == '.' && entry->d_name[2] == '\0'))) {
324+
continue;
325+
}
309326
size_t entry_len = strlen(entry->d_name);
310-
311-
/* does it fit into our buffer? */
312-
if (entry_len + ZSTR_LEN(dirname) + 2 < MAXPATHLEN) {
313-
/* create the full path.. */
327+
if (ZSTR_LEN(dirname) + 1 + entry_len < MAXPATHLEN) {
314328
memcpy(buf + ZSTR_LEN(dirname) + 1, entry->d_name, entry_len);
315-
316-
/* NUL terminate it and */
317-
buf[ZSTR_LEN(dirname) + entry_len + 1] = '\0';
318-
319-
/* check whether its last access was more than maxlifetime ago */
320-
if (VCWD_STAT(buf, &sbuf) == 0 &&
321-
(now - sbuf.st_mtime) > maxlifetime) {
322-
VCWD_UNLINK(buf);
323-
nrdels++;
329+
buf[ZSTR_LEN(dirname) + 1 + entry_len] = '\0';
330+
if (VCWD_STAT(buf, &sbuf) == 0 && S_ISDIR(sbuf.st_mode)) {
331+
zend_string *subdir = zend_string_init(buf, ZSTR_LEN(dirname) + 1 + entry_len, 0);
332+
int n = ps_files_cleanup_dir(subdir, maxlifetime, remaining_depth - 1);
333+
zend_string_release(subdir);
334+
if (n >= 0) {
335+
nrdels += n;
336+
}
324337
}
325338
}
326339
}
327340
}
328341

329342
closedir(dir);
330-
331-
return (nrdels);
343+
return nrdels;
332344
}
333345

334346
static zend_result ps_files_key_exists(ps_files *data, const zend_string *key)
@@ -624,15 +636,7 @@ PS_GC_FUNC(files)
624636
{
625637
PS_FILES_DATA;
626638

627-
/* We don't perform any cleanup, if dirdepth is larger than 0.
628-
we return SUCCESS, since all cleanup should be handled by
629-
an external entity (i.e. find -ctime x | xargs rm) */
630-
631-
if (data->dirdepth == 0) {
632-
*nrdels = ps_files_cleanup_dir(data->basedir, maxlifetime);
633-
} else {
634-
*nrdels = -1; // Cannot process multiple depth save dir
635-
}
639+
*nrdels = ps_files_cleanup_dir(data->basedir, maxlifetime, data->dirdepth);
636640

637641
return *nrdels;
638642
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
--TEST--
2+
session GC cleans expired sessions with save_path dirdepth=2 (two subdir levels)
3+
--EXTENSIONS--
4+
session
5+
--SKIPIF--
6+
<?php include('skipif.inc'); ?>
7+
--INI--
8+
session.gc_probability=0
9+
session.gc_maxlifetime=10
10+
--FILE--
11+
<?php
12+
$base = __DIR__ . '/gc_dirdepth2_test';
13+
@mkdir($base);
14+
@mkdir("$base/a");
15+
@mkdir("$base/a/b");
16+
17+
session_save_path("2;$base");
18+
19+
$stale_id = 'abcdefghijklmnopqrstuvwx';
20+
$stale_file = "$base/a/b/sess_$stale_id";
21+
file_put_contents($stale_file, 'user|s:5:"alice";');
22+
touch($stale_file, time() - 100);
23+
24+
session_id('ab000000000000000000000000');
25+
session_start();
26+
$result = session_gc();
27+
session_destroy();
28+
29+
echo "session_gc() return value: ";
30+
var_dump($result);
31+
32+
echo "expired file removed: ";
33+
var_dump(!file_exists($stale_file));
34+
?>
35+
--CLEAN--
36+
<?php
37+
$base = __DIR__ . '/gc_dirdepth2_test';
38+
@unlink("$base/a/b/sess_ab000000000000000000000000");
39+
@rmdir("$base/a/b");
40+
@rmdir("$base/a");
41+
@rmdir($base);
42+
?>
43+
--EXPECT--
44+
session_gc() return value: int(1)
45+
expired file removed: bool(true)
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
--TEST--
2+
session GC accumulates correct total count across multiple subdirs, including empty ones (dirdepth=1)
3+
--EXTENSIONS--
4+
session
5+
--SKIPIF--
6+
<?php include('skipif.inc'); ?>
7+
--INI--
8+
session.gc_probability=0
9+
session.gc_maxlifetime=10
10+
--FILE--
11+
<?php
12+
$base = __DIR__ . '/gc_multi_subdir_test';
13+
@mkdir($base);
14+
@mkdir("$base/a");
15+
@mkdir("$base/b");
16+
@mkdir("$base/c");
17+
@mkdir("$base/d"); // empty subdir
18+
19+
session_save_path("1;$base");
20+
21+
$files = [
22+
"$base/a/sess_aexpired0000000000000000",
23+
"$base/b/sess_bexpired0000000000000000",
24+
"$base/c/sess_cexpired0000000000000000",
25+
];
26+
foreach ($files as $f) {
27+
file_put_contents($f, 'user|s:5:"alice";');
28+
touch($f, time() - 100);
29+
}
30+
31+
session_id('a0000000000000000000000000');
32+
session_start();
33+
$result = session_gc();
34+
session_destroy();
35+
36+
echo "session_gc() return value: ";
37+
var_dump($result);
38+
39+
echo "all expired files removed: ";
40+
var_dump(!file_exists($files[0]) && !file_exists($files[1]) && !file_exists($files[2]));
41+
?>
42+
--CLEAN--
43+
<?php
44+
$base = __DIR__ . '/gc_multi_subdir_test';
45+
@unlink("$base/a/sess_a0000000000000000000000000");
46+
@rmdir("$base/a");
47+
@rmdir("$base/b");
48+
@rmdir("$base/c");
49+
@rmdir("$base/d");
50+
@rmdir($base);
51+
?>
52+
--EXPECT--
53+
session_gc() return value: int(3)
54+
all expired files removed: bool(true)
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
--TEST--
2+
session GC deletes only expired sess_* files and leaves all other files untouched (dirdepth=1)
3+
--EXTENSIONS--
4+
session
5+
--SKIPIF--
6+
<?php include('skipif.inc'); ?>
7+
--INI--
8+
session.gc_probability=0
9+
session.gc_maxlifetime=10
10+
--FILE--
11+
<?php
12+
$base = __DIR__ . '/gc_selective_test';
13+
@mkdir($base);
14+
@mkdir("$base/a");
15+
16+
session_save_path("1;$base");
17+
18+
$expired = "$base/a/sess_aexpired0000000000000000";
19+
$fresh = "$base/a/sess_afresh000000000000000000";
20+
$other = "$base/a/other_file";
21+
22+
file_put_contents($expired, 'user|s:5:"alice";');
23+
touch($expired, time() - 100); // 100 s old > gc_maxlifetime=10 → deleted
24+
25+
file_put_contents($fresh, 'user|s:5:"alice";');
26+
touch($fresh, time() - 1); // 1 s old < gc_maxlifetime=10 → kept
27+
28+
file_put_contents($other, 'untouched');
29+
touch($other, time() - 100); // old but no sess_ prefix → kept
30+
31+
session_id('a0000000000000000000000000'); // first char 'a' → $base/a/
32+
session_start();
33+
$result = session_gc(); // int(1): exactly one deletion proves selectivity
34+
session_destroy();
35+
36+
echo "session_gc() return value: ";
37+
var_dump($result);
38+
39+
echo "expired sess_ file removed: ";
40+
var_dump(!file_exists($expired));
41+
42+
echo "other file kept: ";
43+
var_dump(file_exists($other));
44+
?>
45+
--CLEAN--
46+
<?php
47+
$base = __DIR__ . '/gc_selective_test';
48+
@unlink("$base/a/sess_afresh000000000000000000");
49+
@unlink("$base/a/sess_a0000000000000000000000000");
50+
@unlink("$base/a/other_file");
51+
@rmdir("$base/a");
52+
@rmdir($base);
53+
?>
54+
--EXPECT--
55+
session_gc() return value: int(1)
56+
expired sess_ file removed: bool(true)
57+
other file kept: bool(true)
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
--TEST--
2+
session GC correctly cleans expired sessions when save_path dirdepth > 0
3+
--EXTENSIONS--
4+
session
5+
--SKIPIF--
6+
<?php include('skipif.inc'); ?>
7+
--INI--
8+
session.gc_probability=0
9+
session.gc_maxlifetime=1
10+
--FILE--
11+
<?php
12+
13+
$base = __DIR__ . '/gc_dirdepth_test';
14+
@mkdir($base);
15+
@mkdir("$base/a");
16+
17+
// ── Part 1: dirdepth=1
18+
session_save_path("1;$base");
19+
20+
$stale_id = 'abcdefghijklmnopqrstuvwx';
21+
$stale_file = "$base/a/sess_$stale_id";
22+
file_put_contents($stale_file, 'user|s:5:"alice";');
23+
touch($stale_file, time() - 100); // 100 s old; gc_maxlifetime=1 → must be GC'd
24+
25+
session_id('a0000000000000000000000000');
26+
session_start();
27+
$result_depth = session_gc();
28+
session_destroy();
29+
$depth_file_gone = !file_exists($stale_file);
30+
31+
// ── Part 2: dirdepth=0
32+
session_save_path($base);
33+
34+
$flat_id = 'bbcdefghijklmnopqrstuvwx';
35+
$flat_file = "$base/sess_$flat_id";
36+
file_put_contents($flat_file, 'user|s:5:"alice";');
37+
touch($flat_file, time() - 100);
38+
39+
session_start();
40+
$result_flat = session_gc();
41+
session_destroy();
42+
$flat_file_gone = !file_exists($flat_file);
43+
44+
echo "dirdepth=1 — session_gc() return value: ";
45+
var_dump($result_depth);
46+
47+
echo "dirdepth=1 — expired session file removed: ";
48+
var_dump($depth_file_gone);
49+
50+
echo "dirdepth=0 — session_gc() return value: ";
51+
var_dump($result_flat);
52+
53+
echo "dirdepth=0 — expired session file removed: ";
54+
var_dump($flat_file_gone);
55+
?>
56+
--CLEAN--
57+
<?php
58+
$base = __DIR__ . '/gc_dirdepth_test';
59+
@unlink("$base/a/sess_abcdefghijklmnopqrstuvwx");
60+
@unlink("$base/a/sess_a0000000000000000000000000");
61+
@rmdir("$base/a");
62+
@rmdir($base);
63+
?>
64+
--EXPECT--
65+
dirdepth=1 — session_gc() return value: int(1)
66+
dirdepth=1 — expired session file removed: bool(true)
67+
dirdepth=0 — session_gc() return value: int(1)
68+
dirdepth=0 — expired session file removed: bool(true)

php.ini-development

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1386,13 +1386,6 @@ session.gc_divisor = 1000
13861386
; https://php.net/session.gc-maxlifetime
13871387
session.gc_maxlifetime = 1440
13881388

1389-
; NOTE: If you are using the subdirectory option for storing session files
1390-
; (see session.save_path above), then garbage collection does *not*
1391-
; happen automatically. You will need to do your own garbage
1392-
; collection through a shell script, cron entry, or some other method.
1393-
; For example, the following script is the equivalent of setting
1394-
; session.gc_maxlifetime to 1440 (1440 seconds = 24 minutes):
1395-
; find /path/to/sessions -cmin +24 -type f | xargs rm
13961389

13971390
; Check HTTP Referer to invalidate externally stored URLs containing ids.
13981391
; HTTP_REFERER has to contain this substring for the session to be

php.ini-production

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1388,13 +1388,6 @@ session.gc_divisor = 1000
13881388
; https://php.net/session.gc-maxlifetime
13891389
session.gc_maxlifetime = 1440
13901390

1391-
; NOTE: If you are using the subdirectory option for storing session files
1392-
; (see session.save_path above), then garbage collection does *not*
1393-
; happen automatically. You will need to do your own garbage
1394-
; collection through a shell script, cron entry, or some other method.
1395-
; For example, the following script is the equivalent of setting
1396-
; session.gc_maxlifetime to 1440 (1440 seconds = 24 minutes):
1397-
; find /path/to/sessions -cmin +24 -type f | xargs rm
13981391

13991392
; Check HTTP Referer to invalidate externally stored URLs containing ids.
14001393
; HTTP_REFERER has to contain this substring for the session to be

0 commit comments

Comments
 (0)