[php-src] PR #24551: var_destroy(): hold serialize_lock around deferred destructor pass

From: 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.

« previous php.git-pulls (#39713) next »