From 143ade75e6872d60b9c1df3e72d357fd1549a57f Mon Sep 17 00:00:00 2001 From: David Carlier Date: Tue, 3 Mar 2026 19:35:54 +0000 Subject: [PATCH] Fix GH-21333: use-after-free when unlinking entries during iteration of a compressed phar. --- ext/phar/phar.c | 2 +- ext/phar/phar_internal.h | 5 +++++ ext/phar/tar.c | 2 +- ext/phar/tests/gh21333.phpt | 38 +++++++++++++++++++++++++++++++++++++ ext/phar/zip.c | 2 +- 5 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 ext/phar/tests/gh21333.phpt diff --git a/ext/phar/phar.c b/ext/phar/phar.c index 0d1f9bddce4e1..b707d425f72f8 100644 --- a/ext/phar/phar.c +++ b/ext/phar/phar.c @@ -2461,7 +2461,7 @@ static int phar_flush_clean_deleted_apply(zval *zv) /* {{{ */ { phar_entry_info *entry = (phar_entry_info *)Z_PTR_P(zv); - if (entry->fp_refcount <= 0 && entry->is_deleted) { + if (entry->is_deleted && phar_entry_can_remove(entry)) { return ZEND_HASH_APPLY_REMOVE; } else { return ZEND_HASH_APPLY_KEEP; diff --git a/ext/phar/phar_internal.h b/ext/phar/phar_internal.h index e707fb4ca4669..7b2b25573d4ad 100644 --- a/ext/phar/phar_internal.h +++ b/ext/phar/phar_internal.h @@ -400,6 +400,11 @@ static inline void phar_set_inode(phar_entry_info *entry) /* {{{ */ } /* }}} */ +static inline bool phar_entry_can_remove(phar_entry_info *entry) +{ + return entry->fp_refcount == 0 && entry->fileinfo_lock_count == 0; +} + void phar_request_initialize(void); void phar_object_init(void); diff --git a/ext/phar/tar.c b/ext/phar/tar.c index c69c16ccd67ae..743349d397a39 100644 --- a/ext/phar/tar.c +++ b/ext/phar/tar.c @@ -730,7 +730,7 @@ static int phar_tar_writeheaders_int(phar_entry_info *entry, void *argument) /* } if (entry->is_deleted) { - if (entry->fp_refcount <= 0 && entry->fileinfo_lock_count == 0) { + if (phar_entry_can_remove(entry)) { return ZEND_HASH_APPLY_REMOVE; } else { /* we can't delete this in-memory until it is closed */ diff --git a/ext/phar/tests/gh21333.phpt b/ext/phar/tests/gh21333.phpt new file mode 100644 index 0000000000000..334986619c328 --- /dev/null +++ b/ext/phar/tests/gh21333.phpt @@ -0,0 +1,38 @@ +--TEST-- +GH-21333 (UAF when unlinking entries during iteration of a compressed phar) +--CREDITS-- +YuanchengJiang +--EXTENSIONS-- +phar +zlib +--INI-- +phar.readonly=0 +--FILE-- +addFromString("file", "initial_content"); +$phar->addEmptyDir("dir"); + +$phar2 = $phar->compress(Phar::GZ); + +$tmp_src = __DIR__ . "/gh21333.tmp"; +file_put_contents($tmp_src, str_repeat("A", 100)); + +foreach ($phar2 as $item) { + @copy($tmp_src, $item); + @unlink($item); +} + +$garbage = get_defined_vars(); + +echo "Done\n"; +?> +--CLEAN-- + +--EXPECT-- +Done diff --git a/ext/phar/zip.c b/ext/phar/zip.c index 9439fe8b94d8a..838475841e125 100644 --- a/ext/phar/zip.c +++ b/ext/phar/zip.c @@ -865,7 +865,7 @@ static int phar_zip_changed_apply_int(phar_entry_info *entry, void *arg) /* {{{ } if (entry->is_deleted) { - if (entry->fp_refcount <= 0 && entry->fileinfo_lock_count == 0) { + if (phar_entry_can_remove(entry)) { return ZEND_HASH_APPLY_REMOVE; } else { /* we can't delete this in-memory until it is closed */