Skip to content

Commit 7f2f73d

Browse files
committed
Allow internal functions to declare if they support compile-time evaluation.
https://wiki.php.net/rfc/strtolower-ascii means that these functions no longer depend on the current locale in php 8.2. Before that, this was unsafe to evaluate at compile time. Followup to phpGH-7506 Add strcmp/strcasecmp/strtolower/strtoupper functions Add bin2hex/hex2bin and related functions Add array_key_first/array_key_last Update test of garbage collection using strtolower - Make sure that this tests the right thing when opcache is enabled. Update this as an example for future tests.
1 parent 98c4a42 commit 7f2f73d

11 files changed

+293
-154
lines changed

Zend/Optimizer/sccp.c

Lines changed: 14 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -799,59 +799,24 @@ static inline zend_result ct_eval_array_key_exists(zval *result, zval *op1, zval
799799
return SUCCESS;
800800
}
801801

802-
static bool can_ct_eval_func_call(zend_string *name, uint32_t num_args, zval **args) {
802+
static bool can_ct_eval_func_call(zend_function *func, zend_string *name, uint32_t num_args, zval **args) {
803+
/* Precondition: func->type == ZEND_INTERNAL_FUNCTION, this is a global function */
803804
/* Functions in this list must always produce the same result for the same arguments,
804805
* and have no dependence on global state (such as locales). It is okay if they throw
805-
* or warn on invalid arguments, as we detect this and will discard the evaluation result. */
806-
if (false
807-
|| zend_string_equals_literal(name, "array_diff")
808-
|| zend_string_equals_literal(name, "array_diff_assoc")
809-
|| zend_string_equals_literal(name, "array_diff_key")
810-
|| zend_string_equals_literal(name, "array_flip")
811-
|| zend_string_equals_literal(name, "array_is_list")
812-
|| zend_string_equals_literal(name, "array_key_exists")
813-
|| zend_string_equals_literal(name, "array_keys")
814-
|| zend_string_equals_literal(name, "array_merge")
815-
|| zend_string_equals_literal(name, "array_merge_recursive")
816-
|| zend_string_equals_literal(name, "array_replace")
817-
|| zend_string_equals_literal(name, "array_replace_recursive")
818-
|| zend_string_equals_literal(name, "array_unique")
819-
|| zend_string_equals_literal(name, "array_values")
820-
|| zend_string_equals_literal(name, "base64_decode")
821-
|| zend_string_equals_literal(name, "base64_encode")
806+
* or warn on invalid arguments, as we detect this and will discard the evaluation result.
807+
*
808+
* In PHP 8.2, many functions stopped depending on locales due to https://wiki.php.net/rfc/strtolower-ascii */
809+
zend_internal_function *internal_func = (zend_internal_function *)func;
810+
if (internal_func->fn_flags & ZEND_ACC_COMPILE_TIME_EVAL) {
811+
/* This has @compile-time-eval in stub info and uses a macro such as ZEND_SUPPORTS_COMPILE_TIME_EVAL_FE */
812+
return true;
813+
}
822814
#ifndef ZEND_WIN32
823-
/* On Windows this function may be code page dependent. */
824-
|| zend_string_equals_literal(name, "dirname")
825-
#endif
826-
|| zend_string_equals_literal(name, "explode")
827-
|| zend_string_equals_literal(name, "imagetypes")
828-
|| zend_string_equals_literal(name, "in_array")
829-
|| zend_string_equals_literal(name, "implode")
830-
|| zend_string_equals_literal(name, "ltrim")
831-
|| zend_string_equals_literal(name, "php_sapi_name")
832-
|| zend_string_equals_literal(name, "php_uname")
833-
|| zend_string_equals_literal(name, "phpversion")
834-
|| zend_string_equals_literal(name, "pow")
835-
|| zend_string_equals_literal(name, "preg_quote")
836-
|| zend_string_equals_literal(name, "rawurldecode")
837-
|| zend_string_equals_literal(name, "rawurlencode")
838-
|| zend_string_equals_literal(name, "rtrim")
839-
|| zend_string_equals_literal(name, "serialize")
840-
|| zend_string_equals_literal(name, "str_contains")
841-
|| zend_string_equals_literal(name, "str_ends_with")
842-
|| zend_string_equals_literal(name, "str_replace")
843-
|| zend_string_equals_literal(name, "str_split")
844-
|| zend_string_equals_literal(name, "str_starts_with")
845-
|| zend_string_equals_literal(name, "strpos")
846-
|| zend_string_equals_literal(name, "strstr")
847-
|| zend_string_equals_literal(name, "substr")
848-
|| zend_string_equals_literal(name, "trim")
849-
|| zend_string_equals_literal(name, "urldecode")
850-
|| zend_string_equals_literal(name, "urlencode")
851-
|| zend_string_equals_literal(name, "version_compare")
852-
) {
815+
/* On Windows this function may be code page dependent. */
816+
if (zend_string_equals_literal(name, "dirname")) {
853817
return true;
854818
}
819+
#endif
855820

856821
if (num_args == 2) {
857822
if (zend_string_equals_literal(name, "str_repeat")) {
@@ -918,7 +883,7 @@ static inline zend_result ct_eval_func_call(
918883
}
919884
}
920885

921-
if (!can_ct_eval_func_call(name, num_args, args)) {
886+
if (!can_ct_eval_func_call(func, name, num_args, args)) {
922887
return FAILURE;
923888
}
924889

Zend/tests/bug38623.phpt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,14 @@
22
Bug #38623 (leaks in a tricky code with switch() and exceptions)
33
--FILE--
44
<?php
5+
/* This used to use strtolower, but opcache evaluates that at compile time as of php 8.2 https://wiki.php.net/rfc/strtolower-ascii */
6+
function create_refcounted_string() {
7+
$x = 'bpache';
8+
$x[0] = 'a';
9+
return $x;
10+
}
511
try {
6-
switch(strtolower("apache")) {
12+
switch(create_refcounted_string()) {
713
case "apache":
814
throw new Exception("test");
915
break;

Zend/zend_API.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,17 @@ typedef struct _zend_fcall_info_cache {
9595
#define ZEND_NS_FENTRY(ns, zend_name, name, arg_info, flags) ZEND_RAW_FENTRY(ZEND_NS_NAME(ns, #zend_name), name, arg_info, flags)
9696

9797
#define ZEND_NS_RAW_FENTRY(ns, zend_name, name, arg_info, flags) ZEND_RAW_FENTRY(ZEND_NS_NAME(ns, zend_name), name, arg_info, flags)
98+
/**
99+
* Note that if you are asserting that a function is compile-time evaluable, you are asserting that
100+
*
101+
* 1. The function will always have the same result for the same arguments
102+
* 2. The function does not depend on global state such as ini settings or locale (e.g. mb_strtolower), float to string conversions, etc.
103+
* 3. The function does not have side effects. It is okay if they throw
104+
* or warn on invalid arguments, as we detect this and will discard the evaluation result.
105+
* 4. The function will not take an unreasonable amount of time or memory to compute on code that may be seen in practice.
106+
* (e.g. str_repeat is special cased to check the length instead of using this)
107+
*/
108+
#define ZEND_SUPPORTS_COMPILE_TIME_EVAL_FE(name, arg_info) ZEND_RAW_FENTRY(#name, zif_##name, arg_info, ZEND_ACC_COMPILE_TIME_EVAL)
98109

99110
/* Same as ZEND_NS_NAMED_FE */
100111
#define ZEND_NS_RAW_NAMED_FE(ns, zend_name, name, arg_info) ZEND_NS_RAW_FENTRY(ns, #zend_name, name, arg_info, 0)

Zend/zend_builtin_functions.stub.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,16 @@ function func_get_args(): array {}
1818

1919
function strlen(string $string): int {}
2020

21+
/** @compile-time-eval */
2122
function strcmp(string $string1, string $string2): int {}
2223

24+
/** @compile-time-eval */
2325
function strncmp(string $string1, string $string2, int $length): int {}
2426

27+
/** @compile-time-eval */
2528
function strcasecmp(string $string1, string $string2): int {}
2629

30+
/** @compile-time-eval */
2731
function strncasecmp(string $string1, string $string2, int $length): int {}
2832

2933
function error_reporting(?int $error_level = null): int {}

Zend/zend_builtin_functions_arginfo.h

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* This is a generated file, edit the .stub.php file instead.
2-
* Stub hash: f87d92c002674c431827895a8d8b3a5da3b95482 */
2+
* Stub hash: 69dcb08ae12b6acbba872f7de5018ca5c0aaf669 */
33

44
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_zend_version, 0, 0, IS_STRING, 0)
55
ZEND_END_ARG_INFO()
@@ -283,10 +283,10 @@ static const zend_function_entry ext_functions[] = {
283283
ZEND_FE(func_get_arg, arginfo_func_get_arg)
284284
ZEND_FE(func_get_args, arginfo_func_get_args)
285285
ZEND_FE(strlen, arginfo_strlen)
286-
ZEND_FE(strcmp, arginfo_strcmp)
287-
ZEND_FE(strncmp, arginfo_strncmp)
288-
ZEND_FE(strcasecmp, arginfo_strcasecmp)
289-
ZEND_FE(strncasecmp, arginfo_strncasecmp)
286+
ZEND_SUPPORTS_COMPILE_TIME_EVAL_FE(strcmp, arginfo_strcmp)
287+
ZEND_SUPPORTS_COMPILE_TIME_EVAL_FE(strncmp, arginfo_strncmp)
288+
ZEND_SUPPORTS_COMPILE_TIME_EVAL_FE(strcasecmp, arginfo_strcasecmp)
289+
ZEND_SUPPORTS_COMPILE_TIME_EVAL_FE(strncasecmp, arginfo_strncasecmp)
290290
ZEND_FE(error_reporting, arginfo_error_reporting)
291291
ZEND_FE(define, arginfo_define)
292292
ZEND_FE(defined, arginfo_defined)

Zend/zend_compile.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ typedef struct _zend_oparray_context {
301301
/* Class cannot be serialized or unserialized | | | */
302302
#define ZEND_ACC_NOT_SERIALIZABLE (1 << 29) /* X | | | */
303303
/* | | | */
304-
/* Function Flags (unused: 27-30) | | | */
304+
/* Function Flags (unused: 28-30) | | | */
305305
/* ============== | | | */
306306
/* | | | */
307307
/* deprecation flag | | | */
@@ -357,6 +357,9 @@ typedef struct _zend_oparray_context {
357357
/* method flag used by Closure::__invoke() (int only) | | | */
358358
#define ZEND_ACC_USER_ARG_INFO (1 << 26) /* | X | | */
359359
/* | | | */
360+
/* supports opcache compile-time evaluation (funcs) | | | */
361+
#define ZEND_ACC_COMPILE_TIME_EVAL (1 << 27) /* | X | | */
362+
/* | | | */
360363
/* op_array uses strict mode types | | | */
361364
#define ZEND_ACC_STRICT_TYPES (1U << 31) /* | X | | */
362365

build/gen_stub.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -959,6 +959,8 @@ class FuncInfo {
959959
/** @var bool */
960960
public $isDeprecated;
961961
/** @var bool */
962+
public $supportsCompileTimeEval;
963+
/** @var bool */
962964
public $verify;
963965
/** @var ArgInfo[] */
964966
public $args;
@@ -979,6 +981,7 @@ public function __construct(
979981
?string $aliasType,
980982
?FunctionOrMethodName $alias,
981983
bool $isDeprecated,
984+
bool $supportsCompileTimeEval,
982985
bool $verify,
983986
array $args,
984987
ReturnInfo $return,
@@ -991,6 +994,7 @@ public function __construct(
991994
$this->aliasType = $aliasType;
992995
$this->alias = $alias;
993996
$this->isDeprecated = $isDeprecated;
997+
$this->supportsCompileTimeEval = $supportsCompileTimeEval;
994998
$this->verify = $verify;
995999
$this->args = $args;
9961000
$this->return = $return;
@@ -1155,6 +1159,10 @@ public function getFunctionEntry(): string {
11551159
"\tZEND_NS_FE(\"%s\", %s, %s)\n",
11561160
addslashes($namespace), $declarationName, $this->getArgInfoName());
11571161
} else {
1162+
if ($this->supportsCompileTimeEval) {
1163+
return sprintf(
1164+
"\tZEND_SUPPORTS_COMPILE_TIME_EVAL_FE(%s, %s)\n", $declarationName, $this->getArgInfoName());
1165+
}
11581166
return sprintf("\tZEND_FE(%s, %s)\n", $declarationName, $this->getArgInfoName());
11591167
}
11601168
} else {
@@ -2232,6 +2240,7 @@ function parseFunctionLike(
22322240
$aliasType = null;
22332241
$alias = null;
22342242
$isDeprecated = false;
2243+
$supportsCompileTimeEval = false;
22352244
$verify = true;
22362245
$docReturnType = null;
22372246
$tentativeReturnType = false;
@@ -2267,6 +2276,8 @@ function parseFunctionLike(
22672276
$docParamTypes[$tag->getVariableName()] = $tag->getType();
22682277
} else if ($tag->name === 'refcount') {
22692278
$refcount = $tag->getValue();
2279+
} else if ($tag->name === 'compile-time-eval') {
2280+
$supportsCompileTimeEval = true;
22702281
}
22712282
}
22722283
}
@@ -2355,6 +2366,7 @@ function parseFunctionLike(
23552366
$aliasType,
23562367
$alias,
23572368
$isDeprecated,
2369+
$supportsCompileTimeEval,
23582370
$verify,
23592371
$args,
23602372
$return,

ext/pcre/php_pcre.stub.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ function preg_replace_callback_array(array $pattern, string|array $subject, int
3535
*/
3636
function preg_split(string $pattern, string $subject, int $limit = -1, int $flags = 0): array|false {}
3737

38+
/** @compile-time-eval */
3839
function preg_quote(string $str, ?string $delimiter = null): string {}
3940

4041
/** @refcount 1 */

ext/pcre/php_pcre_arginfo.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* This is a generated file, edit the .stub.php file instead.
2-
* Stub hash: bc6f31ac17d4f5d1a60dd3dad5f671058f40a224 */
2+
* Stub hash: 39a19378fb1f1aca34bfdd483f5d3095558f0e09 */
33

44
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_preg_match, 0, 2, MAY_BE_LONG|MAY_BE_FALSE)
55
ZEND_ARG_TYPE_INFO(0, pattern, IS_STRING, 0)
@@ -84,7 +84,7 @@ static const zend_function_entry ext_functions[] = {
8484
ZEND_FE(preg_replace_callback, arginfo_preg_replace_callback)
8585
ZEND_FE(preg_replace_callback_array, arginfo_preg_replace_callback_array)
8686
ZEND_FE(preg_split, arginfo_preg_split)
87-
ZEND_FE(preg_quote, arginfo_preg_quote)
87+
ZEND_SUPPORTS_COMPILE_TIME_EVAL_FE(preg_quote, arginfo_preg_quote)
8888
ZEND_FE(preg_grep, arginfo_preg_grep)
8989
ZEND_FE(preg_last_error, arginfo_preg_last_error)
9090
ZEND_FE(preg_last_error_msg, arginfo_preg_last_error_msg)

0 commit comments

Comments
 (0)