parsivori / simplesamlphp

Automatically exported from code.google.com/p/simplesamlphp
Other
0 stars 0 forks source link

Incorrect object handling in Session.php causing memory bloat in memcacheSync.php #594

Closed GoogleCodeExporter closed 8 years ago

GoogleCodeExporter commented 8 years ago
What steps will reproduce the problem?
1. Configure SimpleSAMLphp to use memcache as a session store (I had two 
configured)
2. Populate memcache instances with 10,000 sessions
3. Run memcacheSync.php and watch it crash and burn

What is the expected output? What do you see instead?

With no changes to codebase it runs out of memory:

# time ./bin/memcacheSync.php
Server idp1:11211 has 14973 items.
Connecting to: idp1:11211
Connected. Finding keys.
Found 14985 key(s).
Server idp2:11211 has 14976 items.
Connecting to: idp2:11211
Connected. Finding keys.
Found 14989 key(s).
Total number of keys: 29974
Total number of unique keys: 14989
Starting synchronization.
PHP Fatal error:  Allowed memory size of 134217728 bytes exhausted (tried to 
allocate 71 bytes) in /path/to/simplesamlphp/lib/SimpleSAML/Memcache.php on 
line 50

real    0m37.378s
user    0m0.296s
sys 0m4.600s

After suggested fix (bodge):

# time ./bin/memcacheSync.php
Server idp1:11211 has 9479 items.
Connecting to: idp1:11211
Connected. Finding keys.
Found 9486 key(s).
Server idp2:11211 has 9483 items.
Connecting to: idp2:11211
Connected. Finding keys.
Found 9486 key(s).
Total number of keys: 18972
Total number of unique keys: 9486
Starting synchronization.
Synchronization done.
9486 keys in sync.

real    2m23.906s
user    0m0.726s
sys 0m11.289s

What version of the product are you using? On what operating system?

1.11.0

Behaviour observed on Linux (x64) and Solaris (SPARC)

Suggested fix from RedHat support (marked by them as "quick and probably 
dirty"):

Index: lib/SimpleSAML/Session.php
===================================================================
--- lib/SimpleSAML/Session.php  (revision 9)
+++ lib/SimpleSAML/Session.php  (working copy)
@@ -1081,7 +1081,9 @@
     * Add a shutdown function for saving this session object on exit.
     */
    private function addShutdownFunction() {
-       register_shutdown_function(array($this, 'saveSession'));
+       if (!isset($GLOBALS['syncinprogress'])) {
+           register_shutdown_function(array($this, 'saveSession'));
+       }
    }

Index: bin/memcacheSync.php
===================================================================
--- bin/memcacheSync.php    (revision 9)
+++ bin/memcacheSync.php    (working copy)
@@ -1,6 +1,7 @@
 #!/usr/bin/env php
 <?php

+$GLOBALS['syncinprogress'] = 1;

 /* Check that the memcache library is enabled. */
 if(!class_exists('Memcache')) {
@@ -162,4 +163,4 @@
    return $keys;
 }

-?>
\ No newline at end of file
+?>

Justification (from RedHat):

Each time you unserialize the $data, you create a SimpleSAML_Session.
This raise the __wakeup function which register a shutdown hook:

See http://us2.php.net/register_shutdown_function

  Multiple calls to register_shutdown_function() can be made,
  and each will be called in the same order as they were registered.
  If you call exit() within one registered shutdown function, processing
  will stop completely and no other registered shutdown functions will be called. 

So there is no memory leak, only memory consumption due to each callback being 
add on the stack (and thus, the object cannot be freed)

Try to comment the call to register_shutdown_function in 
lib/SimpleSAML/Session.php, and the issue disappear.

So, I think, this issue have to be reported to SimpleSAML upstream.

As I think there should be a single "session" object (in normal use) a possible 
workaround is to manage this using a singleton.

Original issue reported on code.google.com by matthew.slowe on 15 Nov 2013 at 11:20

GoogleCodeExporter commented 8 years ago
Hi Matthew,

Sorry that it took so long for us to take action on this issue, and thanks a 
lot for the very detailed explanations.

This is indeed a problem in SSP, though a very tough one to solve (and PHP 
itself doesn't help here). The Session class that's being problematic is 
already a singleton. The problem here is the script you are using is basically 
creating one instance of the class per session stored in the memcache server, 
which is intuitively wrong when we are dealing with a singleton. How is that 
possible, then? Because the singleton is something we define programatically, 
but PHP doesn't know about it. Therefore, each time you unserialize the object 
from the server, PHP is calling SimpleSAML_Session::__construct(), even though 
that's private.

In such situation, we end up with as many session objects as keys in the 
servers. The right approach would be to explicitly clean every object after 
each iteration in the "main" loop of the script, something like:

foreach($keys as $key) {
    $res = SimpleSAML_Memcache::get($key);
    if($res === NULL) {
        $skipped += 1;
    } else {
        $sync += 1;
    }
        unset($res);
}

What's the problem then? Well, that'll never work because what unset() does is 
just to remove the reference ($res) to the object, but since the object is a 
singleton, there's more references pointing to it, one inside the object 
itself. This means we are creating objects that we cannot explicitly destroy, 
because they are singletons. It doesn't make any sense at all to destroy a 
singleton, and in fact there's no way to do it in PHP. The GC will get rid of 
it only after the script ends, which is too late for this to work.

So in the end: yes, there's a memory leak in here, and one tough to get rid of, 
to be honest.

The solution that that the RH engineer is suggesting is, indeed, very dirty, 
because it implies modifying the behaviour of one "generic" class 
(SimpleSAML_Session) depending on who is using it in a module. Not to mention 
the use of a global variable, which is never a good idea.

The right way to solve this is to, first, stop registering a shutdown function 
each time we unserialize a session (it's a singleton, so that should only 
happen in the private constructor); and second, stop creating new objects each 
time we unserialize a session. In such cases, when we are unserializing we 
should fetch the instance, and overwrite its values with those we are restoring.

I've implemented this by making the SimpleSAML_Session class to implement the 
Serializable interface, then adding both serialize() and unserialize() 
functions, and getting rid of the __wakeup() function that was creating 
trouble. This is quite a big change, and could create trouble in some ways, but 
I've tried it and both the SP and the IdP keep working, and your bug disappears.

Without the patch and a limit of 10M memory, this is what I get if I run the 
script after automatically creating some thousands of sessions:

$ time ./memcacheSync.php 
Server localhost:11211 has 3276 items.
Connecting to: localhost:11211
Connected. Finding keys.
Found 3276 key(s).
Total number of keys: 3276
Total number of unique keys: 3276
Starting synchronization.
PHP Fatal error:  Allowed memory size of 10485760 bytes exhausted (tried to 
allocate 5215 bytes) in 
/media/sf_Devel/ssp-repos/idp/lib/SimpleSAML/Memcache.php on line 50
PHP Stack trace:
PHP   1. {main}() /media/sf_Devel/ssp-repos/idp/bin/memcacheSync.php:0
PHP   2. SimpleSAML_Memcache::get() 
/media/sf_Devel/ssp-repos/idp/bin/memcacheSync.php:63
PHP   3. unserialize() 
/media/sf_Devel/ssp-repos/idp/lib/SimpleSAML/Memcache.php:50

real    0m0.589s
user    0m0.012s
sys 0m0.284s

If I apply the patch, it runs smoothly (and quite fast, I would say):

$ time ./memcacheSync.php 
Server localhost:11211 has 4460 items.
Connecting to: localhost:11211
Connected. Finding keys.
Found 4460 key(s).
Total number of keys: 4460
Total number of unique keys: 4460
Starting synchronization.
Synchronization done.
4460 keys in sync.

real    0m1.064s
user    0m0.020s
sys 0m0.808s

I'm attaching the patch here for your convenience, though I've applied this in 
trunk too (r3356). Please test it and tell us if you have any problems with it.

Original comment by jaim...@gmail.com on 4 Feb 2014 at 3:08

GoogleCodeExporter commented 8 years ago
By the way!

Bear in mind that the code changes the way sessions are serialized. That means 
if you apply the patch and try to log an old session (stored in memcache 
previous to the point where you applied the patch), the unserialization will 
fail, creating an infinite loop. Therefore, remember to restart memcache 
immediately after applying the patch.

Original comment by jaim...@gmail.com on 4 Feb 2014 at 3:11

GoogleCodeExporter commented 8 years ago
Hi again,

Here's a much better way to solve this, that doesn't break anything and offers 
backwards compatibility. Please try with the patch attached.

Original comment by jaim...@gmail.com on 6 Feb 2014 at 11:39

Attachments: