Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 35 additions & 33 deletions ext/session/mod_files.c
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ static zend_result ps_files_write(ps_files *data, zend_string *key, zend_string
return SUCCESS;
}

static int ps_files_cleanup_dir(const zend_string *dirname, zend_long maxlifetime)
static int ps_files_cleanup_dir(const zend_string *dirname, zend_long maxlifetime, size_t remaining_depth)
{
DIR *dir;
struct dirent *entry;
Expand All @@ -291,44 +291,54 @@ static int ps_files_cleanup_dir(const zend_string *dirname, zend_long maxlifetim
return -1;
}

time(&now);

if (ZSTR_LEN(dirname) >= MAXPATHLEN) {
php_error_docref(NULL, E_NOTICE, "ps_files_cleanup_dir: dirname(%s) is too long", ZSTR_VAL(dirname));
closedir(dir);
return -1;
}

/* Prepare buffer (dirname never changes) */
memcpy(buf, ZSTR_VAL(dirname), ZSTR_LEN(dirname));
buf[ZSTR_LEN(dirname)] = PHP_DIR_SEPARATOR;

if (remaining_depth == 0) {
time(&now);
}

while ((entry = readdir(dir))) {
/* does the file start with our prefix? */
if (!strncmp(entry->d_name, FILE_PREFIX, sizeof(FILE_PREFIX) - 1)) {
size_t entry_len = strlen(entry->d_name);

/* does it fit into our buffer? */
if (entry_len + ZSTR_LEN(dirname) + 2 < MAXPATHLEN) {
/* create the full path.. */
memcpy(buf + ZSTR_LEN(dirname) + 1, entry->d_name, entry_len);

/* NUL terminate it and */
buf[ZSTR_LEN(dirname) + entry_len + 1] = '\0';

/* check whether its last access was more than maxlifetime ago */
if (VCWD_STAT(buf, &sbuf) == 0 &&
(now - sbuf.st_mtime) > maxlifetime) {
VCWD_UNLINK(buf);
nrdels++;
}
if (entry->d_name[0] == '.' &&
(entry->d_name[1] == '\0' ||
(entry->d_name[1] == '.' && entry->d_name[2] == '\0'))) {
continue;
}
if (remaining_depth == 0 && strncmp(entry->d_name, FILE_PREFIX, sizeof(FILE_PREFIX) - 1) != 0) {
continue;
}
size_t entry_len = strlen(entry->d_name);
if (ZSTR_LEN(dirname) + 1 + entry_len >= MAXPATHLEN) {
continue;
}
memcpy(buf + ZSTR_LEN(dirname) + 1, entry->d_name, entry_len);
buf[ZSTR_LEN(dirname) + 1 + entry_len] = '\0';
if (VCWD_STAT(buf, &sbuf) != 0) {
continue;
}
if (remaining_depth == 0) {
if ((now - sbuf.st_mtime) > maxlifetime) {
VCWD_UNLINK(buf);
nrdels++;
}
} else if (S_ISDIR(sbuf.st_mode)) {
zend_string *subdir = zend_string_init(buf, ZSTR_LEN(dirname) + 1 + entry_len, 0);
int n = ps_files_cleanup_dir(subdir, maxlifetime, remaining_depth - 1);
zend_string_release(subdir);
if (n >= 0) {
nrdels += n;
Comment on lines +308 to +335
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please add comments to explain what you're doing.

The previous comments existed for a reason.

}
}
}

closedir(dir);

return (nrdels);
return nrdels;
}

static zend_result ps_files_key_exists(ps_files *data, const zend_string *key)
Expand Down Expand Up @@ -624,15 +634,7 @@ PS_GC_FUNC(files)
{
PS_FILES_DATA;

/* We don't perform any cleanup, if dirdepth is larger than 0.
we return SUCCESS, since all cleanup should be handled by
an external entity (i.e. find -ctime x | xargs rm) */

if (data->dirdepth == 0) {
*nrdels = ps_files_cleanup_dir(data->basedir, maxlifetime);
} else {
*nrdels = -1; // Cannot process multiple depth save dir
}
*nrdels = ps_files_cleanup_dir(data->basedir, maxlifetime, data->dirdepth);

return *nrdels;
}
Expand Down
45 changes: 45 additions & 0 deletions ext/session/tests/mod_files/gc_dirdepth2.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
--TEST--
session GC cleans expired sessions with save_path dirdepth=2 (two subdir levels)
--EXTENSIONS--
session
--SKIPIF--
<?php include(__DIR__ . '/../skipif.inc'); ?>
--INI--
session.gc_probability=0
session.gc_maxlifetime=10
--FILE--
<?php
$base = __DIR__ . '/gc_dirdepth2_test';
@mkdir($base);
@mkdir("$base/a");
@mkdir("$base/a/b");

session_save_path("2;$base");

$stale_id = 'abcdefghijklmnopqrstuvwx';
$stale_file = "$base/a/b/sess_$stale_id";
file_put_contents($stale_file, 'user|s:5:"alice";');
touch($stale_file, time() - 100);

session_id('ab000000000000000000000000');
session_start();
$result = session_gc();
session_destroy();

echo "session_gc() return value: ";
var_dump($result);

echo "expired file removed: ";
var_dump(!file_exists($stale_file));
?>
--CLEAN--
<?php
$base = __DIR__ . '/gc_dirdepth2_test';
@unlink("$base/a/b/sess_ab000000000000000000000000");
@rmdir("$base/a/b");
@rmdir("$base/a");
@rmdir($base);
?>
--EXPECT--
session_gc() return value: int(1)
expired file removed: bool(true)
68 changes: 68 additions & 0 deletions ext/session/tests/mod_files/gc_dirdepth_disabled.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
--TEST--
session GC correctly cleans expired sessions when save_path dirdepth > 0
--EXTENSIONS--
session
--SKIPIF--
<?php include(__DIR__ . '/../skipif.inc'); ?>
--INI--
session.gc_probability=0
session.gc_maxlifetime=1
--FILE--
<?php

$base = __DIR__ . '/gc_dirdepth_test';
@mkdir($base);
@mkdir("$base/a");

// ── Part 1: dirdepth=1
session_save_path("1;$base");

$stale_id = 'abcdefghijklmnopqrstuvwx';
$stale_file = "$base/a/sess_$stale_id";
file_put_contents($stale_file, 'user|s:5:"alice";');
touch($stale_file, time() - 100); // 100 s old; gc_maxlifetime=1 → must be GC'd

session_id('a0000000000000000000000000');
session_start();
$result_depth = session_gc();
session_destroy();
$depth_file_gone = !file_exists($stale_file);

// ── Part 2: dirdepth=0
session_save_path($base);

$flat_id = 'bbcdefghijklmnopqrstuvwx';
$flat_file = "$base/sess_$flat_id";
file_put_contents($flat_file, 'user|s:5:"alice";');
touch($flat_file, time() - 100);

session_start();
$result_flat = session_gc();
session_destroy();
$flat_file_gone = !file_exists($flat_file);

echo "dirdepth=1 — session_gc() return value: ";
var_dump($result_depth);

echo "dirdepth=1 — expired session file removed: ";
var_dump($depth_file_gone);

echo "dirdepth=0 — session_gc() return value: ";
var_dump($result_flat);

echo "dirdepth=0 — expired session file removed: ";
var_dump($flat_file_gone);
?>
--CLEAN--
<?php
$base = __DIR__ . '/gc_dirdepth_test';
@unlink("$base/a/sess_abcdefghijklmnopqrstuvwx");
@unlink("$base/a/sess_a0000000000000000000000000");
@rmdir("$base/a");
@rmdir($base);
?>
--EXPECT--
dirdepth=1 — session_gc() return value: int(1)
dirdepth=1 — expired session file removed: bool(true)
dirdepth=0 — session_gc() return value: int(1)
dirdepth=0 — expired session file removed: bool(true)
54 changes: 54 additions & 0 deletions ext/session/tests/mod_files/gc_dirdepth_multi_subdir_count.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
--TEST--
session GC accumulates correct total count across multiple subdirs, including empty ones (dirdepth=1)
--EXTENSIONS--
session
--SKIPIF--
<?php include(__DIR__ . '/../skipif.inc'); ?>
--INI--
session.gc_probability=0
session.gc_maxlifetime=10
--FILE--
<?php
$base = __DIR__ . '/gc_multi_subdir_test';
@mkdir($base);
@mkdir("$base/a");
@mkdir("$base/b");
@mkdir("$base/c");
@mkdir("$base/d"); // empty subdir

session_save_path("1;$base");

$files = [
"$base/a/sess_aexpired0000000000000000",
"$base/b/sess_bexpired0000000000000000",
"$base/c/sess_cexpired0000000000000000",
];
foreach ($files as $f) {
file_put_contents($f, 'user|s:5:"alice";');
touch($f, time() - 100);
}

session_id('a0000000000000000000000000');
session_start();
$result = session_gc();
session_destroy();

echo "session_gc() return value: ";
var_dump($result);

echo "all expired files removed: ";
var_dump(!file_exists($files[0]) && !file_exists($files[1]) && !file_exists($files[2]));
?>
--CLEAN--
<?php
$base = __DIR__ . '/gc_multi_subdir_test';
@unlink("$base/a/sess_a0000000000000000000000000");
@rmdir("$base/a");
@rmdir("$base/b");
@rmdir("$base/c");
@rmdir("$base/d");
@rmdir($base);
?>
--EXPECT--
session_gc() return value: int(3)
all expired files removed: bool(true)
57 changes: 57 additions & 0 deletions ext/session/tests/mod_files/gc_dirdepth_selective.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
--TEST--
session GC deletes only expired sess_* files and leaves all other files untouched (dirdepth=1)
--EXTENSIONS--
session
--SKIPIF--
<?php include(__DIR__ . '/../skipif.inc'); ?>
--INI--
session.gc_probability=0
session.gc_maxlifetime=10
--FILE--
<?php
$base = __DIR__ . '/gc_selective_test';
@mkdir($base);
@mkdir("$base/a");

session_save_path("1;$base");

$expired = "$base/a/sess_aexpired0000000000000000";
$fresh = "$base/a/sess_afresh000000000000000000";
$other = "$base/a/other_file";

file_put_contents($expired, 'user|s:5:"alice";');
touch($expired, time() - 100); // 100 s old > gc_maxlifetime=10 → deleted

file_put_contents($fresh, 'user|s:5:"alice";');
touch($fresh, time() - 1); // 1 s old < gc_maxlifetime=10 → kept

file_put_contents($other, 'untouched');
touch($other, time() - 100); // old but no sess_ prefix → kept

session_id('a0000000000000000000000000'); // first char 'a' → $base/a/
session_start();
$result = session_gc(); // int(1): exactly one deletion proves selectivity
session_destroy();

echo "session_gc() return value: ";
var_dump($result);

echo "expired sess_ file removed: ";
var_dump(!file_exists($expired));

echo "other file kept: ";
var_dump(file_exists($other));
?>
--CLEAN--
<?php
$base = __DIR__ . '/gc_selective_test';
@unlink("$base/a/sess_afresh000000000000000000");
@unlink("$base/a/sess_a0000000000000000000000000");
@unlink("$base/a/other_file");
@rmdir("$base/a");
@rmdir($base);
?>
--EXPECT--
session_gc() return value: int(1)
expired sess_ file removed: bool(true)
other file kept: bool(true)
7 changes: 0 additions & 7 deletions php.ini-development
Original file line number Diff line number Diff line change
Expand Up @@ -1386,13 +1386,6 @@ session.gc_divisor = 1000
; https://php.net/session.gc-maxlifetime
session.gc_maxlifetime = 1440

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

; Check HTTP Referer to invalidate externally stored URLs containing ids.
; HTTP_REFERER has to contain this substring for the session to be
Expand Down
7 changes: 0 additions & 7 deletions php.ini-production
Original file line number Diff line number Diff line change
Expand Up @@ -1388,13 +1388,6 @@ session.gc_divisor = 1000
; https://php.net/session.gc-maxlifetime
session.gc_maxlifetime = 1440

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

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