Fix GH-16649: Avoid UAF when using array_splice

Closes GH-19399
This commit is contained in:
Alexandre Daubois 2025-08-07 09:41:50 +02:00 committed by Ilija Tovilo
parent 2b415e416e
commit c8774f9e61
No known key found for this signature in database
GPG key ID: 5050C66BFCD1015A
10 changed files with 212 additions and 0 deletions

3
NEWS
View file

@ -6,6 +6,9 @@ PHP NEWS
. Fixed bug GH-19245 (Success error message on TLS stream accept failure). . Fixed bug GH-19245 (Success error message on TLS stream accept failure).
(Jakub Zelenka) (Jakub Zelenka)
- Standard:
. Fixed bug GH-16649 (UAF during array_splice). (alexandre-daubois)
28 Aug 2025, PHP 8.3.25 28 Aug 2025, PHP 8.3.25
- Core: - Core:

View file

@ -3214,6 +3214,9 @@ static void php_splice(HashTable *in_hash, zend_long offset, zend_long length, H
zval *entry; /* Hash entry */ zval *entry; /* Hash entry */
uint32_t iter_pos = zend_hash_iterators_lower_pos(in_hash, 0); uint32_t iter_pos = zend_hash_iterators_lower_pos(in_hash, 0);
GC_ADDREF(in_hash);
HT_ALLOW_COW_VIOLATION(in_hash); /* Will be reset when setting the flags for in_hash */
/* Get number of entries in the input hash */ /* Get number of entries in the input hash */
num_in = zend_hash_num_elements(in_hash); num_in = zend_hash_num_elements(in_hash);
@ -3372,6 +3375,15 @@ static void php_splice(HashTable *in_hash, zend_long offset, zend_long length, H
HT_SET_ITERATORS_COUNT(&out_hash, HT_ITERATORS_COUNT(in_hash)); HT_SET_ITERATORS_COUNT(&out_hash, HT_ITERATORS_COUNT(in_hash));
HT_SET_ITERATORS_COUNT(in_hash, 0); HT_SET_ITERATORS_COUNT(in_hash, 0);
in_hash->pDestructor = NULL; in_hash->pDestructor = NULL;
if (UNEXPECTED(GC_DELREF(in_hash) == 0)) {
/* Array was completely deallocated during the operation */
zend_array_destroy(in_hash);
zend_hash_destroy(&out_hash);
zend_throw_error(NULL, "Array was modified during array_splice operation");
return;
}
zend_hash_destroy(in_hash); zend_hash_destroy(in_hash);
HT_FLAGS(in_hash) = HT_FLAGS(&out_hash); HT_FLAGS(in_hash) = HT_FLAGS(&out_hash);

View file

@ -0,0 +1,20 @@
--TEST--
GH-16649: array_splice with normal destructor should work fine
--FILE--
<?php
class C {
function __destruct() {
echo "Destructor called\n";
}
}
$arr = ["1", new C, "2"];
array_splice($arr, 1, 2);
var_dump($arr);
?>
--EXPECT--
Destructor called
array(1) {
[0]=>
string(1) "1"
}

View file

@ -0,0 +1,22 @@
--TEST--
GH-16649: array_splice UAF when destructor adds elements to array
--FILE--
<?php
class C {
function __destruct() {
global $arr;
$arr[] = 0;
}
}
$arr = ["1", new C, "2"];
try {
array_splice($arr, 1, 2);
echo "ERROR: Should have thrown exception\n";
} catch (Error $e) {
echo "Exception caught: " . $e->getMessage() . "\n";
}
?>
--EXPECT--
Exception caught: Array was modified during array_splice operation

View file

@ -0,0 +1,22 @@
--TEST--
GH-16649: array_splice UAF when array is released entirely
--FILE--
<?php
class C {
function __destruct() {
global $arr;
$arr = null;
}
}
$arr = ["1", new C, "2"];
try {
array_splice($arr, 1, 2);
echo "ERROR: Should have thrown exception\n";
} catch (Error $e) {
echo "Exception caught: " . $e->getMessage() . "\n";
}
?>
--EXPECT--
Exception caught: Array was modified during array_splice operation

View file

@ -0,0 +1,25 @@
--TEST--
GH-16649: array_splice UAF with complex array modification
--FILE--
<?php
class ComplexModifier {
function __destruct() {
global $arr;
// complex modification that causes cow
unset($arr[0]);
$arr["new_key"] = "new_value";
$arr[100] = "another_value";
}
}
$arr = ["first", new ComplexModifier, "last"];
try {
array_splice($arr, 1, 1, ["replacement"]);
echo "ERROR: Should have thrown exception\n";
} catch (Error $e) {
echo "Exception caught: " . $e->getMessage() . "\n";
}
?>
--EXPECT--
Exception caught: Array was modified during array_splice operation

View file

@ -0,0 +1,33 @@
--TEST--
GH-16649: array_splice UAF with multiple destructors
--FILE--
<?php
class MultiDestructor {
public $id;
function __construct($id) {
$this->id = $id;
}
function __destruct() {
global $arr;
echo "Destructor {$this->id} called\n";
if ($this->id == 2) {
$arr = null;
}
}
}
$arr = ["start", new MultiDestructor(1), new MultiDestructor(2), "end"];
try {
array_splice($arr, 1, 2);
echo "ERROR: Should have thrown exception\n";
} catch (Error $e) {
echo "Exception caught: " . $e->getMessage() . "\n";
}
?>
--EXPECT--
Destructor 1 called
Destructor 2 called
Exception caught: Array was modified during array_splice operation

View file

@ -0,0 +1,29 @@
--TEST--
GH-16649: array_splice UAF with destructor modifying array (original case)
--FILE--
<?php
function resize_arr() {
global $arr;
for ($i = 0; $i < 10; $i++) {
$arr[$i] = $i;
}
}
class C {
function __destruct() {
resize_arr();
return "3";
}
}
$arr = ["a" => "1", "3" => new C, "2" => "2"];
try {
array_splice($arr, 1, 2);
echo "ERROR: Should have thrown exception\n";
} catch (Error $e) {
echo "Exception caught: " . $e->getMessage() . "\n";
}
?>
--EXPECT--
Exception caught: Array was modified during array_splice operation

View file

@ -0,0 +1,23 @@
--TEST--
GH-16649: array_splice UAF when array is converted from packed to hash
--FILE--
<?php
class C {
function __destruct() {
global $arr;
// array is converted from packed to hash
$arr["str"] = 0;
}
}
$arr = ["1", new C, "2"];
try {
array_splice($arr, 1, 2);
echo "ERROR: Should have thrown exception\n";
} catch (Error $e) {
echo "Exception caught: " . $e->getMessage() . "\n";
}
?>
--EXPECT--
Exception caught: Array was modified during array_splice operation

View file

@ -0,0 +1,23 @@
--TEST--
GH-16649: array_splice with replacement array when destructor modifies array
--FILE--
<?php
class C {
function __destruct() {
global $arr;
$arr["modified"] = "by_destructor";
}
}
$arr = ["a", new C, "b"];
$replacement = ["replacement1", "replacement2"];
try {
array_splice($arr, 1, 1, $replacement);
echo "ERROR: Should have thrown exception\n";
} catch (Error $e) {
echo "Exception caught: " . $e->getMessage() . "\n";
}
?>
--EXPECT--
Exception caught: Array was modified during array_splice operation