Commit 2c5dc5d8b1f for php.net
commit 2c5dc5d8b1f9c77b706b7e48801a8acd0cdfffb1
Author: Arnaud Le Blanc <arnaud.lb@gmail.com>
Date: Tue Apr 21 11:04:32 2026 +0200
Add ZEND_ACC2_FORBID_DYN_CALLS
Functions that use zend_forbid_dynamic_call() must be flagged with
ZEND_ACC2_FORBID_DYN_CALLS. In stubs, this is done by using
@forbid-dynamic-calls.
To ensure consistency, we assert that the flag exists in
zend_forbid_dynamic_call(), and we assert that flagged functions thrown after a
dynamic call.
Closes GH-21818
diff --git a/UPGRADING.INTERNALS b/UPGRADING.INTERNALS
index 3a24fcce477..1d53df7e4a3 100644
--- a/UPGRADING.INTERNALS
+++ b/UPGRADING.INTERNALS
@@ -88,6 +88,9 @@ PHP 8.6 INTERNALS UPGRADE NOTES
ZEND_AST_TRAIT_METHOD_REFERENCE.
. The EMPTY_SWITCH_DEFAULT_CASE() macro has been removed. Use
default: ZEND_UNREACHABLE(); instead.
+ . Functions using zend_forbid_dynamic_call() *must* be flagged with
+ ZEND_ACC2_FORBID_DYN_CALLS (@forbid-dynamic-calls in stubs). In debug
+ builds, failing to include that flag will lead to assertion failures.
========================
2. Build system changes
diff --git a/Zend/zend_API.h b/Zend/zend_API.h
index aff6a5bba22..9eaa1ec7b3c 100644
--- a/Zend/zend_API.h
+++ b/Zend/zend_API.h
@@ -896,6 +896,8 @@ static zend_always_inline zend_result zend_forbid_dynamic_call(void)
const zend_execute_data *ex = EG(current_execute_data);
ZEND_ASSERT(ex != NULL && ex->func != NULL);
+ ZEND_ASSERT(ex->func->common.fn_flags2 & ZEND_ACC2_FORBID_DYN_CALLS);
+
if (ZEND_CALL_INFO(ex) & ZEND_CALL_DYNAMIC) {
zend_string *function_or_method_name = get_active_function_or_method_name();
zend_throw_error(NULL, "Cannot call %.*s() dynamically",
diff --git a/Zend/zend_builtin_functions.c b/Zend/zend_builtin_functions.c
index 1fe4bcf3ee5..c19bf2779fb 100644
--- a/Zend/zend_builtin_functions.c
+++ b/Zend/zend_builtin_functions.c
@@ -16,6 +16,7 @@
+----------------------------------------------------------------------+
*/
+#include "php_version.h"
#include "zend.h"
#include "zend_API.h"
#include "zend_attributes.h"
diff --git a/Zend/zend_builtin_functions.stub.php b/Zend/zend_builtin_functions.stub.php
index 9b2267b531e..1d405587145 100644
--- a/Zend/zend_builtin_functions.stub.php
+++ b/Zend/zend_builtin_functions.stub.php
@@ -18,11 +18,16 @@ function die(string|int $status = 0): never {}
/** @refcount 1 */
function zend_version(): string {}
+/** @forbid-dynamic-calls */
function func_num_args(): int {}
+/** @forbid-dynamic-calls */
function func_get_arg(int $position): mixed {}
-/** @return array<int, mixed> */
+/**
+ * @return array<int, mixed>
+ * @forbid-dynamic-calls
+ */
function func_get_args(): array {}
function strlen(string $string): int {}
@@ -156,6 +161,7 @@ function get_defined_functions(bool $exclude_disabled = true): array {}
/**
* @return array<string, mixed|ref>
* @refcount 1
+ * @forbid-dynamic-calls
*/
function get_defined_vars(): array {}
diff --git a/Zend/zend_builtin_functions_arginfo.h b/Zend/zend_builtin_functions_arginfo.h
index cb626ff430e..b3af43fef34 100644
Binary files a/Zend/zend_builtin_functions_arginfo.h and b/Zend/zend_builtin_functions_arginfo.h differ
diff --git a/Zend/zend_closures.c b/Zend/zend_closures.c
index 56090cddcaf..314abede406 100644
--- a/Zend/zend_closures.c
+++ b/Zend/zend_closures.c
@@ -749,6 +749,7 @@ static ZEND_NAMED_FUNCTION(zend_closure_internal_handler) /* {{{ */
{
zend_closure *closure = (zend_closure*)ZEND_CLOSURE_OBJECT(EX(func));
closure->orig_internal_handler(INTERNAL_FUNCTION_PARAM_PASSTHRU);
+ ZEND_ASSERT(!(closure->func.common.fn_flags2 & ZEND_ACC2_FORBID_DYN_CALLS) || EG(exception));
// Assign to EX(this) so that it is released after observer checks etc.
ZEND_ADD_CALL_FLAG(execute_data, ZEND_CALL_RELEASE_THIS);
Z_OBJ(EX(This)) = &closure->std;
diff --git a/Zend/zend_compile.h b/Zend/zend_compile.h
index 3b85d52c187..8f67fee2a52 100644
--- a/Zend/zend_compile.h
+++ b/Zend/zend_compile.h
@@ -412,10 +412,11 @@ typedef struct _zend_oparray_context {
/* op_array uses strict mode types | | | */
#define ZEND_ACC_STRICT_TYPES (1U << 31) /* | X | | */
/* | | | */
-/* Function Flags 2 (fn_flags2) (unused: 0-31) | | | */
+/* Function Flags 2 (fn_flags2) (unused: 1-31) | | | */
/* ============================ | | | */
/* | | | */
-/* #define ZEND_ACC2_EXAMPLE (1 << 0) | X | | */
+/* Function forbids dynamic calls | | | */
+#define ZEND_ACC2_FORBID_DYN_CALLS (1 << 0) /* | X | | */
#define ZEND_ACC_PPP_MASK (ZEND_ACC_PUBLIC | ZEND_ACC_PROTECTED | ZEND_ACC_PRIVATE)
#define ZEND_ACC_PPP_SET_MASK (ZEND_ACC_PUBLIC_SET | ZEND_ACC_PROTECTED_SET | ZEND_ACC_PRIVATE_SET)
diff --git a/Zend/zend_execute_API.c b/Zend/zend_execute_API.c
index 5bead7034b9..0022eb4a1df 100644
--- a/Zend/zend_execute_API.c
+++ b/Zend/zend_execute_API.c
@@ -1030,6 +1030,7 @@ zend_result zend_call_function(zend_fcall_info *fci, zend_fcall_info_cache *fci_
}
ZEND_ASSERT((call->func->common.fn_flags & ZEND_ACC_RETURN_REFERENCE)
? Z_ISREF_P(fci->retval) : !Z_ISREF_P(fci->retval));
+ ZEND_ASSERT(!(call->func->common.fn_flags2 & ZEND_ACC2_FORBID_DYN_CALLS));
}
#endif
ZEND_OBSERVER_FCALL_END(call, fci->retval);
diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h
index 391b82241e4..1f7e09d1be3 100644
--- a/Zend/zend_vm_def.h
+++ b/Zend/zend_vm_def.h
@@ -4160,6 +4160,8 @@ ZEND_VM_HOT_HANDLER(129, ZEND_DO_ICALL, ANY, ANY, SPEC(RETVAL,OBSERVER))
ZEND_ASSERT((call->func->common.fn_flags & ZEND_ACC_RETURN_REFERENCE)
? Z_ISREF_P(ret) : !Z_ISREF_P(ret));
zend_verify_internal_func_info(call->func, ret);
+ ZEND_ASSERT(!(ZEND_CALL_INFO(call) & ZEND_CALL_DYNAMIC)
+ || !(call->func->common.fn_flags2 & ZEND_ACC2_FORBID_DYN_CALLS));
}
#endif
ZEND_OBSERVER_FCALL_END(call, EG(exception) ? NULL : ret);
@@ -4291,6 +4293,8 @@ ZEND_VM_HOT_HANDLER(131, ZEND_DO_FCALL_BY_NAME, ANY, ANY, SPEC(RETVAL,OBSERVER))
ZEND_ASSERT((call->func->common.fn_flags & ZEND_ACC_RETURN_REFERENCE)
? Z_ISREF_P(ret) : !Z_ISREF_P(ret));
zend_verify_internal_func_info(call->func, ret);
+ ZEND_ASSERT(!(ZEND_CALL_INFO(call) & ZEND_CALL_DYNAMIC)
+ || !(call->func->common.fn_flags2 & ZEND_ACC2_FORBID_DYN_CALLS));
}
ZEND_ASSERT(opline->result_type != IS_TMP_VAR || !Z_ISREF_P(ret));
#endif
@@ -4422,6 +4426,8 @@ ZEND_VM_HOT_HANDLER(60, ZEND_DO_FCALL, ANY, ANY, SPEC(RETVAL,OBSERVER))
ZEND_ASSERT((call->func->common.fn_flags & ZEND_ACC_RETURN_REFERENCE)
? Z_ISREF_P(ret) : !Z_ISREF_P(ret));
zend_verify_internal_func_info(call->func, ret);
+ ZEND_ASSERT(!(ZEND_CALL_INFO(call) & ZEND_CALL_DYNAMIC)
+ || !(call->func->common.fn_flags2 & ZEND_ACC2_FORBID_DYN_CALLS));
}
ZEND_ASSERT(opline->result_type != IS_TMP_VAR || !Z_ISREF_P(ret));
#endif
@@ -9131,6 +9137,8 @@ ZEND_VM_HANDLER(158, ZEND_CALL_TRAMPOLINE, ANY, ANY, SPEC(OBSERVER))
ZEND_ASSERT((call->func->common.fn_flags & ZEND_ACC_RETURN_REFERENCE)
? Z_ISREF_P(ret) : !Z_ISREF_P(ret));
zend_verify_internal_func_info(call->func, ret);
+ ZEND_ASSERT(!(ZEND_CALL_INFO(call) & ZEND_CALL_DYNAMIC)
+ || !(call->func->common.fn_flags2 & ZEND_ACC2_FORBID_DYN_CALLS));
}
#endif
ZEND_OBSERVER_FCALL_END(call, EG(exception) ? NULL : ret);
diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h
index a2e5eac491d..d5860da23b4 100644
Binary files a/Zend/zend_vm_execute.h and b/Zend/zend_vm_execute.h differ
diff --git a/build/gen_stub.php b/build/gen_stub.php
index 857c6cb7087..619c4c905b6 100755
--- a/build/gen_stub.php
+++ b/build/gen_stub.php
@@ -1304,6 +1304,7 @@ public function __construct(
public ?FunctionOrMethodName $alias,
private readonly bool $isDeprecated,
private bool $supportsCompileTimeEval,
+ private bool $forbidDynamicCalls,
public readonly bool $verify,
public /* readonly */ array $args,
public /* readonly */ ReturnInfo $return,
@@ -1611,6 +1612,10 @@ private function getArginfoFlagsByPhpVersions(): VersionFlags
$flags->addForVersionsAbove("ZEND_ACC_NODISCARD", PHP_85_VERSION_ID);
}
+ if ($this->forbidDynamicCalls) {
+ $flags->addForVersionsAbove("ZEND_ACC2_FORBID_DYN_CALLS", PHP_86_VERSION_ID);
+ }
+
return $flags;
}
@@ -4797,6 +4802,7 @@ function parseFunctionLike(
$alias = null;
$isDeprecated = false;
$supportsCompileTimeEval = false;
+ $forbidDynamicCalls = false;
$verify = true;
$docReturnType = null;
$tentativeReturnType = false;
@@ -4812,6 +4818,7 @@ function parseFunctionLike(
$verify = !array_key_exists('no-verify', $tagMap);
$tentativeReturnType = array_key_exists('tentative-return-type', $tagMap);
$supportsCompileTimeEval = array_key_exists('compile-time-eval', $tagMap);
+ $forbidDynamicCalls = array_key_exists('forbid-dynamic-calls', $tagMap);
$isUndocumentable = $isUndocumentable || array_key_exists('undocumentable', $tagMap);
foreach ($tags as $tag) {
@@ -4944,6 +4951,7 @@ function parseFunctionLike(
$alias,
$isDeprecated,
$supportsCompileTimeEval,
+ $forbidDynamicCalls,
$verify,
$args,
$return,
diff --git a/ext/standard/basic_functions.stub.php b/ext/standard/basic_functions.stub.php
index 0f04cc036d4..1999c9b92be 100644
--- a/ext/standard/basic_functions.stub.php
+++ b/ext/standard/basic_functions.stub.php
@@ -1652,6 +1652,7 @@ function extract(array &$array, int $flags = EXTR_OVERWRITE, string $prefix = ""
* @param array|string $var_names
* @return array<string, mixed|ref>
* @refcount 1
+ * @forbid-dynamic-calls
*/
function compact($var_name, ...$var_names): array {}
diff --git a/ext/standard/basic_functions_arginfo.h b/ext/standard/basic_functions_arginfo.h
index 7ad59cfc689..e51a837ffa4 100644
Binary files a/ext/standard/basic_functions_arginfo.h and b/ext/standard/basic_functions_arginfo.h differ
diff --git a/ext/standard/basic_functions_decl.h b/ext/standard/basic_functions_decl.h
index 0dda2f894f1..b3eb25c5d98 100644
Binary files a/ext/standard/basic_functions_decl.h and b/ext/standard/basic_functions_decl.h differ
diff --git a/ext/zend_test/test.stub.php b/ext/zend_test/test.stub.php
index c9d367d5553..653630ed73b 100644
--- a/ext/zend_test/test.stub.php
+++ b/ext/zend_test/test.stub.php
@@ -187,7 +187,9 @@ class ZendTestClassWithPropertyAttribute {
}
final class ZendTestForbidDynamicCall {
+ /** @forbid-dynamic-calls */
public function call(): void {}
+ /** @forbid-dynamic-calls */
public static function callStatic(): void {}
}
diff --git a/ext/zend_test/test_arginfo.h b/ext/zend_test/test_arginfo.h
index a4da05df2ff..adcae16cdf6 100644
Binary files a/ext/zend_test/test_arginfo.h and b/ext/zend_test/test_arginfo.h differ
diff --git a/ext/zend_test/test_decl.h b/ext/zend_test/test_decl.h
index bc2ebaa93c3..ba6aab90228 100644
Binary files a/ext/zend_test/test_decl.h and b/ext/zend_test/test_decl.h differ
diff --git a/ext/zend_test/test_legacy_arginfo.h b/ext/zend_test/test_legacy_arginfo.h
index b446a0f9a29..b42d524d7a8 100644
Binary files a/ext/zend_test/test_legacy_arginfo.h and b/ext/zend_test/test_legacy_arginfo.h differ