Fix: internal interfaces should not prevent class/enum early binding#21380
Open
AJenbo wants to merge 1 commit intophp:masterfrom
Open
Fix: internal interfaces should not prevent class/enum early binding#21380AJenbo wants to merge 1 commit intophp:masterfrom
AJenbo wants to merge 1 commit intophp:masterfrom
Conversation
cb88e84 to
6a0d490
Compare
6998506 to
9c034f3
Compare
…le, etc.)
When a class implements an internal interface, either implicitly
(Stringable via __toString()) or explicitly (Countable, ArrayAccess,
IteratorAggregate, etc.), the early binding guard in
zend_compile_class_decl() rejects it because ce->num_interfaces is
non-zero. This prevents hoisting, causing "Class not found" fatal
errors on forward references like:
class B extends A {}
class A { public function __toString(): string { return ''; } }
Since certain internal interfaces are registered during engine startup
and are always available, they should not prevent early binding.
An allowlist of known-safe core interfaces is used rather than allowing
all internal interfaces, because some have interface_gets_implemented
callbacks that can trigger fatal errors or user-observable side effects
at compile time (e.g. DateTimeInterface, Throwable, Serializable).
Changes:
- Add zend_can_early_bind_interfaces() using an allowlist of known-safe
internal interfaces (Stringable, Countable, ArrayAccess, Iterator,
IteratorAggregate, Traversable). Interfaces without a callback are
always safe; those with callbacks are only allowed if explicitly
listed.
- Add zend_early_bind_resolve_internal_interfaces() to resolve interface
names via zend_do_implement_interfaces() during early binding, with
correct handling of overlapping interface hierarchies.
- Extend zend_try_early_bind() to resolve a class's own interfaces when
early binding with a parent, and build the traits_and_interfaces array
for opcache's inheritance cache. Also support parent_ce=NULL for
no-parent classes during opcache's delayed early binding.
- Update opcache's zend_accel_do_delayed_early_binding() to also bind
no-parent classes with internal interfaces (empty lc_parent_name).
- Handle polyfill patterns in zend_bind_class_in_slot(): when a
non-toplevel runtime ZEND_DECLARE_CLASS collides with a toplevel
class that was early-bound at compile time, replace the early-bound
entry instead of erroring. This supports patterns like:
if (PHP_VERSION_ID >= 80000) {
class Foo extends \Bar {} // non-toplevel, executes
return;
}
class Foo { ... } // toplevel, early-bound
Detected via ZEND_ACC_TOP_LEVEL: old class has it, new class doesn't.
Closes phpGH-7873
Closes phpGH-8323
Closes phpGH-19729
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Implementing an internal interface prevents the compiler from hoisting (early binding) a class or enum, breaking forward references that would otherwise work:
Previously reported as #7873, #8323, and #19729, all closed as won't fix.
Root cause
The early binding guard in
zend_compile_class_decl()checks!ce->num_interfacesand skips hoisting when any interfaces are present:This makes sense for userland interfaces that may not exist yet at compile time, but internal interfaces like Stringable, Countable, ArrayAccess, Iterator, IteratorAggregate, Traversable, UnitEnum, and BackedEnum are registered during engine startup and are always available.
The most common triggers:
__toString():add_stringable_interface()implicitly adds Stringable during compilationzend_enum_add_interfaces()implicitly adds UnitEnum (and BackedEnum)implementsof a built-in interfaceFix
Zend/zend_compile.c:zend_can_early_bind_interfaces()uses an allowlist of known-safe core interfaces rather than allowing all internal interfaces. Some internal interfaces haveinterface_gets_implementedcallbacks that trigger fatal errors or user-observable side effects at compile time (e.g. DateTimeInterface errors for user classes, Throwable errors for non-Exception/Error classes, Serializable triggersE_DEPRECATEDvia the user error handler). Only interfaces without a callback (Stringable, Countable) or with known-safe callbacks (ArrayAccess, Iterator, IteratorAggregate, Traversable, UnitEnum, BackedEnum) are allowed.zend_inheritance_check_override()moved after interface resolution so#[\Override]works correctly with interface methods.zend_enum_register_funcs()(registerscases(),from(),tryFrom()) andzend_verify_enum()in the early binding path, matching whatzend_do_link_class()does.Zend/zend_inheritance.c:zend_early_bind_resolve_internal_interfaces()for the no-parent early binding path. Useszend_do_implement_interfaces()(same functionzend_do_link_class()uses) for correct deduplication of overlapping interface hierarchies (e.g. a class implementing both RecursiveIterator and Iterator).zend_try_early_bind()extended to handle classes that have their own unresolved interfaces when early binding with a parent, combining them with parent interfaces viazend_do_implement_interfaces(). Also builds thetraits_and_interfacesarray for opcache's inheritance cache functions, which previously received NULL sincenum_interfaceswas always 0 in the early-bind path.Tests
New test
Zend/tests/early_binding_internal_interfaces.phptcovers:__toString())implements Stringable__toString()__toString()cases()from()andtryFrom()