-
Notifications
You must be signed in to change notification settings - Fork 8.1k
GH-22354: support interfaces added by attribute validators in zend_compile_implements() #22355
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: PHP-8.4
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8963,17 +8963,108 @@ static void zend_compile_implements(zend_ast *ast) /* {{{ */ | |
| zend_class_name *interface_names; | ||
| uint32_t i; | ||
|
|
||
| interface_names = emalloc(sizeof(zend_class_name) * list->children); | ||
| /* Attribute validators run before `implements` parts are compiled, an | ||
| * internal attribute from an extension might have added an interface | ||
| * already so we cannot assume that the list of interfaces is empty. | ||
| * See GH-22354. The attribute validator might have also added an interface | ||
| * that the user is trying to manually implement, skip those since otherwise | ||
| * there are errors about trying to implement an interface that was already | ||
| * implemented. */ | ||
| if (EXPECTED(ce->num_interfaces == 0)) { | ||
| interface_names = emalloc(sizeof(zend_class_name) * list->children); | ||
| for (i = 0; i < list->children; ++i) { | ||
| zend_ast *class_ast = list->child[i]; | ||
| interface_names[i].name = | ||
| zend_resolve_const_class_name_reference(class_ast, "interface name"); | ||
| interface_names[i].lc_name = zend_string_tolower(interface_names[i].name); | ||
| } | ||
|
|
||
| ce->num_interfaces = list->children; | ||
| ce->interface_names = interface_names; | ||
| return; | ||
| } | ||
|
|
||
| /* Figure out which interfaces in the list should be skipped; first, resolve | ||
| * the names. BUT, only skip the *first* usage of an interface in the | ||
| * manual `implements` part, if an interface is added by an attribute but | ||
| * also manually declared twice it should still be warned about. */ | ||
| const size_t array_size = zend_safe_address_guarded(list->children, sizeof(zend_string *), 0); | ||
| ALLOCA_FLAG(use_heap_to_add) | ||
| ALLOCA_FLAG(use_heap_skipped) | ||
| zend_string **to_add_names = do_alloca(array_size, use_heap_to_add); | ||
| zend_string **skipped_names = do_alloca(array_size, use_heap_skipped); | ||
| uint32_t to_add_count = 0; | ||
| uint32_t skipped_count = 0; | ||
| for (i = 0; i < list->children; ++i) { | ||
| zend_ast *class_ast = list->child[i]; | ||
| interface_names[i].name = | ||
| zend_resolve_const_class_name_reference(class_ast, "interface name"); | ||
| interface_names[i].lc_name = zend_string_tolower(interface_names[i].name); | ||
| zend_string *name = zend_resolve_const_class_name_reference(class_ast, "interface name"); | ||
| bool already_added = false; | ||
| for (uint32_t idx = 0; idx < ce->num_interfaces; ++idx) { | ||
| if (zend_string_equals_ci(name, ce->interface_names[idx].name)) { | ||
| already_added = true; | ||
| break; | ||
| } | ||
| } | ||
| if (already_added) { | ||
| // Did we already skip this interface name once? | ||
| bool already_skipped = false; | ||
| for (uint32_t idx = 0; idx < skipped_count; ++idx) { | ||
| if (zend_string_equals_ci(name, skipped_names[idx])) { | ||
| already_skipped = true; | ||
| break; | ||
| } | ||
| } | ||
| if (already_skipped) { | ||
| /* Yes, we want to add the interface even though it was already | ||
| * added by an attribute. We get here if 1) an attribute added | ||
| * the interface (already_added), 2) the developer declared | ||
| * the interface manually (this function), and 3) this is | ||
| * *not* the first manual declaration of the interface (i.e. | ||
| * a previous manual declaration was already skipped, the | ||
| * already_skipped flag). In that case, we add the interface | ||
| * again so that the standard warning about implementing the | ||
| * same interface multiple times is shown. */ | ||
| to_add_names[i] = name; | ||
|
TimWolla marked this conversation as resolved.
|
||
| to_add_count++; | ||
| } else { | ||
| to_add_names[i] = NULL; | ||
| skipped_names[i] = name; | ||
| skipped_count++; | ||
| } | ||
| } else { | ||
| to_add_names[i] = name; | ||
| to_add_count++; | ||
| } | ||
| } | ||
| ZEND_ASSERT(skipped_count + to_add_count == list->children); | ||
|
|
||
| // Free the skipped names | ||
| for (uint32_t idx = 0; idx < skipped_count; ++idx) { | ||
| zend_string_release(skipped_names[idx]); | ||
| } | ||
| free_alloca(skipped_names, use_heap_skipped); | ||
|
|
||
| const uint32_t initial_count = ce->num_interfaces; | ||
| interface_names = safe_erealloc(ce->interface_names, (initial_count + to_add_count), sizeof(*interface_names), 0); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You are repeating
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is precisely the only place that that expression occurs? Below I use added_count which also changes each time through the loop so shouldn't be saved
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, yes. In that case using pointer arithmetic might reduce the amount of copy and paste required. |
||
|
|
||
| uint32_t added_count = 0; | ||
| for (i = 0; i < list->children; ++i) { | ||
| zend_string *name = to_add_names[i]; | ||
| if (name == NULL) { | ||
| // This was one of the names that was already added by a validator | ||
| continue; | ||
| } | ||
| interface_names[initial_count + added_count].name = name; | ||
| interface_names[initial_count + added_count].lc_name = zend_string_tolower(name); | ||
| // To make it clear that the to_add_names no longer owns the reference | ||
| to_add_names[i] = NULL; | ||
| added_count++; | ||
| } | ||
| ZEND_ASSERT(added_count == to_add_count); | ||
|
|
||
| ce->num_interfaces = list->children; | ||
| ce->num_interfaces = initial_count + added_count; | ||
| ce->interface_names = interface_names; | ||
| free_alloca(to_add_names, use_heap_to_add); | ||
| } | ||
| /* }}} */ | ||
|
|
||
|
|
||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| --TEST-- | ||
| Verify that #[ZendTestAttributeAddsInterface] adding an interface doesn't leak (no manual implements) | ||
| --EXTENSIONS-- | ||
| zend_test | ||
| --FILE-- | ||
| <?php | ||
|
|
||
| #[ZendTestAttributeAddsInterface] | ||
| class Demo {} | ||
|
|
||
| var_dump(class_implements(Demo::class)); | ||
|
|
||
| ?> | ||
| --EXPECT-- | ||
| array(1) { | ||
| ["_ZendTestInterface"]=> | ||
| string(18) "_ZendTestInterface" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| --TEST-- | ||
| Verify that #[ZendTestAttributeAddsInterface] adding an interface doesn't leak (manually implement same interface) | ||
| --EXTENSIONS-- | ||
| zend_test | ||
| --FILE-- | ||
| <?php | ||
|
|
||
| #[ZendTestAttributeAddsInterface] | ||
| class Demo implements _ZendTestInterface {} | ||
|
|
||
| var_dump(class_implements(Demo::class)); | ||
|
|
||
| ?> | ||
| --EXPECT-- | ||
| array(1) { | ||
| ["_ZendTestInterface"]=> | ||
| string(18) "_ZendTestInterface" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| --TEST-- | ||
| Verify that #[ZendTestAttributeAddsInterface] adding an interface doesn't leak (manually implement different interface) | ||
| --EXTENSIONS-- | ||
| zend_test | ||
| --FILE-- | ||
| <?php | ||
|
|
||
| interface MyInterface {} | ||
|
|
||
| #[ZendTestAttributeAddsInterface] | ||
| class Demo implements MyInterface {} | ||
|
|
||
| var_dump(class_implements(Demo::class)); | ||
|
|
||
| ?> | ||
| --EXPECT-- | ||
| array(2) { | ||
| ["_ZendTestInterface"]=> | ||
| string(18) "_ZendTestInterface" | ||
| ["MyInterface"]=> | ||
| string(11) "MyInterface" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| --TEST-- | ||
| Verify that #[ZendTestAttributeAddsInterface] and manually implementing same interface repeatedly errors | ||
| --EXTENSIONS-- | ||
| zend_test | ||
| --FILE-- | ||
| <?php | ||
|
|
||
| #[ZendTestAttributeAddsInterface] | ||
| class Demo implements _ZendTestInterface, _ZendTestInterface {} | ||
|
|
||
| var_dump(class_implements(Demo::class)); | ||
|
|
||
| ?> | ||
| --EXPECTF-- | ||
| Fatal error: Class Demo cannot implement previously implemented interface _ZendTestInterface in %s on line %d |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| --TEST-- | ||
| Verify that #[ZendTestAttributeAddsInterface] and manually implementing a different interface repeatedly errors | ||
| --EXTENSIONS-- | ||
| zend_test | ||
| --FILE-- | ||
| <?php | ||
|
|
||
| interface MyInterface {} | ||
|
|
||
| #[ZendTestAttributeAddsInterface] | ||
| class Demo implements MyInterface, MyInterface {} | ||
|
|
||
| var_dump(class_implements(Demo::class)); | ||
|
|
||
| ?> | ||
| --EXPECTF-- | ||
| Fatal error: Class Demo cannot implement previously implemented interface MyInterface in %s on line %d |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should also be
safe_emalloc.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This line is unchanged from earlier and I'd prefer not to mess with it - I just moved it into a conditional block