Skip to content
Open
Prev Previous commit
Next Next commit
Count unserialize depth on arrays and objects like unserialize()
unserialize_max_depth was checked for every value, including scalars, so
igbinary_unserialize() failed one level earlier than unserialize(). The
depth is now increased when entering a non-empty array or an object.

References to references, which igbinary_serialize() never emits, are
rejected so that recursion stays bounded by the depth limit.
  • Loading branch information
nicolas-grekas committed Sep 29, 2026
commit 634a23b5eb26c8965f5c35a048d6d30463b1b634
61 changes: 38 additions & 23 deletions src/php7/igbinary.c
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ struct igbinary_unserialize_data {
#if PHP_VERSION_ID >= 70400
HashTable *ref_props; /**< objects&data for calls to __unserialize/__wakeup */
#endif
zend_long cur_depth; /**< current recursion depth in igbinary_unserialize_zval */
zend_long cur_depth; /**< current depth of nested arrays and objects */
zend_long max_depth; /**< snapshot of unserialize_max_depth ini at entry */
};

Expand Down Expand Up @@ -345,8 +345,7 @@ zend_always_inline static int igbinary_unserialize_array(struct igbinary_unseria
zend_always_inline static int igbinary_unserialize_object(struct igbinary_unserialize_data *igsd, enum igbinary_type t, zval *const z, int flags);
static int igbinary_unserialize_object_ser(struct igbinary_unserialize_data *igsd, enum igbinary_type t, zval *const z, zend_class_entry *ce);

zend_always_inline static int igbinary_unserialize_zval(struct igbinary_unserialize_data *igsd, zval *const z, int flags);
static int igbinary_unserialize_zval_inner(struct igbinary_unserialize_data *igsd, zval *const z, int flags);
static int igbinary_unserialize_zval(struct igbinary_unserialize_data *igsd, zval *const z, int flags);
/* }}} */
/* {{{ arginfo */
ZEND_BEGIN_ARG_INFO_EX(arginfo_igbinary_serialize, 0, 0, 1)
Expand Down Expand Up @@ -2655,6 +2654,20 @@ inline static zend_string *igbinary_unserialize_chararray(struct igbinary_unseri
return zstr;
}
/* }}} */
/* {{{ igbinary_unserialize_enter_nested */
/** Accounts for one more level of nested arrays or objects, like process_nested_array_data() and process_nested_object_data() in ext/standard/var_unserializer.re. */
static zend_always_inline int igbinary_unserialize_enter_nested(struct igbinary_unserialize_data *igsd) {
if (UNEXPECTED(igsd->max_depth > 0 && igsd->cur_depth >= igsd->max_depth)) {
php_error_docref(NULL, E_WARNING,
"Maximum depth of " ZEND_LONG_FMT " exceeded. "
"The depth limit can be changed using the unserialize_max_depth ini setting",
igsd->max_depth);
return 1;
}
igsd->cur_depth++;
return 0;
}
/* }}} */
/* {{{ igbinary_unserialize_array */
/** Unserializes a PHP array. */
zend_always_inline static int igbinary_unserialize_array(struct igbinary_unserialize_data *igsd, enum igbinary_type t, zval *const z, int flags, zend_bool create_ref) {
Expand Down Expand Up @@ -2730,6 +2743,11 @@ zend_always_inline static int igbinary_unserialize_array(struct igbinary_unseria
return 1;
}

/* The depth is not restored on failure: igbinary_unserialize() gives up on the first error */
if (create_ref) {
RETURN_1_IF_NON_ZERO(igbinary_unserialize_enter_nested(igsd));
}

zval *z_deref = z;
if (flags & WANT_REF) {
if (!Z_ISREF_P(z)) {
Expand Down Expand Up @@ -2862,6 +2880,10 @@ zend_always_inline static int igbinary_unserialize_array(struct igbinary_unseria
}
}

if (create_ref) {
igsd->cur_depth--;
}

return 0;
}
/* }}} */
Expand Down Expand Up @@ -3393,6 +3415,10 @@ zend_always_inline static int igbinary_unserialize_object(struct igbinary_unseri
case igbinary_type_array16:
case igbinary_type_array32:
{
if (UNEXPECTED(igbinary_unserialize_enter_nested(igsd))) {
r = 1;
break;
}
if (UNEXPECTED(object_init_ex(z, ce) != SUCCESS)) {
php_error_docref(NULL, E_NOTICE, "igbinary unable to create object for class entry");
r = 1;
Expand Down Expand Up @@ -3430,6 +3456,7 @@ zend_always_inline static int igbinary_unserialize_object(struct igbinary_unseri
ZVAL_DEREF(z);
ZEND_ASSERT(Z_TYPE_P(z) == IS_OBJECT);
igsd_defer_unserialize(igsd, Z_OBJ_P(z), param);
igsd->cur_depth--;
return result;
}
#endif
Expand All @@ -3445,6 +3472,7 @@ zend_always_inline static int igbinary_unserialize_object(struct igbinary_unseri
}

r = igbinary_unserialize_object_properties(igsd, t, z, ce);
igsd->cur_depth--;
break;
}
case igbinary_type_object_ser8:
Expand Down Expand Up @@ -3652,26 +3680,8 @@ zend_always_inline static int igbinary_unserialize_ref(struct igbinary_unseriali
/* }}} */
/* {{{ igbinary_unserialize_zval */
/** Unserialize a zval of any serializable type (zval is PHP's internal representation of a value).
* Recursion-depth wrapper -- enforces the unserialize_max_depth ini setting before
* delegating to igbinary_unserialize_zval_inner, mirroring the protection PHP core's
* unserialize() applies via php_var_unserialize. */
zend_always_inline static int igbinary_unserialize_zval(struct igbinary_unserialize_data *igsd, zval *const z, int flags) {
int ret;
if (UNEXPECTED(igsd->cur_depth >= igsd->max_depth && igsd->max_depth > 0)) {
php_error_docref(NULL, E_WARNING,
"Maximum depth of " ZEND_LONG_FMT " exceeded. "
"The depth limit can be changed using the unserialize_max_depth ini setting",
igsd->max_depth);
return 1;
}
igsd->cur_depth++;
ret = igbinary_unserialize_zval_inner(igsd, z, flags);
igsd->cur_depth--;
return ret;
}
/* }}} */
/* {{{ igbinary_unserialize_zval_inner */
static int igbinary_unserialize_zval_inner(struct igbinary_unserialize_data *igsd, zval *const z, int flags) {
* The unserialize_max_depth ini setting is enforced when entering arrays and objects, like unserialize() does. */
static int igbinary_unserialize_zval(struct igbinary_unserialize_data *igsd, zval *const z, int flags) {
enum igbinary_type t;

zend_long tmp_long;
Expand All @@ -3687,6 +3697,11 @@ static int igbinary_unserialize_zval_inner(struct igbinary_unserialize_data *igs

switch (t) {
case igbinary_type_ref:
/* igbinary_serialize() never emits a reference to a reference: rejecting it keeps the recursion bounded by the depth of arrays and objects */
if (UNEXPECTED(!IGB_NEEDS_MORE_DATA(igsd, 1) && igsd->buffer_ptr[0] == igbinary_type_ref)) {
zend_error(E_WARNING, "igbinary_unserialize_zval: unexpected reference to a reference, position %zu", (size_t)IGB_BUFFER_OFFSET(igsd));
return 1;
}
if (UNEXPECTED(igbinary_unserialize_zval(igsd, z, WANT_REF))) {
return 1;
}
Expand Down
55 changes: 55 additions & 0 deletions tests/igbinary_unserialize_max_depth_boundary.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
--TEST--
igbinary_unserialize counts the depth of nested arrays and objects like unserialize()
--SKIPIF--
<?php if (PHP_VERSION_ID < 70400) { echo "skip unserialize_max_depth ini was added in PHP 7.4"; } ?>
--FILE--
<?php
function test($label, $value) {
$php = unserialize(serialize($value));
$igbinary = @igbinary_unserialize(igbinary_serialize($value));
echo $label, ': ', $php === false ? 'false' : 'ok', ' ', $igbinary === null ? 'null' : 'ok', "\n";
}
ini_set('unserialize_max_depth', 1);
test('1', 1);
test('[1]', [1]);
test('[[]]', [[]]);
test('[[1]]', [[1]]);
test('{}', new stdClass());
test('[{}]', [new stdClass()]);
ini_set('unserialize_max_depth', 2);
test('[[1]]', [[1]]);
test('[[[1]]]', [[[1]]]);
test('[ArrayObject([1])]', ['a' => new ArrayObject([1])]);

// References to references are never emitted by igbinary_serialize()
var_dump(igbinary_unserialize("\x00\x00\x00\x02\x25\x25\x06\x01"));
?>
--EXPECTF--
1: ok ok
[1]: ok ok
[[]]: ok ok

Warning: unserialize(): Maximum depth of 1 exceeded. %s

%s: unserialize(): Error at offset %d of %d bytes in %s on line %d
[[1]]: false null
{}: ok ok

Warning: unserialize(): Maximum depth of 1 exceeded. %s

%s: unserialize(): Error at offset %d of %d bytes in %s on line %d
[{}]: false null
[[1]]: ok ok

Warning: unserialize(): Maximum depth of 2 exceeded. %s

%s: unserialize(): Error at offset %d of %d bytes in %s on line %d
[[[1]]]: false null

Warning: unserialize(): Maximum depth of 2 exceeded. %s

%s: unserialize(): Error at offset %d of %d bytes in %s on line %d
[ArrayObject([1])]: false null

Warning: igbinary_unserialize_zval: unexpected reference to a reference, position 5 in %s on line %d
NULL