Skip to content

Commit 93fc88e

Browse files
committed
Fix Enum::from/tryFrom memory leak in JIT for internal enums
when passing an int to a string enum. Previously, the int was coerced to a string. The JIT skips parameter clean up when unnecessary. In this particular case, passing int to from(int|string) normally doesn't cause a coercion so no dtor for the $value zval is generated. To circumvent this we avoid coersion by explicitly allowing ints and converting them to strings ourselves. Then we can free it appropriately. See GH-8518 Closes GH-8633
1 parent 38669f5 commit 93fc88e

File tree

6 files changed

+85
-10
lines changed

6 files changed

+85
-10
lines changed

NEWS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ PHP NEWS
44

55
- Core:
66
. Fixed bug GH-8338 (Intel CET is disabled unintentionally). (Chen, Hu)
7+
. Fixed leak in Enum::from/tryFrom for internal enums when using JIT (ilutov)
78

89
- Date:
910
. Fixed bug #72963 (Null-byte injection in CreateFromFormat and related

Zend/tests/enum/internal_enums.phpt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ var_dump(ZendTestStringEnum::Foo->value);
2121
var_dump($bar = ZendTestStringEnum::from("Test2"));
2222
var_dump($bar === ZendTestStringEnum::Bar);
2323
var_dump(ZendTestStringEnum::tryFrom("Test3"));
24+
var_dump(ZendTestStringEnum::tryFrom(42));
25+
var_dump(ZendTestStringEnum::tryFrom(43));
26+
var_dump(ZendTestStringEnum::tryFrom(0));
2427
var_dump(ZendTestStringEnum::cases());
2528

2629
var_dump($s = serialize($foo));
@@ -47,13 +50,18 @@ string(5) "Test1"
4750
enum(ZendTestStringEnum::Bar)
4851
bool(true)
4952
NULL
50-
array(3) {
53+
enum(ZendTestStringEnum::FortyTwo)
54+
NULL
55+
NULL
56+
array(4) {
5157
[0]=>
5258
enum(ZendTestStringEnum::Foo)
5359
[1]=>
5460
enum(ZendTestStringEnum::Bar)
5561
[2]=>
5662
enum(ZendTestStringEnum::Baz)
63+
[3]=>
64+
enum(ZendTestStringEnum::FortyTwo)
5765
}
5866
string(30) "E:22:"ZendTestStringEnum:Foo";"
5967
enum(ZendTestStringEnum::Foo)
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
--TEST--
2+
Internal enums from/tryFrom in strict_types=1
3+
--EXTENSIONS--
4+
zend_test
5+
--FILE--
6+
<?php
7+
8+
declare(strict_types=1);
9+
10+
var_dump(ZendTestStringEnum::from("Test2"));
11+
12+
try {
13+
var_dump(ZendTestStringEnum::from(42));
14+
} catch (Error $e) {
15+
echo $e->getMessage(), "\n";
16+
}
17+
18+
try {
19+
var_dump(ZendTestStringEnum::tryFrom(43));
20+
} catch (Error $e) {
21+
echo $e->getMessage(), "\n";
22+
}
23+
24+
?>
25+
--EXPECT--
26+
enum(ZendTestStringEnum::Bar)
27+
ZendTestStringEnum::from(): Argument #1 ($value) must be of type string, int given
28+
ZendTestStringEnum::tryFrom(): Argument #1 ($value) must be of type string, int given

Zend/zend_enum.c

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ static ZEND_NAMED_FUNCTION(zend_enum_cases_func)
210210
static void zend_enum_from_base(INTERNAL_FUNCTION_PARAMETERS, bool try)
211211
{
212212
zend_class_entry *ce = execute_data->func->common.scope;
213+
bool release_string = false;
213214
zend_string *string_key;
214215
zend_long long_key;
215216

@@ -221,17 +222,33 @@ static void zend_enum_from_base(INTERNAL_FUNCTION_PARAMETERS, bool try)
221222

222223
case_name_zv = zend_hash_index_find(ce->backed_enum_table, long_key);
223224
} else {
224-
ZEND_PARSE_PARAMETERS_START(1, 1)
225-
Z_PARAM_STR(string_key)
226-
ZEND_PARSE_PARAMETERS_END();
227-
228225
ZEND_ASSERT(ce->enum_backing_type == IS_STRING);
226+
227+
if (ZEND_ARG_USES_STRICT_TYPES()) {
228+
ZEND_PARSE_PARAMETERS_START(1, 1)
229+
Z_PARAM_STR(string_key)
230+
ZEND_PARSE_PARAMETERS_END();
231+
} else {
232+
// We allow long keys so that coercion to string doesn't happen implicitly. The JIT
233+
// skips deallocation of params that don't require it. In the case of from/tryFrom
234+
// passing int to from(int|string) looks like no coercion will happen, so the JIT
235+
// won't emit a dtor call. Thus we allocate/free the string manually.
236+
ZEND_PARSE_PARAMETERS_START(1, 1)
237+
Z_PARAM_STR_OR_LONG(string_key, long_key)
238+
ZEND_PARSE_PARAMETERS_END();
239+
240+
if (string_key == NULL) {
241+
release_string = true;
242+
string_key = zend_long_to_str(long_key);
243+
}
244+
}
245+
229246
case_name_zv = zend_hash_find(ce->backed_enum_table, string_key);
230247
}
231248

232249
if (case_name_zv == NULL) {
233250
if (try) {
234-
RETURN_NULL();
251+
goto return_null;
235252
}
236253

237254
if (ce->enum_backing_type == IS_LONG) {
@@ -240,7 +257,7 @@ static void zend_enum_from_base(INTERNAL_FUNCTION_PARAMETERS, bool try)
240257
ZEND_ASSERT(ce->enum_backing_type == IS_STRING);
241258
zend_value_error("\"%s\" is not a valid backing value for enum \"%s\"", ZSTR_VAL(string_key), ZSTR_VAL(ce->name));
242259
}
243-
RETURN_THROWS();
260+
goto throw;
244261
}
245262

246263
// TODO: We might want to store pointers to constants in backed_enum_table instead of names,
@@ -251,11 +268,26 @@ static void zend_enum_from_base(INTERNAL_FUNCTION_PARAMETERS, bool try)
251268
zval *case_zv = &c->value;
252269
if (Z_TYPE_P(case_zv) == IS_CONSTANT_AST) {
253270
if (zval_update_constant_ex(case_zv, c->ce) == FAILURE) {
254-
RETURN_THROWS();
271+
goto throw;
255272
}
256273
}
257274

258-
ZVAL_COPY(return_value, case_zv);
275+
if (release_string) {
276+
zend_string_release(string_key);
277+
}
278+
RETURN_COPY(case_zv);
279+
280+
throw:
281+
if (release_string) {
282+
zend_string_release(string_key);
283+
}
284+
RETURN_THROWS();
285+
286+
return_null:
287+
if (release_string) {
288+
zend_string_release(string_key);
289+
}
290+
RETURN_NULL();
259291
}
260292

261293
static ZEND_NAMED_FUNCTION(zend_enum_from_func)

ext/zend_test/test.stub.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ enum ZendTestStringEnum: string {
7272
case Foo = "Test1";
7373
case Bar = "Test2";
7474
case Baz = "Test2\\a";
75+
case FortyTwo = "42";
7576
}
7677

7778
function zend_test_array_return(): array {}

ext/zend_test/test_arginfo.h

Lines changed: 6 additions & 1 deletion
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: 7b0abebae0b0eeea0f45dccb04759fceec1096e3 */
2+
* Stub hash: 6d9ff0e2263a39420dd157206331a9e897d9e775 */
33

44
ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_zend_test_array_return, 0, 0, IS_ARRAY, 0)
55
ZEND_END_ARG_INFO()
@@ -417,6 +417,11 @@ static zend_class_entry *register_class_ZendTestStringEnum(void)
417417
ZVAL_STR(&enum_case_Baz_value, enum_case_Baz_value_str);
418418
zend_enum_add_case_cstr(class_entry, "Baz", &enum_case_Baz_value);
419419

420+
zval enum_case_FortyTwo_value;
421+
zend_string *enum_case_FortyTwo_value_str = zend_string_init("42", sizeof("42") - 1, 1);
422+
ZVAL_STR(&enum_case_FortyTwo_value, enum_case_FortyTwo_value_str);
423+
zend_enum_add_case_cstr(class_entry, "FortyTwo", &enum_case_FortyTwo_value);
424+
420425
return class_entry;
421426
}
422427

0 commit comments

Comments
 (0)