- Replace bailout on OOM with UnwindExit
- Properly reset various globals that may be accessed during startup
Fix GHSA-w8hw-w9fx-j79w (CVE-2026-109618)
Fix GHSA-6wmf-4rv5-p3rp (CVE-2026-109619)
Co-authored-by: Ilija Tovilo <ilija.tovilo@me.com>
Co-authored-by: Jakub Zelenka <bukka@php.net>
Co-authored-by: Nora Dossche <7771979+ndossche@users.noreply.github.com>
Co-authored-by: Nicolas Grekas <nicolas.grekas@gmail.com>
zend_object_alloc() zeroes the object, so cached_resize started at 0, which
spl_fixedarray_resize() reads as "resize in progress" and returns early.
setSize() therefore did nothing on subclasses whose constructor does not
call parent::__construct(). Initialise the struct on object creation.
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. cb3dc62fd9 fixed the same leak for a re-entrant
setSize() by testing the resize sentinel first; apply that test to the
other three entry points.
Close GH-23812
Headers required by the generated code (zend_attributes.h, zend_constants.h, zend_enum.h) are collected during generation and emitted as includes at the top of the arginfo file, with the same preprocessor conditions and PHP version guards as the code that needs them.
Removes the manual zend_attributes.h and zend_enum.h includes from sources that only needed them for the arginfo file they include.
* ext/*: Make `X_from_obj(zend_object *obj)` functions macros
With the introduction of the `ZEND_CONTAINER_OF()` macro we can now preserve
whether or not the `zend_object *` is `const` when extracting the
class-specific struct. This change will allow to use `const zend_object *` in
more locations.
Changes performed with Coccinelle followed by some manual cleanup for comment
and whitespace formatting:
@a@
attribute name zend_always_inline;
type T;
identifier obj;
identifier f;
typedef zend_object;
@@
T* f(zend_object *obj) { return ZEND_CONTAINER_OF(obj, T, std); }
+ #define f(obj) ZEND_CONTAINER_OF(obj, T, std)
@@
identifier a.f;
@@
- f(...) { ... }
@b@
attribute name zend_always_inline;
type T;
identifier obj;
identifier f;
typedef zend_object;
@@
T* f(zend_object *obj) { return ZEND_CONTAINER_OF(obj, T, zo); }
+ #define f(obj) ZEND_CONTAINER_OF(obj, T, zo)
@@
identifier b.f;
@@
- f(...) { ... }
* sockets: Fix build
* odbc: Fix build
* zend_portability: Add `ZEND_CONTAINER_OF()`
* tree-wide: Use `ZEND_CONTAINER_OF()`
Changes made with Coccinelle:
@@
type T_container;
identifier member;
expression e;
@@
- (T_container *)(((char *)(e)) - offsetof(T_container, member))
+ ZEND_CONTAINER_OF(e, T_container, member)
@@
type T_container;
identifier member;
expression e;
typedef uintptr_t;
@@
- (T_container *)(((uintptr_t)(e)) - offsetof(T_container, member))
+ ZEND_CONTAINER_OF(e, T_container, member)
@@
type T_container;
identifier member;
expression e;
@@
- (const T_container *)(((char *)(e)) - offsetof(T_container, member))
+ ZEND_CONTAINER_OF(e, T_container, member)
@@
type T_container;
identifier member;
expression e;
typedef uintptr_t;
@@
- (const T_container *)(((uintptr_t)(e)) - offsetof(T_container, member))
+ ZEND_CONTAINER_OF(e, T_container, member)
* bcmath: Fix `get_bcmath_number_from_obj()` const-correctness
A follow-up change to the new `ZEND_CONTAINER_OF()` macro will preserve the
`const`-ness of the input pointer. By wrapping this macro inside a macro rather
an an inline function we can preserve the `const`-ness across all layers.
* zend_portability: Keep `const`-ness in `ZEND_CONTAINER_OF()`
* zend_portability: Add C++ version to `ZEND_CONTAINER_OF()`
All other code caters to dereferencing array elements, except the
unserialize handler. This causes references to be present in the fixed
array even though this seems not intentional as reference assign is
otherwise impossible.
On 8.5+ this causes an assertion failure. On 8.3+ this causes references
to be present where they shouldn't be.
Closes GH-20616.
If the array is packed, then we don't have to loop to get the highest
index.
For this script:
```php
$array = range(1, 100);
for ($i=0;$i<1000000;$i++) {
SplFixedArray::fromArray($array);
}
```
On an i7-4790:
```
Benchmark 1: ./sapi/cli/php spl.php
Time (mean ± σ): 376.5 ms ± 2.0 ms [User: 372.1 ms, System: 2.6 ms]
Range (min … max): 373.7 ms … 379.5 ms 10 runs
Benchmark 2: ./sapi/cli/php_old spl.php
Time (mean ± σ): 511.6 ms ± 1.9 ms [User: 508.0 ms, System: 2.3 ms]
Range (min … max): 509.2 ms … 515.1 ms 10 runs
Summary
./sapi/cli/php spl.php ran
1.36 ± 0.01 times faster than ./sapi/cli/php_old spl.php
```
On an i7-1185G7:
```
Benchmark 1: ./sapi/cli/php spl.php
Time (mean ± σ): 250.4 ms ± 3.5 ms [User: 246.6 ms, System: 2.6 ms]
Range (min … max): 247.0 ms … 258.5 ms 11 runs
Benchmark 2: ./sapi/cli/php_old spl.php
Time (mean ± σ): 328.4 ms ± 1.0 ms [User: 324.4 ms, System: 3.8 ms]
Range (min … max): 327.5 ms … 331.0 ms 10 runs
Summary
./sapi/cli/php spl.php ran
1.31 ± 0.02 times faster than ./sapi/cli/php_old spl.php
```
Bonus: this also decreases the code size of the function.
If there is not yet a dynamic property, and there are no class properties,
then we know that we don't have to build a properties table.
For this (micro-bench) script:
```php
function x() {
$fa = new SplFixedArray(1);
$fa[0] = $fa;
}
for ($i=0;$i<1000000;$i++)
x();
```
On an i7-4790:
```
Benchmark 1: ./sapi/cli/php spl.php
Time (mean ± σ): 140.9 ms ± 1.2 ms [User: 137.5 ms, System: 2.7 ms]
Range (min … max): 138.9 ms … 144.9 ms 21 runs
Benchmark 2: ./sapi/cli/php_old spl.php
Time (mean ± σ): 162.0 ms ± 3.8 ms [User: 157.7 ms, System: 3.2 ms]
Range (min … max): 158.5 ms … 175.0 ms 17 runs
Summary
./sapi/cli/php spl.php ran
1.15 ± 0.03 times faster than ./sapi/cli/php_old spl.php
```
We can use the optimized packed filling code instead of going through
all the logic of the zend_hash update APIs.
For this script:
```php
$test = new SplFixedArray(4);
$test[0] = 0;
$test[1] = 1;
$test[2] = 2;
$test[3] = 3;
for ($i = 0 ; $i< 5000000; $i++)
$test->toArray();
```
On an i7-4790:
```
Benchmark 1: ./sapi/cli/php toarray.php
Time (mean ± σ): 170.0 ms ± 1.8 ms [User: 167.3 ms, System: 2.2 ms]
Range (min … max): 166.9 ms … 173.0 ms 17 runs
Benchmark 2: ./sapi/cli/php_old toarray.php
Time (mean ± σ): 215.7 ms ± 3.6 ms [User: 211.9 ms, System: 3.0 ms]
Range (min … max): 211.3 ms … 222.0 ms 13 runs
Summary
./sapi/cli/php toarray.php ran
1.27 ± 0.02 times faster than ./sapi/cli/php_old toarray.php
```
On an i7-1185G7:
```
Benchmark 1: ./sapi/cli/php toarray.php
Time (mean ± σ): 112.6 ms ± 1.4 ms [User: 109.6 ms, System: 2.9 ms]
Range (min … max): 111.1 ms … 116.4 ms 25 runs
Benchmark 2: ./sapi/cli/php_old toarray.php
Time (mean ± σ): 145.3 ms ± 2.8 ms [User: 141.8 ms, System: 3.4 ms]
Range (min … max): 142.6 ms … 151.8 ms 20 runs
Summary
./sapi/cli/php toarray.php ran
1.29 ± 0.03 times faster than ./sapi/cli/php_old toarray.php
```
This patch optimizes reading and writing from SplFixedArray with the
dimension operators. It accomplishes this due to the following
optimizations:
* Fast-path for long keys (inlined).
* Optimization hints (UNEXPECTED + assertion)
* Using an unsigned index so we can do a single length comparison
For the following script:
```php
$test = new SplFixedArray(4);
for ($i = 0 ; $i< 5000000; $i++)
$test[1] += $i;
```
On an i7-4790:
```
Benchmark 1: ./sapi/cli/php x.php
Time (mean ± σ): 95.4 ms ± 1.6 ms [User: 91.5 ms, System: 3.2 ms]
Range (min … max): 93.7 ms … 100.8 ms 31 runs
Benchmark 2: ./sapi/cli/php_old x.php
Time (mean ± σ): 119.1 ms ± 1.3 ms [User: 114.7 ms, System: 3.6 ms]
Range (min … max): 117.6 ms … 123.1 ms 24 runs
Summary
./sapi/cli/php x.php ran
1.25 ± 0.03 times faster than ./sapi/cli/php_old x.php
```
On an i7-1185G7:
```
Benchmark 1: ./sapi/cli/php x.php
Time (mean ± σ): 67.9 ms ± 1.1 ms [User: 64.8 ms, System: 3.2 ms]
Range (min … max): 66.6 ms … 72.8 ms 43 runs
Benchmark 2: ./sapi/cli/php_old x.php
Time (mean ± σ): 84.8 ms ± 1.1 ms [User: 81.0 ms, System: 3.9 ms]
Range (min … max): 82.6 ms … 88.0 ms 34 runs
Summary
./sapi/cli/php x.php ran
1.25 ± 0.03 times faster than ./sapi/cli/php_old x.php
```
Because the properties table contains both a numeric index and a string
index that map to 0 in a symbol table, this causes an assertion failure.
Looking at the manual page of get_object_vars(), it seems that only real
properties must be included. Given that SplFixedArray's elements are not
accessible like properties, they should be excluded. This restores PHP
8.3 behaviour. The reason that this didn't cause problems on 8.3 is
because it used a different handler (get_properties).
Closes GH-17206.
* Include from build dir first
This fixes out of tree builds by ensuring that configure artifacts are included
from the build dir.
Before, out of tree builds would preferably include files from the src dir, as
the include path was defined as follows (ignoring includes from ext/ and sapi/) :
-I$(top_builddir)/main
-I$(top_srcdir)
-I$(top_builddir)/TSRM
-I$(top_builddir)/Zend
-I$(top_srcdir)/main
-I$(top_srcdir)/Zend
-I$(top_srcdir)/TSRM
-I$(top_builddir)/
As a result, an out of tree build would include configure artifacts such as
`main/php_config.h` from the src dir.
After this change, the include path is defined as follows:
-I$(top_builddir)/main
-I$(top_builddir)
-I$(top_srcdir)/main
-I$(top_srcdir)
-I$(top_builddir)/TSRM
-I$(top_builddir)/Zend
-I$(top_srcdir)/Zend
-I$(top_srcdir)/TSRM
* Fix extension include path for out of tree builds
* Include config.h with the brackets form
`#include "config.h"` searches in the directory containing the including-file
before any other include path. This can include the wrong config.h when building
out of tree and a config.h exists in the source tree.
Using `#include <config.h>` uses exclusively the include path, and gives
priority to the build dir.
When unserializing, the cached_resize field was not reset to -1
correctly, causing the setSize() method to think we were inside of a
resize operation.
Closes GH-13543.
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.