Built-in PHP Session Handling Quirks
There’s a lot of PHP code out there that uses PHP’s built-in session handling in anything from simple per-visitor customization to user authentication. In the usual case, everything seems to work fine. That is until authentication systems seem to timeout after the user has been idle for a while, usually after 30 minutes. Most probably, this is caused by PHP invalidating your session data.
I have been using the built-in PHP session handling in most of my work for quite a while and it’s only now I realize how much it sucks. During the PHP 4.0 days, the built-in session handling seemed horribly broken and behaved inconsistenty in-between patch levels. At that time, I wrote my own session handling routines.
Fast forward to the 4.3 era and built-in session handling seems to have improved and this time there seemed to be no inconsistencies between patch levels. If there is one thing that is consistent with the built-in session handling in the recent PHP4 versions, then it is its ability to screw up authentication systems that rely on sessions timing out properly.
Case in point: I have a system that requires authentication to be able to upload very large files via HTTP POST. I have configured PHP to allow file uploads as large as 128 MB. I have also configured PHP to have an unlimited execution time for scripts. The reason for this is that very large files may take about an hour or so to upload, even on a fast DSL connection. By default, built-in PHP sessions are configured with the following parameters:
session.gc_maxlifetime = 1440 (24 minutes)
session.gc_probability = 1
session.gc_divisor = 100
This allows for sessions to be valid for up to at least 24 minutes. If a session is older than 24 minutes (based on mtime in more recent versions of PHP), then the garbage collector will be started. However, based on the settings above, the GC will only have a 1% chance of starting on each request. Here is the actual code from the [PHP CVS][phpcvs] that does the session cleanup (reformatted to fit the layout):
static int
ps_files_cleanup_dir(const char *dirname,
int maxlifetime TSRMLS_DC)
{
DIR *dir;
char dentry[sizeof(struct dirent) + MAXPATHLEN];
struct dirent *entry = (struct dirent *) &dentry;
struct stat sbuf;
char buf[MAXPATHLEN];
time_t now;
int nrdels = 0;
size_t dirname_len;
dir = opendir(dirname);
if (!dir) {
php_error_docref(NULL TSRMLS_CC,
E_NOTICE,
“ps_files_cleanup_dir: opendir(%s)”
” failed: %s (%d)\n”,
dirname,
strerror(errno),
errno);
return (0);
}
time(&now);
dirname_len = strlen(dirname);
/* Prepare buffer (dirname never changes) */
memcpy(buf, dirname, dirname_len);
buf[dirname_len] = PHP_DIR_SEPARATOR;
while (php_readdir_r(dir,
(struct dirent *) dentry,
&entry) == 0
&& entry) {
/* does the file start with our prefix? */
if (!strncmp(entry->d_name, FILE_PREFIX,
sizeof(FILE_PREFIX) – 1)) {
size_t entry_len;
entry_len = strlen(entry->d_name);
/* does it fit into our buffer? */
if (entry_len + dirname_len + 2 < MAXPATHLEN) {
/* create the full path.. */
memcpy(buf + dirname_len + 1,
entry->d_name, entry_len);
/* NUL terminate it and */
buf[dirname_len + entry_len + 1] = ‘\0′;
/* check whether its last access was more
than maxlifet ago */
if (VCWD_STAT(buf, &sbuf) == 0
&& (now – sbuf.st_mtime) > maxlifetime) {
VCWD_UNLINK(buf);
nrdels++;
}
}
}
}
closedir(dir);
return (nrdels);
}
I don’t see any code that takes into account session.gc_probability and session.gc_divisor but
maybe I might be looking in the wrong place. In the function above, session.gc_maxlifetime
is taken into account. There should be no problem if you want your sessions to
live longer than 24 minutes. But if your script resides on a virtual hosting
environment, then you will be in trouble.
The thing is that, different scripts can set different session.gc_maxlifetime settings
and this is where things get really ugly. On most virtual hosting evironments,
the session.save_path points to a common directory (usually /tmp). Someone else
using PHP on a different virtual host may be using a shorter gc_maxlifetime.
What happens is that, PHP will take the shortest session.gc_maxlifetime value and use
that in the function above. So it is possible for someone else’s session.gc_maxlifetime
settings to screw up your sessions.
According to the [PHP reference on session handling][phpsess], you should also
consider setting session.save_path to a different value than the default, when using
a different session.gc_maxlifetime. Here’s what I did:
$defaultSavePath = ini_get(’session.save_path’);
$customSavePath = $defaultSavePath
.DIRECTORY_SEPARATOR
.’sitename_sessions’;
if (!is_dir($customSavePath)) {
if (mkdir($customSavePath)) {
ini_set(’session.save_path’, $customSavePath);
}
}
ini_set(’session.gc_maxlifetime’, 3600 * 2);
session_start();
Notice that I called session_start() after setting up the
session.save_path and session.gc_maxlifetime. Now,
based on what can be gathered from the PHP documentation and from the source
for PHP’s session handling, we can assume that we should be able to have a
session that lasts for up to two hours. However, this assumption falls flat on
its face as sessions seem to last only for an hour or so despite the fact that
we have already configured the parameters as above.
I am at a loss as to why this is happening and it is very frustrating to debug because:
You have to wait until about an hour or so[1][note1] to know if the session has been invalidated.
It may turn out to be a bug in PHP4 itself and may get fixed in a future version. In which case, any work-arounds would likely break.
Tested and observed on the following configurations:
Ubuntu Breezy 5.10 – Apache 2.0.54 using pre-fork MPM – PHP4 4.4.0 using SAPI Apache 2.0 Handler
FreeBSD 4.10 – Apache 1.3.33 – PHP4 4.4.1 using SAPI Apache
Notes:
1 It does not seem to matter what you set the session.gc_maxlifetime to.
I have attempted to set it to something lower like 60 seconds, but it seems that
session.gc_maxlifetime is not being taken into account by the
garbage collector somehow.
[phpcvs]: http://cvs.php.net/viewcvs.cgi/php-src/ext/session/mod_files.c?view=markup&rev=1.83.2.9&pathrev=php_4_4_0 “PHP source code for session handling from CVS.” [phpsess]: http://www.php.net/session “PHP Documentation: Session Handling Functions” [note1]: #note1 “Note 1″
