Skip to content

serialize() corruption with debug_backtrace() and Serializable interface #14502

Open
@Korkman

Description

@Korkman

Description

The following code:

<?php

error_reporting(E_ALL & ~E_DEPRECATED);

// test serialize() functionality on shutdown
register_shutdown_function('shutdown_serialize_test');
function shutdown_serialize_test() {
	$bug_test = new stdClass();
	$bug_test_serialized = serialize([$bug_test, $bug_test]);
	$bug_test_unserialized = unserialize($bug_test_serialized);
	if ($bug_test_unserialized === false) {
		echo "\nserialize() / unserialize() round trip failed on shutdown\n";
	}
	
	$reference_serialized = 'a:2:{i:0;O:8:"stdClass":0:{}i:1;r:2;}';
	if ($bug_test_serialized !== $reference_serialized) {
		echo "serialize() on shutdown does not match reference:\n";
		echo "$bug_test_serialized\n";
		echo "should be:\n";
		echo "$reference_serialized\n";
	}
	
}

// records $test into trace
function crash_step_1($test) {
	crash_step_2(debug_backtrace());
}

// works on the first trace and serializes it, which triggers the second trace
function crash_step_2($trace1) {
	$new_trace = [];
	foreach ($trace1 as $traceline) {
		if (isset($traceline['args'])) {
			$new_args = [];
			foreach ($traceline['args'] as $traceline_arg) {
				$new_args[] = $traceline_arg;
			}
			$traceline['args'] = $new_args;
			// this fails as well
			# $traceline['args'] = $traceline['args'];
		}
		$new_trace[] = $traceline;
	}

	return serialize($new_trace);
}

class class_crash_step_3 {
	protected function trace2() {
		$trace2 = debug_backtrace();
		
		$var_list = [];
		foreach ($trace2 as $traceline) {
			if (!isset($traceline['args'])) continue;
			foreach ($traceline['args'] as $tracearg) {
				$var_idx = false;
				foreach ($var_list as $found_idx => $var_list_item) {
					// eventually, this comparison triggers the Fatal error
					if ($var_list_item === $tracearg) {
						$var_idx = $found_idx;
						break;
					}
				}
				
				if ($var_idx === false) {
					$var_list[] = $tracearg;
				}
			}
		}
	}
}

// this one triggers "Fatal error: Nesting level too deep - recursive dependency?"
// and corrupts serialize() during shutdown, also affecting session handlers
class class_crash_step_3_variant_A extends class_crash_step_3 implements Serializable {
	public function serialize() {
		$this->trace2();
	}
	public function unserialize($data) { }
}

// interface Serializable is deprecated. with magic methods, corruption does not occur,
// but "Fatal error: Nesting level too deep - recursive dependency?" is still triggered
class class_crash_step_3_variant_B extends class_crash_step_3 {
	public function __serialize() {
		$this->trace2();
	}
	public function __unserialize($data) { }
}

$test = new class_crash_step_3_variant_A();
#$test = new class_crash_step_3_variant_B();
crash_step_1($test);
echo "End of script";

Resulted in this output:

Fatal error: Nesting level too deep - recursive dependency? in /in/trjSb on line 60

Warning: unserialize(): Error at offset 36 of 37 bytes in /in/trjSb on line 10

serialize() / unserialize() round trip failed on shutdown
serialize() on shutdown does not match reference:
a:2:{i:0;O:8:"stdClass":0:{}i:1;r:9;}
should be:
a:2:{i:0;O:8:"stdClass":0:{}i:1;r:2;}

But I expected this output instead:

End of script

The bug appeared in 7.3.0 and is present up to 8.3.8.
5.4.0 - 7.2.34 are not affected.

It is not synthetic - this has hit me in production (corrupted user sessions). It's a minimum example as small as I could make it.

3v4l Link: https://3v4l.org/9qWTh

PHP Version

7.3.0 - 8.3.8

Operating System

No response

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions