Bug #72011 [Com]: DateTime serializes as empty object

From: Date: Mon, 20 Jun 2016 21:38:52 +0000
Subject: Bug #72011 [Com]: DateTime serializes as empty object
References: 1  Groups: php.bugs 
Request: Send a blank email to php-bugs+get-201765@lists.php.net to get a copy of this message
Edit report at https://bugs.php.net/bug.php?id=72011&edit=1

 ID:                 72011
 Comment by:         ladirecciondeangel at gmail dot com
 Reported by:        benjamin dot roth at jaumo dot com
 Summary:            DateTime serializes as empty object
 Status:             Open
 Type:               Bug
 Package:            Date/time related
 Operating System:   Linux
 PHP Version:        7.0.5
 Block user comment: N
 Private report:     N

 New Comment:

Is there a way to catch this error?


Previous Comments:
------------------------------------------------------------------------
[2016-06-17 14:19:20] bill at zeroedin dot com

@nikic and I chatted some on SO and he found a test case that reproduces the bug.

The bug occurs when the max_execution_time is reached *while* GC is running. Because of the way the
timeout is handled, GC is never given an opportunity to clean up and reset the gc_active flag or
finish collecting garbage. 

In PHP-FPM, the worker does not exit when the timeout error occurs, but it doesn't clean up the
invalid GC state either. When the bug has occurred in an FPM worker process, the worker continues
serving requests, but any DateTime objects created by the worker appear to have no properties when
accessed via the get_properties handler. Additionally, a bugged FPM worker will segfault if the bug
test code is run twice. I'm not sure why that is happening, but it's not surprising, as
once the bug occurs, the worker is in an invalid and indeterminate state anyway.

The issue occurs on Linux only, as Windows builds do not have ZEND_SIGNALS enabled. It appears both
7 and 5.x are affected.

Because this bug affects the state of the fpm worker itself, and affects subsequent requests,
I'd rate it as fairly high priority. At the very least, the fpm worker should die when this
condition occurs.

I'm not entirely sure, but should the body of gc_collect_cycles be wrapped in a
HANDLE_BLOCK_INTERRUPTIONS ... HANDLE_UNBLOCK_INTERRUPTIONS block?

The following test case works for me on Ubuntu 14.04 with PHP 5.6.22. It only occurs in a fraction
of runs, as the timeout coincidence with the GC run is probabilistic.

If you test via a curl call to this script served via php-fpm, you can see that subsequent calls to
the same test script handled by that process will segfault, but if you call a script containing just
the first 2 lines instead, it will simply output the PID and an empty DateTime.

Result below is as output by php-cli - fpm output differs.

Test script:
---------------
<?php
// Uncomment to see PID - useful for checking fpm behavior
//echo "PID: " . getmypid() . "\n";
print_r(DateTime::createFromFormat('Y-m-d H:i:s', '2016-01-01 00:00:00'));

set_time_limit(1);
ini_set('display_errors', 1);
error_reporting(E_ALL);

function shutdownFunc()
{
	print_r(DateTime::createFromFormat('Y-m-d H:i:s', '2016-01-01 00:00:00'));
}

register_shutdown_function('shutdownFunc');

while (true) {
    for ($i = 0; $i < 100; $i++) {
        $a = new stdClass;
        $a->b = $a;
        unset($a);
    }
    gc_collect_cycles();
}

Expected result:
----------------
DateTime Object
(
    [date] => 2016-01-01 00:00:00.000000
    [timezone_type] => 3
    [timezone] => America/New_York
)
PHP Fatal error:  Maximum execution time of 1 second exceeded in /var/www/testcase.php on line 23

Fatal error: Maximum execution time of 1 second exceeded in /var/www/testcase.php on line 23
DateTime Object
(
    [date] => 2016-01-01 00:00:00.000000
    [timezone_type] => 3
    [timezone] => America/New_York
)

Actual result:
--------------
DateTime Object
(
    [date] => 2016-01-01 00:00:00.000000
    [timezone_type] => 3
    [timezone] => America/New_York
)
PHP Fatal error:  Maximum execution time of 1 second exceeded in /var/www/testcase.php on line 23

Fatal error: Maximum execution time of 1 second exceeded in /var/www/testcase.php on line 23
DateTime Object
(
)

------------------------------------------------------------------------
[2016-06-15 00:53:04] bill at zeroedin dot com

Ok, I have a test case that exhibits the broken datetime behavior on Windows, but I can't seem
to get the conditions right on Linux because the gc seems to behave quite differently, happily
freeing cycles without overrunning memory limits.

I have no php-fpm to test on Windows, so I can't go all the way with the bug, but I think this
does provide a further hint that under certain memory utilization conditions, the gc_active flag is
getting set and not reset in php-fpm.

Maybe on Thursday if anyone has any hints I can tinker with this test case a bit more and see if I
can get it to zombie out php-fpm on linux.

Test script:
---------------
<?php
$memoryLimit = 1024*1024*((int)ini_get("memory_limit")); // assuming your memory limit is
megs...

echo "Memory limit: $memoryLimit\n";
ini_set('display_errors', 1);
error_reporting(E_ALL);

	echo sprintf("usage: %s real: %s peak: %s peak_real: %s\n", memory_get_usage(),
memory_get_usage(true), memory_get_peak_usage(), memory_get_peak_usage(true));
$cycles = [];
function shutdownFunc()
{
	
	echo sprintf("usage: %s real: %s peak: %s peak_real: %s\n", memory_get_usage(),
memory_get_usage(true), memory_get_peak_usage(), memory_get_peak_usage(true));

	// Serialize goes wrong
	echo serialize(new DateTime);
	
	echo "\n";
	
	// So does print_r
	print_r(new DateTime);
	
	// And createFromFormat. Just super borked.
	print_r(DateTime::createFromFormat('Y-m-d', '2004-01-01'));
}

register_shutdown_function('shutdownFunc');

class F {
	public $next;
	public function __destruct() {
		global $foo;
		//$foo[] = 'bar';
	}
}

// Do this until allowed memory is full
while(memory_get_usage(true) < $memoryLimit) {
	//echo sprintf("cycle: %d usage: %s real: %s peak: %s peak_real: %s\n", count($cycles),
memory_get_usage(), memory_get_usage(true), memory_get_peak_usage(), memory_get_peak_usage(true));
	$a = new F();
	$cycles[] = $a;
	$a->next = $a;
	$last = $a;
	$i = 0;
	// Build a cycle.
	while($i++ < 500 && memory_get_usage(true) < $memoryLimit) {
		$b = new F();
		$last->next = $b;
		$b->next = $a;
		$last = $b;
	}
}
// Orphan all the cycles
echo "Orphaning " . count($cycles) . " cycles.\n";
echo sprintf("usage: %s real: %s peak: %s peak_real: %s\n", memory_get_usage(),
memory_get_usage(true), memory_get_peak_usage(), memory_get_peak_usage(true));
unset($cycles);
echo sprintf("usage: %s real: %s peak: %s peak_real: %s\n", memory_get_usage(),
memory_get_usage(true), memory_get_peak_usage(), memory_get_peak_usage(true));

gc_collect_cycles();
echo sprintf("usage: %s real: %s peak: %s peak_real: %s\n", memory_get_usage(),
memory_get_usage(true), memory_get_peak_usage(), memory_get_peak_usage(true));

Expected result:
----------------
Memory limit: 268435456
usage: 388040 real: 2097152 peak: 400640 peak_real: 2097152
--snip--
O:8:"DateTime":3:{s:4:"date";s:26:"2016-06-14
17:42:49.000000";s:13:"timezone_type";i:3;s:8:"timezone";s:19:"America/Los_Angeles";}
DateTime Object
(
    [date] => 2016-06-14 17:42:49.000000
    [timezone_type] => 3
    [timezone] => America/Los_Angeles
)
DateTime Object
(
    [date] => 2004-01-01 17:42:49.000000
    [timezone_type] => 3
    [timezone] => America/Los_Angeles
)

Actual result:
--------------
Memory limit: 268435456
Orphaning 11505 cycles.
usage: 264856032 real: 268435456 peak: 264856032 peak_real: 268435456

Fatal error: Allowed memory size of 268435456 bytes exhausted (tried to allocate 4096 bytes) in
E:\wwwroot\platform-git\segfault_linux.php on line 55

Call Stack:
    0.2040     365312   1. {main}() E:\wwwroot\platform-git\segfault_linux.php:0

usage: 266926344 real: 268435456 peak: 266926920 peak_real: 268435456
O:8:"DateTime":0:{}
DateTime Object
(
)
DateTime Object
(
)

------------------------------------------------------------------------
[2016-06-14 22:44:46] bill at zeroedin dot com

Actually no, that is just straight up crashing, nothing DateTime involved there.

I'll have to try again for a working test case...

<?php
$a = new stdClass();
$a->next = $a;
$last = $a;
$i = 0;
while($i++ < 500000) {
    $b = new stdClass();
    $last->next = $b;
    $b->next = $a;
    $last = $b;
}

------------------------------------------------------------------------
[2016-06-14 21:20:06] bill at zeroedin dot com

Basically that code just makes a lot of work for the garbage collector to do, then waits until the
execution time is almost up, and triggers the garbage collector so that the execution time expires
while the garbage collector is running. Or at least I *think* that's what it's doing. Who
knows.

------------------------------------------------------------------------
[2016-06-14 21:16:05] bill at zeroedin dot com

@nikic thanks! I'll be running with execution time and memory limits relaxed for now, but once
the patch hits a release I'll bump things back down and retest.

I still can't reliably reproduce the issue. Is it possible that the garbage collector is
allocating just enough memory at some stage to trigger the memory exhaustion error? I managed to get
a segfault out of it on both linux and windows, fpm and cli, 5.6.22 and 7.0.4, but not the broken
DateTime behavior.

<?php
// Segfault this shizz
$start = microtime(true);
set_time_limit(3);
ini_set('display_errors', 1);
error_reporting(E_ALL);

function shutdownFunc()
{
    if (!is_null($e = error_get_last()))
    {
        echo serialize(new DateTime);
    }
}

register_shutdown_function('shutdownFunc');
$a = new stdClass();
$a->next = $a;
$last = $a;
$mem_start = memory_get_usage();
echo "Building cycle...\n";
while((microtime(true) - $start < 2.9) && (memory_get_usage() - $mem_start) <
1024*1024*128) {
    $b = new stdClass();
    $last->next = $b;
    $b->next = $a;
    $last = $b;
}
$i=0;
echo "Waiting...\n";
while ((microtime(true)-$start < 2.9)) {
    $i++;
}
unset ($last);
unset ($a);
echo "Explode!\n";
gc_collect_cycles();

------------------------------------------------------------------------


The remainder of the comments for this report are too long. To view
the rest of the comments, please view the bug report online at

    https://bugs.php.net/bug.php?id=72011


--
Edit this bug report at https://bugs.php.net/bug.php?id=72011&edit=1


Thread (19 messages)

« previous php.bugs (#201765) next »