From 4b0ee2c5c151b183da1c5fd7d104ae46de93d551 Mon Sep 17 00:00:00 2001 From: omaraltamimi1 <225493951+omaraltamimi1@users.noreply.github.com> Date: Thu, 22 Oct 2026 15:37:38 +0000 Subject: [PATCH] Fix GH-24481: var_destroy(): hold serialize_lock around deferred destructor pass 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() then takes the else branch of php_var_unserialize_init() and receives the very context whose chunks were just freed; the nested var_push() then reads used_slots from freed memory and writes zval pointers into the freed chunk. Hold BG(serialize_lock) around i_zval_ptr_dtor() in var_destroy(), so a re-entering unserialize() cannot reuse the chain being destroyed. --- NEWS | 2 ++ ext/standard/tests/serialize/gh24481.phpt | 35 +++++++++++++++++++++++ ext/standard/var_unserializer.re | 4 +++ 3 files changed, 41 insertions(+) create mode 100644 ext/standard/tests/serialize/gh24481.phpt diff --git a/NEWS b/NEWS index 8bd02dc6d6ac..0a4c2bee6009 100644 --- a/NEWS +++ b/NEWS @@ -137,6 +137,8 @@ PHP NEWS (Nicolas Grekas) . Fixed bug GH-24373 (serialize() called from a destructor during var_hash destruction reuses the outer var_hash). (David Carlier) + . Fixed bug GH-24481 (use-after-free in var_destroy() during unserialize() + re-entrancy). (omaraltamimi1) - Streams: . Fixed bug GH-24508 (HTTP wrapper leaks the dechunk filter when header diff --git a/ext/standard/tests/serialize/gh24481.phpt b/ext/standard/tests/serialize/gh24481.phpt new file mode 100644 index 000000000000..1e7dddc93824 --- /dev/null +++ b/ext/standard/tests/serialize/gh24481.phpt @@ -0,0 +1,35 @@ +--TEST-- +GH-24481 (use-after-free in var_destroy() during unserialize() re-entrancy) +--FILE-- + +--EXPECT-- +int(1020) +int(1017) diff --git a/ext/standard/var_unserializer.re b/ext/standard/var_unserializer.re index e5cc1acd758c..7b164bb6ed29 100644 --- a/ext/standard/var_unserializer.re +++ b/ext/standard/var_unserializer.re @@ -316,7 +316,11 @@ PHPAPI void var_destroy(php_unserialize_data_t *var_hashx) } } + /* Do not let a destructor re-enter the unserializer: it would reuse the + * var_hash (and its var_entries chunks) that we are destroying here. */ + BG(serialize_lock)++; i_zval_ptr_dtor(zv); + BG(serialize_lock)--; } next = var_dtor_hash->next; efree_size(var_dtor_hash, sizeof(var_dtor_entries));