From 212f923846520a84c880efcb2ca75b03fe609353 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Wed, 9 Apr 2025 16:13:39 +0200 Subject: [PATCH 01/13] zend_string: Add ZEND_STR_CLONE to known strings --- Zend/zend_string.h | 1 + build/gen_stub.php | 1 + 2 files changed, 2 insertions(+) diff --git a/Zend/zend_string.h b/Zend/zend_string.h index 0b2a484016ec3..f60e4dec4e71f 100644 --- a/Zend/zend_string.h +++ b/Zend/zend_string.h @@ -575,6 +575,7 @@ EMPTY_SWITCH_DEFAULT_CASE() _(ZEND_STR_UNKNOWN, "unknown") \ _(ZEND_STR_UNKNOWN_CAPITALIZED, "Unknown") \ _(ZEND_STR_EXIT, "exit") \ + _(ZEND_STR_CLONE, "clone") \ _(ZEND_STR_EVAL, "eval") \ _(ZEND_STR_INCLUDE, "include") \ _(ZEND_STR_REQUIRE, "require") \ diff --git a/build/gen_stub.php b/build/gen_stub.php index fcef8213d0b55..27225b2f62dc8 100755 --- a/build/gen_stub.php +++ b/build/gen_stub.php @@ -3049,6 +3049,7 @@ class PropertyInfo extends VariableLike "parent" => "ZEND_STR_PARENT", "username" => "ZEND_STR_USERNAME", "password" => "ZEND_STR_PASSWORD", + "clone" => "ZEND_STR_CLONE", ]; /** From 71df8dd83dca22686b4a99d9eb963645fcc4d0d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Thu, 19 Jun 2025 11:25:22 +0200 Subject: [PATCH 02/13] zend_vm: Change error when cloning non-objects to TypeError and align wording with functions This is in preparation of also exposing `clone()` as a function. --- Zend/tests/clone/bug36071.phpt | 2 +- Zend/tests/clone/bug42817.phpt | 2 +- Zend/tests/clone/bug42818.phpt | 2 +- Zend/tests/clone/clone_001.phpt | 2 +- Zend/tests/clone/clone_003.phpt | 2 +- Zend/zend_vm_def.h | 2 +- Zend/zend_vm_execute.h | 8 ++++---- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Zend/tests/clone/bug36071.phpt b/Zend/tests/clone/bug36071.phpt index 945118fef3754..c780c4d8053b0 100644 --- a/Zend/tests/clone/bug36071.phpt +++ b/Zend/tests/clone/bug36071.phpt @@ -8,7 +8,7 @@ $a = clone 0; $a[0]->b = 0; ?> --EXPECTF-- -Fatal error: Uncaught Error: __clone method called on non-object in %sbug36071.php:2 +Fatal error: Uncaught TypeError: clone(): Argument #1 ($object) must be of type object, int given in %s:%d Stack trace: #0 {main} thrown in %sbug36071.php on line 2 diff --git a/Zend/tests/clone/bug42817.phpt b/Zend/tests/clone/bug42817.phpt index a681d861d0c8f..cef61e4d4ff5d 100644 --- a/Zend/tests/clone/bug42817.phpt +++ b/Zend/tests/clone/bug42817.phpt @@ -6,7 +6,7 @@ $a = clone(null); array_push($a->b, $c); ?> --EXPECTF-- -Fatal error: Uncaught Error: __clone method called on non-object in %sbug42817.php:2 +Fatal error: Uncaught TypeError: clone(): Argument #1 ($object) must be of type object, null given in %s:%d Stack trace: #0 {main} thrown in %sbug42817.php on line 2 diff --git a/Zend/tests/clone/bug42818.phpt b/Zend/tests/clone/bug42818.phpt index b37ce13fd174a..bc410a4940467 100644 --- a/Zend/tests/clone/bug42818.phpt +++ b/Zend/tests/clone/bug42818.phpt @@ -5,7 +5,7 @@ Bug #42818 ($foo = clone(array()); leaks memory) $foo = clone(array()); ?> --EXPECTF-- -Fatal error: Uncaught Error: __clone method called on non-object in %sbug42818.php:2 +Fatal error: Uncaught TypeError: clone(): Argument #1 ($object) must be of type object, array given in %s:%d Stack trace: #0 {main} thrown in %sbug42818.php on line 2 diff --git a/Zend/tests/clone/clone_001.phpt b/Zend/tests/clone/clone_001.phpt index 87024c3cd5614..c380cd342b774 100644 --- a/Zend/tests/clone/clone_001.phpt +++ b/Zend/tests/clone/clone_001.phpt @@ -7,7 +7,7 @@ $a = clone array(); ?> --EXPECTF-- -Fatal error: Uncaught Error: __clone method called on non-object in %s:%d +Fatal error: Uncaught TypeError: clone(): Argument #1 ($object) must be of type object, array given in %s:%d Stack trace: #0 {main} thrown in %s on line %d diff --git a/Zend/tests/clone/clone_003.phpt b/Zend/tests/clone/clone_003.phpt index f163616a876dc..c8d7b42bfb7cd 100644 --- a/Zend/tests/clone/clone_003.phpt +++ b/Zend/tests/clone/clone_003.phpt @@ -9,7 +9,7 @@ $a = clone $b; --EXPECTF-- Warning: Undefined variable $b in %s on line %d -Fatal error: Uncaught Error: __clone method called on non-object in %s:%d +Fatal error: Uncaught TypeError: clone(): Argument #1 ($object) must be of type object, null given in %s:%d Stack trace: #0 {main} thrown in %s on line %d diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h index 617e114dd05db..8c008decbcbb5 100644 --- a/Zend/zend_vm_def.h +++ b/Zend/zend_vm_def.h @@ -6022,7 +6022,7 @@ ZEND_VM_COLD_CONST_HANDLER(110, ZEND_CLONE, CONST|TMPVAR|UNUSED|THIS|CV, ANY) HANDLE_EXCEPTION(); } } - zend_throw_error(NULL, "__clone method called on non-object"); + zend_type_error("clone(): Argument #1 ($object) must be of type object, %s given", zend_zval_value_name(obj)); FREE_OP1(); HANDLE_EXCEPTION(); } diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h index 791e4b4e88437..57e4ff1f6b16a 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -5196,7 +5196,7 @@ static ZEND_VM_COLD ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_CLONE_SPEC_CONST_ HANDLE_EXCEPTION(); } } - zend_throw_error(NULL, "__clone method called on non-object"); + zend_type_error("clone(): Argument #1 ($object) must be of type object, %s given", zend_zval_value_name(obj)); HANDLE_EXCEPTION(); } @@ -15444,7 +15444,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_CLONE_SPEC_TMPVAR_HANDLER(ZEND HANDLE_EXCEPTION(); } } - zend_throw_error(NULL, "__clone method called on non-object"); + 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)); HANDLE_EXCEPTION(); } @@ -33539,7 +33539,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_CLONE_SPEC_UNUSED_HANDLER(ZEND HANDLE_EXCEPTION(); } } - zend_throw_error(NULL, "__clone method called on non-object"); + zend_type_error("clone(): Argument #1 ($object) must be of type object, %s given", zend_zval_value_name(obj)); HANDLE_EXCEPTION(); } @@ -41058,7 +41058,7 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_CLONE_SPEC_CV_HANDLER(ZEND_OPC HANDLE_EXCEPTION(); } } - zend_throw_error(NULL, "__clone method called on non-object"); + zend_type_error("clone(): Argument #1 ($object) must be of type object, %s given", zend_zval_value_name(obj)); HANDLE_EXCEPTION(); } From 24c0ce71f621bcf1b2543dfd1a843593dd63f33a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Wed, 9 Apr 2025 15:53:47 +0200 Subject: [PATCH 03/13] zend_builtin_functions: Add `clone()` as function --- Zend/tests/clone/clone_005.phpt | 43 +++++++++++++++++++++++++ Zend/zend_API.c | 1 + Zend/zend_builtin_functions.c | 46 +++++++++++++++++++++++++++ Zend/zend_builtin_functions.stub.php | 2 ++ Zend/zend_builtin_functions_arginfo.h | 8 ++++- build/gen_stub.php | 3 ++ 6 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 Zend/tests/clone/clone_005.phpt diff --git a/Zend/tests/clone/clone_005.phpt b/Zend/tests/clone/clone_005.phpt new file mode 100644 index 0000000000000..e820810ee53b7 --- /dev/null +++ b/Zend/tests/clone/clone_005.phpt @@ -0,0 +1,43 @@ +--TEST-- +Clone as a function. +--FILE-- +clone_me()[0]; + +var_dump($f !== $clone); + +?> +--EXPECTF-- +object(stdClass)#%d (0) { +} +array(3) { + [0]=> + object(stdClass)#%d (0) { + } + [1]=> + object(stdClass)#%d (0) { + } + [2]=> + object(stdClass)#%d (0) { + } +} +bool(true) diff --git a/Zend/zend_API.c b/Zend/zend_API.c index e0006e7d7275f..259481ebfb0e8 100644 --- a/Zend/zend_API.c +++ b/Zend/zend_API.c @@ -3641,6 +3641,7 @@ static void zend_disable_function(const char *function_name, size_t function_nam if (UNEXPECTED( (function_name_length == strlen("exit") && !memcmp(function_name, "exit", strlen("exit"))) || (function_name_length == strlen("die") && !memcmp(function_name, "die", strlen("die"))) + || (function_name_length == strlen("clone") && !memcmp(function_name, "clone", strlen("clone"))) )) { zend_error(E_WARNING, "Cannot disable function %s()", function_name); return; diff --git a/Zend/zend_builtin_functions.c b/Zend/zend_builtin_functions.c index 7a07ceadce2e2..4c466e7b841f7 100644 --- a/Zend/zend_builtin_functions.c +++ b/Zend/zend_builtin_functions.c @@ -69,6 +69,52 @@ zend_result zend_startup_builtin_functions(void) /* {{{ */ } /* }}} */ +ZEND_FUNCTION(clone) +{ + zend_object *zobj; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_OBJ(zobj) + ZEND_PARSE_PARAMETERS_END(); + + zend_class_entry *scope = zend_get_executed_scope(); + + zend_class_entry *ce = zobj->ce; + zend_function *clone = ce->clone; + + if (UNEXPECTED(zobj->handlers->clone_obj == NULL)) { + zend_throw_error(NULL, "Trying to clone an uncloneable object of class %s", ZSTR_VAL(ce->name)); + RETURN_THROWS(); + } + + if (clone && !(clone->common.fn_flags & ZEND_ACC_PUBLIC)) { + if (clone->common.scope != scope) { + if (UNEXPECTED(clone->common.fn_flags & ZEND_ACC_PRIVATE) + || UNEXPECTED(!zend_check_protected(zend_get_function_root_class(clone), scope))) { + zend_throw_error(NULL, "Call to %s %s::__clone() from %s%s", + zend_visibility_string(clone->common.fn_flags), ZSTR_VAL(clone->common.scope->name), + scope ? "scope " : "global scope", + scope ? ZSTR_VAL(scope->name) : "" + ); + RETURN_THROWS(); + } + } + } + + zend_object *cloned; + cloned = zobj->handlers->clone_obj(zobj); + + if (EG(exception)) { + if (cloned) { + OBJ_RELEASE(cloned); + } + + RETURN_THROWS(); + } + + RETURN_OBJ(cloned); +} + ZEND_FUNCTION(exit) { zend_string *str = NULL; diff --git a/Zend/zend_builtin_functions.stub.php b/Zend/zend_builtin_functions.stub.php index 7f316835aea6b..5c453f4523fcf 100644 --- a/Zend/zend_builtin_functions.stub.php +++ b/Zend/zend_builtin_functions.stub.php @@ -7,6 +7,8 @@ class stdClass { } +function _clone(object $object): object {} + function exit(string|int $status = 0): never {} /** @alias exit */ diff --git a/Zend/zend_builtin_functions_arginfo.h b/Zend/zend_builtin_functions_arginfo.h index 9498b8292f892..e4867c6610451 100644 --- a/Zend/zend_builtin_functions_arginfo.h +++ b/Zend/zend_builtin_functions_arginfo.h @@ -1,5 +1,9 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: a24761186f1ddf758e648b0a764826537cbd33b9 */ + * Stub hash: 52e1b67d6ea82f832365b092020de4e49d6a1c38 */ + +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_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_exit, 0, 0, IS_NEVER, 0) ZEND_ARG_TYPE_MASK(0, status, MAY_BE_STRING|MAY_BE_LONG, "0") @@ -243,6 +247,7 @@ static const zend_frameless_function_info frameless_function_infos_class_exists[ { 0 }, }; +ZEND_FUNCTION(clone); ZEND_FUNCTION(exit); ZEND_FUNCTION(zend_version); ZEND_FUNCTION(func_num_args); @@ -306,6 +311,7 @@ ZEND_FUNCTION(gc_disable); ZEND_FUNCTION(gc_status); static const zend_function_entry ext_functions[] = { + ZEND_FE(clone, arginfo_clone) ZEND_FE(exit, arginfo_exit) ZEND_RAW_FENTRY("die", zif_exit, arginfo_die, 0, NULL, NULL) ZEND_FE(zend_version, arginfo_zend_version) diff --git a/build/gen_stub.php b/build/gen_stub.php index 27225b2f62dc8..d9327b67d042f 100755 --- a/build/gen_stub.php +++ b/build/gen_stub.php @@ -1009,6 +1009,9 @@ class FunctionName implements FunctionOrMethodName { private /* readonly */ Name $name; public function __construct(Name $name) { + if ($name->name === '_clone') { + $name = new Name('clone', $name->getAttributes()); + } $this->name = $name; } From c043b5e4ad9c2dcda010397254d8c8a479f1e61c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Mon, 23 Jun 2025 11:06:02 +0200 Subject: [PATCH 04/13] zend_language_parser: Compile `clone` as a function call --- Zend/tests/assert/expect_015.phpt | 2 +- Zend/tests/clone/ast.phpt | 24 ++++++++++++++++++++++++ Zend/tests/clone/bug36071.phpt | 15 ++++++++------- Zend/tests/clone/bug42817.phpt | 15 ++++++++------- Zend/tests/clone/bug42818.phpt | 13 +++++++------ Zend/tests/clone/clone_001.phpt | 13 +++++++------ Zend/tests/clone/clone_003.phpt | 12 ++++++------ Zend/tests/magic_methods/bug73288.phpt | 8 +++++--- Zend/zend_language_parser.y | 6 +++++- 9 files changed, 71 insertions(+), 37 deletions(-) create mode 100644 Zend/tests/clone/ast.phpt diff --git a/Zend/tests/assert/expect_015.phpt b/Zend/tests/assert/expect_015.phpt index 695c4c166a83c..9f8e9b77003bc 100644 --- a/Zend/tests/assert/expect_015.phpt +++ b/Zend/tests/assert/expect_015.phpt @@ -183,7 +183,7 @@ assert(0 && ($a = function () { $x = $a ?? $b; [$a, $b, $c] = [1, 2 => 'x', 'z' => 'c']; @foo(); - $y = clone $x; + $y = \clone($x); yield 1 => 2; yield from $x; })) diff --git a/Zend/tests/clone/ast.phpt b/Zend/tests/clone/ast.phpt new file mode 100644 index 0000000000000..a7cc71d0ec32d --- /dev/null +++ b/Zend/tests/clone/ast.phpt @@ -0,0 +1,24 @@ +--TEST-- +Ast Printing +--FILE-- +getMessage(), PHP_EOL; +} + +try { + assert(false && $y = clone($x)); +} catch (Error $e) { + echo $e->getMessage(), PHP_EOL; +} + +?> +--EXPECT-- +assert(false && ($y = \clone($x))) +assert(false && ($y = \clone($x))) diff --git a/Zend/tests/clone/bug36071.phpt b/Zend/tests/clone/bug36071.phpt index c780c4d8053b0..f6d9b25afaf05 100644 --- a/Zend/tests/clone/bug36071.phpt +++ b/Zend/tests/clone/bug36071.phpt @@ -4,11 +4,12 @@ Bug #36071 (Engine Crash related with 'clone') error_reporting=4095 --FILE-- b = 0; +try { + $a = clone 0; + $a[0]->b = 0; +} catch (Error $e) { + echo $e::class, ": ", $e->getMessage(), PHP_EOL; +} ?> ---EXPECTF-- -Fatal error: Uncaught TypeError: clone(): Argument #1 ($object) must be of type object, int given in %s:%d -Stack trace: -#0 {main} - thrown in %sbug36071.php on line 2 +--EXPECT-- +TypeError: clone(): Argument #1 ($object) must be of type object, int given diff --git a/Zend/tests/clone/bug42817.phpt b/Zend/tests/clone/bug42817.phpt index cef61e4d4ff5d..51468280fea9b 100644 --- a/Zend/tests/clone/bug42817.phpt +++ b/Zend/tests/clone/bug42817.phpt @@ -2,11 +2,12 @@ Bug #42817 (clone() on a non-object does not result in a fatal error) --FILE-- b, $c); +try { + $a = clone(null); + array_push($a->b, $c); +} catch (Error $e) { + echo $e::class, ": ", $e->getMessage(), PHP_EOL; +} ?> ---EXPECTF-- -Fatal error: Uncaught TypeError: clone(): Argument #1 ($object) must be of type object, null given in %s:%d -Stack trace: -#0 {main} - thrown in %sbug42817.php on line 2 +--EXPECT-- +TypeError: clone(): Argument #1 ($object) must be of type object, null given diff --git a/Zend/tests/clone/bug42818.phpt b/Zend/tests/clone/bug42818.phpt index bc410a4940467..08ba05fcfaa22 100644 --- a/Zend/tests/clone/bug42818.phpt +++ b/Zend/tests/clone/bug42818.phpt @@ -2,10 +2,11 @@ Bug #42818 ($foo = clone(array()); leaks memory) --FILE-- getMessage(), PHP_EOL; +} ?> ---EXPECTF-- -Fatal error: Uncaught TypeError: clone(): Argument #1 ($object) must be of type object, array given in %s:%d -Stack trace: -#0 {main} - thrown in %sbug42818.php on line 2 +--EXPECT-- +TypeError: clone(): Argument #1 ($object) must be of type object, array given diff --git a/Zend/tests/clone/clone_001.phpt b/Zend/tests/clone/clone_001.phpt index c380cd342b774..91fa6f551176d 100644 --- a/Zend/tests/clone/clone_001.phpt +++ b/Zend/tests/clone/clone_001.phpt @@ -3,11 +3,12 @@ Using clone statement on non-object --FILE-- getMessage(), PHP_EOL; +} ?> ---EXPECTF-- -Fatal error: Uncaught TypeError: clone(): Argument #1 ($object) must be of type object, array given in %s:%d -Stack trace: -#0 {main} - thrown in %s on line %d +--EXPECT-- +TypeError: clone(): Argument #1 ($object) must be of type object, array given diff --git a/Zend/tests/clone/clone_003.phpt b/Zend/tests/clone/clone_003.phpt index c8d7b42bfb7cd..b8bb2833dc257 100644 --- a/Zend/tests/clone/clone_003.phpt +++ b/Zend/tests/clone/clone_003.phpt @@ -3,13 +3,13 @@ Using clone statement on undefined variable --FILE-- getMessage(), PHP_EOL; +} ?> --EXPECTF-- Warning: Undefined variable $b in %s on line %d - -Fatal error: Uncaught TypeError: clone(): Argument #1 ($object) must be of type object, null given in %s:%d -Stack trace: -#0 {main} - thrown in %s on line %d +TypeError: clone(): Argument #1 ($object) must be of type object, null given diff --git a/Zend/tests/magic_methods/bug73288.phpt b/Zend/tests/magic_methods/bug73288.phpt index 52e8eedeaf013..0a17c82e7a7f9 100644 --- a/Zend/tests/magic_methods/bug73288.phpt +++ b/Zend/tests/magic_methods/bug73288.phpt @@ -23,12 +23,14 @@ function test_clone() { $b = clone $c->x; } +// No catch, because we want to test Exception::__toString(). test_clone(); ?> --EXPECTF-- Fatal error: Uncaught Exception: No Cloneable in %sbug73288.php:%d Stack trace: -#0 %s(%d): NoClone->__clone() -#1 %s(%d): test_clone() -#2 {main} +#0 [internal function]: NoClone->__clone() +#1 %s(%d): clone(Object(NoClone)) +#2 %s(%d): test_clone() +#3 {main} thrown in %sbug73288.php on line %d diff --git a/Zend/zend_language_parser.y b/Zend/zend_language_parser.y index 816b8126cbf25..5cb9be3172890 100644 --- a/Zend/zend_language_parser.y +++ b/Zend/zend_language_parser.y @@ -1228,7 +1228,11 @@ expr: { $$ = zend_ast_create(ZEND_AST_ASSIGN, $1, $3); } | variable '=' ampersand variable { $$ = zend_ast_create(ZEND_AST_ASSIGN_REF, $1, $4); } - | T_CLONE expr { $$ = zend_ast_create(ZEND_AST_CLONE, $2); } + | T_CLONE 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(1, ZEND_AST_ARG_LIST, $2)); + } | variable T_PLUS_EQUAL expr { $$ = zend_ast_create_assign_op(ZEND_ADD, $1, $3); } | variable T_MINUS_EQUAL expr From e1729f8597adf6d46c69b16a0e4e1d57e772bab5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Mon, 23 Jun 2025 11:24:49 +0200 Subject: [PATCH 05/13] zend_ast: Remove now-unused `ZEND_AST_CLONE` AST type --- Zend/zend_ast.c | 2 -- Zend/zend_ast.h | 1 - Zend/zend_compile.c | 14 -------------- 3 files changed, 17 deletions(-) diff --git a/Zend/zend_ast.c b/Zend/zend_ast.c index cdc86faa95aa3..728695bd9e930 100644 --- a/Zend/zend_ast.c +++ b/Zend/zend_ast.c @@ -2345,8 +2345,6 @@ static ZEND_COLD void zend_ast_export_ex(smart_str *str, zend_ast *ast, int prio } smart_str_appendc(str, '`'); break; - case ZEND_AST_CLONE: - PREFIX_OP("clone ", 270, 271); case ZEND_AST_PRINT: PREFIX_OP("print ", 60, 61); case ZEND_AST_INCLUDE_OR_EVAL: diff --git a/Zend/zend_ast.h b/Zend/zend_ast.h index c82ca66c9f573..08400cff5dd8e 100644 --- a/Zend/zend_ast.h +++ b/Zend/zend_ast.h @@ -89,7 +89,6 @@ enum _zend_ast_kind { ZEND_AST_ISSET, ZEND_AST_SILENCE, ZEND_AST_SHELL_EXEC, - ZEND_AST_CLONE, ZEND_AST_PRINT, ZEND_AST_INCLUDE_OR_EVAL, ZEND_AST_UNARY_OP, diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 28bea1a21d759..4448720bc8407 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -5391,17 +5391,6 @@ static void zend_compile_new(znode *result, zend_ast *ast) /* {{{ */ } /* }}} */ -static void zend_compile_clone(znode *result, zend_ast *ast) /* {{{ */ -{ - zend_ast *obj_ast = ast->child[0]; - - znode obj_node; - zend_compile_expr(&obj_node, obj_ast); - - zend_emit_op_tmp(result, ZEND_CLONE, &obj_node, NULL); -} -/* }}} */ - static void zend_compile_global_var(zend_ast *ast) /* {{{ */ { zend_ast *var_ast = ast->child[0]; @@ -11717,9 +11706,6 @@ static void zend_compile_expr_inner(znode *result, zend_ast *ast) /* {{{ */ case ZEND_AST_NEW: zend_compile_new(result, ast); return; - case ZEND_AST_CLONE: - zend_compile_clone(result, ast); - return; case ZEND_AST_ASSIGN_OP: zend_compile_compound_assign(result, ast); return; From 573cd162ffd1ea416419481be94b3791b82bada7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Mon, 23 Jun 2025 12:47:16 +0200 Subject: [PATCH 06/13] zend_language_parser: Support first-class-callable syntax for `clone()` --- Zend/tests/clone/ast.phpt | 7 +++++++ Zend/tests/clone/clone_005.phpt | 12 ++++++++++++ Zend/zend_language_parser.y | 5 +++++ 3 files changed, 24 insertions(+) diff --git a/Zend/tests/clone/ast.phpt b/Zend/tests/clone/ast.phpt index a7cc71d0ec32d..89a1a0a481000 100644 --- a/Zend/tests/clone/ast.phpt +++ b/Zend/tests/clone/ast.phpt @@ -18,7 +18,14 @@ try { echo $e->getMessage(), PHP_EOL; } +try { + assert(false && $y = clone(...)); +} catch (Error $e) { + echo $e->getMessage(), PHP_EOL; +} + ?> --EXPECT-- assert(false && ($y = \clone($x))) assert(false && ($y = \clone($x))) +assert(false && ($y = \clone(...))) diff --git a/Zend/tests/clone/clone_005.phpt b/Zend/tests/clone/clone_005.phpt index e820810ee53b7..e0366cae67cb5 100644 --- a/Zend/tests/clone/clone_005.phpt +++ b/Zend/tests/clone/clone_005.phpt @@ -7,6 +7,7 @@ $x = new stdClass(); \var_dump(\clone($x)); \var_dump(\array_map('clone', [$x, $x, $x])); +\var_dump(\array_map(clone(...), [$x, $x, $x])); class Foo { private function __clone() { @@ -40,4 +41,15 @@ array(3) { object(stdClass)#%d (0) { } } +array(3) { + [0]=> + object(stdClass)#%d (0) { + } + [1]=> + object(stdClass)#%d (0) { + } + [2]=> + object(stdClass)#%d (0) { + } +} bool(true) diff --git a/Zend/zend_language_parser.y b/Zend/zend_language_parser.y index 5cb9be3172890..016c6e5c9d098 100644 --- a/Zend/zend_language_parser.y +++ b/Zend/zend_language_parser.y @@ -1228,6 +1228,11 @@ 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 ')' { + 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 { zend_ast *name = zend_ast_create_zval_from_str(ZSTR_KNOWN(ZEND_STR_CLONE)); name->attr = ZEND_NAME_FQ; From 5457a23463797a7626bbeaa9b37160fc141e42fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Mon, 23 Jun 2025 10:26:30 +0200 Subject: [PATCH 07/13] zend_compile: Optimize the `clone()` function into the `ZEND_CLONE` OPcode --- Zend/tests/magic_methods/bug73288.phpt | 7 +++---- Zend/zend_compile.c | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/Zend/tests/magic_methods/bug73288.phpt b/Zend/tests/magic_methods/bug73288.phpt index 0a17c82e7a7f9..5e1334cacd07c 100644 --- a/Zend/tests/magic_methods/bug73288.phpt +++ b/Zend/tests/magic_methods/bug73288.phpt @@ -29,8 +29,7 @@ test_clone(); --EXPECTF-- Fatal error: Uncaught Exception: No Cloneable in %sbug73288.php:%d Stack trace: -#0 [internal function]: NoClone->__clone() -#1 %s(%d): clone(Object(NoClone)) -#2 %s(%d): test_clone() -#3 {main} +#0 %s(%d): NoClone->__clone() +#1 %s(%d): test_clone() +#2 {main} thrown in %sbug73288.php on line %d diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 4448720bc8407..f3f6d1b75aec1 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -4930,6 +4930,20 @@ static zend_result zend_compile_func_sprintf(znode *result, zend_ast_list *args) return SUCCESS; } +static zend_result zend_compile_func_clone(znode *result, zend_ast_list *args) +{ + znode arg_node; + + if (args->children != 1) { + return FAILURE; + } + + zend_compile_expr(&arg_node, args->child[0]); + zend_emit_op_tmp(result, ZEND_CLONE, &arg_node, NULL); + + return SUCCESS; +} + static zend_result zend_try_compile_special_func_ex(znode *result, zend_string *lcname, zend_ast_list *args, zend_function *fbc, uint32_t type) /* {{{ */ { if (zend_string_equals_literal(lcname, "strlen")) { @@ -4998,6 +5012,8 @@ static zend_result zend_try_compile_special_func_ex(znode *result, zend_string * return zend_compile_func_array_key_exists(result, args); } else if (zend_string_equals_literal(lcname, "sprintf")) { return zend_compile_func_sprintf(result, args); + } else if (zend_string_equals(lcname, ZSTR_KNOWN(ZEND_STR_CLONE))) { + return zend_compile_func_clone(result, args); } else { return FAILURE; } From f3462cc514be05bfcb7576371e7a23b51bcdc380 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Mon, 23 Jun 2025 14:10:03 +0200 Subject: [PATCH 08/13] opcache: Use fully-qualified function names in func_info.phpt This is necessary for `clone()` which does not currently accept a parameter-less syntax, unless fully-qualified. --- ext/opcache/tests/func_info.phpt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/opcache/tests/func_info.phpt b/ext/opcache/tests/func_info.phpt index 8b1f9ef436c4b..9596aa23199d2 100644 --- a/ext/opcache/tests/func_info.phpt +++ b/ext/opcache/tests/func_info.phpt @@ -16,7 +16,7 @@ foreach (get_defined_functions()["internal"] as $function) { if (in_array($function, ["extract", "compact", "get_defined_vars"])) { continue; } - $contents .= " \$result = {$function}();\n"; + $contents .= " \$result = \\{$function}();\n"; } $contents .= "}\n"; From 8b25a9b713e05a5942d904c3088b3cce2bdd479a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Mon, 23 Jun 2025 14:37:45 +0200 Subject: [PATCH 09/13] Optimizer: Mark `clone()` as RC1 --- Zend/Optimizer/zend_func_infos.h | 1 + Zend/zend_builtin_functions.stub.php | 1 + Zend/zend_builtin_functions_arginfo.h | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Zend/Optimizer/zend_func_infos.h b/Zend/Optimizer/zend_func_infos.h index 3655e5fd21c35..c36b7490de62c 100644 --- a/Zend/Optimizer/zend_func_infos.h +++ b/Zend/Optimizer/zend_func_infos.h @@ -1,6 +1,7 @@ /* This is a generated file, edit the .stub.php files instead. */ static const func_info_t func_infos[] = { + F1("clone", MAY_BE_OBJECT), F1("zend_version", MAY_BE_STRING), FN("func_get_args", MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_LONG|MAY_BE_ARRAY_OF_ANY), F1("get_class_vars", MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_STRING|MAY_BE_ARRAY_OF_ANY|MAY_BE_ARRAY_OF_REF), diff --git a/Zend/zend_builtin_functions.stub.php b/Zend/zend_builtin_functions.stub.php index 5c453f4523fcf..256c405c71c28 100644 --- a/Zend/zend_builtin_functions.stub.php +++ b/Zend/zend_builtin_functions.stub.php @@ -7,6 +7,7 @@ class stdClass { } +/** @refcount 1 */ function _clone(object $object): 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 e4867c6610451..1c595ecd5777c 100644 --- a/Zend/zend_builtin_functions_arginfo.h +++ b/Zend/zend_builtin_functions_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: 52e1b67d6ea82f832365b092020de4e49d6a1c38 */ + * Stub hash: 12327caa3fe940ccef68ed99f9278982dc0173a5 */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_clone, 0, 1, IS_OBJECT, 0) ZEND_ARG_TYPE_INFO(0, object, IS_OBJECT, 0) From 0acfc73910d20e5120c17d876ea74a5b8e59a06c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Tue, 24 Jun 2025 10:05:51 +0200 Subject: [PATCH 10/13] Add note to `clone()` / `ZEND_CLONE` that implementations need to be kept in sync --- Zend/zend_builtin_functions.c | 2 ++ Zend/zend_vm_def.h | 2 ++ Zend/zend_vm_execute.h | 8 ++++++++ 3 files changed, 12 insertions(+) diff --git a/Zend/zend_builtin_functions.c b/Zend/zend_builtin_functions.c index 4c466e7b841f7..9cd1c94273a2f 100644 --- a/Zend/zend_builtin_functions.c +++ b/Zend/zend_builtin_functions.c @@ -77,6 +77,8 @@ ZEND_FUNCTION(clone) Z_PARAM_OBJ(zobj) ZEND_PARSE_PARAMETERS_END(); + /* clone() also exists as the ZEND_CLONE OPcode and both implementations must be kept in sync. */ + zend_class_entry *scope = zend_get_executed_scope(); zend_class_entry *ce = zobj->ce; diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h index 8c008decbcbb5..be7bc8b37b7dd 100644 --- a/Zend/zend_vm_def.h +++ b/Zend/zend_vm_def.h @@ -6006,6 +6006,8 @@ ZEND_VM_COLD_CONST_HANDLER(110, ZEND_CLONE, CONST|TMPVAR|UNUSED|THIS|CV, ANY) SAVE_OPLINE(); obj = GET_OP1_OBJ_ZVAL_PTR_UNDEF(BP_VAR_R); + /* ZEND_CLONE also exists as the clone() function and both implementations must be kept in sync. */ + do { if (OP1_TYPE == IS_CONST || (OP1_TYPE != IS_UNUSED && UNEXPECTED(Z_TYPE_P(obj) != IS_OBJECT))) { diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h index 57e4ff1f6b16a..3a13f4244d361 100644 --- a/Zend/zend_vm_execute.h +++ b/Zend/zend_vm_execute.h @@ -5180,6 +5180,8 @@ static ZEND_VM_COLD ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_CLONE_SPEC_CONST_ SAVE_OPLINE(); obj = RT_CONSTANT(opline, opline->op1); + /* ZEND_CLONE also exists as the clone() function and both implementations must be kept in sync. */ + do { if (IS_CONST == IS_CONST || (IS_CONST != IS_UNUSED && UNEXPECTED(Z_TYPE_P(obj) != IS_OBJECT))) { @@ -15428,6 +15430,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_CLONE_SPEC_TMPVAR_HANDLER(ZEND SAVE_OPLINE(); obj = _get_zval_ptr_var(opline->op1.var EXECUTE_DATA_CC); + /* ZEND_CLONE also exists as the clone() function and both implementations must be kept in sync. */ + do { if ((IS_TMP_VAR|IS_VAR) == IS_CONST || ((IS_TMP_VAR|IS_VAR) != IS_UNUSED && UNEXPECTED(Z_TYPE_P(obj) != IS_OBJECT))) { @@ -33523,6 +33527,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_CLONE_SPEC_UNUSED_HANDLER(ZEND SAVE_OPLINE(); obj = &EX(This); + /* ZEND_CLONE also exists as the clone() function and both implementations must be kept in sync. */ + do { if (IS_UNUSED == IS_CONST || (IS_UNUSED != IS_UNUSED && UNEXPECTED(Z_TYPE_P(obj) != IS_OBJECT))) { @@ -41042,6 +41048,8 @@ static ZEND_OPCODE_HANDLER_RET ZEND_FASTCALL ZEND_CLONE_SPEC_CV_HANDLER(ZEND_OPC SAVE_OPLINE(); obj = EX_VAR(opline->op1.var); + /* ZEND_CLONE also exists as the clone() function and both implementations must be kept in sync. */ + do { if (IS_CV == IS_CONST || (IS_CV != IS_UNUSED && UNEXPECTED(Z_TYPE_P(obj) != IS_OBJECT))) { From e2bd32c47e88434032bef3ab9bf6c7c47e4903c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Tue, 24 Jun 2025 10:06:35 +0200 Subject: [PATCH 11/13] Zend: Remove dead code from clone tests --- Zend/tests/clone/bug36071.phpt | 1 - Zend/tests/clone/bug42817.phpt | 1 - 2 files changed, 2 deletions(-) diff --git a/Zend/tests/clone/bug36071.phpt b/Zend/tests/clone/bug36071.phpt index f6d9b25afaf05..e1a4baa7226ee 100644 --- a/Zend/tests/clone/bug36071.phpt +++ b/Zend/tests/clone/bug36071.phpt @@ -6,7 +6,6 @@ error_reporting=4095 b = 0; } catch (Error $e) { echo $e::class, ": ", $e->getMessage(), PHP_EOL; } diff --git a/Zend/tests/clone/bug42817.phpt b/Zend/tests/clone/bug42817.phpt index 51468280fea9b..b5f53222d7ee5 100644 --- a/Zend/tests/clone/bug42817.phpt +++ b/Zend/tests/clone/bug42817.phpt @@ -4,7 +4,6 @@ Bug #42817 (clone() on a non-object does not result in a fatal error) b, $c); } catch (Error $e) { echo $e::class, ": ", $e->getMessage(), PHP_EOL; } From 6ee0ef1b01ca66f7777d676546521758c7297de2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Tue, 24 Jun 2025 19:28:45 +0200 Subject: [PATCH 12/13] zend_builtin_functions: Simplify error path for `clone()` --- Zend/zend_builtin_functions.c | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/Zend/zend_builtin_functions.c b/Zend/zend_builtin_functions.c index 9cd1c94273a2f..48e5c70897294 100644 --- a/Zend/zend_builtin_functions.c +++ b/Zend/zend_builtin_functions.c @@ -105,16 +105,11 @@ ZEND_FUNCTION(clone) zend_object *cloned; cloned = zobj->handlers->clone_obj(zobj); - - if (EG(exception)) { - if (cloned) { - OBJ_RELEASE(cloned); - } - RETURN_THROWS(); + ZEND_ASSERT(cloned || EG(exception)); + if (EXPECTED(cloned)) { + RETURN_OBJ(cloned); } - - RETURN_OBJ(cloned); } ZEND_FUNCTION(exit) From 69bedf80dab91289ab8115d6ef0d29e02ff2e709 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Tue, 24 Jun 2025 19:32:45 +0200 Subject: [PATCH 13/13] NEWS / UPGRADING --- NEWS | 1 + UPGRADING | 2 ++ 2 files changed, 3 insertions(+) diff --git a/NEWS b/NEWS index 8f129d715d873..930dbec180221 100644 --- a/NEWS +++ b/NEWS @@ -59,6 +59,7 @@ PHP NEWS . Added support for `final` with constructor property promotion. (DanielEScherzer) . Do not use RTLD_DEEPBIND if dlmopen is available. (Daniil Gentili) + . Make `clone() a function. (timwolla, edorian) - Curl: . Added curl_multi_get_handles(). (timwolla) diff --git a/UPGRADING b/UPGRADING index 4dca8f2b2c213..98537007f65cd 100644 --- a/UPGRADING +++ b/UPGRADING @@ -374,6 +374,8 @@ PHP 8.5 UPGRADE NOTES . get_exception_handler() allows retrieving the current user-defined exception handler function. RFC: https://wiki.php.net/rfc/get-error-exception-handler + . The clone language construct is now a function. + RFC: https://wiki.php.net/rfc/clone_with_v2 - Curl: . curl_multi_get_handles() allows retrieving all CurlHandles current