From 2210fda0e1c19ce610d430ba67914d503cc5c73e Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Sun, 14 Jun 2026 11:56:50 -0400 Subject: [PATCH 1/3] Fix use-after-free when ArrayObject sort comparator replaces backing store spl_array_method() caches the backing HashTable pointer across a user-supplied comparator (uasort/uksort and the sort handlers). The comparator can re-enter __construct() or __unserialize(), which route through spl_array_set_array() and swap intern->array out from under the cached pointer, leaving the post-sort cleanup to release and dereference freed memory. Mirror the nApplyCount guard the other mutators already use so replacing the backing store during a sort throws instead. Closes GH-22310 --- ext/spl/spl_array.c | 4 +++ .../ArrayObject_construct_during_sorting.phpt | 34 +++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 ext/spl/tests/ArrayObject_construct_during_sorting.phpt diff --git a/ext/spl/spl_array.c b/ext/spl/spl_array.c index 01fdccf251b5..b60b3b9c0dac 100644 --- a/ext/spl/spl_array.c +++ b/ext/spl/spl_array.c @@ -927,6 +927,10 @@ static zend_result spl_array_skip_protected(spl_array_object *intern, HashTable static void spl_array_set_array(zval *object, spl_array_object *intern, zval *array, zend_long ar_flags, bool just_array) { /* Handled by ZPP prior to this, or for __unserialize() before passing to here */ ZEND_ASSERT(Z_TYPE_P(array) == IS_ARRAY || Z_TYPE_P(array) == IS_OBJECT); + if (intern->nApplyCount > 0) { + zend_throw_error(NULL, "Modification of ArrayObject during sorting is prohibited"); + return; + } zval garbage; ZVAL_UNDEF(&garbage); if (Z_TYPE_P(array) == IS_ARRAY) { diff --git a/ext/spl/tests/ArrayObject_construct_during_sorting.phpt b/ext/spl/tests/ArrayObject_construct_during_sorting.phpt new file mode 100644 index 000000000000..cec41dc92cd0 --- /dev/null +++ b/ext/spl/tests/ArrayObject_construct_during_sorting.phpt @@ -0,0 +1,34 @@ +--TEST-- +Can't use __construct() to replace the backing store while ArrayObject is being sorted +--FILE-- +uasort(function($a, $b) use ($ao, $other, &$i) { + if ($i++ == 0) { + try { + $ao->__construct($other); + } catch (Error $e) { + echo $e->getMessage(), "\n"; + } + } + return $a <=> $b; +}); +var_dump($ao); + +?> +--EXPECT-- +Modification of ArrayObject during sorting is prohibited +object(ArrayObject)#1 (1) { + ["storage":"ArrayObject":private]=> + array(3) { + [0]=> + int(1) + [1]=> + int(2) + [2]=> + int(3) + } +} From 90569082bbb446ab60e03541f3487731edb84342 Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Mon, 15 Jun 2026 08:17:38 -0600 Subject: [PATCH 2/3] fix GH-20469: unsafe inheritance cache replay with reentrant autoloading (#22221) Inheritance cache dependencies are collected while a class is being linked. During delayed variance resolution, autoloading can re-enter class linking and use the current class while it is only nearly linked. If that class is persisted in the inheritance cache, a later request can replay dependencies in a different order and observe an incomplete hierarchy. When delayed autoloading causes the class to be used through the unlinked/nearly-linked lookup path, mark it as non-cacheable after load_delayed_classes() returns. This also catches cases where the class's variance obligations were resolved reentrantly, before the direct resolve_delayed_variance_obligations() call would run. If dependency tracking already allocated a temporary dependency table, free it when cache insertion is skipped. Restrict this cleanup to classes that entered inheritance-cache construction, because otherwise inheritance_cache is not a dependency table and may contain unrelated or uninitialized data. This preserves inheritance-cache use for delayed-variance classes that did not participate in this reentrant cycle. With the invalid cache entry prevented, unlinked_instanceof() can keep using instanceof_function() for linked classes. --- NEWS | 4 + Zend/zend_inheritance.c | 13 ++ ext/opcache/tests/gh20469.phpt | 134 +++++++++++++ ...h20469_child_variance_resolves_parent.phpt | 181 ++++++++++++++++++ .../gh20469_inheritance_cache_cleanup.phpt | 22 +++ .../tests/gh20469_inherited_method.phpt | 138 +++++++++++++ 6 files changed, 492 insertions(+) create mode 100644 ext/opcache/tests/gh20469.phpt create mode 100644 ext/opcache/tests/gh20469_child_variance_resolves_parent.phpt create mode 100644 ext/opcache/tests/gh20469_inheritance_cache_cleanup.phpt create mode 100644 ext/opcache/tests/gh20469_inherited_method.phpt diff --git a/NEWS b/NEWS index 3ac671dda658..e0375b21d4fc 100644 --- a/NEWS +++ b/NEWS @@ -44,6 +44,10 @@ PHP NEWS . Fix stmt->query leak in mysqli_execute_query() validation errors. (David Carlier) +- Opcache: + . Fixed bug GH-20469 (Unsafe inheritance cache replay with reentrant + autoloading). (Levi Morrison) + - Phar: . Fixed a bypass of the magic ".phar" directory protection in Phar::addEmptyDir() for paths starting with "/.phar", while allowing diff --git a/Zend/zend_inheritance.c b/Zend/zend_inheritance.c index eba21dd8e82a..a4dd5f1893d5 100644 --- a/Zend/zend_inheritance.c +++ b/Zend/zend_inheritance.c @@ -3735,6 +3735,11 @@ ZEND_API zend_class_entry *zend_do_link_class(zend_class_entry *ce, zend_string if (ce->ce_flags & ZEND_ACC_UNRESOLVED_VARIANCE) { resolve_delayed_variance_obligations(ce); } + /* Delayed variance resolution can re-enter linking before the full + * hierarchy is linked. See ext/opcache/tests/gh20469*.phpt. */ + if (CG(unlinked_uses) && zend_hash_index_exists(CG(unlinked_uses), (zend_long)(uintptr_t) ce)) { + ce->ce_flags &= ~ZEND_ACC_CACHEABLE; + } if (ce->ce_flags & ZEND_ACC_CACHEABLE) { ce->ce_flags &= ~ZEND_ACC_CACHEABLE; } else { @@ -3742,6 +3747,7 @@ ZEND_API zend_class_entry *zend_do_link_class(zend_class_entry *ce, zend_string } } + bool was_cacheable = is_cacheable; if (!CG(current_linking_class)) { is_cacheable = 0; } @@ -3762,6 +3768,13 @@ ZEND_API zend_class_entry *zend_do_link_class(zend_class_entry *ce, zend_string zend_hash_destroy(ht); FREE_HASHTABLE(ht); } + } else if (was_cacheable && ce->inheritance_cache) { + /* Cacheability can be disabled after dependency tracking prepared + * an inheritance-cache dependency table. Discard it here. */ + HashTable *ht = (HashTable*)ce->inheritance_cache; + ce->inheritance_cache = NULL; + zend_hash_destroy(ht); + FREE_HASHTABLE(ht); } if (!orig_record_errors) { diff --git a/ext/opcache/tests/gh20469.phpt b/ext/opcache/tests/gh20469.phpt new file mode 100644 index 000000000000..1cd826c177ef --- /dev/null +++ b/ext/opcache/tests/gh20469.phpt @@ -0,0 +1,134 @@ +--TEST-- +GH-20469: Inheritance cache with reentrant autoloading must not crash +--EXTENSIONS-- +opcache +--CONFLICTS-- +server +--FILE-- + ParentBeingLinked -> CovariantReturnWithTrait + * -> RequiresRootReturnTrait -> ChildOfParentBeingLinked. + */ +file_put_contents($dir . '/test1.php', <<<'PHP' + +--CLEAN-- +isDir()) { + rmdir($file->getPathname()); + } else { + unlink($file->getPathname()); + } + } + rmdir($dir); +} +?> +--EXPECT-- +3 +3 diff --git a/ext/opcache/tests/gh20469_child_variance_resolves_parent.phpt b/ext/opcache/tests/gh20469_child_variance_resolves_parent.phpt new file mode 100644 index 000000000000..4a66c3b0513d --- /dev/null +++ b/ext/opcache/tests/gh20469_child_variance_resolves_parent.phpt @@ -0,0 +1,181 @@ +--TEST-- +GH-20469: Child delayed variance can resolve parent before direct delayed resolution +--DESCRIPTION-- +This variant ensures the cacheability check after load_delayed_classes() is +needed. Loading the delayed child resolves the parent class's variance +obligations reentrantly, so the parent no longer has ZEND_ACC_UNRESOLVED_VARIANCE +when control returns from load_delayed_classes(). The parent was still used while +nearly linked, and must not be inserted into the inheritance cache. +--EXTENSIONS-- +opcache +--CONFLICTS-- +server +--FILE-- + CovariantReturnWithTrait -> RequiresRootReturnTrait + * -> ChildOfParentBeingLinked -> ParentBeingLinked. + * + * ChildOfParentBeingLinked also has delayed variance, so resolving the child's + * dependency on ParentBeingLinked can resolve ParentBeingLinked before it + * reaches its direct resolve_delayed_variance_obligations() call. + */ +file_put_contents($dir . '/test1.php', <<<'PHP' +test()); +PHP); + +file_put_contents($dir . '/test3.php', <<<'PHP' +test()); +PHP); + +file_put_contents($dir . '/classes/RootForTraitReturn.php', <<<'PHP' + +--CLEAN-- +isDir()) { + rmdir($file->getPathname()); + } else { + unlink($file->getPathname()); + } + } + rmdir($dir); +} +?> +--EXPECT-- +3 +3NULL +3NULL diff --git a/ext/opcache/tests/gh20469_inheritance_cache_cleanup.phpt b/ext/opcache/tests/gh20469_inheritance_cache_cleanup.phpt new file mode 100644 index 000000000000..aabbc398cbc3 --- /dev/null +++ b/ext/opcache/tests/gh20469_inheritance_cache_cleanup.phpt @@ -0,0 +1,22 @@ +--TEST-- +GH-20469: Skipped inheritance cache cleanup must ignore non-cacheable classes +--DESCRIPTION-- +Autoloading the parent makes the child use the runtime class-linking path, but +the child does not enter inheritance-cache construction. Under ASAN, the +uninitialized inheritance_cache field is filled with non-zero bytes. Skipped +cache insertion must not treat that value as a temporary dependency table. +--EXTENSIONS-- +opcache +--FILE-- + +--EXPECT-- +ok diff --git a/ext/opcache/tests/gh20469_inherited_method.phpt b/ext/opcache/tests/gh20469_inherited_method.phpt new file mode 100644 index 000000000000..f3c038bdc330 --- /dev/null +++ b/ext/opcache/tests/gh20469_inherited_method.phpt @@ -0,0 +1,138 @@ +--TEST-- +GH-20469: Inheritance cache with reentrant autoloading must preserve inherited methods +--EXTENSIONS-- +opcache +--CONFLICTS-- +server +--FILE-- + ParentBeingLinked -> CovariantReturnWithTrait + * -> RequiresRootReturnTrait -> ChildOfParentBeingLinked. + */ +file_put_contents($dir . '/test1.php', <<<'PHP' +test()); +PHP); + +file_put_contents($dir . '/classes/RootForTraitReturn.php', <<<'PHP' + +--CLEAN-- +isDir()) { + rmdir($file->getPathname()); + } else { + unlink($file->getPathname()); + } + } + rmdir($dir); +} +?> +--EXPECT-- +3 +3NULL From 8a43a0aa7bc6ba294c07f7b2f7b008d64c1242c9 Mon Sep 17 00:00:00 2001 From: Levi Morrison Date: Mon, 15 Jun 2026 08:20:04 -0600 Subject: [PATCH 3/3] Update NEWS for PR 22221 --- NEWS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/NEWS b/NEWS index 1f4271f93cbe..235699b12bea 100644 --- a/NEWS +++ b/NEWS @@ -29,6 +29,8 @@ PHP NEWS - Opcache: . Fixed bug GH-22265 (Another tailcall vm_interrupt bug). (Levi Morrison) + . Fixed bug GH-20469 (Unsafe inheritance cache replay with reentrant + autoloading). (Levi Morrison) - Phar: . Fixed a bypass of the magic ".phar" directory protection in