Skip to content

Commit d73b46e

Browse files
committed
Fix GH-20112: Fatal error during autoloading of complex inheritance chain
When load_delayed_classes() is called recursively (e.g., autoloading a class whose linking triggers another class with unresolved variance), the shared delayed_autoloads table may contain entries from an outer caller whose dependencies are not yet available. The inner call would attempt to load these unrelated entries, causing a fatal error when their parent class is still mid-linking higher up the call stack. In nested calls, catch such loading failures and re-append the entry for the outer caller to process later, when the dependency chain is complete. A consecutive-failure counter ensures no infinite retry loops. The nesting depth counter is kept as a file-local ZEND_TLS variable rather than added to zend_compiler_globals, since PHP-8.4 forbids ABI changes. Closes GH-20112
1 parent b97dd33 commit d73b46e

2 files changed

Lines changed: 64 additions & 1 deletion

File tree

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
--TEST--
2+
GH-20112 (Nested load_delayed_classes must not process unrelated outer entries)
3+
--FILE--
4+
<?php
5+
spl_autoload_register(function ($class) {
6+
if ($class == 'IFace') {
7+
interface IFace {}
8+
} elseif ($class == 'IFaceImplementor') {
9+
class IFaceImplementor implements IFace {}
10+
} elseif ($class == 'InterfaceOfMainParent') {
11+
interface InterfaceOfMainParent {
12+
public function methodForSecondaryLspCheck(): IFace;
13+
}
14+
} elseif ($class == 'MainParent') {
15+
abstract class MainParent implements InterfaceOfMainParent {
16+
public function methodForSecondaryLspCheck(): IFaceImplementor {}
17+
}
18+
} elseif ($class == 'Intermediate') {
19+
abstract class Intermediate extends MainParent {}
20+
} elseif ($class == 'Child1') {
21+
class Child1 extends Intermediate {}
22+
} elseif ($class == 'Child2') {
23+
class Child2 extends Intermediate {}
24+
} elseif ($class == 'EntrypointParent') {
25+
abstract class EntrypointParent {
26+
abstract public function methodForLspCheck1(): MainParent;
27+
abstract public function methodForLspCheck2(): MainParent;
28+
}
29+
} elseif ($class == 'Entrypoint') {
30+
class Entrypoint extends EntrypointParent {
31+
public function methodForLspCheck1(): Child1 {}
32+
public function methodForLspCheck2(): Child2 {}
33+
}
34+
}
35+
});
36+
37+
class_exists(Entrypoint::class);
38+
echo "OK\n";
39+
?>
40+
--EXPECT--
41+
OK

Zend/zend_inheritance.c

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
ZEND_API zend_class_entry* (*zend_inheritance_cache_get)(zend_class_entry *ce, zend_class_entry *parent, zend_class_entry **traits_and_interfaces) = NULL;
3535
ZEND_API zend_class_entry* (*zend_inheritance_cache_add)(zend_class_entry *ce, zend_class_entry *proto, zend_class_entry *parent, zend_class_entry **traits_and_interfaces, HashTable *dependencies) = NULL;
3636

37+
ZEND_TLS uint32_t delayed_autoloads_depth = 0;
38+
3739
/* Unresolved means that class declarations that are currently not available are needed to
3840
* determine whether the inheritance is valid or not. At runtime UNRESOLVED should be treated
3941
* as an ERROR. */
@@ -3280,6 +3282,8 @@ static void load_delayed_classes(zend_class_entry *ce) {
32803282
* this triggers linking, then the remaining classes may get loaded when linking the newly
32813283
* loaded class. This is important, as otherwise necessary dependencies may not be available
32823284
* if the new class is lower in the hierarchy than the current one. */
3285+
delayed_autoloads_depth++;
3286+
uint32_t consecutive_failures = 0;
32833287
HashPosition pos = 0;
32843288
zend_string *name;
32853289
zend_ulong idx;
@@ -3288,13 +3292,31 @@ static void load_delayed_classes(zend_class_entry *ce) {
32883292
zend_string_addref(name);
32893293
zend_hash_del(delayed_autoloads, name);
32903294
zend_lookup_class(name);
3291-
zend_string_release(name);
32923295
if (EG(exception)) {
3296+
if (delayed_autoloads_depth > 1) {
3297+
zend_string *lc_name = zend_string_tolower(name);
3298+
bool class_was_loaded = zend_hash_exists(CG(class_table), lc_name);
3299+
zend_string_release(lc_name);
3300+
if (!class_was_loaded) {
3301+
zend_clear_exception();
3302+
zend_hash_add_empty_element(delayed_autoloads, name);
3303+
zend_string_release(name);
3304+
consecutive_failures++;
3305+
if (consecutive_failures >= zend_hash_num_elements(delayed_autoloads)) {
3306+
break;
3307+
}
3308+
continue;
3309+
}
3310+
}
3311+
delayed_autoloads_depth--;
32933312
zend_exception_uncaught_error(
32943313
"During inheritance of %s, while autoloading %s",
32953314
ZSTR_VAL(ce->name), ZSTR_VAL(name));
32963315
}
3316+
zend_string_release(name);
3317+
consecutive_failures = 0;
32973318
}
3319+
delayed_autoloads_depth--;
32983320
}
32993321

33003322
static void resolve_delayed_variance_obligations(zend_class_entry *ce) {

0 commit comments

Comments
 (0)