Skip to content

Commit 094b72f

Browse files
GH-22354: support interfaces added by attribute validators
In `zend_compile_implements()`, when there were no interfaces already added, keep the existing code of just adding the newly declared interfaces. But, when some interfaces are already present, don't overwrite them. Instead, add to the existing list. In case the developer tries to add an interface that was already added by an attribute, skip the manual interface addition to avoid errors about trying to apply the same interface multiple times. But, only skip the *first* manual interface addition of the same interface, if there are multiple such additions then there really was an attempt to apply the same interface multiple times. Fixes #22354
1 parent 26253b3 commit 094b72f

6 files changed

Lines changed: 121 additions & 9 deletions

File tree

NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ PHP NEWS
22
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
33
?? ??? ????, PHP 8.4.24
44

5+
- Core:
6+
. Fixed bug GH-22354 (zend_compile_implements() assumes that the class entry
7+
has no interfaces already). (DanielEScherzer)
8+
59
- Reflection:
610
. Fixed bug GH-22324 (Ignore leading namespace separator in
711
ReflectionParameter::__construct()). (jorgsowa)

Zend/zend_compile.c

Lines changed: 85 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8963,17 +8963,97 @@ static void zend_compile_implements(zend_ast *ast) /* {{{ */
89638963
zend_class_name *interface_names;
89648964
uint32_t i;
89658965

8966-
interface_names = emalloc(sizeof(zend_class_name) * list->children);
8966+
// Attribute validators run before `implements` parts are compiled, an
8967+
// internal attribute from an extension might have added an interface
8968+
// already so we cannot assume that the list of interfaces is empty.
8969+
// See GH-22354. The attribute validator might have also added an interface
8970+
// that the user is trying to manually implement, skip those since otherwise
8971+
// there are errors about trying to implement an interface that was already
8972+
// implemented.
8973+
if (EXPECTED(ce->num_interfaces == 0)) {
8974+
interface_names = emalloc(sizeof(zend_class_name) * list->children);
8975+
for (i = 0; i < list->children; ++i) {
8976+
zend_ast *class_ast = list->child[i];
8977+
interface_names[i].name =
8978+
zend_resolve_const_class_name_reference(class_ast, "interface name");
8979+
interface_names[i].lc_name = zend_string_tolower(interface_names[i].name);
8980+
}
8981+
8982+
ce->num_interfaces = list->children;
8983+
ce->interface_names = interface_names;
8984+
return;
8985+
}
89678986

8987+
// Figure out which interfaces in the list should be skipped; first, resolve
8988+
// the names
8989+
// BUT, only skip the *first* usage of an interface in the manual `implements`
8990+
// part, if an interface is added by an attribute but also manually declared
8991+
// twice it should still be warned about
8992+
zend_string **to_add_names = safe_emalloc(list->children, sizeof(*to_add_names), 0);
8993+
zend_string **skipped_names = safe_emalloc(list->children, sizeof(*skipped_names), 0);
8994+
uint32_t to_add_count = 0;
8995+
uint32_t skipped_count = 0;
89688996
for (i = 0; i < list->children; ++i) {
89698997
zend_ast *class_ast = list->child[i];
8970-
interface_names[i].name =
8971-
zend_resolve_const_class_name_reference(class_ast, "interface name");
8972-
interface_names[i].lc_name = zend_string_tolower(interface_names[i].name);
8998+
zend_string *name = zend_resolve_const_class_name_reference(class_ast, "interface name");
8999+
bool already_added = false;
9000+
for (uint32_t idx = 0; idx < ce->num_interfaces; ++idx) {
9001+
if (zend_string_equals_ci(name, ce->interface_names[idx].name)) {
9002+
already_added = true;
9003+
break;
9004+
}
9005+
}
9006+
if (already_added) {
9007+
// Did we already skip this interface name once?
9008+
bool already_skipped = false;
9009+
for (uint32_t idx = 0; idx < skipped_count; ++idx) {
9010+
if (zend_string_equals_ci(name, skipped_names[idx])) {
9011+
already_skipped = true;
9012+
break;
9013+
}
9014+
}
9015+
if (already_skipped) {
9016+
to_add_names[i] = name;
9017+
to_add_count++;
9018+
} else {
9019+
to_add_names[i] = NULL;
9020+
skipped_names[i] = name;
9021+
skipped_count++;
9022+
}
9023+
} else {
9024+
to_add_names[i] = name;
9025+
to_add_count++;
9026+
}
9027+
}
9028+
ZEND_ASSERT(skipped_count + to_add_count == list->children);
9029+
9030+
// Free the skipped names
9031+
for (uint32_t idx = 0; idx < skipped_count; ++idx) {
9032+
zend_string_release(skipped_names[idx]);
9033+
}
9034+
efree(skipped_names);
9035+
9036+
const uint32_t initial_count = ce->num_interfaces;
9037+
interface_names = safe_erealloc(ce->interface_names, (initial_count + to_add_count), sizeof(*interface_names), 0);
9038+
9039+
uint32_t added_count = 0;
9040+
for (i = 0; i < list->children; ++i) {
9041+
zend_string *name = to_add_names[i];
9042+
if (name == NULL) {
9043+
// This was one of the names that was already added by a validator
9044+
continue;
9045+
}
9046+
interface_names[initial_count + added_count].name = name;
9047+
interface_names[initial_count + added_count].lc_name = zend_string_tolower(name);
9048+
// To make it clear that the to_add_names no longer owns the reference
9049+
to_add_names[i] = NULL;
9050+
added_count += 1;
89739051
}
9052+
ZEND_ASSERT(added_count == to_add_count);
89749053

8975-
ce->num_interfaces = list->children;
9054+
ce->num_interfaces = initial_count + added_count;
89769055
ce->interface_names = interface_names;
9056+
efree(to_add_names);
89779057
}
89789058
/* }}} */
89799059

ext/zend_test/tests/attribute-adds-interface-02.phpt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
--TEST--
22
Verify that #[ZendTestAttributeAddsInterface] adding an interface doesn't leak (manually implement same interface)
3-
--XFAIL--
4-
Currently leaks
53
--EXTENSIONS--
64
zend_test
75
--FILE--

ext/zend_test/tests/attribute-adds-interface-03.phpt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
--TEST--
22
Verify that #[ZendTestAttributeAddsInterface] adding an interface doesn't leak (manually implement different interface)
3-
--XFAIL--
4-
Currently leaks and overwrites the interface added by the attribute
53
--EXTENSIONS--
64
zend_test
75
--FILE--
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
--TEST--
2+
Verify that #[ZendTestAttributeAddsInterface] and manually implementing same interface repeatedly errors
3+
--EXTENSIONS--
4+
zend_test
5+
--FILE--
6+
<?php
7+
8+
#[ZendTestAttributeAddsInterface]
9+
class Demo implements _ZendTestInterface, _ZendTestInterface {}
10+
11+
var_dump(class_implements(Demo::class));
12+
13+
?>
14+
--EXPECTF--
15+
Fatal error: Class Demo cannot implement previously implemented interface _ZendTestInterface in %s on line %d
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
--TEST--
2+
Verify that #[ZendTestAttributeAddsInterface] and manually implementing a different interface repeatedly errors
3+
--EXTENSIONS--
4+
zend_test
5+
--FILE--
6+
<?php
7+
8+
interface MyInterface {}
9+
10+
#[ZendTestAttributeAddsInterface]
11+
class Demo implements MyInterface, MyInterface {}
12+
13+
var_dump(class_implements(Demo::class));
14+
15+
?>
16+
--EXPECTF--
17+
Fatal error: Class Demo cannot implement previously implemented interface MyInterface in %s on line %d

0 commit comments

Comments
 (0)