Skip to content

Commit 4cfcc8e

Browse files
deepclone_from_array: reject building an uninitialized object
deepclone_to_array() always emits a class that has __unserialize() as a negative-wakeup state replay. A hand-crafted payload that instead flags such a class for plain creation (wakeup >= 0, no replay) made from_array() build a bare object_init_ex() shell that __unserialize() never initialized — for BcMath\Number that shell has a NULL bc_num, so any operation on it crashed. Reject such a payload with a ValueError before creating the object, so no uninitialized shell is ever built. A php-src guard for the bcmath case (php/php-src#22259) was declined: an uninitialized BcMath\Number cannot be reached from userland, so the extension must not produce one. The test that exercised the crash path now asserts the rejection instead.
1 parent d4beb94 commit 4cfcc8e

2 files changed

Lines changed: 21 additions & 13 deletions

File tree

deepclone.c

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3365,6 +3365,15 @@ PHP_FUNCTION(deepclone_from_array)
33653365
}
33663366
class_ces[cid] = ce;
33673367
}
3368+
/* deepclone_to_array() always emits a class that has
3369+
* __unserialize() as a negative-wakeup state replay. A payload
3370+
* that creates such a class without that flag would leave the bare
3371+
* object_init_ex() shell uninitialized — e.g. BcMath\Number's
3372+
* bc_num stays NULL and any operation on it crashes. Reject rather
3373+
* than build an unusable object. */
3374+
if (UNEXPECTED(ce->__unserialize != NULL && (!obj_wakeups || obj_wakeups[id] >= 0))) {
3375+
DC_INVALID("deepclone_from_array(): Argument #1 ($data) object %u of class %s has an __unserialize() method but \"objectMeta\" does not flag it for an __unserialize state", id, ZSTR_VAL(ce->name));
3376+
}
33683377
if (UNEXPECTED(object_init_ex(&obj_zval, ce) != SUCCESS)) {
33693378
goto cleanup;
33703379
}

tests/deepclone_bcmath_number.phpt

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
--TEST--
2-
deepclone round-trips BcMath\Number, refuses to hydrate it, and never crashes on an uninitialized one (symfony/symfony#64323)
2+
deepclone round-trips BcMath\Number, refuses to hydrate it, and rejects a payload that would create an uninitialized one (symfony/symfony#64323)
33
--EXTENSIONS--
44
deepclone
55
--SKIPIF--
@@ -37,17 +37,19 @@ try {
3737
echo $e->getMessage(), "\n";
3838
}
3939

40-
// ── Defensive: an uninitialized instance (forced here via a hand-mutated
41-
// payload that drops the __unserialize replay) must throw a clean Error on
42-
// use, never segfault. ──
40+
// ── A crafted payload that drops the __unserialize replay for a class that
41+
// needs it (here by clearing the wakeup flag) is rejected, rather than
42+
// building an uninitialized instance that would crash on use. An
43+
// uninitialized BcMath\Number cannot be reached from userland, so the
44+
// engine offers no guard; the extension must not produce one either. ──
4345
$d = deepclone_to_array(new Number('1'));
4446
$d['objectMeta'][0][1] = 0; // clear the __unserialize flag
4547
$d['states'] = []; // and drop the replay
46-
$u = deepclone_from_array($d);
47-
var_dump($u instanceof Number);
48-
foreach (['toString' => fn() => (string) $u, 'clone' => fn() => clone $u, 'add' => fn() => $u->add(1)] as $op => $f) {
49-
try { $f(); echo "$op: no error\n"; }
50-
catch (\Error $e) { echo "$op: ", $e->getMessage(), "\n"; }
48+
try {
49+
deepclone_from_array($d);
50+
echo "not rejected\n";
51+
} catch (\ValueError $e) {
52+
echo "rejected with ValueError\n";
5153
}
5254
?>
5355
--EXPECT--
@@ -62,7 +64,4 @@ string(6) "99.999"
6264
int(3)
6365
string(7) "100.499"
6466
Class "BcMath\Number" is not instantiable.
65-
bool(true)
66-
toString: The BcMath\Number object has not been correctly initialized by its constructor
67-
clone: The BcMath\Number object has not been correctly initialized by its constructor
68-
add: The BcMath\Number object has not been correctly initialized by its constructor
67+
rejected with ValueError

0 commit comments

Comments
 (0)