Skip to content

Commit 02bfd2e

Browse files
committed
Store class names in canonical case in the class table
1 parent 425cd3d commit 02bfd2e

126 files changed

Lines changed: 1783 additions & 1266 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

NEWS

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,15 @@ PHP NEWS
33
?? ??? ????, PHP 8.6.0alpha1
44

55
- Core:
6+
. PHP is now case-sensitive for function and class names. Calling a function
7+
using incorrect casing (e.g. STRLEN() instead of strlen()) now raises a
8+
fatal Error. (jorgsowa)
9+
. Class, interface, trait, and enum names are now stored and looked up using
10+
their canonical casing. Referencing them with incorrect casing now raises a
11+
fatal Error across all language constructs and APIs that accept a class
12+
name. (jorgsowa)
13+
. Namespace segments in references and use imports are now matched
14+
case-sensitively. Using incorrect casing raises a fatal Error. (jorgsowa)
615
. Added first-class callable cache to share instances for the duration of the
716
request. (ilutov)
817
. It is now possible to use reference assign on WeakMap without the key

UPGRADING

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,26 @@ PHP 8.6 UPGRADE NOTES
1919
1. Backward Incompatible Changes
2020
========================================
2121

22+
- Core:
23+
. PHP is now case-sensitive for function and class names. The global function
24+
table and class table are now keyed by the canonical (declared) name rather
25+
than its lowercase equivalent. As a result, calling a function with
26+
incorrect casing (e.g. STRLEN() instead of strlen()) now raises a fatal
27+
Error ("Call to undefined function STRLEN()"), and referencing a class,
28+
interface, trait, or enum with incorrect casing (e.g. new FOO() when the
29+
class is declared as Foo) now raises a fatal Error ("Class 'FOO' not
30+
found"). This applies to all language constructs and APIs that perform a
31+
name lookup: new, instanceof, catch, extends, implements, use (trait), type
32+
declarations, callable strings/arrays, class_exists() and related
33+
introspection functions, Closure::bind()/bindTo(), Reflection constructors
34+
and methods, and the SOAP classmap option.
35+
RFC: https://wiki.php.net/rfc/case_sensitive_php
36+
. Namespace segments in use imports are now matched case-sensitively. A use
37+
import whose path does not match the canonical casing of the target
38+
namespace segment will produce a fatal Error when the imported name is
39+
first resolved.
40+
RFC: https://wiki.php.net/rfc/case_sensitive_php
41+
2242
- DOM:
2343
. Properties previously documented as @readonly (e.g. DOMNode::$nodeType,
2444
DOMDocument::$xmlEncoding, DOMEntity::$actualEncoding, ::$encoding,

Zend/Optimizer/compact_literals.c

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -134,25 +134,25 @@ void zend_optimizer_compact_literals(zend_op_array *op_array, zend_optimizer_ctx
134134
LITERAL_INFO(opline->op1.constant, 1);
135135
break;
136136
case ZEND_INIT_FCALL_BY_NAME:
137-
LITERAL_INFO(opline->op2.constant, 2);
137+
LITERAL_INFO(opline->op2.constant, 1);
138138
break;
139139
case ZEND_INIT_NS_FCALL_BY_NAME:
140-
LITERAL_INFO(opline->op2.constant, 3);
140+
LITERAL_INFO(opline->op2.constant, 2);
141141
break;
142142
case ZEND_INIT_METHOD_CALL:
143143
if (opline->op1_type == IS_CONST) {
144144
LITERAL_INFO(opline->op1.constant, 1);
145145
}
146146
if (opline->op2_type == IS_CONST) {
147-
LITERAL_INFO(opline->op2.constant, 2);
147+
LITERAL_INFO(opline->op2.constant, 1);
148148
}
149149
break;
150150
case ZEND_INIT_STATIC_METHOD_CALL:
151151
if (opline->op1_type == IS_CONST) {
152152
LITERAL_INFO(opline->op1.constant, 2);
153153
}
154154
if (opline->op2_type == IS_CONST) {
155-
LITERAL_INFO(opline->op2.constant, 2);
155+
LITERAL_INFO(opline->op2.constant, 1);
156156
}
157157
break;
158158
case ZEND_INIT_PARENT_PROPERTY_HOOK_CALL:

Zend/Optimizer/dfa_pass.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -410,7 +410,7 @@ static uint32_t zend_dfa_optimize_calls(zend_op_array *op_array, zend_ssa *ssa)
410410
if ((op->opcode == ZEND_FRAMELESS_ICALL_2
411411
|| (op->opcode == ZEND_FRAMELESS_ICALL_3 && (op + 1)->op1_type == IS_CONST))
412412
&& call_info->callee_func
413-
&& zend_string_equals_literal_ci(call_info->callee_func->common.function_name, "in_array")) {
413+
&& zend_string_equals_literal(call_info->callee_func->common.function_name, "in_array")) {
414414
bool strict = false;
415415
bool has_opdata = op->opcode == ZEND_FRAMELESS_ICALL_3;
416416
ZEND_ASSERT(!call_info->is_prototype);

Zend/Optimizer/optimize_func_calls.c

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -200,14 +200,19 @@ void zend_optimize_func_calls(zend_op_array *op_array, zend_optimizer_ctx *ctx)
200200
} else if (fcall->opcode == ZEND_INIT_FCALL_BY_NAME) {
201201
fcall->opcode = ZEND_INIT_FCALL;
202202
fcall->op1.num = zend_vm_calc_used_stack(fcall->extended_value, call_stack[call].func);
203-
literal_dtor(&ZEND_OP2_LITERAL(fcall));
204-
fcall->op2.constant = fcall->op2.constant + 1;
205203
} else if (fcall->opcode == ZEND_INIT_NS_FCALL_BY_NAME) {
206204
fcall->opcode = ZEND_INIT_FCALL;
207205
fcall->op1.num = zend_vm_calc_used_stack(fcall->extended_value, call_stack[call].func);
208-
literal_dtor(&op_array->literals[fcall->op2.constant]);
209-
literal_dtor(&op_array->literals[fcall->op2.constant + 2]);
210-
fcall->op2.constant = fcall->op2.constant + 1;
206+
/* slot 0 = ns-qualified name, slot 1 = unqualified fallback */
207+
zval *ns_slot = &op_array->literals[fcall->op2.constant];
208+
zval *unq_slot = &op_array->literals[fcall->op2.constant + 1];
209+
if (!zend_hash_find(EG(function_table), Z_STR_P(ns_slot))
210+
&& Z_TYPE_P(unq_slot) == IS_STRING) {
211+
literal_dtor(ns_slot);
212+
fcall->op2.constant++;
213+
} else {
214+
literal_dtor(&op_array->literals[fcall->op2.constant + 1]);
215+
}
211216
} else if (fcall->opcode == ZEND_INIT_STATIC_METHOD_CALL
212217
|| fcall->opcode == ZEND_INIT_METHOD_CALL
213218
|| fcall->opcode == ZEND_INIT_PARENT_PROPERTY_HOOK_CALL

Zend/Optimizer/pass1.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ void zend_optimizer_pass1(zend_op_array *op_array, zend_optimizer_ctx *ctx)
220220
}
221221

222222
/* define("name", scalar); */
223-
if (zend_string_equals_literal_ci(Z_STR(ZEND_OP2_LITERAL(init_opline)), "define")) {
223+
if (zend_string_equals_literal(Z_STR(ZEND_OP2_LITERAL(init_opline)), "define")) {
224224

225225
if (Z_TYPE(ZEND_OP1_LITERAL(send1_opline)) == IS_STRING && send2_opline) {
226226

Zend/Optimizer/zend_optimizer.c

Lines changed: 43 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -104,9 +104,7 @@ zend_result zend_optimizer_eval_special_func_call(
104104
zval *result, const zend_string *name, zend_string *arg) {
105105
if (zend_string_equals_literal(name, "function_exists") ||
106106
zend_string_equals_literal(name, "is_callable")) {
107-
zend_string *lc_name = zend_string_tolower(arg);
108-
const zend_internal_function *func = zend_hash_find_ptr(EG(function_table), lc_name);
109-
zend_string_release_ex(lc_name, 0);
107+
const zend_internal_function *func = zend_hash_find_ptr(EG(function_table), arg);
110108

111109
if (func && func->type == ZEND_INTERNAL_FUNCTION
112110
&& func->module->type == MODULE_PERSISTENT
@@ -290,7 +288,8 @@ bool zend_optimizer_update_op1_const(zend_op_array *op_array,
290288
drop_leading_backslash(val);
291289
opline->op1.constant = zend_optimizer_add_literal(op_array, val);
292290
opline->extended_value = alloc_cache_slots(op_array, 1) | (opline->extended_value & ZEND_LAST_CATCH);
293-
zend_optimizer_add_literal_string(op_array, zend_string_tolower(Z_STR_P(val)));
291+
zend_string_addref(Z_STR_P(val));
292+
zend_optimizer_add_literal_string(op_array, Z_STR_P(val));
294293
break;
295294
case ZEND_DEFINED:
296295
REQUIRES_STRING(val);
@@ -304,7 +303,8 @@ bool zend_optimizer_update_op1_const(zend_op_array *op_array,
304303
drop_leading_backslash(val);
305304
opline->op1.constant = zend_optimizer_add_literal(op_array, val);
306305
opline->op2.num = alloc_cache_slots(op_array, 1);
307-
zend_optimizer_add_literal_string(op_array, zend_string_tolower(Z_STR_P(val)));
306+
zend_string_addref(Z_STR_P(val));
307+
zend_optimizer_add_literal_string(op_array, Z_STR_P(val));
308308
break;
309309
case ZEND_INIT_STATIC_METHOD_CALL:
310310
REQUIRES_STRING(val);
@@ -313,7 +313,8 @@ bool zend_optimizer_update_op1_const(zend_op_array *op_array,
313313
if (opline->op2_type != IS_CONST) {
314314
opline->result.num = alloc_cache_slots(op_array, 1);
315315
}
316-
zend_optimizer_add_literal_string(op_array, zend_string_tolower(Z_STR_P(val)));
316+
zend_string_addref(Z_STR_P(val));
317+
zend_optimizer_add_literal_string(op_array, Z_STR_P(val));
317318
break;
318319
case ZEND_FETCH_CLASS_CONSTANT:
319320
REQUIRES_STRING(val);
@@ -322,7 +323,8 @@ bool zend_optimizer_update_op1_const(zend_op_array *op_array,
322323
if (opline->op2_type != IS_CONST) {
323324
opline->extended_value = alloc_cache_slots(op_array, 1);
324325
}
325-
zend_optimizer_add_literal_string(op_array, zend_string_tolower(Z_STR_P(val)));
326+
zend_string_addref(Z_STR_P(val));
327+
zend_optimizer_add_literal_string(op_array, Z_STR_P(val));
326328
break;
327329
case ZEND_ASSIGN_OP:
328330
case ZEND_ASSIGN_DIM_OP:
@@ -428,14 +430,14 @@ bool zend_optimizer_update_op2_const(zend_op_array *op_array,
428430
REQUIRES_STRING(val);
429431
drop_leading_backslash(val);
430432
opline->op2.constant = zend_optimizer_add_literal(op_array, val);
431-
zend_optimizer_add_literal_string(op_array, zend_string_tolower(Z_STR_P(val)));
433+
zend_string_addref(Z_STR_P(val));
434+
zend_optimizer_add_literal_string(op_array, Z_STR_P(val));
432435
opline->extended_value = alloc_cache_slots(op_array, 1);
433436
break;
434437
case ZEND_INIT_FCALL_BY_NAME:
435438
REQUIRES_STRING(val);
436439
drop_leading_backslash(val);
437440
opline->op2.constant = zend_optimizer_add_literal(op_array, val);
438-
zend_optimizer_add_literal_string(op_array, zend_string_tolower(Z_STR_P(val)));
439441
opline->result.num = alloc_cache_slots(op_array, 1);
440442
break;
441443
case ZEND_ASSIGN_STATIC_PROP:
@@ -456,20 +458,14 @@ bool zend_optimizer_update_op2_const(zend_op_array *op_array,
456458
REQUIRES_STRING(val);
457459
drop_leading_backslash(val);
458460
opline->op2.constant = zend_optimizer_add_literal(op_array, val);
459-
zend_optimizer_add_literal_string(op_array, zend_string_tolower(Z_STR_P(val)));
461+
zend_string_addref(Z_STR_P(val));
462+
zend_optimizer_add_literal_string(op_array, Z_STR_P(val));
460463
if (opline->op1_type != IS_CONST) {
461464
opline->extended_value = alloc_cache_slots(op_array, 1) | (opline->extended_value & (ZEND_RETURNS_FUNCTION|ZEND_ISEMPTY|ZEND_FETCH_OBJ_FLAGS));
462465
}
463466
break;
464467
case ZEND_INIT_FCALL:
465468
REQUIRES_STRING(val);
466-
if (Z_REFCOUNT_P(val) == 1) {
467-
zend_str_tolower(Z_STRVAL_P(val), Z_STRLEN_P(val));
468-
} else {
469-
ZVAL_STR(&tmp, zend_string_tolower(Z_STR_P(val)));
470-
zval_ptr_dtor_nogc(val);
471-
val = &tmp;
472-
}
473469
opline->op2.constant = zend_optimizer_add_literal(op_array, val);
474470
opline->result.num = alloc_cache_slots(op_array, 1);
475471
break;
@@ -488,7 +484,6 @@ bool zend_optimizer_update_op2_const(zend_op_array *op_array,
488484
opline->opcode = ZEND_INIT_FCALL_BY_NAME;
489485
drop_leading_backslash(val);
490486
opline->op2.constant = zend_optimizer_add_literal(op_array, val);
491-
zend_optimizer_add_literal_string(op_array, zend_string_tolower(Z_STR_P(val)));
492487
opline->result.num = alloc_cache_slots(op_array, 1);
493488
} else {
494489
opline->op2.constant = zend_optimizer_add_literal(op_array, val);
@@ -497,13 +492,11 @@ bool zend_optimizer_update_op2_const(zend_op_array *op_array,
497492
case ZEND_INIT_METHOD_CALL:
498493
REQUIRES_STRING(val);
499494
opline->op2.constant = zend_optimizer_add_literal(op_array, val);
500-
zend_optimizer_add_literal_string(op_array, zend_string_tolower(Z_STR_P(val)));
501495
opline->result.num = alloc_cache_slots(op_array, 2);
502496
break;
503497
case ZEND_INIT_STATIC_METHOD_CALL:
504498
REQUIRES_STRING(val);
505499
opline->op2.constant = zend_optimizer_add_literal(op_array, val);
506-
zend_optimizer_add_literal_string(op_array, zend_string_tolower(Z_STR_P(val)));
507500
if (opline->op1_type != IS_CONST) {
508501
opline->result.num = alloc_cache_slots(op_array, 2);
509502
}
@@ -922,9 +915,8 @@ zend_function *zend_optimizer_get_called_func(
922915
break;
923916
}
924917
case ZEND_INIT_FCALL_BY_NAME:
925-
case ZEND_INIT_NS_FCALL_BY_NAME:
926918
if (opline->op2_type == IS_CONST && Z_TYPE_P(CRT_CONSTANT(opline->op2)) == IS_STRING) {
927-
const zval *function_name = CRT_CONSTANT(opline->op2) + 1;
919+
const zval *function_name = CRT_CONSTANT(opline->op2);
928920
zend_function *func;
929921
zval *func_zv;
930922
if (script && (func = zend_hash_find_ptr(&script->function_table, Z_STR_P(function_name)))) {
@@ -936,12 +928,39 @@ zend_function *zend_optimizer_get_called_func(
936928
}
937929
}
938930
break;
931+
case ZEND_INIT_NS_FCALL_BY_NAME:
932+
if (opline->op2_type == IS_CONST && Z_TYPE_P(CRT_CONSTANT(opline->op2)) == IS_STRING) {
933+
/* slot 0 = ns-qualified name, slot 1 = unqualified fallback */
934+
const zval *ns_name = CRT_CONSTANT(opline->op2);
935+
const zval *unqualified_name = CRT_CONSTANT(opline->op2) + 1;
936+
zend_function *func;
937+
zval *func_zv;
938+
/* Try ns-qualified first */
939+
if (script && (func = zend_hash_find_ptr(&script->function_table, Z_STR_P(ns_name)))) {
940+
return func;
941+
} else if ((func_zv = zend_hash_find(EG(function_table), Z_STR_P(ns_name))) != NULL) {
942+
if (!zend_optimizer_ignore_function(func_zv, op_array->filename)) {
943+
return Z_PTR_P(func_zv);
944+
}
945+
}
946+
/* Fall back to unqualified global name */
947+
if (Z_TYPE_P(unqualified_name) == IS_STRING) {
948+
if (script && (func = zend_hash_find_ptr(&script->function_table, Z_STR_P(unqualified_name)))) {
949+
return func;
950+
} else if ((func_zv = zend_hash_find(EG(function_table), Z_STR_P(unqualified_name))) != NULL) {
951+
if (!zend_optimizer_ignore_function(func_zv, op_array->filename)) {
952+
return Z_PTR_P(func_zv);
953+
}
954+
}
955+
}
956+
}
957+
break;
939958
case ZEND_INIT_STATIC_METHOD_CALL:
940959
if (opline->op2_type == IS_CONST && Z_TYPE_P(CRT_CONSTANT(opline->op2)) == IS_STRING) {
941960
const zend_class_entry *ce = zend_optimizer_get_class_entry_from_op1(
942961
script, op_array, opline);
943962
if (ce) {
944-
zend_string *func_name = Z_STR_P(CRT_CONSTANT(opline->op2) + 1);
963+
zend_string *func_name = Z_STR_P(CRT_CONSTANT(opline->op2));
945964
zend_function *fbc = zend_hash_find_ptr(&ce->function_table, func_name);
946965
if (fbc && !(fbc->common.fn_flags & ZEND_ACC_ABSTRACT)) {
947966
bool is_public = (fbc->common.fn_flags & ZEND_ACC_PUBLIC) != 0;
@@ -959,7 +978,7 @@ zend_function *zend_optimizer_get_called_func(
959978
&& op_array->scope
960979
&& !(op_array->fn_flags & ZEND_ACC_TRAIT_CLONE)
961980
&& !(op_array->scope->ce_flags & ZEND_ACC_TRAIT)) {
962-
zend_string *method_name = Z_STR_P(CRT_CONSTANT(opline->op2) + 1);
981+
zend_string *method_name = Z_STR_P(CRT_CONSTANT(opline->op2));
963982
zend_function *fbc = zend_hash_find_ptr(
964983
&op_array->scope->function_table, method_name);
965984
if (fbc) {
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
--TEST--
2+
Class name with leading backslash uses correct case
3+
--FILE--
4+
<?php
5+
class Foo {}
6+
7+
$o = new Foo();
8+
9+
// Correct case with leading backslash
10+
var_dump($o instanceof \Foo);
11+
var_dump(is_a($o, '\Foo'));
12+
var_dump(class_exists('\Foo'));
13+
14+
// Wrong case with leading backslash - case-sensitive, returns false
15+
var_dump(is_a($o, '\FOO'));
16+
var_dump(class_exists('\FOO'));
17+
18+
// Dynamic new with leading backslash and wrong case - error
19+
$cls = '\FOO';
20+
try {
21+
$obj = new $cls();
22+
} catch (Error $e) {
23+
echo $e->getMessage() . "\n";
24+
}
25+
?>
26+
--EXPECT--
27+
bool(true)
28+
bool(true)
29+
bool(true)
30+
bool(false)
31+
bool(false)
32+
Class "\FOO" not found
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
--TEST--
2+
Class name with incorrect case fails in catch clause
3+
--FILE--
4+
<?php
5+
class FooException extends Exception {}
6+
7+
// correct case
8+
try {
9+
throw new FooException('test');
10+
} catch (FooException $e) {
11+
echo "caught\n";
12+
}
13+
14+
// wrong case - class not found, exception is not caught
15+
try {
16+
try {
17+
throw new FooException('test');
18+
} catch (FOOEXCEPTION $e) {
19+
echo "should not reach\n";
20+
}
21+
} catch (FooException $e) {
22+
echo "not caught by wrong case\n";
23+
}
24+
25+
// wrong case, exception does not match this catch
26+
try {
27+
try {
28+
throw new RuntimeException('test');
29+
} catch (FOOEXCEPTION $e) {
30+
echo "should not reach\n";
31+
}
32+
} catch (RuntimeException $e) {
33+
echo "rethrown\n";
34+
}
35+
?>
36+
--EXPECT--
37+
caught
38+
not caught by wrong case
39+
rethrown
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
--TEST--
2+
Class name with incorrect case in catch clause is not caught
3+
--FILE--
4+
<?php
5+
class FooException extends Exception {}
6+
7+
try {
8+
try {
9+
throw new FooException('test');
10+
} catch (FOOEXCEPTION $e) {
11+
echo "should not reach\n";
12+
}
13+
} catch (FooException $e) {
14+
echo "not caught by wrong case\n";
15+
}
16+
?>
17+
--EXPECT--
18+
not caught by wrong case

0 commit comments

Comments
 (0)