Skip to content

Commit 28dbe8b

Browse files
committed
Probe instantiability for final internal classes that lack a serialization API
Some final internal classes (e.g. tidyNode, MongoDB\BSON\MinKey/MaxKey) carry a C-level create_object handler but no __serialize/__unserialize/__sleep/__wakeup. Previously they were all rejected as not instantiable. Instead of rejecting them unconditionally, call object_init_ex() as a probe at class-info cache time. If the call succeeds the class is deepcloned normally; if it throws the class is still rejected. The probe runs once per class and its result is cached.
1 parent 6e6cb67 commit 28dbe8b

3 files changed

Lines changed: 55 additions & 7 deletions

File tree

.github/workflows/test.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ jobs:
3636
uses: shivammathur/setup-php@v2
3737
with:
3838
php-version: ${{ matrix.php }}
39-
extensions: mongodb
39+
extensions: mongodb, tidy
4040
coverage: none
4141
tools: none
4242
env:
@@ -79,7 +79,7 @@ jobs:
7979
uses: shivammathur/setup-php@v2
8080
with:
8181
php-version: '8.4'
82-
extensions: mongodb
82+
extensions: mongodb, tidy
8383
coverage: none
8484
tools: none
8585

@@ -170,7 +170,7 @@ jobs:
170170
uses: shivammathur/setup-php@v2
171171
with:
172172
php-version: ${{ matrix.php }}
173-
extensions: mongodb
173+
extensions: mongodb, tidy
174174
coverage: none
175175
tools: none
176176

deepclone.c

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -394,11 +394,29 @@ static uint8_t dc_get_class_info(dc_ctx *ctx, zend_class_entry *ce)
394394
flags |= DC_CI_NOT_INSTANTIABLE;
395395
}
396396

397-
/* Internal classes with C-level state (create_object != NULL) and no
398-
* declared serialization API are rejected. Classes that declare
399-
* __serialize/__unserialize/__sleep/__wakeup are trusted — they
400-
* round-trip via object_init_ex() + __unserialize(). */
397+
/* Internal classes with C-level state (create_object != NULL):
398+
* Rule A: final + no serialization API → probe instantiation; reject if it fails.
399+
* Rule B: non-final + no serialization API → reject.
400+
* Classes declaring __serialize/__unserialize/__sleep/__wakeup are trusted:
401+
* they round-trip via object_init_ex() + __unserialize(), same as PHP's
402+
* own serialize/unserialize.
403+
* Rule A uses a probe instead of an unconditional reject because some final
404+
* internal classes are stateless and fully reconstructable from their PHP-
405+
* visible properties (e.g. MongoDB\BSON\MinKey / MaxKey): object_init_ex()
406+
* succeeds and produces a complete object with no hidden C-level state. */
401407
if (ce->type == ZEND_INTERNAL_CLASS
408+
&& ce->create_object != NULL
409+
&& (ce->ce_flags & ZEND_ACC_FINAL)
410+
&& !(flags & (DC_CI_HAS_SERIALIZE | DC_CI_HAS_UNSERIALIZE | DC_CI_HAS_SLEEP | DC_CI_HAS_WAKEUP))
411+
&& ce != php_ce_incomplete_class) {
412+
zval probe;
413+
if (object_init_ex(&probe, ce) != SUCCESS || EG(exception)) {
414+
zend_clear_exception();
415+
flags |= DC_CI_NOT_INSTANTIABLE;
416+
} else {
417+
zval_ptr_dtor(&probe);
418+
}
419+
} else if (ce->type == ZEND_INTERNAL_CLASS
402420
&& ce->create_object != NULL
403421
&& ce->serialize == NULL
404422
&& !(flags & (DC_CI_HAS_SERIALIZE | DC_CI_HAS_UNSERIALIZE | DC_CI_HAS_SLEEP | DC_CI_HAS_WAKEUP))

tests/deepclone_rule_a_probe.phpt

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
--TEST--
2+
Rule A: final internal classes with create_object but no serialization API round-trip via probe
3+
--EXTENSIONS--
4+
deepclone
5+
tidy
6+
--FILE--
7+
<?php
8+
9+
// tidyNode is a final internal class with a C-level create_object handler
10+
// and no __serialize/__unserialize/__sleep/__wakeup.
11+
12+
$tidy = new tidy();
13+
$tidy->parseString('<p><b>hello</b></p>', [], 'utf8');
14+
$b = $tidy->body()->child[0]->child[0]; // <b> node
15+
16+
$clone = deepclone_from_array(deepclone_to_array($b));
17+
18+
var_dump($clone instanceof tidyNode);
19+
var_dump($clone !== $b);
20+
var_dump($clone->name === $b->name);
21+
var_dump($clone->value === $b->value);
22+
23+
echo "Done\n";
24+
?>
25+
--EXPECT--
26+
bool(true)
27+
bool(true)
28+
bool(true)
29+
bool(true)
30+
Done

0 commit comments

Comments
 (0)