From b74ba45feca7ac594187dd06e0d0151a00a6c55f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Wed, 9 Apr 2025 15:56:06 +0200 Subject: [PATCH 1/8] Add `clone_obj_with` object handler --- Zend/zend_iterators.c | 1 + Zend/zend_lazy_objects.c | 6 ++-- Zend/zend_lazy_objects.h | 2 +- Zend/zend_object_handlers.c | 1 + Zend/zend_object_handlers.h | 2 ++ Zend/zend_objects.c | 60 +++++++++++++++++++++++++++++++++---- Zend/zend_objects.h | 1 + 7 files changed, 63 insertions(+), 10 deletions(-) diff --git a/Zend/zend_iterators.c b/Zend/zend_iterators.c index f67033b11161c..64dbb0541a80d 100644 --- a/Zend/zend_iterators.c +++ b/Zend/zend_iterators.c @@ -31,6 +31,7 @@ static const zend_object_handlers iterator_object_handlers = { iter_wrapper_free, iter_wrapper_dtor, NULL, /* clone_obj */ + NULL, /* clone_obj_with */ NULL, /* prop read */ NULL, /* prop write */ NULL, /* read dim */ diff --git a/Zend/zend_lazy_objects.c b/Zend/zend_lazy_objects.c index d1b950160e1cc..0300f078e48a5 100644 --- a/Zend/zend_lazy_objects.c +++ b/Zend/zend_lazy_objects.c @@ -709,7 +709,7 @@ ZEND_API HashTable *zend_lazy_object_get_properties(zend_object *object) /* Initialize object and clone it. For proxies, we clone both the proxy and its * real instance, and we don't call __clone() on the proxy. */ -zend_object *zend_lazy_object_clone(zend_object *old_obj) +zend_object *zend_lazy_object_clone(zend_object *old_obj, zend_class_entry *scope, const HashTable *properties) { ZEND_ASSERT(zend_object_is_lazy(old_obj)); @@ -724,7 +724,7 @@ zend_object *zend_lazy_object_clone(zend_object *old_obj) } if (!zend_object_is_lazy_proxy(old_obj)) { - return zend_objects_clone_obj(old_obj); + return zend_objects_clone_obj_with(old_obj, scope, properties); } zend_lazy_object_info *info = zend_lazy_object_get_info(old_obj); @@ -748,7 +748,7 @@ zend_object *zend_lazy_object_clone(zend_object *old_obj) zend_lazy_object_info *new_info = emalloc(sizeof(*info)); *new_info = *info; - new_info->u.instance = zend_objects_clone_obj(info->u.instance); + new_info->u.instance = zend_objects_clone_obj_with(info->u.instance, scope, properties); zend_lazy_object_set_info(new_proxy, new_info); diff --git a/Zend/zend_lazy_objects.h b/Zend/zend_lazy_objects.h index 64f68d66360cd..f1850f0b97c34 100644 --- a/Zend/zend_lazy_objects.h +++ b/Zend/zend_lazy_objects.h @@ -71,7 +71,7 @@ zend_object *zend_lazy_object_get_instance(zend_object *obj); zend_lazy_object_flags_t zend_lazy_object_get_flags(zend_object *obj); void zend_lazy_object_del_info(zend_object *obj); ZEND_API HashTable *zend_lazy_object_get_properties(zend_object *object); -zend_object *zend_lazy_object_clone(zend_object *old_obj); +zend_object *zend_lazy_object_clone(zend_object *old_obj, zend_class_entry *scope, const HashTable *properties); HashTable *zend_lazy_object_debug_info(zend_object *object, int *is_temp); HashTable *zend_lazy_object_get_gc(zend_object *zobj, zval **table, int *n); bool zend_lazy_object_decr_lazy_props(zend_object *obj); diff --git a/Zend/zend_object_handlers.c b/Zend/zend_object_handlers.c index 3d782b03fe174..d482aeed67309 100644 --- a/Zend/zend_object_handlers.c +++ b/Zend/zend_object_handlers.c @@ -2545,6 +2545,7 @@ ZEND_API const zend_object_handlers std_object_handlers = { zend_object_std_dtor, /* free_obj */ zend_objects_destroy_object, /* dtor_obj */ zend_objects_clone_obj, /* clone_obj */ + zend_objects_clone_obj_with, /* clone_obj_with */ zend_std_read_property, /* read_property */ zend_std_write_property, /* write_property */ diff --git a/Zend/zend_object_handlers.h b/Zend/zend_object_handlers.h index 7e7d3df37a6ad..7b99f56454f18 100644 --- a/Zend/zend_object_handlers.h +++ b/Zend/zend_object_handlers.h @@ -180,6 +180,7 @@ typedef void (*zend_object_free_obj_t)(zend_object *object); typedef void (*zend_object_dtor_obj_t)(zend_object *object); typedef zend_object* (*zend_object_clone_obj_t)(zend_object *object); +typedef zend_object* (*zend_object_clone_obj_with_t)(zend_object *object, zend_class_entry *scope, const HashTable *properties); /* Get class name for display in var_dump and other debugging functions. * Must be defined and must return a non-NULL value. */ @@ -209,6 +210,7 @@ struct _zend_object_handlers { zend_object_free_obj_t free_obj; /* required */ zend_object_dtor_obj_t dtor_obj; /* required */ zend_object_clone_obj_t clone_obj; /* optional */ + zend_object_clone_obj_with_t clone_obj_with; /* optional */ zend_object_read_property_t read_property; /* required */ zend_object_write_property_t write_property; /* required */ zend_object_read_dimension_t read_dimension; /* required */ diff --git a/Zend/zend_objects.c b/Zend/zend_objects.c index fd0e97c5f4131..6a1db9fb26a52 100644 --- a/Zend/zend_objects.c +++ b/Zend/zend_objects.c @@ -213,9 +213,9 @@ ZEND_API zend_object* ZEND_FASTCALL zend_objects_new(zend_class_entry *ce) return object; } -ZEND_API void ZEND_FASTCALL zend_objects_clone_members(zend_object *new_object, zend_object *old_object) +ZEND_API void ZEND_FASTCALL zend_objects_clone_members_ex(zend_object *new_object, zend_object *old_object, zend_class_entry *scope, const HashTable *properties) { - bool has_clone_method = old_object->ce->clone != NULL; + bool has_clone_method = old_object->ce->clone != NULL || properties != NULL; if (old_object->ce->default_properties_count) { zval *src = old_object->properties_table; @@ -289,7 +289,29 @@ ZEND_API void ZEND_FASTCALL zend_objects_clone_members(zend_object *new_object, if (has_clone_method) { GC_ADDREF(new_object); - zend_call_known_instance_method_with_0_params(new_object->ce->clone, new_object, NULL); + if (old_object->ce->clone) { + zend_call_known_instance_method_with_0_params(new_object->ce->clone, new_object, NULL); + } + + if (EXPECTED(!EG(exception)) && properties != NULL) { + zend_ulong num_key; + zend_string *key; + zval *val; + ZEND_HASH_FOREACH_KEY_VAL(properties, num_key, key, val) { + if (UNEXPECTED(key == NULL)) { + key = zend_long_to_str(num_key); + zend_update_property_ex(scope, new_object, key, val); + zend_string_release_ex(key, false); + } else { + zend_update_property_ex(scope, new_object, key, val); + } + + if (UNEXPECTED(EG(exception))) { + break; + } + } ZEND_HASH_FOREACH_END(); + } + if (ZEND_CLASS_HAS_READONLY_PROPS(new_object->ce)) { for (uint32_t i = 0; i < new_object->ce->default_properties_count; i++) { @@ -303,12 +325,33 @@ ZEND_API void ZEND_FASTCALL zend_objects_clone_members(zend_object *new_object, } } -ZEND_API zend_object *zend_objects_clone_obj(zend_object *old_object) +ZEND_API void ZEND_FASTCALL zend_objects_clone_members(zend_object *new_object, zend_object *old_object) +{ + ZEND_ASSERT(old_object->ce == new_object->ce); + + zend_objects_clone_members_ex(new_object, old_object, old_object->ce, NULL); +} + +ZEND_API zend_object *zend_objects_clone_obj_with(zend_object *old_object, zend_class_entry *scope, const HashTable *properties) { zend_object *new_object; + /* Compatibility with code that only overrides clone_obj. */ + if (UNEXPECTED(old_object->handlers->clone_obj != zend_objects_clone_obj)) { + if (!old_object->handlers->clone_obj) { + zend_throw_error(NULL, "Trying to clone an uncloneable object of class %s", ZSTR_VAL(old_object->ce->name)); + return NULL; + } + if (properties && zend_hash_num_elements(properties) > 0) { + zend_throw_error(NULL, "Trying to clone an object with updated properties that is not compatible %s", ZSTR_VAL(old_object->ce->name)); + return NULL; + } else { + return old_object->handlers->clone_obj(old_object); + } + } + if (UNEXPECTED(zend_object_is_lazy(old_object))) { - return zend_lazy_object_clone(old_object); + return zend_lazy_object_clone(old_object, scope, properties); } /* assume that create isn't overwritten, so when clone depends on the @@ -325,7 +368,12 @@ ZEND_API zend_object *zend_objects_clone_obj(zend_object *old_object) } while (p != end); } - zend_objects_clone_members(new_object, old_object); + zend_objects_clone_members_ex(new_object, old_object, scope, properties); return new_object; } + +ZEND_API zend_object *zend_objects_clone_obj(zend_object *old_object) +{ + return zend_objects_clone_obj_with(old_object, old_object->ce, NULL); +} diff --git a/Zend/zend_objects.h b/Zend/zend_objects.h index 41e3bcd9594b1..9aea4a6245b36 100644 --- a/Zend/zend_objects.h +++ b/Zend/zend_objects.h @@ -30,6 +30,7 @@ ZEND_API void ZEND_FASTCALL zend_objects_clone_members(zend_object *new_object, ZEND_API void zend_object_std_dtor(zend_object *object); ZEND_API void zend_objects_destroy_object(zend_object *object); ZEND_API zend_object *zend_objects_clone_obj(zend_object *object); +ZEND_API zend_object *zend_objects_clone_obj_with(zend_object *object, zend_class_entry *scope, const HashTable *properties); void zend_object_dtor_dynamic_properties(zend_object *object); void zend_object_dtor_property(zend_object *object, zval *p); From c06830cec460edfc800f1f0cdd3a894d372fef50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Thu, 10 Apr 2025 09:45:16 +0200 Subject: [PATCH 2/8] Use the `write_property()` handler directly in `zend_objects_clone_members_ex()` --- Zend/zend_objects.c | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Zend/zend_objects.c b/Zend/zend_objects.c index 6a1db9fb26a52..4dfa79416d1d0 100644 --- a/Zend/zend_objects.c +++ b/Zend/zend_objects.c @@ -294,22 +294,28 @@ ZEND_API void ZEND_FASTCALL zend_objects_clone_members_ex(zend_object *new_objec } if (EXPECTED(!EG(exception)) && properties != NULL) { + zend_class_entry *old_scope = EG(fake_scope); + + EG(fake_scope) = scope; + zend_ulong num_key; zend_string *key; zval *val; ZEND_HASH_FOREACH_KEY_VAL(properties, num_key, key, val) { if (UNEXPECTED(key == NULL)) { key = zend_long_to_str(num_key); - zend_update_property_ex(scope, new_object, key, val); + new_object->handlers->write_property(new_object, key, val, NULL); zend_string_release_ex(key, false); } else { - zend_update_property_ex(scope, new_object, key, val); + new_object->handlers->write_property(new_object, key, val, NULL); } if (UNEXPECTED(EG(exception))) { break; } } ZEND_HASH_FOREACH_END(); + + EG(fake_scope) = old_scope; } From 0b780a87e99e162219d2dd67a7b95925bf968ca5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Wed, 9 Apr 2025 16:56:54 +0200 Subject: [PATCH 3/8] ext/com_dotnet: Fix object handlers --- ext/com_dotnet/com_handlers.c | 1 + ext/com_dotnet/com_saproxy.c | 1 + 2 files changed, 2 insertions(+) diff --git a/ext/com_dotnet/com_handlers.c b/ext/com_dotnet/com_handlers.c index af980b7b86f2a..638fc5d8a3ae6 100644 --- a/ext/com_dotnet/com_handlers.c +++ b/ext/com_dotnet/com_handlers.c @@ -514,6 +514,7 @@ zend_object_handlers php_com_object_handlers = { php_com_object_free_storage, zend_objects_destroy_object, php_com_object_clone, + NULL, /* clone_with */ com_property_read, com_property_write, com_read_dimension, diff --git a/ext/com_dotnet/com_saproxy.c b/ext/com_dotnet/com_saproxy.c index ea0e9e47a13d9..ec79faa30a32b 100644 --- a/ext/com_dotnet/com_saproxy.c +++ b/ext/com_dotnet/com_saproxy.c @@ -402,6 +402,7 @@ zend_object_handlers php_com_saproxy_handlers = { saproxy_free_storage, zend_objects_destroy_object, saproxy_clone, + NULL, /* clone_with */ saproxy_property_read, saproxy_property_write, saproxy_read_dimension, From 97994dda5fdc1a22940298acc19100ce160833cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Sun, 30 Mar 2025 19:49:39 +0200 Subject: [PATCH 4/8] Add `withProperties` parameter to `clone()` function --- Zend/tests/clone/ast.phpt | 14 ++++ Zend/tests/clone/clone_with_001.phpt | 71 ++++++++++++++++ Zend/tests/clone/clone_with_002.phpt | 114 ++++++++++++++++++++++++++ Zend/tests/clone/clone_with_003.phpt | 23 ++++++ Zend/tests/clone/clone_with_004.phpt | 82 ++++++++++++++++++ Zend/tests/clone/clone_with_005.phpt | 64 +++++++++++++++ Zend/tests/clone/clone_with_006.phpt | 16 ++++ Zend/tests/clone/clone_with_007.phpt | 29 +++++++ Zend/tests/clone/clone_with_008.phpt | 35 ++++++++ Zend/tests/clone/clone_with_009.phpt | 72 ++++++++++++++++ Zend/tests/clone/clone_with_010.phpt | 21 +++++ Zend/tests/clone/clone_with_011.phpt | 18 ++++ Zend/zend_builtin_functions.c | 15 +++- Zend/zend_builtin_functions.stub.php | 2 +- Zend/zend_builtin_functions_arginfo.h | 3 +- Zend/zend_language_parser.y | 5 ++ 16 files changed, 580 insertions(+), 4 deletions(-) create mode 100644 Zend/tests/clone/clone_with_001.phpt create mode 100644 Zend/tests/clone/clone_with_002.phpt create mode 100644 Zend/tests/clone/clone_with_003.phpt create mode 100644 Zend/tests/clone/clone_with_004.phpt create mode 100644 Zend/tests/clone/clone_with_005.phpt create mode 100644 Zend/tests/clone/clone_with_006.phpt create mode 100644 Zend/tests/clone/clone_with_007.phpt create mode 100644 Zend/tests/clone/clone_with_008.phpt create mode 100644 Zend/tests/clone/clone_with_009.phpt create mode 100644 Zend/tests/clone/clone_with_010.phpt create mode 100644 Zend/tests/clone/clone_with_011.phpt diff --git a/Zend/tests/clone/ast.phpt b/Zend/tests/clone/ast.phpt index 89a1a0a481000..b8c5bf18d9a6a 100644 --- a/Zend/tests/clone/ast.phpt +++ b/Zend/tests/clone/ast.phpt @@ -18,6 +18,18 @@ try { echo $e->getMessage(), PHP_EOL; } +try { + assert(false && $y = clone($x, [ "foo" => $foo, "bar" => $bar ])); +} catch (Error $e) { + echo $e->getMessage(), PHP_EOL; +} + +try { + assert(false && $y = clone($x, $array)); +} catch (Error $e) { + echo $e->getMessage(), PHP_EOL; +} + try { assert(false && $y = clone(...)); } catch (Error $e) { @@ -28,4 +40,6 @@ try { --EXPECT-- assert(false && ($y = \clone($x))) assert(false && ($y = \clone($x))) +assert(false && ($y = \clone($x, ['foo' => $foo, 'bar' => $bar]))) +assert(false && ($y = \clone($x, $array))) assert(false && ($y = \clone(...))) diff --git a/Zend/tests/clone/clone_with_001.phpt b/Zend/tests/clone/clone_with_001.phpt new file mode 100644 index 0000000000000..3623706a980c8 --- /dev/null +++ b/Zend/tests/clone/clone_with_001.phpt @@ -0,0 +1,71 @@ +--TEST-- +Clone with basic +--FILE-- + 'BAZ', + 'array' => [1, 2, 3], +]; + +var_dump(clone $x); +var_dump(clone($x)); +var_dump(clone($x, [ 'foo' => $foo, 'bar' => $bar ])); +var_dump(clone($x, $array)); +var_dump(clone($x, [ 'obj' => $x ])); + +var_dump(clone($x, [ + 'abc', + 'def', + new Dummy(), + 'named' => 'value', +])); + +?> +--EXPECTF-- +object(stdClass)#%d (0) { +} +object(stdClass)#%d (0) { +} +object(stdClass)#%d (2) { + ["foo"]=> + string(3) "FOO" + ["bar"]=> + object(Dummy)#%d (0) { + } +} +object(stdClass)#%d (2) { + ["baz"]=> + string(3) "BAZ" + ["array"]=> + array(3) { + [0]=> + int(1) + [1]=> + int(2) + [2]=> + int(3) + } +} +object(stdClass)#%d (1) { + ["obj"]=> + object(stdClass)#%d (0) { + } +} +object(stdClass)#%d (4) { + ["0"]=> + string(3) "abc" + ["1"]=> + string(3) "def" + ["2"]=> + object(Dummy)#%d (0) { + } + ["named"]=> + string(5) "value" +} diff --git a/Zend/tests/clone/clone_with_002.phpt b/Zend/tests/clone/clone_with_002.phpt new file mode 100644 index 0000000000000..8b86e64c76aa8 --- /dev/null +++ b/Zend/tests/clone/clone_with_002.phpt @@ -0,0 +1,114 @@ +--TEST-- +Clone with respects visiblity +--FILE-- + 'updated A', 'b' => 'updated B', 'c' => 'updated C', 'd' => 'updated D' ]); + } +} + +class C extends P { + public function m2() { + return clone($this, [ 'a' => 'updated A', 'b' => 'updated B', 'c' => 'dynamic C' ]); + } + + public function m3() { + return clone($this, [ 'd' => 'inaccessible' ]); + } +} + +class Unrelated { + public function m3(P $p) { + return clone($p, [ 'b' => 'inaccessible' ]); + } +} + +$p = new P(); + +var_dump(clone($p, [ 'a' => 'updated A' ])); +var_dump($p->m1()); + +$c = new C(); +var_dump($c->m1()); +var_dump($c->m2()); +try { + var_dump($c->m3()); +} catch (Error $e) { + echo $e::class, ": ", $e->getMessage(), PHP_EOL; +} + +try { + var_dump(clone($p, [ 'b' => 'inaccessible' ])); +} catch (Error $e) { + echo $e::class, ": ", $e->getMessage(), PHP_EOL; +} + +try { + var_dump(clone($p, [ 'd' => 'inaccessible' ])); +} catch (Error $e) { + echo $e::class, ": ", $e->getMessage(), PHP_EOL; +} + +try { + var_dump((new Unrelated())->m3($p)); +} catch (Error $e) { + echo $e::class, ": ", $e->getMessage(), PHP_EOL; +} + +?> +--EXPECTF-- +object(P)#%d (4) { + ["a"]=> + string(9) "updated A" + ["b":protected]=> + string(7) "default" + ["c":"P":private]=> + string(7) "default" + ["d"]=> + string(7) "default" +} +object(P)#%d (4) { + ["a"]=> + string(9) "updated A" + ["b":protected]=> + string(9) "updated B" + ["c":"P":private]=> + string(9) "updated C" + ["d"]=> + string(9) "updated D" +} +object(C)#%d (4) { + ["a"]=> + string(9) "updated A" + ["b":protected]=> + string(9) "updated B" + ["c":"P":private]=> + string(9) "updated C" + ["d"]=> + string(9) "updated D" +} + +Deprecated: Creation of dynamic property C::$c is deprecated in %s on line %d +object(C)#%d (5) { + ["a"]=> + string(9) "updated A" + ["b":protected]=> + string(9) "updated B" + ["c":"P":private]=> + string(7) "default" + ["d"]=> + string(7) "default" + ["c"]=> + string(9) "dynamic C" +} +Error: Cannot modify private(set) property P::$d from scope C +Error: Cannot access protected property P::$b +Error: Cannot modify private(set) property P::$d from global scope +Error: Cannot access protected property P::$b diff --git a/Zend/tests/clone/clone_with_003.phpt b/Zend/tests/clone/clone_with_003.phpt new file mode 100644 index 0000000000000..fa7ece0fe04fa --- /dev/null +++ b/Zend/tests/clone/clone_with_003.phpt @@ -0,0 +1,23 @@ +--TEST-- +Clone with supports property hooks +--FILE-- +hooked = strtoupper($value); + } + } +} + +$c = new Clazz(); + +var_dump(clone($c, [ 'hooked' => 'updated' ])); + +?> +--EXPECTF-- +object(Clazz)#%d (1) { + ["hooked"]=> + string(7) "UPDATED" +} diff --git a/Zend/tests/clone/clone_with_004.phpt b/Zend/tests/clone/clone_with_004.phpt new file mode 100644 index 0000000000000..14d01bb75fa5b --- /dev/null +++ b/Zend/tests/clone/clone_with_004.phpt @@ -0,0 +1,82 @@ +--TEST-- +Clone with evaluation order +--FILE-- +hooked = strtoupper($value); + } + } + + public string $maxLength { + set (string $value) { + echo __FUNCTION__, PHP_EOL; + + if (strlen($value) > 5) { + throw new \Exception('Length exceeded'); + } + + $this->maxLength = $value; + } + } + + public string $minLength { + set (string $value) { + echo __FUNCTION__, PHP_EOL; + + if (strlen($value) < 5) { + throw new \Exception('Length unsufficient'); + } + + $this->minLength = $value; + } + } +} + +$c = new Clazz(); + +var_dump(clone($c, [ 'hooked' => 'updated' ])); +echo PHP_EOL; +var_dump(clone($c, [ 'hooked' => 'updated', 'maxLength' => 'abc', 'minLength' => 'abcdef' ])); +echo PHP_EOL; +var_dump(clone($c, [ 'minLength' => 'abcdef', 'hooked' => 'updated', 'maxLength' => 'abc' ])); + +?> +--EXPECTF-- +$hooked::set +object(Clazz)#%d (1) { + ["hooked"]=> + string(7) "UPDATED" + ["maxLength"]=> + uninitialized(string) + ["minLength"]=> + uninitialized(string) +} + +$hooked::set +$maxLength::set +$minLength::set +object(Clazz)#%d (3) { + ["hooked"]=> + string(7) "UPDATED" + ["maxLength"]=> + string(3) "abc" + ["minLength"]=> + string(6) "abcdef" +} + +$minLength::set +$hooked::set +$maxLength::set +object(Clazz)#%d (3) { + ["hooked"]=> + string(7) "UPDATED" + ["maxLength"]=> + string(3) "abc" + ["minLength"]=> + string(6) "abcdef" +} diff --git a/Zend/tests/clone/clone_with_005.phpt b/Zend/tests/clone/clone_with_005.phpt new file mode 100644 index 0000000000000..40569c59192ad --- /dev/null +++ b/Zend/tests/clone/clone_with_005.phpt @@ -0,0 +1,64 @@ +--TEST-- +Clone with error handling +--FILE-- +hooked = strtoupper($value); + } + } + + public string $maxLength { + set (string $value) { + echo __FUNCTION__, PHP_EOL; + + if (strlen($value) > 5) { + throw new \Exception('Length exceeded'); + } + + $this->maxLength = $value; + } + } + + public string $minLength { + set (string $value) { + echo __FUNCTION__, PHP_EOL; + + if (strlen($value) < 5) { + throw new \Exception('Length insufficient'); + } + + $this->minLength = $value; + } + } +} + +$c = new Clazz(); + +try { + var_dump(clone($c, [ 'hooked' => 'updated', 'maxLength' => 'abcdef', 'minLength' => 'abc' ])); +} catch (Throwable $e) { + echo $e::class, ": ", $e->getMessage(), PHP_EOL; +} + +echo PHP_EOL; + +try { + var_dump(clone($c, [ 'hooked' => 'updated', 'minLength' => 'abc', 'maxLength' => 'abcdef' ])); +} catch (Throwable $e) { + echo $e::class, ": ", $e->getMessage(), PHP_EOL; +} + +?> +--EXPECT-- +$hooked::set +$maxLength::set +Exception: Length exceeded + +$hooked::set +$minLength::set +Exception: Length insufficient diff --git a/Zend/tests/clone/clone_with_006.phpt b/Zend/tests/clone/clone_with_006.phpt new file mode 100644 index 0000000000000..7b0b8520b8a82 --- /dev/null +++ b/Zend/tests/clone/clone_with_006.phpt @@ -0,0 +1,16 @@ +--TEST-- +Clone with error cases +--FILE-- +getMessage(), PHP_EOL; +} + +?> +--EXPECT-- +TypeError: clone(): Argument #2 ($withProperties) must be of type array, int given diff --git a/Zend/tests/clone/clone_with_007.phpt b/Zend/tests/clone/clone_with_007.phpt new file mode 100644 index 0000000000000..08cefd7f8cbe3 --- /dev/null +++ b/Zend/tests/clone/clone_with_007.phpt @@ -0,0 +1,29 @@ +--TEST-- +Clone with supports __clone +--FILE-- +foo = 'foo updated in __clone'; + $this->bar = 'bar updated in __clone'; + } +} + +$c = new Clazz('foo', 'bar'); + +var_dump(clone($c, [ 'foo' => 'foo updated in clone-with' ])); + +?> +--EXPECTF-- +object(Clazz)#%d (2) { + ["foo"]=> + string(25) "foo updated in clone-with" + ["bar"]=> + string(22) "bar updated in __clone" +} diff --git a/Zend/tests/clone/clone_with_008.phpt b/Zend/tests/clone/clone_with_008.phpt new file mode 100644 index 0000000000000..e72f21d1e0ec5 --- /dev/null +++ b/Zend/tests/clone/clone_with_008.phpt @@ -0,0 +1,35 @@ +--TEST-- +Clone with readonly +--FILE-- +b = '__clone'; + } +} + +$c = new Clazz('default', 'default'); + +var_dump(clone($c, [ 'a' => "with" ])); + +try { + var_dump(clone($c, [ 'b' => "with" ])); +} catch (Throwable $e) { + echo $e::class, ": ", $e->getMessage(), PHP_EOL; +} + +?> +--EXPECTF-- +object(Clazz)#%d (2) { + ["a"]=> + string(4) "with" + ["b"]=> + string(7) "__clone" +} +Error: Cannot modify readonly property Clazz::$b diff --git a/Zend/tests/clone/clone_with_009.phpt b/Zend/tests/clone/clone_with_009.phpt new file mode 100644 index 0000000000000..c6a7d2d18b982 --- /dev/null +++ b/Zend/tests/clone/clone_with_009.phpt @@ -0,0 +1,72 @@ +--TEST-- +Clone with lazy objects +--FILE-- + 2 ]); + + var_dump($reflector->isUninitializedLazyObject($obj)); + var_dump($obj); + var_dump($reflector->isUninitializedLazyObject($clone)); + var_dump($clone); +} + +$reflector = new ReflectionClass(C::class); + +$obj = $reflector->newLazyGhost(function ($obj) { + var_dump("initializer"); + $obj->__construct(); +}); + +test('Ghost', $obj); + +$obj = $reflector->newLazyProxy(function ($obj) { + var_dump("initializer"); + return new C(); +}); + +test('Proxy', $obj); + +?> +--EXPECTF-- +# Ghost: +string(11) "initializer" +bool(false) +object(C)#%d (1) { + ["a"]=> + int(1) +} +bool(false) +object(C)#%d (1) { + ["a"]=> + int(2) +} +# Proxy: +string(11) "initializer" +bool(false) +lazy proxy object(C)#%d (1) { + ["instance"]=> + object(C)#%d (1) { + ["a"]=> + int(1) + } +} +bool(false) +lazy proxy object(C)#%d (1) { + ["instance"]=> + object(C)#%d (1) { + ["a"]=> + int(2) + } +} diff --git a/Zend/tests/clone/clone_with_010.phpt b/Zend/tests/clone/clone_with_010.phpt new file mode 100644 index 0000000000000..97afa737ab091 --- /dev/null +++ b/Zend/tests/clone/clone_with_010.phpt @@ -0,0 +1,21 @@ +--TEST-- +Clone with native classes +--FILE-- + "something" ])); +} catch (Throwable $e) { + echo $e::class, ": ", $e->getMessage(), PHP_EOL; +} + +try { + var_dump(clone(new \Random\Engine\Xoshiro256StarStar(), [ 'with' => "something" ])); +} catch (Throwable $e) { + echo $e::class, ": ", $e->getMessage(), PHP_EOL; +} + +?> +--EXPECT-- +Error: Trying to clone an uncloneable object of class Random\Engine\Secure +Error: Trying to clone an object with updated properties that is not compatible Random\Engine\Xoshiro256StarStar diff --git a/Zend/tests/clone/clone_with_011.phpt b/Zend/tests/clone/clone_with_011.phpt new file mode 100644 index 0000000000000..5f8e99bb65f2f --- /dev/null +++ b/Zend/tests/clone/clone_with_011.phpt @@ -0,0 +1,18 @@ +--TEST-- +Clone with name mangling +--FILE-- + 'updated'])); +} catch (Throwable $e) { + echo $e::class, ": ", $e->getMessage(), PHP_EOL; +} + +?> +--EXPECT-- +Error: Cannot access property starting with "\0" diff --git a/Zend/zend_builtin_functions.c b/Zend/zend_builtin_functions.c index 48e5c70897294..3b07f77153a1f 100644 --- a/Zend/zend_builtin_functions.c +++ b/Zend/zend_builtin_functions.c @@ -72,9 +72,12 @@ zend_result zend_startup_builtin_functions(void) /* {{{ */ ZEND_FUNCTION(clone) { zend_object *zobj; + HashTable *with = NULL; - ZEND_PARSE_PARAMETERS_START(1, 1) + ZEND_PARSE_PARAMETERS_START(1, 2) Z_PARAM_OBJ(zobj) + Z_PARAM_OPTIONAL + Z_PARAM_ARRAY_HT(with) ZEND_PARSE_PARAMETERS_END(); /* clone() also exists as the ZEND_CLONE OPcode and both implementations must be kept in sync. */ @@ -104,7 +107,15 @@ ZEND_FUNCTION(clone) } zend_object *cloned; - cloned = zobj->handlers->clone_obj(zobj); + if (zobj->handlers->clone_obj_with) { + cloned = zobj->handlers->clone_obj_with(zobj, scope, with); + } else { + if (UNEXPECTED(with != NULL && zend_hash_num_elements(with) > 0)) { + zend_throw_error(NULL, "Trying to clone an object with updated properties that is not compatible %s", ZSTR_VAL(ce->name)); + RETURN_THROWS(); + } + cloned = zobj->handlers->clone_obj(zobj); + } ZEND_ASSERT(cloned || EG(exception)); if (EXPECTED(cloned)) { diff --git a/Zend/zend_builtin_functions.stub.php b/Zend/zend_builtin_functions.stub.php index 256c405c71c28..ef73da339e341 100644 --- a/Zend/zend_builtin_functions.stub.php +++ b/Zend/zend_builtin_functions.stub.php @@ -8,7 +8,7 @@ class stdClass } /** @refcount 1 */ -function _clone(object $object): object {} +function _clone(object $object, array $withProperties = []): object {} function exit(string|int $status = 0): never {} diff --git a/Zend/zend_builtin_functions_arginfo.h b/Zend/zend_builtin_functions_arginfo.h index 1c595ecd5777c..df68148638a6b 100644 --- a/Zend/zend_builtin_functions_arginfo.h +++ b/Zend/zend_builtin_functions_arginfo.h @@ -1,8 +1,9 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: 12327caa3fe940ccef68ed99f9278982dc0173a5 */ + * Stub hash: 0be87bb6b55e100c022e70aa6f3b17001725784f */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_clone, 0, 1, IS_OBJECT, 0) ZEND_ARG_TYPE_INFO(0, object, IS_OBJECT, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, withProperties, IS_ARRAY, 0, "[]") ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_exit, 0, 0, IS_NEVER, 0) diff --git a/Zend/zend_language_parser.y b/Zend/zend_language_parser.y index 016c6e5c9d098..2c3fabbd54c69 100644 --- a/Zend/zend_language_parser.y +++ b/Zend/zend_language_parser.y @@ -1233,6 +1233,11 @@ expr: name->attr = ZEND_NAME_FQ; $$ = zend_ast_create(ZEND_AST_CALL, name, zend_ast_create_fcc()); } + | T_CLONE '(' expr ',' expr ')' { + zend_ast *name = zend_ast_create_zval_from_str(ZSTR_KNOWN(ZEND_STR_CLONE)); + name->attr = ZEND_NAME_FQ; + $$ = zend_ast_create(ZEND_AST_CALL, name, zend_ast_create_list(2, ZEND_AST_ARG_LIST, $3, $5)); + } | T_CLONE expr { zend_ast *name = zend_ast_create_zval_from_str(ZSTR_KNOWN(ZEND_STR_CLONE)); name->attr = ZEND_NAME_FQ; From 8259b37cbabb0c426326551e4a6eecbe6da1c339 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Sun, 30 Mar 2025 20:57:38 +0200 Subject: [PATCH 5/8] zend_vm: Use the `clone_obj_with` handler in ZEND_CLONE --- Zend/zend_vm_def.h | 48 +++++++++- Zend/zend_vm_execute.h | 195 +++++++++++++++++++++++++++++++++++++---- 2 files changed, 223 insertions(+), 20 deletions(-) diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h index be7bc8b37b7dd..0557345f7160b 100644 --- a/Zend/zend_vm_def.h +++ b/Zend/zend_vm_def.h @@ -6001,7 +6001,6 @@ ZEND_VM_COLD_CONST_HANDLER(110, ZEND_CLONE, CONST|TMPVAR|UNUSED|THIS|CV, ANY) zend_object *zobj; zend_class_entry *ce, *scope; zend_function *clone; - zend_object_clone_obj_t clone_call; SAVE_OPLINE(); obj = GET_OP1_OBJ_ZVAL_PTR_UNDEF(BP_VAR_R); @@ -6026,6 +6025,7 @@ ZEND_VM_COLD_CONST_HANDLER(110, ZEND_CLONE, CONST|TMPVAR|UNUSED|THIS|CV, ANY) } zend_type_error("clone(): Argument #1 ($object) must be of type object, %s given", zend_zval_value_name(obj)); FREE_OP1(); + FREE_OP2(); HANDLE_EXCEPTION(); } } while (0); @@ -6033,10 +6033,10 @@ ZEND_VM_COLD_CONST_HANDLER(110, ZEND_CLONE, CONST|TMPVAR|UNUSED|THIS|CV, ANY) zobj = Z_OBJ_P(obj); ce = zobj->ce; clone = ce->clone; - clone_call = zobj->handlers->clone_obj; - if (UNEXPECTED(clone_call == NULL)) { + if (UNEXPECTED(zobj->handlers->clone_obj == NULL)) { zend_throw_error(NULL, "Trying to clone an uncloneable object of class %s", ZSTR_VAL(ce->name)); FREE_OP1(); + FREE_OP2(); ZVAL_UNDEF(EX_VAR(opline->result.var)); HANDLE_EXCEPTION(); } @@ -6048,15 +6048,55 @@ ZEND_VM_COLD_CONST_HANDLER(110, ZEND_CLONE, CONST|TMPVAR|UNUSED|THIS|CV, ANY) || UNEXPECTED(!zend_check_protected(zend_get_function_root_class(clone), scope))) { zend_wrong_clone_call(clone, scope); FREE_OP1(); + FREE_OP2(); ZVAL_UNDEF(EX_VAR(opline->result.var)); HANDLE_EXCEPTION(); } } } - ZVAL_OBJ(EX_VAR(opline->result.var), clone_call(zobj)); + zend_object *cloned; + if (zobj->handlers->clone_obj_with) { + scope = EX(func)->op_array.scope; + if (OP2_TYPE != IS_UNUSED) { + zval *properties = GET_OP2_ZVAL_PTR(BP_VAR_R); + if (Z_TYPE_P(properties) != IS_ARRAY) { + zend_type_error("clone(): Argument #2 ($withProperties) must be of type array, %s given", zend_zval_value_name(obj)); + FREE_OP1(); + FREE_OP2(); + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + + cloned = zobj->handlers->clone_obj_with(zobj, scope, Z_ARR_P(properties)); + } else { + cloned = zobj->handlers->clone_obj_with(zobj, scope, NULL); + } + + if (UNEXPECTED(EG(exception))) { + if (cloned) { + OBJ_RELEASE(cloned); + } + FREE_OP1(); + FREE_OP2(); + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + } else { + if (OP2_TYPE != IS_UNUSED) { + zend_throw_error(NULL, "Trying to clone an object with updated properties that is not compatible %s", ZSTR_VAL(ce->name)); + FREE_OP1(); + FREE_OP2(); + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + + cloned = zobj->handlers->clone_obj(zobj); + } + ZVAL_OBJ(EX_VAR(opline->result.var), cloned); FREE_OP1(); + FREE_OP2(); ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION(); } diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h index 3a13f4244d361..926989a61448b 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -5175,7 +5175,6 @@ static ZEND_VM_COLD ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_CLONE_SPEC_CONST_ zend_object *zobj; zend_class_entry *ce, *scope; zend_function *clone; - zend_object_clone_obj_t clone_call; SAVE_OPLINE(); obj = RT_CONSTANT(opline, opline->op1); @@ -5200,6 +5199,7 @@ static ZEND_VM_COLD ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_CLONE_SPEC_CONST_ } zend_type_error("clone(): Argument #1 ($object) must be of type object, %s given", zend_zval_value_name(obj)); + FREE_OP(opline->op2_type, opline->op2.var); HANDLE_EXCEPTION(); } } while (0); @@ -5207,10 +5207,10 @@ static ZEND_VM_COLD ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_CLONE_SPEC_CONST_ zobj = Z_OBJ_P(obj); ce = zobj->ce; clone = ce->clone; - clone_call = zobj->handlers->clone_obj; - if (UNEXPECTED(clone_call == NULL)) { + if (UNEXPECTED(zobj->handlers->clone_obj == NULL)) { zend_throw_error(NULL, "Trying to clone an uncloneable object of class %s", ZSTR_VAL(ce->name)); + FREE_OP(opline->op2_type, opline->op2.var); ZVAL_UNDEF(EX_VAR(opline->result.var)); HANDLE_EXCEPTION(); } @@ -5222,14 +5222,55 @@ static ZEND_VM_COLD ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_CLONE_SPEC_CONST_ || UNEXPECTED(!zend_check_protected(zend_get_function_root_class(clone), scope))) { zend_wrong_clone_call(clone, scope); + FREE_OP(opline->op2_type, opline->op2.var); + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + } + } + + zend_object *cloned; + if (zobj->handlers->clone_obj_with) { + scope = EX(func)->op_array.scope; + if (opline->op2_type != IS_UNUSED) { + zval *properties = get_zval_ptr(opline->op2_type, opline->op2, BP_VAR_R); + if (Z_TYPE_P(properties) != IS_ARRAY) { + zend_type_error("clone(): Argument #2 ($withProperties) must be of type array, %s given", zend_zval_value_name(obj)); + + FREE_OP(opline->op2_type, opline->op2.var); ZVAL_UNDEF(EX_VAR(opline->result.var)); HANDLE_EXCEPTION(); } + + cloned = zobj->handlers->clone_obj_with(zobj, scope, Z_ARR_P(properties)); + } else { + cloned = zobj->handlers->clone_obj_with(zobj, scope, NULL); + } + + if (UNEXPECTED(EG(exception))) { + if (cloned) { + OBJ_RELEASE(cloned); + } + + FREE_OP(opline->op2_type, opline->op2.var); + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + } else { + if (opline->op2_type != IS_UNUSED) { + zend_throw_error(NULL, "Trying to clone an object with updated properties that is not compatible %s", ZSTR_VAL(ce->name)); + + FREE_OP(opline->op2_type, opline->op2.var); + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); } + + cloned = zobj->handlers->clone_obj(zobj); } - ZVAL_OBJ(EX_VAR(opline->result.var), clone_call(zobj)); + ZVAL_OBJ(EX_VAR(opline->result.var), cloned); + FREE_OP(opline->op2_type, opline->op2.var); ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION(); } @@ -15425,7 +15466,6 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_CLONE_SPEC_TMPVAR_HANDLER(ZEND zend_object *zobj; zend_class_entry *ce, *scope; zend_function *clone; - zend_object_clone_obj_t clone_call; SAVE_OPLINE(); obj = _get_zval_ptr_var(opline->op1.var EXECUTE_DATA_CC); @@ -15450,6 +15490,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_CLONE_SPEC_TMPVAR_HANDLER(ZEND } zend_type_error("clone(): Argument #1 ($object) must be of type object, %s given", zend_zval_value_name(obj)); zval_ptr_dtor_nogc(EX_VAR(opline->op1.var)); + FREE_OP(opline->op2_type, opline->op2.var); HANDLE_EXCEPTION(); } } while (0); @@ -15457,10 +15498,10 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_CLONE_SPEC_TMPVAR_HANDLER(ZEND zobj = Z_OBJ_P(obj); ce = zobj->ce; clone = ce->clone; - clone_call = zobj->handlers->clone_obj; - if (UNEXPECTED(clone_call == NULL)) { + if (UNEXPECTED(zobj->handlers->clone_obj == NULL)) { zend_throw_error(NULL, "Trying to clone an uncloneable object of class %s", ZSTR_VAL(ce->name)); zval_ptr_dtor_nogc(EX_VAR(opline->op1.var)); + FREE_OP(opline->op2_type, opline->op2.var); ZVAL_UNDEF(EX_VAR(opline->result.var)); HANDLE_EXCEPTION(); } @@ -15472,15 +15513,55 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_CLONE_SPEC_TMPVAR_HANDLER(ZEND || UNEXPECTED(!zend_check_protected(zend_get_function_root_class(clone), scope))) { zend_wrong_clone_call(clone, scope); zval_ptr_dtor_nogc(EX_VAR(opline->op1.var)); + FREE_OP(opline->op2_type, opline->op2.var); ZVAL_UNDEF(EX_VAR(opline->result.var)); HANDLE_EXCEPTION(); } } } - ZVAL_OBJ(EX_VAR(opline->result.var), clone_call(zobj)); + zend_object *cloned; + if (zobj->handlers->clone_obj_with) { + scope = EX(func)->op_array.scope; + if (opline->op2_type != IS_UNUSED) { + zval *properties = get_zval_ptr(opline->op2_type, opline->op2, BP_VAR_R); + if (Z_TYPE_P(properties) != IS_ARRAY) { + zend_type_error("clone(): Argument #2 ($withProperties) must be of type array, %s given", zend_zval_value_name(obj)); + zval_ptr_dtor_nogc(EX_VAR(opline->op1.var)); + FREE_OP(opline->op2_type, opline->op2.var); + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + + cloned = zobj->handlers->clone_obj_with(zobj, scope, Z_ARR_P(properties)); + } else { + cloned = zobj->handlers->clone_obj_with(zobj, scope, NULL); + } + + if (UNEXPECTED(EG(exception))) { + if (cloned) { + OBJ_RELEASE(cloned); + } + zval_ptr_dtor_nogc(EX_VAR(opline->op1.var)); + FREE_OP(opline->op2_type, opline->op2.var); + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + } else { + if (opline->op2_type != IS_UNUSED) { + zend_throw_error(NULL, "Trying to clone an object with updated properties that is not compatible %s", ZSTR_VAL(ce->name)); + zval_ptr_dtor_nogc(EX_VAR(opline->op1.var)); + FREE_OP(opline->op2_type, opline->op2.var); + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + + cloned = zobj->handlers->clone_obj(zobj); + } + ZVAL_OBJ(EX_VAR(opline->result.var), cloned); zval_ptr_dtor_nogc(EX_VAR(opline->op1.var)); + FREE_OP(opline->op2_type, opline->op2.var); ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION(); } @@ -33522,7 +33603,6 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_CLONE_SPEC_UNUSED_HANDLER(ZEND zend_object *zobj; zend_class_entry *ce, *scope; zend_function *clone; - zend_object_clone_obj_t clone_call; SAVE_OPLINE(); obj = &EX(This); @@ -33547,6 +33627,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_CLONE_SPEC_UNUSED_HANDLER(ZEND } zend_type_error("clone(): Argument #1 ($object) must be of type object, %s given", zend_zval_value_name(obj)); + FREE_OP(opline->op2_type, opline->op2.var); HANDLE_EXCEPTION(); } } while (0); @@ -33554,10 +33635,10 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_CLONE_SPEC_UNUSED_HANDLER(ZEND zobj = Z_OBJ_P(obj); ce = zobj->ce; clone = ce->clone; - clone_call = zobj->handlers->clone_obj; - if (UNEXPECTED(clone_call == NULL)) { + if (UNEXPECTED(zobj->handlers->clone_obj == NULL)) { zend_throw_error(NULL, "Trying to clone an uncloneable object of class %s", ZSTR_VAL(ce->name)); + FREE_OP(opline->op2_type, opline->op2.var); ZVAL_UNDEF(EX_VAR(opline->result.var)); HANDLE_EXCEPTION(); } @@ -33569,14 +33650,55 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_CLONE_SPEC_UNUSED_HANDLER(ZEND || UNEXPECTED(!zend_check_protected(zend_get_function_root_class(clone), scope))) { zend_wrong_clone_call(clone, scope); + FREE_OP(opline->op2_type, opline->op2.var); + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + } + } + + zend_object *cloned; + if (zobj->handlers->clone_obj_with) { + scope = EX(func)->op_array.scope; + if (opline->op2_type != IS_UNUSED) { + zval *properties = get_zval_ptr(opline->op2_type, opline->op2, BP_VAR_R); + if (Z_TYPE_P(properties) != IS_ARRAY) { + zend_type_error("clone(): Argument #2 ($withProperties) must be of type array, %s given", zend_zval_value_name(obj)); + + FREE_OP(opline->op2_type, opline->op2.var); ZVAL_UNDEF(EX_VAR(opline->result.var)); HANDLE_EXCEPTION(); } + + cloned = zobj->handlers->clone_obj_with(zobj, scope, Z_ARR_P(properties)); + } else { + cloned = zobj->handlers->clone_obj_with(zobj, scope, NULL); + } + + if (UNEXPECTED(EG(exception))) { + if (cloned) { + OBJ_RELEASE(cloned); + } + + FREE_OP(opline->op2_type, opline->op2.var); + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + } else { + if (opline->op2_type != IS_UNUSED) { + zend_throw_error(NULL, "Trying to clone an object with updated properties that is not compatible %s", ZSTR_VAL(ce->name)); + + FREE_OP(opline->op2_type, opline->op2.var); + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); } + + cloned = zobj->handlers->clone_obj(zobj); } - ZVAL_OBJ(EX_VAR(opline->result.var), clone_call(zobj)); + ZVAL_OBJ(EX_VAR(opline->result.var), cloned); + FREE_OP(opline->op2_type, opline->op2.var); ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION(); } @@ -41043,7 +41165,6 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_CLONE_SPEC_CV_HANDLER(ZEND_OPC zend_object *zobj; zend_class_entry *ce, *scope; zend_function *clone; - zend_object_clone_obj_t clone_call; SAVE_OPLINE(); obj = EX_VAR(opline->op1.var); @@ -41068,6 +41189,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_CLONE_SPEC_CV_HANDLER(ZEND_OPC } zend_type_error("clone(): Argument #1 ($object) must be of type object, %s given", zend_zval_value_name(obj)); + FREE_OP(opline->op2_type, opline->op2.var); HANDLE_EXCEPTION(); } } while (0); @@ -41075,10 +41197,10 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_CLONE_SPEC_CV_HANDLER(ZEND_OPC zobj = Z_OBJ_P(obj); ce = zobj->ce; clone = ce->clone; - clone_call = zobj->handlers->clone_obj; - if (UNEXPECTED(clone_call == NULL)) { + if (UNEXPECTED(zobj->handlers->clone_obj == NULL)) { zend_throw_error(NULL, "Trying to clone an uncloneable object of class %s", ZSTR_VAL(ce->name)); + FREE_OP(opline->op2_type, opline->op2.var); ZVAL_UNDEF(EX_VAR(opline->result.var)); HANDLE_EXCEPTION(); } @@ -41090,14 +41212,55 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_CLONE_SPEC_CV_HANDLER(ZEND_OPC || UNEXPECTED(!zend_check_protected(zend_get_function_root_class(clone), scope))) { zend_wrong_clone_call(clone, scope); + FREE_OP(opline->op2_type, opline->op2.var); ZVAL_UNDEF(EX_VAR(opline->result.var)); HANDLE_EXCEPTION(); } } } - ZVAL_OBJ(EX_VAR(opline->result.var), clone_call(zobj)); + zend_object *cloned; + if (zobj->handlers->clone_obj_with) { + scope = EX(func)->op_array.scope; + if (opline->op2_type != IS_UNUSED) { + zval *properties = get_zval_ptr(opline->op2_type, opline->op2, BP_VAR_R); + if (Z_TYPE_P(properties) != IS_ARRAY) { + zend_type_error("clone(): Argument #2 ($withProperties) must be of type array, %s given", zend_zval_value_name(obj)); + + FREE_OP(opline->op2_type, opline->op2.var); + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + + cloned = zobj->handlers->clone_obj_with(zobj, scope, Z_ARR_P(properties)); + } else { + cloned = zobj->handlers->clone_obj_with(zobj, scope, NULL); + } + + if (UNEXPECTED(EG(exception))) { + if (cloned) { + OBJ_RELEASE(cloned); + } + FREE_OP(opline->op2_type, opline->op2.var); + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + } else { + if (opline->op2_type != IS_UNUSED) { + zend_throw_error(NULL, "Trying to clone an object with updated properties that is not compatible %s", ZSTR_VAL(ce->name)); + + FREE_OP(opline->op2_type, opline->op2.var); + ZVAL_UNDEF(EX_VAR(opline->result.var)); + HANDLE_EXCEPTION(); + } + + cloned = zobj->handlers->clone_obj(zobj); + } + + ZVAL_OBJ(EX_VAR(opline->result.var), cloned); + + FREE_OP(opline->op2_type, opline->op2.var); ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION(); } From f753ede191b4c8d951785dc9cd65c5e0bbcc6084 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Mon, 31 Mar 2025 15:40:11 +0200 Subject: [PATCH 6/8] Support all parameter syntax for `clone()` --- Zend/tests/clone/ast.phpt | 42 +++++++++++++++++++++++++++++++++++++ Zend/zend_language_parser.y | 11 +++------- 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/Zend/tests/clone/ast.phpt b/Zend/tests/clone/ast.phpt index b8c5bf18d9a6a..13d94eb91d26c 100644 --- a/Zend/tests/clone/ast.phpt +++ b/Zend/tests/clone/ast.phpt @@ -30,6 +30,42 @@ try { echo $e->getMessage(), PHP_EOL; } +try { + assert(false && $y = clone($x, $array, $extraParameter, $trailingComma, )); +} catch (Error $e) { + echo $e->getMessage(), PHP_EOL; +} + +try { + assert(false && $y = clone(object: $x, withProperties: [ "foo" => $foo, "bar" => $bar ])); +} catch (Error $e) { + echo $e->getMessage(), PHP_EOL; +} + +try { + assert(false && $y = clone($x, withProperties: [ "foo" => $foo, "bar" => $bar ])); +} catch (Error $e) { + echo $e->getMessage(), PHP_EOL; +} + +try { + assert(false && $y = clone(object: $x)); +} catch (Error $e) { + echo $e->getMessage(), PHP_EOL; +} + +try { + assert(false && $y = clone(object: $x, [ "foo" => $foo, "bar" => $bar ])); +} catch (Error $e) { + echo $e->getMessage(), PHP_EOL; +} + +try { + assert(false && $y = clone(...["object" => $x, "withProperties" => [ "foo" => $foo, "bar" => $bar ]])); +} catch (Error $e) { + echo $e->getMessage(), PHP_EOL; +} + try { assert(false && $y = clone(...)); } catch (Error $e) { @@ -42,4 +78,10 @@ assert(false && ($y = \clone($x))) assert(false && ($y = \clone($x))) assert(false && ($y = \clone($x, ['foo' => $foo, 'bar' => $bar]))) assert(false && ($y = \clone($x, $array))) +assert(false && ($y = \clone($x, $array, $extraParameter, $trailingComma))) +assert(false && ($y = \clone(object: $x, withProperties: ['foo' => $foo, 'bar' => $bar]))) +assert(false && ($y = \clone($x, withProperties: ['foo' => $foo, 'bar' => $bar]))) +assert(false && ($y = \clone(object: $x))) +assert(false && ($y = \clone(object: $x, ['foo' => $foo, 'bar' => $bar]))) +assert(false && ($y = \clone(...['object' => $x, 'withProperties' => ['foo' => $foo, 'bar' => $bar]]))) assert(false && ($y = \clone(...))) diff --git a/Zend/zend_language_parser.y b/Zend/zend_language_parser.y index 2c3fabbd54c69..2be3df6a6cc55 100644 --- a/Zend/zend_language_parser.y +++ b/Zend/zend_language_parser.y @@ -46,7 +46,7 @@ static YYSIZE_T zend_yytnamerr(char*, const char*); %define api.pure full %define api.value.type {zend_parser_stack_elem} %define parse.error verbose -%expect 0 +%expect 1 %destructor { zend_ast_destroy($$); } %destructor { if ($$) zend_string_release_ex($$, 0); } @@ -1228,15 +1228,10 @@ expr: { $$ = zend_ast_create(ZEND_AST_ASSIGN, $1, $3); } | variable '=' ampersand variable { $$ = zend_ast_create(ZEND_AST_ASSIGN_REF, $1, $4); } - | T_CLONE '(' T_ELLIPSIS ')' { + | T_CLONE argument_list { zend_ast *name = zend_ast_create_zval_from_str(ZSTR_KNOWN(ZEND_STR_CLONE)); name->attr = ZEND_NAME_FQ; - $$ = zend_ast_create(ZEND_AST_CALL, name, zend_ast_create_fcc()); - } - | T_CLONE '(' expr ',' expr ')' { - zend_ast *name = zend_ast_create_zval_from_str(ZSTR_KNOWN(ZEND_STR_CLONE)); - name->attr = ZEND_NAME_FQ; - $$ = zend_ast_create(ZEND_AST_CALL, name, zend_ast_create_list(2, ZEND_AST_ARG_LIST, $3, $5)); + $$ = zend_ast_create(ZEND_AST_CALL, name, $2); } | T_CLONE expr { zend_ast *name = zend_ast_create_zval_from_str(ZSTR_KNOWN(ZEND_STR_CLONE)); From c504ab7a6d7c9212ad6c895e1b484ef169fa2556 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Tue, 27 May 2025 15:31:02 +0200 Subject: [PATCH 7/8] Improve parser definition for `clone()` --- Zend/zend_language_parser.y | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/Zend/zend_language_parser.y b/Zend/zend_language_parser.y index 2be3df6a6cc55..43058f2b159a5 100644 --- a/Zend/zend_language_parser.y +++ b/Zend/zend_language_parser.y @@ -46,7 +46,7 @@ static YYSIZE_T zend_yytnamerr(char*, const char*); %define api.pure full %define api.value.type {zend_parser_stack_elem} %define parse.error verbose -%expect 1 +%expect 0 %destructor { zend_ast_destroy($$); } %destructor { if ($$) zend_string_release_ex($$, 0); } @@ -287,7 +287,7 @@ static YYSIZE_T zend_yytnamerr(char*, const char*); %type enum_declaration_statement enum_backing_type enum_case enum_case_expr %type function_name non_empty_member_modifiers %type property_hook property_hook_list optional_property_hook_list hooked_property property_hook_body -%type optional_parameter_list +%type optional_parameter_list parens_less_argument_list non_empty_parens_less_argument_list %type returns_ref function fn is_reference is_variadic property_modifiers property_hook_modifiers %type method_modifiers class_const_modifiers member_modifier optional_cpp_modifiers @@ -907,6 +907,22 @@ argument_list: | '(' T_ELLIPSIS ')' { $$ = zend_ast_create_fcc(); } ; +parens_less_argument_list: + '(' ')' { $$ = zend_ast_create_list(0, ZEND_AST_ARG_LIST); } + | '(' non_empty_parens_less_argument_list possible_comma ')' { $$ = $2; } + | '(' T_ELLIPSIS ')' { $$ = zend_ast_create_fcc(); } +; + +non_empty_parens_less_argument_list: + expr ',' argument + { $$ = zend_ast_list_add(zend_ast_create_list(1, ZEND_AST_ARG_LIST, $1), $3); } + | identifier ':' expr + { $$ = zend_ast_create_list(1, ZEND_AST_ARG_LIST, zend_ast_create(ZEND_AST_NAMED_ARG, $1, $3)); } + | T_ELLIPSIS expr { $$ = zend_ast_create_list(1, ZEND_AST_ARG_LIST, zend_ast_create(ZEND_AST_UNPACK, $2)); } + | non_empty_parens_less_argument_list ',' argument + { $$ = zend_ast_list_add($1, $3); } +; + non_empty_argument_list: argument { $$ = zend_ast_create_list(1, ZEND_AST_ARG_LIST, $1); } @@ -1228,7 +1244,7 @@ expr: { $$ = zend_ast_create(ZEND_AST_ASSIGN, $1, $3); } | variable '=' ampersand variable { $$ = zend_ast_create(ZEND_AST_ASSIGN_REF, $1, $4); } - | T_CLONE argument_list { + | T_CLONE parens_less_argument_list { zend_ast *name = zend_ast_create_zval_from_str(ZSTR_KNOWN(ZEND_STR_CLONE)); name->attr = ZEND_NAME_FQ; $$ = zend_ast_create(ZEND_AST_CALL, name, $2); From 753f065831097db2a0f60640ffe705073a15cbf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Thu, 19 Jun 2025 11:13:42 +0200 Subject: [PATCH 8/8] Unlock readonly properties after `__clone()` for clone-with --- Zend/tests/clone/clone_with_008.phpt | 7 ++++++- Zend/zend_objects.c | 8 ++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/Zend/tests/clone/clone_with_008.phpt b/Zend/tests/clone/clone_with_008.phpt index e72f21d1e0ec5..aa2c639fb7f1c 100644 --- a/Zend/tests/clone/clone_with_008.phpt +++ b/Zend/tests/clone/clone_with_008.phpt @@ -32,4 +32,9 @@ object(Clazz)#%d (2) { ["b"]=> string(7) "__clone" } -Error: Cannot modify readonly property Clazz::$b +object(Clazz)#%d (2) { + ["a"]=> + string(7) "default" + ["b"]=> + string(4) "with" +} diff --git a/Zend/zend_objects.c b/Zend/zend_objects.c index 4dfa79416d1d0..db3f09c5adee7 100644 --- a/Zend/zend_objects.c +++ b/Zend/zend_objects.c @@ -294,6 +294,14 @@ ZEND_API void ZEND_FASTCALL zend_objects_clone_members_ex(zend_object *new_objec } if (EXPECTED(!EG(exception)) && properties != NULL) { + /* Unlock readonly properties once more. */ + if (ZEND_CLASS_HAS_READONLY_PROPS(new_object->ce) && old_object->ce->clone) { + for (uint32_t i = 0; i < new_object->ce->default_properties_count; i++) { + zval* prop = OBJ_PROP_NUM(new_object, i); + Z_PROP_FLAG_P(prop) |= IS_PROP_REINITABLE; + } + } + zend_class_entry *old_scope = EG(fake_scope); EG(fake_scope) = scope;