Skip to content

Fix GH-16649: Avoid UAF when using array_splice #19399

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: PHP-8.3
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ PHP NEWS
. Fix theoretical issues with hrtime() not being available. (nielsdos)
. Fixed bug GH-19300 (Nested array_multisort invocation with error breaks).
(nielsdos)
. Fixed bug GH-16649 (UAF during array_splice). (alexandre-daubois)

- Windows:
. Free opened_path when opened_path_len >= MAXPATHLEN. (dixyes)
Expand Down
12 changes: 12 additions & 0 deletions ext/standard/array.c
Original file line number Diff line number Diff line change
Expand Up @@ -3214,6 +3214,9 @@ static void php_splice(HashTable *in_hash, zend_long offset, zend_long length, H
zval *entry; /* Hash entry */
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 */
num_in = zend_hash_num_elements(in_hash);

Expand Down Expand Up @@ -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(in_hash, 0);
in_hash->pDestructor = NULL;

if (UNEXPECTED(GC_DELREF(in_hash) == 0)) {
/* Array was completely deallocated during the operation */
zend_array_destroy(in_hash);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default branch uses zend_hash_destroy(in_hash);. zend_array_destroy() seems slightly more optimized (or at least optimized in different ways), but we should stay consistent. Adjusting the default branch would also be possible, but should be measured and done on master.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it!

zend_hash_destroy(&out_hash);
zend_throw_error(NULL, "Array was modified during array_splice operation");
return;
}

zend_hash_destroy(in_hash);

HT_FLAGS(in_hash) = HT_FLAGS(&out_hash);
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
}
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Loading