Skip to content

Commit 5f80ef5

Browse files
committed
Fix SplFixedArray leak when re-initialised from a destructor
setSize(0) clears the array before destroying its elements, so it looks unconstructed to userland. __construct(), __wakeup() and __unserialize() then re-initialised it, and the in-progress clear discarded the buffer they had installed. cb3dc62 fixed the same leak for a re-entrant setSize() by testing the resize sentinel first; apply that test to the other three entry points.
1 parent 7831180 commit 5f80ef5

3 files changed

Lines changed: 88 additions & 6 deletions

File tree

‎NEWS‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ PHP NEWS
2828
- SPL:
2929
. Fixed SplFixedArray::setSize() doing nothing on subclasses whose
3030
constructor does not call parent::__construct(). (Marc Bennewitz)
31+
. Fixed memory leak when __construct(), __wakeup() or __unserialize() is
32+
called from an element destructor during setSize(0). (Marc Bennewitz)
3133

3234
- Zip:
3335
. Fixed ZipArchive::extractTo() ignoring files given in a non-list array.

‎ext/spl/spl_fixedarray.c‎

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,14 @@ static bool spl_fixedarray_empty(spl_fixedarray *array)
7777
return true;
7878
}
7979

80+
/* True while spl_fixedarray_resize() runs. A clear empties the array before
81+
* destroying its elements, so emptiness alone cannot tell "never constructed"
82+
* from "clear in progress"; re-initialising in that window leaks. */
83+
static bool spl_fixedarray_resize_in_progress(const spl_fixedarray *array)
84+
{
85+
return array->cached_resize >= 0;
86+
}
87+
8088
static void spl_fixedarray_default_ctor(spl_fixedarray *array)
8189
{
8290
array->size = 0;
@@ -188,9 +196,9 @@ static void spl_fixedarray_resize(spl_fixedarray *array, zend_long size)
188196

189197
/* clearing the array */
190198
if (size == 0) {
199+
/* Clears elements and size; resetting them afterwards would leak
200+
* anything a destructor re-installed. */
191201
spl_fixedarray_dtor(array);
192-
array->elements = NULL;
193-
array->size = 0;
194202
} else if (size > array->size) {
195203
array->elements = safe_erealloc(array->elements, size, sizeof(zval), 0);
196204
spl_fixedarray_init_elems(array, array->size, size);
@@ -201,8 +209,12 @@ static void spl_fixedarray_resize(spl_fixedarray *array, zend_long size)
201209
array->elements = erealloc(array->elements, sizeof(zval) * size);
202210
}
203211

204-
/* If resized within the destructor, take the last resize command and perform it */
212+
/* If resized within the destructor, take the last resize command and
213+
* perform it. The sentinel is still set: re-initialising during a
214+
* resize is refused. */
205215
zend_long cached_resize = array->cached_resize;
216+
ZEND_ASSERT(cached_resize >= 0);
217+
206218
array->cached_resize = -1;
207219
if (cached_resize != size) {
208220
spl_fixedarray_resize(array, cached_resize);
@@ -557,7 +569,7 @@ PHP_METHOD(SplFixedArray, __construct)
557569

558570
intern = Z_SPLFIXEDARRAY_P(object);
559571

560-
if (!spl_fixedarray_empty(&intern->array)) {
572+
if (UNEXPECTED(!spl_fixedarray_empty(&intern->array) || spl_fixedarray_resize_in_progress(&intern->array))) {
561573
/* called __construct() twice, bail out */
562574
return;
563575
}
@@ -575,7 +587,7 @@ PHP_METHOD(SplFixedArray, __wakeup)
575587
RETURN_THROWS();
576588
}
577589

578-
if (intern->array.size == 0) {
590+
if (EXPECTED(intern->array.size == 0 && !spl_fixedarray_resize_in_progress(&intern->array))) {
579591
int index = 0;
580592
int size = zend_hash_num_elements(intern_ht);
581593

@@ -637,7 +649,7 @@ PHP_METHOD(SplFixedArray, __unserialize)
637649
RETURN_THROWS();
638650
}
639651

640-
if (intern->array.size == 0) {
652+
if (EXPECTED(intern->array.size == 0 && !spl_fixedarray_resize_in_progress(&intern->array))) {
641653
size = zend_hash_num_elements(data);
642654
spl_fixedarray_init_non_empty_struct(&intern->array, size);
643655
if (!size) {
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
--TEST--
2+
SplFixedArray::setSize: re-initialising from a destructor during clear (GH-23811)
3+
--DESCRIPTION--
4+
setSize(0) clears elements and size before running the element destructors, so
5+
the array momentarily looks like it was never constructed. __construct(),
6+
__wakeup() and __unserialize() must not re-initialise it in that window: the
7+
in-progress clear would discard whatever they installed, leaking it.
8+
--FILE--
9+
<?php
10+
class Reentrant {
11+
public static $arr = null;
12+
public static $action = null;
13+
public function __destruct() {
14+
if (self::$action === null) {
15+
return;
16+
}
17+
$fn = self::$action;
18+
self::$action = null;
19+
$fn(self::$arr);
20+
}
21+
}
22+
23+
function clear_with(callable $action): void {
24+
$arr = new SplFixedArray(2);
25+
$arr[0] = new Reentrant();
26+
$arr[1] = "tail";
27+
Reentrant::$arr = $arr;
28+
Reentrant::$action = $action;
29+
30+
$arr->setSize(0);
31+
echo "size: ", $arr->getSize(), "\n";
32+
33+
/* The array must still be usable. */
34+
$arr->setSize(1);
35+
$arr[0] = "ok";
36+
var_dump($arr[0]);
37+
38+
Reentrant::$arr = null;
39+
Reentrant::$action = null;
40+
}
41+
42+
echo "-- __construct() --\n";
43+
clear_with(function ($arr) { $arr->__construct(5); });
44+
45+
/* __construct() is ignored, but the following setSize() is still recorded as
46+
* the pending resize and applied once the clear finishes. */
47+
echo "-- __construct() then setSize() --\n";
48+
clear_with(function ($arr) { $arr->__construct(7); $arr->setSize(3); });
49+
50+
echo "-- __unserialize() --\n";
51+
clear_with(function ($arr) { $arr->__unserialize(["a", "b", "c"]); });
52+
53+
echo "-- __wakeup() --\n";
54+
clear_with(function ($arr) { @$arr->__wakeup(); });
55+
?>
56+
--EXPECT--
57+
-- __construct() --
58+
size: 0
59+
string(2) "ok"
60+
-- __construct() then setSize() --
61+
size: 3
62+
string(2) "ok"
63+
-- __unserialize() --
64+
size: 0
65+
string(2) "ok"
66+
-- __wakeup() --
67+
size: 0
68+
string(2) "ok"

0 commit comments

Comments
 (0)