[php-src] PR #24551: var_destroy(): hold serialize_lock around deferred destructor pass
| From: | omaraltamimi1 | Date: | Thu, 22 Oct 2026 15:38:18 +0000 |
| Subject: | [php-src] PR #24551: var_destroy(): hold serialize_lock around deferred destructor pass | ||
| Groups: | php.git-pulls | ||
| Request: | Send a blank email to git-pulls+get-39713@lists.php.net to get a copy of this message | ||
Pull Request: https://github.com/php/php-src/pull/24551
Author: omaraltamimi1
Backport of #24546 to PHP-8.4 (bug fix).
---
Reported in GH-24481 (ASAN traces, gdb capture and PoCs attached there). Master already contains the
php_var_unserialize_init() context-isolation part of the fix; the destructor path was
still open.
## Problem
var_destroy() frees the var_entries chain first, then runs the deferred
__wakeup()/destructor pass **without** holding BG(serialize_lock). A
destructor reached from that pass that calls unserialize() takes the else
branch of php_var_unserialize_init() (lock not held, BG(unserialize).level
still 1) and receives the very context whose chunks were just efree()d. The nested
var_push() then reads used_slots from freed memory and writes zval
pointers into the freed chunk (ASAN: heap-use-after-free read at
var_unserializer.c:906, write at :143); with a groomed chunk the write
index comes from attacker-controlled bytes.
Observable without any crash: a crafted 1017-element payload silently returns
ARRAY(1016) on an unpatched build - element loss caused by the corrupted slot count.
## Fix
Hold BG(serialize_lock) around i_zval_ptr_dtor() in
var_destroy(), so a re-entering unserialize() cannot reuse the chain being
destroyed.
## Validation
php-8.5.12 ASAN build with this change (plus the init isolation master already has): the three PoCs
posted in GH-24481 run clean (exit 0, no ASAN report) and return correct array counts;
the unpatched build reports heap-use-after-free (read + write).
---
The var_destroy() shape is identical on this branch, so the four-line change applies
unchanged, together with the same regression test
(ext/standard/tests/serialize/gh24481.phpt) and the same NEWS entry.
The test was verified on 8.2.35, with and without the change: unpatched, the outer
unserialize() dies with SIGSEGV in the re-entrant var_push() write; patched, it prints
int(1020) / int(1017) and exits 0. The same applies to PHP-8.5, PHP-8.3 and PHP-8.2
(same code shape, same four lines) - say the word and I will open those as well.