From ebb31fd12ed33ef994cce968f943bf400e3ccaef Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 29 Oct 2026 23:25:10 +0000 Subject: [PATCH 1/3] Fix GH-8469: Phar::decompressFiles() keeps compressed contents Phar::decompressFiles() and Phar::compressFiles() only changed the compression flags of the entries. phar_flush() then read the entry contents from the archive as they were stored, so the old compressed bytes were written out as the new contents. Decompress entries whose compression changes before updating the flags, so phar_flush() writes the real contents with the new compression. Co-Authored-By: raphael paez --- ext/phar/phar_object.c | 47 ++++++++++++++++++++++++++++++-- ext/phar/tests/gh8469.phpt | 56 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 3 deletions(-) create mode 100644 ext/phar/tests/gh8469.phpt diff --git a/ext/phar/phar_object.c b/ext/phar/phar_object.c index a1736f221e27..f6d0479ec5a7 100644 --- a/ext/phar/phar_object.c +++ b/ext/phar/phar_object.c @@ -3173,6 +3173,30 @@ static int phar_set_compression(zval *zv, void *argument) /* {{{ */ } /* }}} */ +typedef struct { + uint32_t compress; + char *error; +} phar_compression_args; + +static int phar_decompress_for_compression_change(zval *zv, void *argument) /* {{{ */ +{ + phar_entry_info *entry = (phar_entry_info *)Z_PTR_P(zv); + phar_compression_args *args = (phar_compression_args *)argument; + uint32_t current = entry->flags & PHAR_ENT_COMPRESSION_MASK; + + if (entry->is_deleted || !current || current == args->compress) { + return ZEND_HASH_APPLY_KEEP; + } + + /* phar_flush() would otherwise copy the old compressed bytes as the new contents */ + if (FAILURE == phar_open_entry_fp(entry, &args->error, 0)) { + return ZEND_HASH_APPLY_STOP; + } + + return ZEND_HASH_APPLY_KEEP; +} +/* }}} */ + static int phar_test_compression(zval *zv, void *argument) /* {{{ */ { phar_entry_info *entry = (phar_entry_info *)Z_PTR_P(zv); @@ -3197,9 +3221,18 @@ static int phar_test_compression(zval *zv, void *argument) /* {{{ */ } /* }}} */ -static void pharobj_set_compression(HashTable *manifest, uint32_t compress) /* {{{ */ +static zend_result pharobj_set_compression(HashTable *manifest, uint32_t compress, char **error) /* {{{ */ { + phar_compression_args args = { .compress = compress, .error = NULL }; + + zend_hash_apply_with_argument(manifest, phar_decompress_for_compression_change, &args); + if (args.error) { + *error = args.error; + return FAILURE; + } + zend_hash_apply_with_argument(manifest, phar_set_compression, &compress); + return SUCCESS; } /* }}} */ @@ -3390,7 +3423,11 @@ PHP_METHOD(Phar, compressFiles) zend_throw_exception_ex(phar_ce_PharException, 0, "phar \"%s\" is persistent, unable to copy on write", phar_obj->archive->fname); RETURN_THROWS(); } - pharobj_set_compression(&phar_obj->archive->manifest, flags); + if (FAILURE == pharobj_set_compression(&phar_obj->archive->manifest, flags, &error)) { + zend_throw_exception_ex(spl_ce_BadMethodCallException, 0, "%s", error); + efree(error); + RETURN_THROWS(); + } phar_obj->archive->is_modified = 1; phar_flush(phar_obj->archive, &error); @@ -3432,7 +3469,11 @@ PHP_METHOD(Phar, decompressFiles) zend_throw_exception_ex(phar_ce_PharException, 0, "phar \"%s\" is persistent, unable to copy on write", phar_obj->archive->fname); RETURN_THROWS(); } - pharobj_set_compression(&phar_obj->archive->manifest, PHAR_ENT_COMPRESSED_NONE); + if (FAILURE == pharobj_set_compression(&phar_obj->archive->manifest, PHAR_ENT_COMPRESSED_NONE, &error)) { + zend_throw_exception_ex(spl_ce_BadMethodCallException, 0, "%s", error); + efree(error); + RETURN_THROWS(); + } } phar_obj->archive->is_modified = 1; diff --git a/ext/phar/tests/gh8469.phpt b/ext/phar/tests/gh8469.phpt new file mode 100644 index 000000000000..1874dde85133 --- /dev/null +++ b/ext/phar/tests/gh8469.phpt @@ -0,0 +1,56 @@ +--TEST-- +GH-8469 (Phar::decompressFiles() and Phar::compressFiles() keep the old compressed contents) +--EXTENSIONS-- +phar +zlib +bz2 +--INI-- +phar.require_hash=0 +phar.readonly=0 +--FILE-- +compressFiles(Phar::GZ); + $phar->compressFiles(Phar::BZ2); + var_dump($phar['a.txt']->isCompressed(Phar::BZ2)); + var_dump($phar['a.txt']->getContent() === $contents); + + $phar->decompressFiles(); + var_dump($phar['a.txt']->isCompressed()); + var_dump($phar['a.txt']->getContent() === $contents); + var_dump($phar['b.txt']->getContent()); + unset($phar); + + $phar = new Phar($fname); + var_dump($phar['a.txt']->getContent() === $contents); + var_dump(file_get_contents("phar://$fname/b.txt")); + unset($phar); +} +?> +--CLEAN-- + +--EXPECT-- +bool(true) +bool(true) +bool(false) +bool(true) +string(11) "our fathers" +bool(true) +string(11) "our fathers" +bool(true) +bool(true) +bool(false) +bool(true) +string(11) "our fathers" +bool(true) +string(11) "our fathers" From a2f587aa58c3549e19d7cf7b20d4b85c55b792ad Mon Sep 17 00:00:00 2001 From: Raphael Paez Date: Fri, 30 Oct 2026 01:55:50 +0000 Subject: [PATCH 2/3] Add NEWS entry for GH-8469 Co-Authored-By: raphael paez --- NEWS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/NEWS b/NEWS index edd2429a3407..c715f4578a5f 100644 --- a/NEWS +++ b/NEWS @@ -199,6 +199,8 @@ PHP NEWS - Phar: . Fixed bug GH-24740 (Oversized tar metadata exhausts the memory limit). (Lazizbek Ergashev) + . Fixed bug GH-8469 (Phar Class decompressFiles() bad in PHP 7.4 and 8.x + branches). (Raphael Paez) - Phpdbg: . Fix wrong command names. (ndossche, Weilin Du) From 87b5720dcb333db477ad96302ebd7b08579fa240 Mon Sep 17 00:00:00 2001 From: Raphael Paez Date: Fri, 30 Oct 2026 16:50:25 +0000 Subject: [PATCH 3/3] Fix GH-8469: skip directory entries on compression change Directory entries have no contents to decompress; trying to open their file pointer failed with "local header does not match central directory" for zip-based phars. Co-Authored-By: raphael paez --- ext/phar/phar_object.c | 2 +- ext/phar/tests/gh8469.phpt | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/ext/phar/phar_object.c b/ext/phar/phar_object.c index f6d0479ec5a7..79ea1cf2df98 100644 --- a/ext/phar/phar_object.c +++ b/ext/phar/phar_object.c @@ -3184,7 +3184,7 @@ static int phar_decompress_for_compression_change(zval *zv, void *argument) /* { phar_compression_args *args = (phar_compression_args *)argument; uint32_t current = entry->flags & PHAR_ENT_COMPRESSION_MASK; - if (entry->is_deleted || !current || current == args->compress) { + if (entry->is_deleted || entry->is_dir || !current || current == args->compress) { return ZEND_HASH_APPLY_KEEP; } diff --git a/ext/phar/tests/gh8469.phpt b/ext/phar/tests/gh8469.phpt index 1874dde85133..ca34dca256cc 100644 --- a/ext/phar/tests/gh8469.phpt +++ b/ext/phar/tests/gh8469.phpt @@ -16,13 +16,14 @@ foreach (['phar', 'phar.zip'] as $ext) { $phar = new Phar($fname); $phar['a.txt'] = $contents; $phar['b.txt'] = 'our fathers'; + $phar->addEmptyDir('dir'); $phar->compressFiles(Phar::GZ); $phar->compressFiles(Phar::BZ2); var_dump($phar['a.txt']->isCompressed(Phar::BZ2)); var_dump($phar['a.txt']->getContent() === $contents); - $phar->decompressFiles(); + var_dump($phar->decompressFiles()); var_dump($phar['a.txt']->isCompressed()); var_dump($phar['a.txt']->getContent() === $contents); var_dump($phar['b.txt']->getContent()); @@ -42,6 +43,7 @@ foreach (['phar', 'phar.zip'] as $ext) { --EXPECT-- bool(true) bool(true) +bool(true) bool(false) bool(true) string(11) "our fathers" @@ -49,6 +51,7 @@ bool(true) string(11) "our fathers" bool(true) bool(true) +bool(true) bool(false) bool(true) string(11) "our fathers"