Skip to content

Commit 10eb766

Browse files
Add regression tests for interfaces added via attribute validators
1 parent b0c7865 commit 10eb766

6 files changed

Lines changed: 186 additions & 1 deletion

File tree

ext/zend_test/test.c

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
#include "zend_attributes.h"
3131
#include "zend_enum.h"
3232
#include "zend_interfaces.h"
33+
#include "zend_inheritance.h"
3334
#include "zend_weakrefs.h"
3435
#include "Zend/Optimizer/zend_optimizer.h"
3536
#include "Zend/zend_alloc.h"
@@ -61,6 +62,7 @@ static zend_class_entry *zend_test_attribute;
6162
static zend_class_entry *zend_test_repeatable_attribute;
6263
static zend_class_entry *zend_test_parameter_attribute;
6364
static zend_class_entry *zend_test_property_attribute;
65+
static zend_class_entry *zend_test_attribute_add_interface;
6466
static zend_class_entry *zend_test_attribute_with_arguments;
6567
static zend_class_entry *zend_test_class_with_method_with_parameter_attribute;
6668
static zend_class_entry *zend_test_child_class_with_method_with_parameter_attribute;
@@ -913,6 +915,96 @@ void zend_attribute_validate_zendtestattribute(zend_attribute *attr, uint32_t ta
913915
}
914916
}
915917

918+
/**
919+
* Recursive handler to check if a class implements the test interface, either
920+
* directly or via inheritance.
921+
*
922+
* This is needed because even though attribute validators run before interfaces
923+
* are added, another attribute validator might have done something weird like
924+
* add *our* interface... better safe than sorry
925+
*/
926+
static bool check_class_has_interface(zend_class_entry *scope) {
927+
/* Might be *linked* (so already have class entries) but not *resolved*,
928+
/* since that waits until the inherited parent class is resolved */
929+
if (scope->ce_flags & ZEND_ACC_LINKED) {
930+
for (uint32_t iii = 0; iii < scope->num_interfaces; iii++) {
931+
if (scope->interfaces[iii] == zend_test_interface) {
932+
return true;
933+
}
934+
}
935+
} else {
936+
for (uint32_t iii = 0; iii < scope->num_interfaces; iii++) {
937+
if (zend_string_equals_literal(
938+
scope->interface_names[iii].lc_name,
939+
"_zendtestinterface"
940+
)) {
941+
// Interface was added manually be the developer
942+
return true;
943+
}
944+
}
945+
}
946+
zend_class_entry *parent = NULL;
947+
if (scope->ce_flags & ZEND_ACC_LINKED) {
948+
if (scope->parent == NULL) {
949+
return false;
950+
}
951+
parent = scope->parent;
952+
} else if (scope->parent_name == NULL) {
953+
return false;
954+
} else {
955+
parent = zend_lookup_class_ex(
956+
scope->parent_name,
957+
NULL,
958+
ZEND_FETCH_CLASS_ALLOW_UNLINKED
959+
);
960+
}
961+
if (parent == NULL) {
962+
// Invalid class to extend? Leave that up to normal PHP to deal with
963+
return false;
964+
}
965+
return check_class_has_interface(parent);
966+
}
967+
968+
void zend_attribute_validate_add_interface(zend_attribute *attr, uint32_t target, zend_class_entry *scope)
969+
{
970+
if (target != ZEND_ATTRIBUTE_TARGET_CLASS) {
971+
return;
972+
}
973+
if (scope->ce_flags & (ZEND_ACC_ENUM|ZEND_ACC_INTERFACE|ZEND_ACC_TRAIT)) {
974+
zend_error_noreturn(E_ERROR, "Only classes can be marked with #[ZendTestAttributeAddsInterface]");
975+
}
976+
if (check_class_has_interface(scope)) {
977+
return;
978+
}
979+
if (scope->ce_flags & ZEND_ACC_LINKED) {
980+
// There is already a method to add interfaces
981+
zend_do_implement_interface(scope, zend_test_interface);
982+
return;
983+
}
984+
985+
// Add the interface automatically to the list
986+
const uint32_t interfaceIdx = scope->num_interfaces;
987+
scope->num_interfaces++;
988+
989+
zend_class_name *newInterfaceSet = safe_erealloc(
990+
scope->interface_names,
991+
scope->num_interfaces,
992+
sizeof(*newInterfaceSet),
993+
0
994+
);
995+
newInterfaceSet[interfaceIdx].name = zend_string_init(
996+
"_ZendTestInterface",
997+
strlen("_ZendTestInterface"),
998+
0
999+
);
1000+
newInterfaceSet[interfaceIdx].lc_name = zend_string_init(
1001+
"_zendtestinterface",
1002+
strlen("_zendtestinterface"),
1003+
0
1004+
);
1005+
scope->interface_names = newInterfaceSet;
1006+
}
1007+
9161008
static ZEND_METHOD(_ZendTestClass, __toString)
9171009
{
9181010
ZEND_PARSE_PARAMETERS_NONE();
@@ -1296,6 +1388,12 @@ PHP_MINIT_FUNCTION(zend_test)
12961388
zend_test_property_attribute = register_class_ZendTestPropertyAttribute();
12971389
zend_mark_internal_attribute(zend_test_property_attribute);
12981390

1391+
zend_test_attribute_add_interface = register_class_ZendTestAttributeAddsInterface();
1392+
{
1393+
zend_internal_attribute *attr = zend_mark_internal_attribute(zend_test_attribute_add_interface);
1394+
attr->validator = zend_attribute_validate_add_interface;
1395+
}
1396+
12991397
zend_test_attribute_with_arguments = register_class_ZendTestAttributeWithArguments();
13001398
zend_mark_internal_attribute(zend_test_attribute_with_arguments);
13011399

ext/zend_test/test.stub.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,9 @@ final class ZendTestPropertyAttribute {
154154
public function __construct(string $parameter) {}
155155
}
156156

157+
#[Attribute(Attribute::TARGET_CLASS)]
158+
final class ZendTestAttributeAddsInterface {}
159+
157160
class ZendTestClassWithMethodWithParameterAttribute {
158161
final public function no_override(
159162
#[ZendTestParameterAttribute("value2")]

ext/zend_test/test_arginfo.h

Lines changed: 23 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
--TEST--
2+
Verify that #[ZendTestAttributeAddsInterface] adding an interface doesn't leak (no manual implements)
3+
--EXTENSIONS--
4+
zend_test
5+
--FILE--
6+
<?php
7+
8+
#[ZendTestAttributeAddsInterface]
9+
class Demo {}
10+
11+
var_dump(class_implements(Demo::class));
12+
13+
?>
14+
--EXPECT--
15+
array(1) {
16+
["_ZendTestInterface"]=>
17+
string(18) "_ZendTestInterface"
18+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
--TEST--
2+
Verify that #[ZendTestAttributeAddsInterface] adding an interface doesn't leak (manually implement same interface)
3+
--XFAIL--
4+
Currently leaks
5+
--EXTENSIONS--
6+
zend_test
7+
--FILE--
8+
<?php
9+
10+
#[ZendTestAttributeAddsInterface]
11+
class Demo implements _ZendTestInterface {}
12+
13+
var_dump(class_implements(Demo::class));
14+
15+
?>
16+
--EXPECT--
17+
array(1) {
18+
["_ZendTestInterface"]=>
19+
string(18) "_ZendTestInterface"
20+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
--TEST--
2+
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
5+
--EXTENSIONS--
6+
zend_test
7+
--FILE--
8+
<?php
9+
10+
interface MyInterface {}
11+
12+
#[ZendTestAttributeAddsInterface]
13+
class Demo implements MyInterface {}
14+
15+
var_dump(class_implements(Demo::class));
16+
17+
?>
18+
--EXPECT--
19+
array(2) {
20+
["_ZendTestInterface"]=>
21+
string(18) "_ZendTestInterface"
22+
["MyInterface"]=>
23+
string(11) "MyInterface"
24+
}

0 commit comments

Comments
 (0)