From 032357313687ad76333c92962922da23b2b793cb Mon Sep 17 00:00:00 2001 From: lazerg Date: Thu, 5 Nov 2026 06:09:42 +0500 Subject: [PATCH 1/2] Reject phar entries and archives beyond the 32-bit format limits Phar::mount() truncated the size of files of 4 GiB or more. The tar writer accepted sizes the tar reader rejects, the zip writer wrapped the central directory offset and size, and compressed sizes above 4 GiB were narrowed. These now fail instead of writing archives that can't be read back. Follow-up to GH-25053. --- ext/phar/phar.c | 12 +++- ext/phar/tar.c | 6 +- ext/phar/tests/gh25053_compressed_size.phpt | 70 +++++++++++++++++++++ ext/phar/tests/gh25053_format_limits.phpt | 66 +++++++++++++++++++ ext/phar/util.c | 5 ++ ext/phar/zip.c | 23 ++++++- 6 files changed, 178 insertions(+), 4 deletions(-) create mode 100644 ext/phar/tests/gh25053_compressed_size.phpt create mode 100644 ext/phar/tests/gh25053_format_limits.phpt diff --git a/ext/phar/phar.c b/ext/phar/phar.c index bee5fe796ebd..fab69d62b7c4 100644 --- a/ext/phar/phar.c +++ b/ext/phar/phar.c @@ -2786,7 +2786,17 @@ ZEND_ATTRIBUTE_NONNULL_ARGS(1, 4) int phar_flush_ex(phar_archive_data *phar, zen php_stream_flush(entry->cfp); php_stream_filter_remove(filter, 1); php_stream_seek(entry->cfp, 0, SEEK_END); - entry->compressed_filesize = ((uint32_t) php_stream_tell(entry->cfp)) - entry->header_offset; + if ((uint64_t) (php_stream_tell(entry->cfp) - entry->header_offset) > UINT32_MAX) { + if (must_close_old_file) { + php_stream_close(oldfile); + } + php_stream_close(newfile); + if (error) { + spprintf(error, 0, "unable to compress file \"%s\" to new phar \"%s\", compressed file is too large", ZSTR_VAL(entry->filename), ZSTR_VAL(phar->fname)); + } + goto cleanup; + } + entry->compressed_filesize = php_stream_tell(entry->cfp) - entry->header_offset; /* generate crc on compressed file */ entry->old_flags = entry->flags; entry->is_modified = 1; diff --git a/ext/phar/tar.c b/ext/phar/tar.c index e2d6165b9898..ed05a20c2370 100644 --- a/ext/phar/tar.c +++ b/ext/phar/tar.c @@ -844,9 +844,11 @@ static int phar_tar_writeheaders_int(phar_entry_info *entry, void *argument) /* phar_tar_octal(header.mode, entry->flags & PHAR_ENT_PERM_MASK, sizeof(header.mode)-1); - if (FAILURE == phar_tar_octal(header.size, entry->uncompressed_filesize, sizeof(header.size)-1)) { + /* the reader rejects sizes above this limit, as they overflow the 512 byte padding */ + if (entry->uncompressed_filesize > UINT32_MAX - 511 + || FAILURE == phar_tar_octal(header.size, entry->uncompressed_filesize, sizeof(header.size)-1)) { if (fp->error) { - spprintf(fp->error, 4096, "tar-based phar \"%s\" cannot be created, filename \"%s\" is too large for tar file format", ZSTR_VAL(entry->phar->fname), ZSTR_VAL(entry->filename)); + spprintf(fp->error, 4096, "tar-based phar \"%s\" cannot be created, file \"%s\" is too large for tar file format", ZSTR_VAL(entry->phar->fname), ZSTR_VAL(entry->filename)); } return ZEND_HASH_APPLY_STOP; } diff --git a/ext/phar/tests/gh25053_compressed_size.phpt b/ext/phar/tests/gh25053_compressed_size.phpt new file mode 100644 index 000000000000..fd1fff27a0e9 --- /dev/null +++ b/ext/phar/tests/gh25053_compressed_size.phpt @@ -0,0 +1,70 @@ +--TEST-- +GH-25053: Phar rejects compressed entries whose size exceeds the 32-bit size field +--EXTENSIONS-- +phar +zlib +hash +--SKIPIF-- + +--ENV-- +TEST_TIMEOUT=600 +--INI-- +phar.readonly=0 +--FILE-- + 0) { + $length = min($remaining, strlen($block)); + if (fwrite($fp, substr($block, 0, $length)) !== $length) { + throw new RuntimeException('Could not create the input file'); + } + $remaining -= $length; +} +fclose($fp); +unset($block); + +foreach (['phar', 'zip'] as $format) { + echo $format, "\n"; + $path = __DIR__ . '/gh25053_compressed_size.' . $format; + $phar = $format === 'phar' ? new Phar($path) : new PharData($path); + $phar->startBuffering(); + $phar->addFile($src, 'data'); + + try { + $phar['data']->compress(Phar::GZ); + $phar->stopBuffering(); + echo "Compression unexpectedly succeeded\n"; + } catch (PharException $e) { + // The format limit must be reported, rather than silently narrowing + // the compressed size and writing only that many bytes. + echo $e::class, "\n"; + } +} +?> +--CLEAN-- + +--EXPECT-- +phar +PharException +zip +PharException diff --git a/ext/phar/tests/gh25053_format_limits.phpt b/ext/phar/tests/gh25053_format_limits.phpt new file mode 100644 index 000000000000..c999cc06af8d --- /dev/null +++ b/ext/phar/tests/gh25053_format_limits.phpt @@ -0,0 +1,66 @@ +--TEST-- +GH-25053: Phar rejects mounts, tar entries and zip archives beyond the 32-bit format limits +--EXTENSIONS-- +phar +--SKIPIF-- + +--ENV-- +TEST_TIMEOUT=600 +--INI-- +phar.readonly=0 +--FILE-- +addFromString('small', 'x'); +try { + Phar::mount('phar://' . __DIR__ . '/gh25053_format_limits.phar/big', $src); +} catch (PharException $e) { + echo "mount failed\n"; +} + +$tar = new PharData(__DIR__ . '/gh25053_format_limits.tar'); +$fp = fopen($src, 'rb'); +fseek($fp, 102); +try { + $tar['big'] = $fp; +} catch (PharException $e) { + echo $e->getMessage(), "\n"; +} + +$zip = new PharData(__DIR__ . '/gh25053_format_limits.zip'); +$zip->addFromString('keep', 'old'); +$fp = fopen($src, 'rb'); +fseek($fp, 12); +try { + $zip['big'] = $fp; +} catch (PharException $e) { + echo $e->getMessage(), "\n"; +} +var_dump($zip['keep']->getContent()); +$zip->addFromString('big', 'small'); +var_dump($zip['keep']->getContent()); +var_dump($zip['big']->getContent()); +?> +--CLEAN-- + +--EXPECTF-- +mount failed +tar-based phar "%sgh25053_format_limits.tar" cannot be created, file "big" is too large for tar file format +phar zip flush of "%sgh25053_format_limits.zip" failed: archive is too large for zip file format +string(3) "old" +string(3) "old" +string(5) "small" diff --git a/ext/phar/util.c b/ext/phar/util.c index 2d636e9b84f5..f27706bb49d5 100644 --- a/ext/phar/util.c +++ b/ext/phar/util.c @@ -275,6 +275,11 @@ zend_result phar_mount_entry(phar_archive_data *phar, const char *filename, size return FAILURE; } } else { + if ((uint64_t) ssb.sb.st_size > UINT32_MAX) { + efree(entry.tmp); + zend_string_efree(entry.filename); + return FAILURE; + } entry.is_dir = false; entry.uncompressed_filesize = entry.compressed_filesize = ssb.sb.st_size; } diff --git a/ext/phar/zip.c b/ext/phar/zip.c index 13c34d8a3b0f..6876855f1198 100644 --- a/ext/phar/zip.c +++ b/ext/phar/zip.c @@ -1025,7 +1025,11 @@ static int phar_zip_changed_apply_int(phar_entry_info *entry, void *arg) /* {{{ php_stream_flush(entry->cfp); php_stream_filter_remove(filter, 1); php_stream_seek(entry->cfp, 0, SEEK_END); - entry->compressed_filesize = (uint32_t) php_stream_tell(entry->cfp); + if ((uint64_t) php_stream_tell(entry->cfp) > UINT32_MAX) { + spprintf(p->error, 0, "unable to compress file \"%s\" to zip-based phar \"%s\", compressed file is too large", ZSTR_VAL(entry->filename), ZSTR_VAL(entry->phar->fname)); + return ZEND_HASH_APPLY_STOP; + } + entry->compressed_filesize = php_stream_tell(entry->cfp); PHAR_SET_32(central.compsize, entry->compressed_filesize); PHAR_SET_32(local.compsize, entry->compressed_filesize); /* generate crc on compressed file */ @@ -1441,6 +1445,23 @@ ZEND_ATTRIBUTE_NONNULL_ARGS(1, 4) int phar_zip_flush(phar_archive_data *phar, ze } /* save zip */ + /* zip64 is not supported, so the central directory offset and size must fit in 32 bits */ + if ((uint64_t) php_stream_tell(pass.filefp) > UINT32_MAX + || (uint64_t) php_stream_tell(pass.centralfp) > UINT32_MAX) { + if (error) { + spprintf(error, 4096, "phar zip flush of \"%s\" failed: archive is too large for zip file format", ZSTR_VAL(phar->fname)); + } + php_stream_close(pass.centralfp); + if (phar->fp && pass.free_fp) { + php_stream_close(phar->fp); + } else if (must_close_old_file) { + php_stream_close(oldfile); + } + phar->fp = pass.filefp; + phar->is_brandnew = 0; + return EOF; + } + cdir_size = php_stream_tell(pass.centralfp); cdir_offset = php_stream_tell(pass.filefp); PHAR_SET_32(eocd.cdir_size, cdir_size); From 4cea9a2b53c4d1a9b17d15ed9cdffd4797aaa058 Mon Sep 17 00:00:00 2001 From: lazerg Date: Fri, 6 Nov 2026 08:39:27 +0500 Subject: [PATCH 2/2] Drop null checks on nonnull error parameter --- ext/phar/phar.c | 4 +--- ext/phar/zip.c | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/ext/phar/phar.c b/ext/phar/phar.c index fab69d62b7c4..fdb37e5e68b2 100644 --- a/ext/phar/phar.c +++ b/ext/phar/phar.c @@ -2791,9 +2791,7 @@ ZEND_ATTRIBUTE_NONNULL_ARGS(1, 4) int phar_flush_ex(phar_archive_data *phar, zen php_stream_close(oldfile); } php_stream_close(newfile); - if (error) { - spprintf(error, 0, "unable to compress file \"%s\" to new phar \"%s\", compressed file is too large", ZSTR_VAL(entry->filename), ZSTR_VAL(phar->fname)); - } + spprintf(error, 0, "unable to compress file \"%s\" to new phar \"%s\", compressed file is too large", ZSTR_VAL(entry->filename), ZSTR_VAL(phar->fname)); goto cleanup; } entry->compressed_filesize = php_stream_tell(entry->cfp) - entry->header_offset; diff --git a/ext/phar/zip.c b/ext/phar/zip.c index 6876855f1198..e3283532b3cd 100644 --- a/ext/phar/zip.c +++ b/ext/phar/zip.c @@ -1448,9 +1448,7 @@ ZEND_ATTRIBUTE_NONNULL_ARGS(1, 4) int phar_zip_flush(phar_archive_data *phar, ze /* zip64 is not supported, so the central directory offset and size must fit in 32 bits */ if ((uint64_t) php_stream_tell(pass.filefp) > UINT32_MAX || (uint64_t) php_stream_tell(pass.centralfp) > UINT32_MAX) { - if (error) { - spprintf(error, 4096, "phar zip flush of \"%s\" failed: archive is too large for zip file format", ZSTR_VAL(phar->fname)); - } + spprintf(error, 4096, "phar zip flush of \"%s\" failed: archive is too large for zip file format", ZSTR_VAL(phar->fname)); php_stream_close(pass.centralfp); if (phar->fp && pass.free_fp) { php_stream_close(phar->fp);