diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2617a46..4d2b464 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -36,7 +36,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - extensions: mongodb + extensions: mongodb, tidy coverage: none tools: none env: @@ -79,7 +79,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: '8.4' - extensions: mongodb + extensions: mongodb, tidy coverage: none tools: none @@ -170,7 +170,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - extensions: mongodb + extensions: mongodb, tidy coverage: none tools: none diff --git a/deepclone.c b/deepclone.c index a9dad7c..6a4f585 100644 --- a/deepclone.c +++ b/deepclone.c @@ -394,11 +394,29 @@ static uint8_t dc_get_class_info(dc_ctx *ctx, zend_class_entry *ce) flags |= DC_CI_NOT_INSTANTIABLE; } - /* Internal classes with C-level state (create_object != NULL) and no - * declared serialization API are rejected. Classes that declare - * __serialize/__unserialize/__sleep/__wakeup are trusted — they - * round-trip via object_init_ex() + __unserialize(). */ + /* Internal classes with C-level state (create_object != NULL): + * Rule A: final + no serialization API → probe instantiation; reject if it fails. + * Rule B: non-final + no serialization API → reject. + * Classes declaring __serialize/__unserialize/__sleep/__wakeup are trusted: + * they round-trip via object_init_ex() + __unserialize(), same as PHP's + * own serialize/unserialize. + * Rule A uses a probe instead of an unconditional reject because some final + * internal classes are stateless and fully reconstructable from their PHP- + * visible properties (e.g. MongoDB\BSON\MinKey / MaxKey): object_init_ex() + * succeeds and produces a complete object with no hidden C-level state. */ if (ce->type == ZEND_INTERNAL_CLASS + && ce->create_object != NULL + && (ce->ce_flags & ZEND_ACC_FINAL) + && !(flags & (DC_CI_HAS_SERIALIZE | DC_CI_HAS_UNSERIALIZE | DC_CI_HAS_SLEEP | DC_CI_HAS_WAKEUP)) + && ce != php_ce_incomplete_class) { + zval probe; + if (object_init_ex(&probe, ce) != SUCCESS || EG(exception)) { + zend_clear_exception(); + flags |= DC_CI_NOT_INSTANTIABLE; + } else { + zval_ptr_dtor(&probe); + } + } else if (ce->type == ZEND_INTERNAL_CLASS && ce->create_object != NULL && ce->serialize == NULL && !(flags & (DC_CI_HAS_SERIALIZE | DC_CI_HAS_UNSERIALIZE | DC_CI_HAS_SLEEP | DC_CI_HAS_WAKEUP)) diff --git a/tests/deepclone_rule_a_probe.phpt b/tests/deepclone_rule_a_probe.phpt new file mode 100644 index 0000000..e935824 --- /dev/null +++ b/tests/deepclone_rule_a_probe.phpt @@ -0,0 +1,30 @@ +--TEST-- +Rule A: final internal classes with create_object but no serialization API round-trip via probe +--EXTENSIONS-- +deepclone +tidy +--FILE-- +parseString('

hello

', [], 'utf8'); +$b = $tidy->body()->child[0]->child[0]; // node + +$clone = deepclone_from_array(deepclone_to_array($b)); + +var_dump($clone instanceof tidyNode); +var_dump($clone !== $b); +var_dump($clone->name === $b->name); +var_dump($clone->value === $b->value); + +echo "Done\n"; +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) +bool(true) +Done