Skip to content

Commit b71c6b2

Browse files
committed
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.
1 parent 2012fd3 commit b71c6b2

File tree

4 files changed

+123
-1
lines changed

4 files changed

+123
-1
lines changed

NEWS

+4
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ PHP NEWS
5555
. Revert behaviour of receiving SIGCHLD signals back to the behaviour
5656
before 8.1.22. (nielsdos)
5757

58+
- SPL:
59+
. Fixed bug #81992 (SplFixedArray::setSize() causes use-after-free).
60+
(nielsdos)
61+
5862
- Standard:
5963
. Prevent int overflow on $decimals in number_format. (Marc Bennewitz)
6064
. Fixed bug GH-11870 (Fix off-by-one bug when truncating tempnam prefix)

ext/spl/spl_fixedarray.c

+21-1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ typedef struct _spl_fixedarray {
4646
zval *elements;
4747
/* True if this was modified after the last call to get_properties or the hash table wasn't rebuilt. */
4848
bool should_rebuild_properties;
49+
/* If positive, it's a resize within a resize and the value gives the desired size. If -1, it's not. */
50+
zend_long cached_resize;
4951
} spl_fixedarray;
5052

5153
typedef struct _spl_fixedarray_methods {
@@ -117,6 +119,7 @@ static void spl_fixedarray_init(spl_fixedarray *array, zend_long size)
117119
} else {
118120
spl_fixedarray_default_ctor(array);
119121
}
122+
array->cached_resize = -1;
120123
}
121124

122125
/* 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)
148151
*/
149152
static void spl_fixedarray_dtor_range(spl_fixedarray *array, zend_long from, zend_long to)
150153
{
154+
array->size = from;
151155
zval *begin = array->elements + from, *end = array->elements + to;
152156
while (begin != end) {
153157
zval_ptr_dtor(begin++);
@@ -184,19 +188,35 @@ static void spl_fixedarray_resize(spl_fixedarray *array, zend_long size)
184188
return;
185189
}
186190

191+
if (UNEXPECTED(array->cached_resize >= 0)) {
192+
/* We're already resizing, so just remember the desired size.
193+
* The resize will happen later. */
194+
array->cached_resize = size;
195+
return;
196+
}
197+
array->cached_resize = size;
198+
187199
/* clearing the array */
188200
if (size == 0) {
189201
spl_fixedarray_dtor(array);
190202
array->elements = NULL;
203+
array->size = 0;
191204
} else if (size > array->size) {
192205
array->elements = safe_erealloc(array->elements, size, sizeof(zval), 0);
193206
spl_fixedarray_init_elems(array, array->size, size);
207+
array->size = size;
194208
} else { /* size < array->size */
209+
/* Size set in spl_fixedarray_dtor_range() */
195210
spl_fixedarray_dtor_range(array, size, array->size);
196211
array->elements = erealloc(array->elements, sizeof(zval) * size);
197212
}
198213

199-
array->size = size;
214+
/* If resized within the destructor, take the last resize command and perform it */
215+
zend_long cached_resize = array->cached_resize;
216+
array->cached_resize = -1;
217+
if (cached_resize != size) {
218+
spl_fixedarray_resize(array, cached_resize);
219+
}
200220
}
201221

202222
static HashTable* spl_fixedarray_object_get_gc(zend_object *obj, zval **table, int *n)

ext/spl/tests/bug81992.phpt

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
--TEST--
2+
Bug #81992 (SplFixedArray::setSize() causes use-after-free)
3+
--FILE--
4+
<?php
5+
class InvalidDestructor {
6+
public function __destruct() {
7+
global $obj;
8+
var_dump($obj[0]);
9+
try {
10+
var_dump($obj[2]);
11+
} catch (Throwable $e) {
12+
echo $e->getMessage(), "\n";
13+
}
14+
try {
15+
var_dump($obj[4]);
16+
} catch (Throwable $e) {
17+
echo $e->getMessage(), "\n";
18+
}
19+
}
20+
}
21+
22+
$obj = new SplFixedArray(5);
23+
$obj[0] = str_repeat("A", 10);
24+
$obj[2] = str_repeat('B', 10);
25+
$obj[3] = new InvalidDestructor();
26+
$obj[4] = str_repeat('C', 10);
27+
$obj->setSize(2);
28+
?>
29+
--EXPECT--
30+
string(10) "AAAAAAAAAA"
31+
Index invalid or out of range
32+
Index invalid or out of range

ext/spl/tests/bug81992b.phpt

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
--TEST--
2+
Bug #81992 (SplFixedArray::setSize() causes use-after-free) - setSize variation
3+
--FILE--
4+
<?php
5+
class InvalidDestructor {
6+
public function __construct(
7+
private int $desiredSize,
8+
private SplFixedArray $obj,
9+
) {}
10+
11+
public function __destruct() {
12+
echo "In destructor\n";
13+
$this->obj->setSize($this->desiredSize);
14+
echo "Destroyed, size is now still ", $this->obj->getSize(), "\n";
15+
}
16+
}
17+
18+
class DestructorLogger {
19+
public function __construct(private int $id) {}
20+
21+
public function __destruct() {
22+
echo "Destroyed the logger with id ", $this->id, "\n";
23+
}
24+
}
25+
26+
function test(int $desiredSize) {
27+
$obj = new SplFixedArray(5);
28+
$obj[0] = str_repeat("A", 10);
29+
$obj[1] = new DestructorLogger(1);
30+
$obj[2] = str_repeat('B', 10);
31+
$obj[3] = new InvalidDestructor($desiredSize, $obj);
32+
$obj[4] = new DestructorLogger(4);
33+
$obj->setSize(2);
34+
echo "Size is now ", $obj->getSize(), "\n";
35+
echo "Done\n";
36+
}
37+
38+
echo "--- Smaller size test ---\n";
39+
test(1);
40+
echo "--- Equal size test ---\n";
41+
test(2);
42+
echo "--- Larger size test ---\n";
43+
test(10);
44+
?>
45+
--EXPECT--
46+
--- Smaller size test ---
47+
In destructor
48+
Destroyed, size is now still 2
49+
Destroyed the logger with id 4
50+
Destroyed the logger with id 1
51+
Size is now 1
52+
Done
53+
--- Equal size test ---
54+
In destructor
55+
Destroyed, size is now still 2
56+
Destroyed the logger with id 4
57+
Size is now 2
58+
Done
59+
Destroyed the logger with id 1
60+
--- Larger size test ---
61+
In destructor
62+
Destroyed, size is now still 2
63+
Destroyed the logger with id 4
64+
Size is now 10
65+
Done
66+
Destroyed the logger with id 1

0 commit comments

Comments
 (0)