Skip to content

Commit af2a409

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. Closes GH-20112
1 parent b97dd33 commit af2a409

4 files changed

Lines changed: 78 additions & 6 deletions

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_compile.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -457,6 +457,7 @@ void init_compiler(void) /* {{{ */
457457

458458
CG(delayed_variance_obligations) = NULL;
459459
CG(delayed_autoloads) = NULL;
460+
CG(delayed_autoloads_depth) = 0;
460461
CG(unlinked_uses) = NULL;
461462
CG(current_linking_class) = NULL;
462463
}

Zend/zend_globals.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ struct _zend_compiler_globals {
149149

150150
HashTable *delayed_variance_obligations;
151151
HashTable *delayed_autoloads;
152+
uint32_t delayed_autoloads_depth;
152153
HashTable *unlinked_uses;
153154
zend_class_entry *current_linking_class;
154155

Zend/zend_inheritance.c

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3275,11 +3275,19 @@ static void load_delayed_classes(zend_class_entry *ce) {
32753275
return;
32763276
}
32773277

3278-
/* Autoloading can trigger linking of another class, which may register new delayed autoloads.
3279-
* For that reason, this code uses a loop that pops and loads the first element of the HT. If
3280-
* this triggers linking, then the remaining classes may get loaded when linking the newly
3281-
* loaded class. This is important, as otherwise necessary dependencies may not be available
3282-
* if the new class is lower in the hierarchy than the current one. */
3278+
/* Autoloading can trigger linking of another class, which may register new delayed
3279+
* autoloads. For that reason, this code uses a loop that pops and loads the first
3280+
* element of the HT. If this triggers linking, then the remaining classes may get
3281+
* loaded when linking the newly loaded class. This is important, as otherwise
3282+
* necessary dependencies may not be available if the new class is lower in the
3283+
* hierarchy than the current one.
3284+
*
3285+
* However, when load_delayed_classes() is called recursively, the shared table may
3286+
* contain entries from an outer caller whose dependencies are not yet available.
3287+
* Loading such an entry would fail. In nested calls, such failures are caught and
3288+
* the entry is re-appended for the outer caller to process later (GH-20112). */
3289+
CG(delayed_autoloads_depth)++;
3290+
uint32_t consecutive_failures = 0;
32833291
HashPosition pos = 0;
32843292
zend_string *name;
32853293
zend_ulong idx;
@@ -3288,13 +3296,34 @@ static void load_delayed_classes(zend_class_entry *ce) {
32883296
zend_string_addref(name);
32893297
zend_hash_del(delayed_autoloads, name);
32903298
zend_lookup_class(name);
3291-
zend_string_release(name);
32923299
if (EG(exception)) {
3300+
if (CG(delayed_autoloads_depth) > 1) {
3301+
zend_string *lc_name = zend_string_tolower(name);
3302+
bool class_was_loaded = zend_hash_exists(CG(class_table), lc_name);
3303+
zend_string_release(lc_name);
3304+
if (!class_was_loaded) {
3305+
/* Nested call: class could not be loaded, likely because a
3306+
* dependency higher in the call chain is not yet available.
3307+
* Re-append for the outer caller to process later. */
3308+
zend_clear_exception();
3309+
zend_hash_add_empty_element(delayed_autoloads, name);
3310+
zend_string_release(name);
3311+
consecutive_failures++;
3312+
if (consecutive_failures >= zend_hash_num_elements(delayed_autoloads)) {
3313+
break;
3314+
}
3315+
continue;
3316+
}
3317+
}
3318+
CG(delayed_autoloads_depth)--;
32933319
zend_exception_uncaught_error(
32943320
"During inheritance of %s, while autoloading %s",
32953321
ZSTR_VAL(ce->name), ZSTR_VAL(name));
32963322
}
3323+
zend_string_release(name);
3324+
consecutive_failures = 0;
32973325
}
3326+
CG(delayed_autoloads_depth)--;
32983327
}
32993328

33003329
static void resolve_delayed_variance_obligations(zend_class_entry *ce) {

0 commit comments

Comments
 (0)