From 02471831f6ab46fc3888823c2fd8372d970cd0e6 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Sat, 11 Jul 2026 06:37:14 -0400 Subject: [PATCH] Fix use-after-free in extract() EXTR_REFS collision modes The GH-18209 fix guarded only php_extract_ref_overwrite. The other five per-collision-mode helpers (if_exists, prefix_if_exists, prefix_same, prefix_all, prefix_invalid), split out when extract() was refactored, kept the pre-fix order: they dropped the old value of orig_var before writing the new reference. A destructor run by that drop can unset the symbol being written, so the following ZVAL_REF wrote into freed memory. Apply the same swap-then-dtor already used by php_extract_ref_overwrite: copy the old value aside, write the reference, then drop the copy. Fixes GH-18209 --- ext/standard/array.c | 20 +++++++++--- .../array/extract_refs_reentrant_dtor.phpt | 32 +++++++++++++++++++ 2 files changed, 47 insertions(+), 5 deletions(-) create mode 100644 ext/standard/tests/array/extract_refs_reentrant_dtor.phpt diff --git a/ext/standard/array.c b/ext/standard/array.c index a233e6c4dcb5..b665157c0ec9 100644 --- a/ext/standard/array.c +++ b/ext/standard/array.c @@ -1764,8 +1764,10 @@ static zend_long php_extract_ref_if_exists(const zend_array *arr, const zend_arr } else { ZVAL_MAKE_REF_EX(entry, 2); } - zval_ptr_dtor(orig_var); + zval garbage; + ZVAL_COPY_VALUE(&garbage, orig_var); ZVAL_REF(orig_var, Z_REF_P(entry)); + zval_ptr_dtor(&garbage); count++; } } ZEND_HASH_FOREACH_END(); @@ -1957,8 +1959,10 @@ static zend_long php_extract_ref_prefix_if_exists(const zend_array *arr, zend_ar if (Z_TYPE_P(orig_var) == IS_INDIRECT) { orig_var = Z_INDIRECT_P(orig_var); } - zval_ptr_dtor(orig_var); + zval garbage; + ZVAL_COPY_VALUE(&garbage, orig_var); ZVAL_REF(orig_var, Z_REF_P(entry)); + zval_ptr_dtor(&garbage); } else { zend_hash_add_new(symbol_table, final_name, entry); } @@ -2070,8 +2074,10 @@ prefix:; if (Z_TYPE_P(orig_var) == IS_INDIRECT) { orig_var = Z_INDIRECT_P(orig_var); } - zval_ptr_dtor(orig_var); + zval garbage; + ZVAL_COPY_VALUE(&garbage, orig_var); ZVAL_REF(orig_var, Z_REF_P(entry)); + zval_ptr_dtor(&garbage); } else { zend_hash_add_new(symbol_table, final_name, entry); } @@ -2198,8 +2204,10 @@ static zend_long php_extract_ref_prefix_all(const zend_array *arr, zend_array *s if (Z_TYPE_P(orig_var) == IS_INDIRECT) { orig_var = Z_INDIRECT_P(orig_var); } - zval_ptr_dtor(orig_var); + zval garbage; + ZVAL_COPY_VALUE(&garbage, orig_var); ZVAL_REF(orig_var, Z_REF_P(entry)); + zval_ptr_dtor(&garbage); } else { zend_hash_add_new(symbol_table, final_name, entry); } @@ -2300,8 +2308,10 @@ static zend_long php_extract_ref_prefix_invalid(const zend_array *arr, zend_arra if (Z_TYPE_P(orig_var) == IS_INDIRECT) { orig_var = Z_INDIRECT_P(orig_var); } - zval_ptr_dtor(orig_var); + zval garbage; + ZVAL_COPY_VALUE(&garbage, orig_var); ZVAL_REF(orig_var, Z_REF_P(entry)); + zval_ptr_dtor(&garbage); } else { zend_hash_add_new(symbol_table, final_name, entry); } diff --git a/ext/standard/tests/array/extract_refs_reentrant_dtor.phpt b/ext/standard/tests/array/extract_refs_reentrant_dtor.phpt new file mode 100644 index 000000000000..060ad457532d --- /dev/null +++ b/ext/standard/tests/array/extract_refs_reentrant_dtor.phpt @@ -0,0 +1,32 @@ +--TEST-- +extract() EXTR_REFS: destructor freeing the target symbol during the ref write (GH-18209 siblings) +--FILE-- + &$r1], EXTR_REFS | EXTR_IF_EXISTS); +var_dump($b ?? 'b-unset'); + +// EXTR_PREFIX_ALL path: php_extract_ref_prefix_all +class B { + public function __destruct() { + unset($GLOBALS['p_c']); + } +} +$p_c = new B(); +$r2 = 'r2'; +extract(['c' => &$r2], EXTR_REFS | EXTR_PREFIX_ALL, 'p'); +var_dump($p_c ?? 'p_c-unset'); + +echo "done\n"; +?> +--EXPECT-- +string(7) "b-unset" +string(9) "p_c-unset" +done