diff --git a/Zend/zend_inheritance.c b/Zend/zend_inheritance.c index e85b4ea42250..8df6a5599d30 100644 --- a/Zend/zend_inheritance.c +++ b/Zend/zend_inheritance.c @@ -3757,6 +3757,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 { @@ -3764,6 +3769,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; } @@ -3784,6 +3790,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 diff --git a/ext/spl/spl_array.c b/ext/spl/spl_array.c index a3df888a6102..39d27bc40105 100644 --- a/ext/spl/spl_array.c +++ b/ext/spl/spl_array.c @@ -921,6 +921,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) + } +}