From 2210fda0e1c19ce610d430ba67914d503cc5c73e Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Sun, 14 Jun 2026 11:56:50 -0400 Subject: [PATCH] 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) + } +}