Page MenuHomePhabricator

PHPSessionHandler::gc uses wrong timestamp format, causing early loss of sessions
Open, MediumPublicBUG REPORT

Description

We noticed that users were getting randomly disconnected from our mediawiki instances.

After some digging through the code related to session handling and object caching, I have found out what is deleting sessions from the objectcache table in the database. I have been able to reproduce this both on two remote servers as well as locally. Both times, my set-up is running in a docker environment. (A slightly modified wikibase docker-compose set-up).

The call-stack that is responsible for deleting sessions from the objectcache table looks like this:

#0  SqlBagOStuff->deleteServerObjectsExpiringBefore() called at [/var/www/html/includes/objectcache/SqlBagOStuff.php:668]
#1  SqlBagOStuff->deleteObjectsExpiringBefore() called at [/var/www/html/includes/libs/objectcache/CachedBagOStuff.php:148]
#2  CachedBagOStuff->deleteObjectsExpiringBefore() called at [/var/www/html/includes/session/PHPSessionHandler.php:376]
#3  MediaWiki\Session\PHPSessionHandler->gc()
#4  session_start() called at [/var/www/html/includes/Setup.php:757]
#5  require_once(/var/www/html/includes/Setup.php) called at [/var/www/html/includes/WebStart.php:89]
#6  require(/var/www/html/includes/WebStart.php) called at [/var/www/html/index.php:44]

Usually, SqlBagOStuff::deleteServerObjectsExpiringBefore is called from MWCallableUpdate->doUpdate() for deferred updates and passed a Unix timestamp in that case. This Unix timestamp is a UTC value and handled as such.

However, sometimes, as can be seen from the backtrace above, SqlBagOStuff::deleteServerObjectsExpiringBefore is called from the Garbage Collector method (gc) in PHPSessionHandler:

PHPSessionHandler.php
public function gc( $maxlifetime ) {
    if ( self::$instance !== $this ) {
        throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
    }
    $before = date( 'YmdHis', time() );
    $this->store->deleteObjectsExpiringBefore( $before );
    return true;
}

The problem here is that – probably for historical reasons - the function is passed a date string here instead of a Unix timestamp. And the actual problem with that is that the date() function applies timezone information (if $wgLocaltimezone is set).

The resulting timestamp will thus be in local time but interpreted by SqlBagOStuff::deleteServerObjectsExpiringBefore as UTC value. With $wgLocaltimezone being e.g. Europe/Luxembourg as in our case, the timestamp will thus be a CET/GMT+1+DST value and thus gc() will cause all current sessions to be deleted since they have a standard expiry time of 1h, stored as UTC value in the database. (E.g. if I log in at 18:05 CET, the expiry value in the DB gets stored as 17:05 [UTC value of 16:05 + 1 hour]. Garbage collection then proceeds to delete all sessions smaller (earlier) than 18:05, effectively deleting all active sessions).

If this happens, and the session is not declared persistent ($session->isPersistent(), Keep me logged in), then all users are immediately logged out of the mediawiki instance.

Since the $timestamp parameter passed to SqlBagOStuff::deleteServerObjectsExpiringBefore is passed to ConvertibleTimestamp::convert anyway, I do not see any reason to pass a date string from PHPSessionHandler::gc. My suggestion for a fix – if this is confirmed as bug – would be to change the gc method as follows:

PHPSessionHandler.php
public function gc( $maxlifetime ) {
    if ( self::$instance !== $this ) {
        throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
    }
    $this->store->deleteObjectsExpiringBefore( microtime(true) );    // time() would be sufficient though
    return true;
}

An add-on question to this would be: What controls if and when PHPSessionHandler::gc is called? Is it indeed the session configuration for the PHP process (i.e. session.gc_probability, session.gc_divisor and session.gc_maxlifetime) ?

The original post mentioning this can be found on the wikitech-l mailing list.

Event Timeline

What MediaWiki version? What PHP version?

It's mediawiki 1.35.6 running on PHP 7.4

root@22edce4cf143:/var/www/html# php --version
PHP 7.4.28 (cli) (built: Mar 29 2022 03:23:10) ( NTS )
Copyright (c) The PHP Group
Zend Engine v3.4.0, Copyright (c) Zend Technologies
    with Zend OPcache v7.4.28, Copyright (c), by Zend Technologies

Change 797450 had a related patch set uploaded (by Umherirrender; author: Umherirrender):

[mediawiki/core@master] session: Use wfTimestampNow() in PHPSessionHandler::gc for now

https://gerrit.wikimedia.org/r/797450

Umherirrender triaged this task as Medium priority.