php.net |  support |  documentation |  report a bug |  advanced search |  search howto |  statistics |  random bug |  login
Bug #10404 Timing issue - new page reads session before old page has saved it
Submitted: 2001-04-19 11:41 UTC Modified: 2001-06-18 09:49 UTC
From: bhlyons at mail dot com Assigned: sas (profile)
Status: Closed Package: Session related
PHP Version: 4.0.4pl1 OS: Linux - RH7.0
Private report: No CVE-ID: None
 [2001-04-19 11:41 UTC] bhlyons at mail dot com
Summary:  On a high-speed network system, a page with a header(Location: ...) call does not always write the session file before the referred-to page reads it.  The result is a loss of session continuity.  I have not been able to reproduce this behavior across the internet.  Additionally, I have not been able to reproduce this behavior on the local net with IE as the client, but it is reproducable with NS 4.7 running on Linux 6.0 and win98.

My PHP configuration line:  ./configure --with-apxs --enable-bcmath --with-bz2 --with-config-file-path=/etc --disable-debug --enable-debugger --enable-ftp --with-gd --with-gettext --enable-inline-optimization --with-jpeg-dir=/usr --with-mysql=/usr --enable-magic-quotes --with-png --with-regex=system --enable-safe-mode --disable-short-tags --disable-static --enable-sysvsem --enable-sysvshm --enable-track-vars --enable-trans-sid --enable-yp --with-zlib

The server is an Athlon 600 with 128MB, running a stock RH7.0 system with apache 1.13.12 as installed with RH7.0.  The php.ini file is identical to php.ini-dist.  I compiled MySQL 3.23.33 (relevant for the session-based variant discussed later).  The clients are an Athlon 600 with 384 MB (Linux) and a K6-200 with 128MB (win98), each with NS4.7.  The network is switched at 100 Mb/s.

The three following files can reproduce the behavior.  I'm sorry that they're so long, but this is the minimum set.  The originals were much, much longer.

begin index.html +++++++++++++++++++++++++++++++++++++
<HTML>
<HEAD>
<TITLE>Home Page</TITLE>
</HEAD>
<BODY>
<HR>

<a href="foo.html"><IMG SRC="foo.gif"></a>
<HR>
Home<BR>
<a href="users.php">Users</a><BR>

<HR>

<HR>
</BODY>
</HTML>
end index.html ++++++++++++++++++++++++++++++++++++++++

begin users.php +++++++++++++++++++++++++++++++++++++++
<?php

$fd = fopen("/tmp/debuglog", "a");
fwrite($fd , "=====================================================\n");

# fire up the session tracking and register the
# session variables
session_start();
session_register(array('user' , 'pass' , 'returnfromlogin' , 'loginfailures'));
session_register("count");
$count++;
$returnfromlogin=$PHP_SELF;

if(TRUE) {
  $sessdata = session_encode();
  fwrite($fd , "      PHPSESSID:$PHPSESSID:\n");
  fwrite($fd , "         sessid:" . session_id() . ":\n");
  fwrite($fd , "           user:$user:\n");
  fwrite($fd , "           pass:$pass:\n");
  fwrite($fd , "returnfromlogin:$returnfromlogin:\n");
  fwrite($fd , "  loginfailures:$loginfailures:\n");
  fwrite($fd , " session_encode:" . session_encode() . ":\n");
  fwrite($fd , "      microtime:" . microtime() . ":\n");
  header("Location: login.php");
  # without this exit, PHP keeps running and wastes cpu
  # and bandwidth
  exit;
}

?>

<HTML>
<HEAD>
<TITLE>Administration - Users</TITLE>
</HEAD>
<BODY >
<P>The body goes here</P>
</BODY>
</HTML>
end users.php ++++++++++++++++++++++++++++++++++++++++

begin login.php ++++++++++++++++++++++++++++++++++++++
<?php

# if PHPSESSID is set, investigate the session state
# file (/tmp/sess_...)
# when things are not working properly, the session
# state file will be of
# zero length because the write from the previous page
# has not completed yet
if(isset($PHPSESSID)) {
  $sfilename = "/tmp/sess_" . $PHPSESSID;
  clearstatcache();
  $mtime = microtime();
  if(file_exists($sfilename)) {
    $exists = "True";
    $length = filesize($sfilename);
    $sfile = fopen($sfilename , "r");
    $scontent = fread($sfile , filesize($sfilename));
    fclose($sfile);
    if($exists=="True" && $length==0) {
      # the usleep fixes everything
      # usleep(1);
    }
  } else {
    $exists = "False";
  }
}

# open the debugging log and write out the current state
$fd = fopen("/tmp/debuglog", "a");
fwrite($fd , "---------------------------------------------\n");
fwrite($fd , "Initial\n");
fwrite($fd , "      PHPSESSID:$PHPSESSID:\n");
fwrite($fd , " sessfileexists:$exists:\n");
fwrite($fd , " sessfilelength:$length:\n");
fwrite($fd , "sessfilecontent:$scontent:\n");

# fire up the session tracking
session_start();
$count++;

# clear the stat cache and re-investigate the session
# state file now that
# session_start has been called
clearstatcache();
if(file_exists($sfilename)) {
  $exists = "True";
  $length = filesize($sfilename);
}
fwrite($fd , "--------------------------\n");
fwrite($fd , "After session_start\n");
fwrite($fd , "      PHPSESSID:$PHPSESSID:\n");
fwrite($fd , "         sessid:" . session_id() . ":\n");
fwrite($fd , "           user:$user:\n");
fwrite($fd , "           pass:$pass:\n");
fwrite($fd , "returnfromlogin:$returnfromlogin:\n");
fwrite($fd , "  loginfailures:$loginfailures:\n");
fwrite($fd , " sessfileexists:$exists:\n");
fwrite($fd , " sessfilelength:$length:\n");
fwrite($fd , " session_encode:" . session_encode() . ":\n");

usleep(1);
# clear the stat cache and re-investigate the session
# state file now that
# usleep(1) has been called
clearstatcache();
if(file_exists($sfilename)) {
  $exists = "True";
  $length = filesize($sfilename);
  $sfile = fopen($sfilename , "r");
  $scontent = fread($sfile , filesize($sfilename));
  fclose($sfile);
}
fwrite($fd , "--------------------------\n");
fwrite($fd , "After usleep(1)\n");
fwrite($fd , " sessfileexists:$exists:\n");
fwrite($fd , " sessfilelength:$length:\n");
fwrite($fd , "sessfilecontent:$scontent:\n");

?>

<HTML>
<HEAD>
<TITLE>Administration - Login</TITLE>
</HEAD>
<BODY >
<TABLE border="5">
<TR>
	<TD>
		<center><font size="+3">Administration - Login</font></center>

<?php

# the next line should hold 'user.php', but it doesn't
# most of the time
print "<P>Return to: $returnfromlogin</P>\n";
print "<P>Loginpage: $loginpage</P>\n";
?>

<FORM METHOD="POST">
<P>Usename:  <INPUT TYPE="text" SIZE=16 NAME="username"></P>
<P>Password: <INPUT TYPE="password" SIZE=16 NAME="password"></P>
<INPUT TYPE="hidden" NAME="validate" VALUE=1>
<P><INPUT TYPE="submit" VALUE="Login"></P>
</FORM>

	</TD>
</TR>

</TABLE>
</BODY>
</HTML>

end login.php ++++++++++++++++++++++++++++++++++++++++++

The original idea was that I would store the desired page uri in a session variable and redirect un-authenticated users to the login page.  Upon successfully login in, the user could be redirected back to the page they really wanted.  For debugging purposes, the login.php page shows the name of the page that dumped the user to the login page.  With the files provided above, it should always show "Return to: somepath/users.php", but will often be "Return to:" - thus indicating a failure.

I have not been able to reproduce the behavior by either typing in the uri "users.php" or creating a bookmark to it.  It is reproducible if you start from the index.html and click on the 'Users' link.  Adding a 'foo.gif' image to load on the index.html page increases the failure rate to 4 out of 5.  Without the foo.gif image, the failure rate is about 1 out of 10.  I start a fresh copy of netscape for each test, thus removing the session cookie.

There are debugging statements in the php code which log to the file /tmp/debuglog.  The log when the session functions properly looks like:

=====================================================
      PHPSESSID::
         sessid:1ed96ede8bcb57ebe228d3d72778f786:
           user::
           pass::
returnfromlogin:/lyons/session-bug-demo-file/users.php:
  loginfailures::
 session_encode:!user|!pass|returnfromlogin|s:38:"/lyons/session-bug-demo-file/users.php";!loginfailures|count|i:1;:
      microtime:0.19839100 987667153:
---------------------------------------------
Initial
      PHPSESSID:1ed96ede8bcb57ebe228d3d72778f786:
 sessfileexists:True:
 sessfilelength:99:
sessfilecontent:!user|!pass|returnfromlogin|s:38:"/lyons/session-bug-demo-file/users.php";!loginfailures|count|i:1;:
--------------------------
After session_start
      PHPSESSID:1ed96ede8bcb57ebe228d3d72778f786:
         sessid:1ed96ede8bcb57ebe228d3d72778f786:
           user::
           pass::
returnfromlogin:/lyons/session-bug-demo-file/users.php:
  loginfailures::
 sessfileexists:True:
 sessfilelength:99:
 session_encode:!user|!pass|returnfromlogin|s:38:"/lyons/session-bug-demo-file/users.php";!loginfailures|count|i:2;:
--------------------------
After usleep(1)
 sessfileexists:True:
 sessfilelength:99:
sessfilecontent:!user|!pass|returnfromlogin|s:38:"/lyons/session-bug-demo-file/users.php";!loginfailures|count|i:1;:

users.php write the first block between the === and the ---.  It shows that the session id has been created, the returnfromlogin variable has a value, and that session_encode knows about all of the session variables.

login.php writes the remaining blocks.  The 'Initial' block shows that the session id has been retrieved from the cookie, that the session file exists and that it is 99 bytes long.  It also reads the file which has the appropriate contents.

The 'After session_start' block shows that the variables have been successfully recovered by session_start, and then re-confirms the session file's existence, length, and contents.

The 'After usleep(1)' block confirms once-again the session file's existence, length, and contents.

But, when the session doesn't work properly, here is what the log shows:

=====================================================
      PHPSESSID::
         sessid:42ea7fe7ec4d1bfec7732b5e6c425d92:
           user::
           pass::
returnfromlogin:/lyons/session-bug-demo-file/users.php:
  loginfailures::
 session_encode:!user|!pass|returnfromlogin|s:38:"/lyons/session-bug-demo-file/users.php";!loginfailures|count|i:1;:
      microtime:0.18177800 987667522:
---------------------------------------------
Initial
      PHPSESSID:42ea7fe7ec4d1bfec7732b5e6c425d92:
 sessfileexists:True:
 sessfilelength:0:
sessfilecontent::
--------------------------
After session_start
      PHPSESSID:42ea7fe7ec4d1bfec7732b5e6c425d92:
         sessid:42ea7fe7ec4d1bfec7732b5e6c425d92:
           user::
           pass::
returnfromlogin::
  loginfailures::
 sessfileexists:True:
 sessfilelength:0:
 session_encode::
--------------------------
After usleep(1)
 sessfileexists:True:
 sessfilelength:99:
sessfilecontent:!user|!pass|returnfromlogin|s:38:"/lyons/session-bug-demo-file/users.php";!loginfailures|count|i:1;:

The users.php write of the first block between the === and the --- is just as before.

The 'Initial' block shows that the session file exists, but is of 0 length.  That certainly explains why the login.php page isn't able to load the session variables.

The 'After session_start' block shows that the session_start was not able to load the session variables, and that the session file is still 0 length.

The 'After usleep(1)' block (which really does follow a 1 micro-second sleep), shows that the session file has finally been written by the first page, but long after the second page needed it.  The session file shows up with the proper length and contents, only to be overwritten an instant later when the login.php page saves a 0 length file because it doesn't know about the variables previously registered or their values.

It is so interesting that a 1 microsecond sleep gives the first page enough of a kick to get its session information written.  A quick-and-dirty fix has been to add a usleep(1) before the session_start in login.php, which gets the users.php session write to finish before the new session loads.

I hope that you are able to reproduce this on your local networks.  It doesn't appear to be an issue when the latency of the internet is involved, but it could certainly become an issue on high-speed internal company networks.

My thinking is that there are two solutions.  The first is a rigorous flushing setup that guarantees the first page is finished before the second one begins.  It would probably slow down the whole php session system.  The second solution revolves around removing the ambiguity between a session with no registered values and a session file that has yet to be written - both are 0 length.  Session files that are yet to be written will always be 0 length, so maybe the answer is to put some content into the session file for a session with no registered variables, maybe just 2 bytes of content "!!", which could never occur in a real session file, would allow the session_start to differentiate between these two situations.  If session_start sees a 0 length file, it sleeps and checks again a few times before giving up.  If session_start sees !!, it knows there are no registered variables and it keeps on going.

I've also tested the behavior with the PHP4 MySQL Session Handler, Version 1.00, by Ying Zhang.  I was surprised that the behavior is worse, failing almost 100% of the time - and no amount of usleep seems to help in the least.

I really am sorry that this posting is so long, but this is a very sensitive behavior that is difficult to reproduce, and may be entirely fine on your systems.  I am certain that you can see how this behavior could occur, even if you are not able to reproduce it.

I will be happy to assist in any way that I can.  I think PHP is awesome and hope to help it grow by fortifying it against this behavior.

Sincerely,
Bryan

Patches

Add a Patch

Pull Requests

Add a Pull Request

History

AllCommentsChangesGit/SVN commitsRelated reports
 [2001-04-19 12:45 UTC] hholzgra@php.net
this should not happen as the session extension
uses file locking to prevent this

Sascha?
 [2001-05-30 12:02 UTC] kalowsky@php.net
there was a recent cvs commit which found a small error in the file locking (submitted by sascha).  please try one of the RCs for 4.0.6, current cvs, or wait for the 4.0.6 and see if this continues there.... 
 [2001-05-30 12:03 UTC] kalowsky@php.net
umm, that last post should also say "could you try this with a recent..." :)  sorry about that!
 [2001-06-18 09:49 UTC] kalowsky@php.net
no user feedback... considered fixed in CVS.  if this is untrue, please reopen the bug.
 
PHP Copyright © 2001-2024 The PHP Group
All rights reserved.
Last updated: Wed Apr 24 10:01:31 2024 UTC