Skip to content

Commit 286faa0

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 e11ae25 commit 286faa0

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
@@ -75,6 +75,8 @@ PHP NEWS
7575
__serialize() removes an element). (David Carlier)
7676
. Fixed SplFixedArray::setSize() doing nothing on subclasses whose
7777
constructor does not call parent::__construct(). (Marc Bennewitz)
78+
. Fixed memory leak when __construct(), __wakeup() or __unserialize() is
79+
called from an element destructor during setSize(0). (Marc Bennewitz)
7880

7981
- SQLite:
8082
. Fixed a crash when SQLite3::close() is called from a userland callback.

ext/spl/spl_fixedarray.c

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,14 @@ static bool spl_fixedarray_empty(spl_fixedarray *array)
7171
return true;
7272
}
7373

74+
/* True while spl_fixedarray_resize() runs. A clear empties the array before
75+
* destroying its elements, so emptiness alone cannot tell "never constructed"
76+
* from "clear in progress"; re-initialising in that window leaks. */
77+
static bool spl_fixedarray_resize_in_progress(const spl_fixedarray *array)
78+
{
79+
return array->cached_resize >= 0;
80+
}
81+
7482
static void spl_fixedarray_default_ctor(spl_fixedarray *array)
7583
{
7684
array->size = 0;
@@ -182,9 +190,9 @@ static void spl_fixedarray_resize(spl_fixedarray *array, zend_long size)
182190

183191
/* clearing the array */
184192
if (size == 0) {
193+
/* Clears elements and size; resetting them afterwards would leak
194+
* anything a destructor re-installed. */
185195
spl_fixedarray_dtor(array);
186-
array->elements = NULL;
187-
array->size = 0;
188196
} else if (size > array->size) {
189197
array->elements = safe_erealloc(array->elements, size, sizeof(zval), 0);
190198
spl_fixedarray_init_elems(array, array->size, size);
@@ -195,8 +203,12 @@ static void spl_fixedarray_resize(spl_fixedarray *array, zend_long size)
195203
array->elements = erealloc(array->elements, sizeof(zval) * size);
196204
}
197205

198-
/* If resized within the destructor, take the last resize command and perform it */
206+
/* If resized within the destructor, take the last resize command and
207+
* perform it. The sentinel is still set: re-initialising during a
208+
* resize is refused. */
199209
zend_long cached_resize = array->cached_resize;
210+
ZEND_ASSERT(cached_resize >= 0);
211+
200212
array->cached_resize = -1;
201213
if (cached_resize != size) {
202214
spl_fixedarray_resize(array, cached_resize);
@@ -545,7 +557,7 @@ PHP_METHOD(SplFixedArray, __construct)
545557

546558
intern = Z_SPLFIXEDARRAY_P(object);
547559

548-
if (!spl_fixedarray_empty(&intern->array)) {
560+
if (UNEXPECTED(!spl_fixedarray_empty(&intern->array) || spl_fixedarray_resize_in_progress(&intern->array))) {
549561
/* called __construct() twice, bail out */
550562
return;
551563
}
@@ -561,7 +573,7 @@ PHP_METHOD(SplFixedArray, __wakeup)
561573

562574
ZEND_PARSE_PARAMETERS_NONE();
563575

564-
if (intern->array.size == 0) {
576+
if (EXPECTED(intern->array.size == 0 && !spl_fixedarray_resize_in_progress(&intern->array))) {
565577
int index = 0;
566578
int size = zend_hash_num_elements(intern_ht);
567579

@@ -621,7 +633,7 @@ PHP_METHOD(SplFixedArray, __unserialize)
621633
RETURN_THROWS();
622634
}
623635

624-
if (intern->array.size == 0) {
636+
if (EXPECTED(intern->array.size == 0 && !spl_fixedarray_resize_in_progress(&intern->array))) {
625637
size = zend_hash_num_elements(data);
626638
spl_fixedarray_init_non_empty_struct(&intern->array, size);
627639
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)