Fix #81992: SplFixedArray::setSize() causes use-after-free

Upon resizing, the elements are destroyed from lower index to higher
index. When an element refers to an object with a destructor, it can
refer to a lower (i.e. already destroyed) element, causing a uaf.
Set refcounted zvals to NULL after destroying them to avoid a uaf.

Closes GH-11959.
This commit is contained in:
Niels Dossche 2023-08-14 01:41:54 +02:00
parent 2012fd3f04
commit b71c6b2c6c
4 changed files with 123 additions and 1 deletions

4
NEWS
View File

@ -55,6 +55,10 @@ PHP NEWS
. Revert behaviour of receiving SIGCHLD signals back to the behaviour
before 8.1.22. (nielsdos)
- SPL:
. Fixed bug #81992 (SplFixedArray::setSize() causes use-after-free).
(nielsdos)
- Standard:
. Prevent int overflow on $decimals in number_format. (Marc Bennewitz)
. Fixed bug GH-11870 (Fix off-by-one bug when truncating tempnam prefix)

View File

@ -46,6 +46,8 @@ typedef struct _spl_fixedarray {
zval *elements;
/* True if this was modified after the last call to get_properties or the hash table wasn't rebuilt. */
bool should_rebuild_properties;
/* If positive, it's a resize within a resize and the value gives the desired size. If -1, it's not. */
zend_long cached_resize;
} spl_fixedarray;
typedef struct _spl_fixedarray_methods {
@ -117,6 +119,7 @@ static void spl_fixedarray_init(spl_fixedarray *array, zend_long size)
} else {
spl_fixedarray_default_ctor(array);
}
array->cached_resize = -1;
}
/* Copies the range [begin, end) into the fixedarray, beginning at `offset`.
@ -148,6 +151,7 @@ static void spl_fixedarray_copy_ctor(spl_fixedarray *to, spl_fixedarray *from)
*/
static void spl_fixedarray_dtor_range(spl_fixedarray *array, zend_long from, zend_long to)
{
array->size = from;
zval *begin = array->elements + from, *end = array->elements + to;
while (begin != end) {
zval_ptr_dtor(begin++);
@ -184,19 +188,35 @@ static void spl_fixedarray_resize(spl_fixedarray *array, zend_long size)
return;
}
if (UNEXPECTED(array->cached_resize >= 0)) {
/* We're already resizing, so just remember the desired size.
* The resize will happen later. */
array->cached_resize = size;
return;
}
array->cached_resize = size;
/* clearing the array */
if (size == 0) {
spl_fixedarray_dtor(array);
array->elements = NULL;
array->size = 0;
} else if (size > array->size) {
array->elements = safe_erealloc(array->elements, size, sizeof(zval), 0);
spl_fixedarray_init_elems(array, array->size, size);
array->size = size;
} else { /* size < array->size */
/* Size set in spl_fixedarray_dtor_range() */
spl_fixedarray_dtor_range(array, size, array->size);
array->elements = erealloc(array->elements, sizeof(zval) * size);
}
array->size = size;
/* If resized within the destructor, take the last resize command and perform it */
zend_long cached_resize = array->cached_resize;
array->cached_resize = -1;
if (cached_resize != size) {
spl_fixedarray_resize(array, cached_resize);
}
}
static HashTable* spl_fixedarray_object_get_gc(zend_object *obj, zval **table, int *n)

View File

@ -0,0 +1,32 @@
--TEST--
Bug #81992 (SplFixedArray::setSize() causes use-after-free)
--FILE--
<?php
class InvalidDestructor {
public function __destruct() {
global $obj;
var_dump($obj[0]);
try {
var_dump($obj[2]);
} catch (Throwable $e) {
echo $e->getMessage(), "\n";
}
try {
var_dump($obj[4]);
} catch (Throwable $e) {
echo $e->getMessage(), "\n";
}
}
}
$obj = new SplFixedArray(5);
$obj[0] = str_repeat("A", 10);
$obj[2] = str_repeat('B', 10);
$obj[3] = new InvalidDestructor();
$obj[4] = str_repeat('C', 10);
$obj->setSize(2);
?>
--EXPECT--
string(10) "AAAAAAAAAA"
Index invalid or out of range
Index invalid or out of range

View File

@ -0,0 +1,66 @@
--TEST--
Bug #81992 (SplFixedArray::setSize() causes use-after-free) - setSize variation
--FILE--
<?php
class InvalidDestructor {
public function __construct(
private int $desiredSize,
private SplFixedArray $obj,
) {}
public function __destruct() {
echo "In destructor\n";
$this->obj->setSize($this->desiredSize);
echo "Destroyed, size is now still ", $this->obj->getSize(), "\n";
}
}
class DestructorLogger {
public function __construct(private int $id) {}
public function __destruct() {
echo "Destroyed the logger with id ", $this->id, "\n";
}
}
function test(int $desiredSize) {
$obj = new SplFixedArray(5);
$obj[0] = str_repeat("A", 10);
$obj[1] = new DestructorLogger(1);
$obj[2] = str_repeat('B', 10);
$obj[3] = new InvalidDestructor($desiredSize, $obj);
$obj[4] = new DestructorLogger(4);
$obj->setSize(2);
echo "Size is now ", $obj->getSize(), "\n";
echo "Done\n";
}
echo "--- Smaller size test ---\n";
test(1);
echo "--- Equal size test ---\n";
test(2);
echo "--- Larger size test ---\n";
test(10);
?>
--EXPECT--
--- Smaller size test ---
In destructor
Destroyed, size is now still 2
Destroyed the logger with id 4
Destroyed the logger with id 1
Size is now 1
Done
--- Equal size test ---
In destructor
Destroyed, size is now still 2
Destroyed the logger with id 4
Size is now 2
Done
Destroyed the logger with id 1
--- Larger size test ---
In destructor
Destroyed, size is now still 2
Destroyed the logger with id 4
Size is now 10
Done
Destroyed the logger with id 1