From b86b0b745e507004720a66cffa0d5843fcc0700a Mon Sep 17 00:00:00 2001 From: Martin Mirchev Date: Thu, 22 Oct 2026 23:35:16 +0800 Subject: [PATCH 1/2] Fix use-after-free in Randomizer::shuffleArray() when the engine throws php_array_data_shuffle() first compacts the holes of a packed array and only then updates nNumUsed. When the engine threw in between, the stale tail slots were still counted, so the moved values were released twice when the copied array was destroyed. Update nNumUsed right after the compaction. --- ...shuffleArray_engine_throws_with_holes.phpt | 61 +++++++++++++++++++ ext/standard/array.c | 3 + 2 files changed, 64 insertions(+) create mode 100644 ext/random/tests/03_randomizer/methods/shuffleArray_engine_throws_with_holes.phpt diff --git a/ext/random/tests/03_randomizer/methods/shuffleArray_engine_throws_with_holes.phpt b/ext/random/tests/03_randomizer/methods/shuffleArray_engine_throws_with_holes.phpt new file mode 100644 index 000000000000..b7c0a3b416a2 --- /dev/null +++ b/ext/random/tests/03_randomizer/methods/shuffleArray_engine_throws_with_holes.phpt @@ -0,0 +1,61 @@ +--TEST-- +Random: Randomizer: shuffleArray(): Engine throwing on an array with holes +--FILE-- +calls-- <= 0) { + throw new Exception('Error'); + } + + return "\x01\x00\x00\x00\x00\x00\x00\x00"; + } +} + +foreach ([0, 1] as $calls) { + $array = [new stdClass, new stdClass, new stdClass, new stdClass]; + unset($array[1]); + + try { + (new Randomizer(new ThrowingEngine($calls)))->shuffleArray($array); + } catch (Exception $e) { + echo $e->getMessage(), PHP_EOL; + } + + var_dump($array); +} + +?> +--EXPECTF-- +Error +array(3) { + [0]=> + object(stdClass)#%d (0) { + } + [2]=> + object(stdClass)#%d (0) { + } + [3]=> + object(stdClass)#%d (0) { + } +} +Error +array(3) { + [0]=> + object(stdClass)#%d (0) { + } + [2]=> + object(stdClass)#%d (0) { + } + [3]=> + object(stdClass)#%d (0) { + } +} diff --git a/ext/standard/array.c b/ext/standard/array.c index 8454e9616c02..32cc1b533585 100644 --- a/ext/standard/array.c +++ b/ext/standard/array.c @@ -3301,6 +3301,8 @@ PHPAPI bool php_array_data_shuffle(php_random_algo_with_state engine, zval *arra } j++; } + /* The tail now holds stale copies; drop it before the engine can throw. */ + hash->nNumUsed = n_elems; } while (--n_left) { rnd_idx = algo->range(state, 0, n_left); @@ -3329,6 +3331,7 @@ PHPAPI bool php_array_data_shuffle(php_random_algo_with_state engine, zval *arra } j++; } + hash->nNumUsed = n_elems; } while (--n_left) { rnd_idx = algo->range(state, 0, n_left); From 5af120858686f277eca3579a010d44bbe83d9b09 Mon Sep 17 00:00:00 2001 From: Martin Mirchev Date: Sat, 24 Oct 2026 18:22:16 +0800 Subject: [PATCH 2/2] Reset hash counters right after compaction in php_array_data_shuffle() --- ext/standard/array.c | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/ext/standard/array.c b/ext/standard/array.c index 32cc1b533585..ade0c536677d 100644 --- a/ext/standard/array.c +++ b/ext/standard/array.c @@ -3301,9 +3301,11 @@ PHPAPI bool php_array_data_shuffle(php_random_algo_with_state engine, zval *arra } j++; } - /* The tail now holds stale copies; drop it before the engine can throw. */ - hash->nNumUsed = n_elems; } + /* Truncate the stale slots left by compaction before the engine can throw. */ + hash->nNumUsed = n_elems; + hash->nInternalPointer = 0; + hash->nNextFreeElement = n_elems; while (--n_left) { rnd_idx = algo->range(state, 0, n_left); if (EG(exception)) { @@ -3331,8 +3333,10 @@ PHPAPI bool php_array_data_shuffle(php_random_algo_with_state engine, zval *arra } j++; } - hash->nNumUsed = n_elems; } + hash->nNumUsed = n_elems; + hash->nInternalPointer = 0; + hash->nNextFreeElement = n_elems; while (--n_left) { rnd_idx = algo->range(state, 0, n_left); if (EG(exception)) { @@ -3346,9 +3350,6 @@ PHPAPI bool php_array_data_shuffle(php_random_algo_with_state engine, zval *arra } } } - hash->nNumUsed = n_elems; - hash->nInternalPointer = 0; - hash->nNextFreeElement = n_elems; return true; }