Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions ext/standard/php_var.h
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ PHPAPI void php_unserialize_with_options(zval *return_value, const char *buf, co
PHPAPI void var_replace(php_unserialize_data_t *var_hash, zval *ozval, zval *nzval);
PHPAPI void var_push_dtor(php_unserialize_data_t *var_hash, zval *val);
PHPAPI zval *var_tmp_var(php_unserialize_data_t *var_hashx);
PHPAPI zend_long var_delayed_calls_mark(php_unserialize_data_t *var_hash);
PHPAPI void var_invoke_delayed_calls(php_unserialize_data_t *var_hash, zend_long from);
PHPAPI void var_destroy(php_unserialize_data_t *var_hash);

#endif /* PHP_VAR_H */
40 changes: 40 additions & 0 deletions ext/standard/tests/serialize/gh9618.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
--TEST--
GH-9618 (unserialize __wakeup bypass via malformed payload)
--FILE--
<?php
class A
{
public $info;

public function __destruct()
{
if (is_object($this->info)) {
$this->info->probe();
}
}
}

class B
{
public $end;

public function __wakeup()
{
$this->end = 'wakeup-guard';
echo "B::__wakeup\n";
}

public function __call($method, $args)
{
echo "B::__call end=" . var_export($this->end, true) . "\n";
}
}

$payload = 'O:1:"A":2:{s:4:"info";O:1:"B":1:{s:3:"end";N;}s:6:"Aend";s:1:"1";}';

var_dump(@unserialize($payload));
?>
--EXPECT--
B::__wakeup
B::__call end='wakeup-guard'
bool(false)
43 changes: 43 additions & 0 deletions ext/standard/tests/serialize/gh9618_nested.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
--TEST--
GH-9618 (a failing nested unserialize() must not drain the outer call's queue)
--FILE--
<?php
class A
{
public function __wakeup()
{
echo "A::__wakeup\n";
}
}

class B implements Serializable
{
public function serialize(): string
{
return 'x';
}

public function unserialize($data): void
{
@unserialize('O:1:"X":{BAD');
echo "B::unserialize done\n";
}

public function __serialize(): array
{
return [];
}

public function __unserialize(array $data): void
{
}
}

$payload = 'a:2:{i:0;O:1:"A":0:{}i:1;C:1:"B":1:{x}}';

var_dump(@unserialize($payload) !== false);
?>
--EXPECT--
B::unserialize done
A::__wakeup
bool(true)
62 changes: 62 additions & 0 deletions ext/standard/tests/serialize/gh9618_nested_bypass.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
--TEST--
GH-9618 (__wakeup runs before sibling destructors when the failure is nested)
--FILE--
<?php
class A
{
public $info;

public function __destruct()
{
if (is_object($this->info)) {
$this->info->probe();
}
}
}

class B
{
public $end;

public function __wakeup()
{
$this->end = 'wakeup-guard';
echo "B::__wakeup\n";
}

public function __call($method, $args)
{
echo "B::__call end=" . var_export($this->end, true) . "\n";
}
}

class W implements Serializable
{
public function serialize(): string
{
return 'x';
}

public function unserialize($data): void
{
@unserialize('O:1:"A":2:{s:4:"info";O:1:"B":1:{s:3:"end";N;}s:6:"Aend";s:1:"1";}');
echo "W::unserialize done\n";
}

public function __serialize(): array
{
return [];
}

public function __unserialize(array $data): void
{
}
}

var_dump(@unserialize('C:1:"W":1:{x}') !== false);
?>
--EXPECT--
B::__wakeup
W::unserialize done
B::__call end='wakeup-guard'
bool(true)
45 changes: 45 additions & 0 deletions ext/standard/tests/serialize/gh9618_unserialize.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
--TEST--
GH-9618 (__unserialize drained before destructors on failure path)
--FILE--
<?php
class A
{
public $info;

public function __destruct()
{
if (is_object($this->info)) {
$this->info->probe();
}
}
}

class C
{
public $end;

public function __unserialize(array $data): void
{
$this->end = 'unserialize-guard';
echo "C::__unserialize\n";
}

public function __serialize(): array
{
return ['end' => $this->end];
}

public function __call($method, $args)
{
echo "C::__call end=" . var_export($this->end, true) . "\n";
}
}

$payload = 'O:1:"A":2:{s:4:"info";O:1:"C":1:{s:3:"end";N;}s:6:"Aend";s:1:"1";}';

var_dump(@unserialize($payload));
?>
--EXPECT--
C::__unserialize
C::__call end='unserialize-guard'
bool(false)
4 changes: 3 additions & 1 deletion ext/standard/var.c
Original file line number Diff line number Diff line change
Expand Up @@ -1409,7 +1409,7 @@ PHPAPI void php_unserialize_with_options(zval *return_value, const char *buf, co
php_unserialize_data_t var_hash;
zval *retval;
HashTable *class_hash = NULL, *prev_class_hash;
zend_long prev_max_depth, prev_cur_depth;
zend_long prev_max_depth, prev_cur_depth, delayed_calls_mark;

if (buf_len == 0) {
RETURN_FALSE;
Expand Down Expand Up @@ -1480,6 +1480,7 @@ PHPAPI void php_unserialize_with_options(zval *return_value, const char *buf, co
}
}

delayed_calls_mark = var_delayed_calls_mark(&var_hash);
if (BG(unserialize).level > 1) {
retval = var_tmp_var(&var_hash);
} else {
Expand All @@ -1490,6 +1491,7 @@ PHPAPI void php_unserialize_with_options(zval *return_value, const char *buf, co
php_error_docref(NULL, E_WARNING, "Error at offset " ZEND_LONG_FMT " of %zd bytes",
(zend_long)((char*)p - buf), buf_len);
}
var_invoke_delayed_calls(&var_hash, delayed_calls_mark);
if (BG(unserialize).level <= 1) {
zval_ptr_dtor(return_value);
}
Expand Down
Loading
Loading