Skip to content

SplFixedArray: leak on re-initialisation during setSize(0), and setSize() broken on missing parent::__construct() #23811

Description

@marc-mabe

Description

Two defects in SplFixedArray. Both come down to cached_resize being unreliable where it is read, and the fix touches the same function, so I am reporting them together.


1. Memory leak when the array is re-initialised from an element destructor during setSize(0)

spl_fixedarray_dtor() clears elements and size before running the element destructors (from the GH-11959 use-after-free fix), so the array then looks like one that was never constructed. __construct(), __wakeup() and __unserialize() gate on emptiness and re-initialise it, and the in-progress
clear discards the buffer they allocated.

The following code:

<?php
class Reentrant {
    public static ?SplFixedArray $arr = null;
    public function __destruct() {
        if (self::$arr !== null) {
            $arr = self::$arr;
            self::$arr = null;
            $arr->__construct(5);
        }
    }
}

$arr = new SplFixedArray(2);
Reentrant::$arr = $arr;
$arr[0] = new Reentrant();
$arr[1] = "tail";

$arr->setSize(0);
var_dump($arr->getSize());

Resulted in this output:

int(0)
ext/spl/spl_fixedarray.c(95) :  Freeing 0x0000e716d0e84070 (80 bytes)
=== Total 1 memory leaks detected ===

But I expected this output instead:

int(0)

Replacing __construct(5) with __unserialize(["a", "b", "c"]) or __wakeup() leaks the same way. A second site leaks when the destructor follows the re-init with a setSize(): the re-init resets the sentinel, so the setSize() performs a real nested shrink and its erealloc() buffer is discarded too.

This is the same bug as GH-21920, which cb3dc62 fixed for a re-entrant setSize() by testing the resize sentinel first. The other three entry points were not covered.


2. setSize() silently does nothing on a subclass whose constructor does not call parent::__construct()

The following code:

<?php
class S extends SplFixedArray {
    public function __construct() {
    }
}

$s = new S();
var_dump($s->setSize(5));
var_dump($s->getSize());
$s[0] = 'x';

Resulted in this output:

bool(true)
int(0)

Fatal error: Uncaught OutOfBoundsException: Index invalid or out of range in /tmp/rep2.php:10
Stack trace:
#0 {main}
  thrown in /tmp/rep2.php on line 10

But I expected this output instead:

bool(true)
int(5)

zend_object_alloc() zeroes the object, so cached_resize starts at 0 instead of the documented -1. spl_fixedarray_resize() reads >= 0 as "a resize is already in progress" and returns early, so setSize() returns true and does nothing — permanently, for every call on that instance.

This is a regression: cb3dc62 moved the "first initialization" branch below the sentinel check, which exposed the uninitialised sentinel. Before that, this path reached spl_fixedarray_init() and worked.

ext/spl/tests/bug62904.phpt already uses this class shape but only asserts "No crash" after a clone, which is why it was not caught.

PHP Version

PHP 8.4.27-dev (cli) (built from git, NTS DEBUG)
PHP 8.6.0-dev  (cli) (built from git, NTS DEBUG)

Operating System

Linux aarch64 (Ubuntu 24.04)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions