From 61603fd46eef4ac8e435aa4e310bbcfd1228491e Mon Sep 17 00:00:00 2001 From: lazerg Date: Fri, 30 Oct 2026 17:12:29 +0500 Subject: [PATCH 1/9] Fix GH-25053: Phar truncates entries of 4 GiB or more --- ext/phar/phar_object.c | 27 ++++++++++++++++++++++++ ext/phar/stream.c | 5 +++++ ext/phar/tests/gh25053.phpt | 42 +++++++++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+) create mode 100644 ext/phar/tests/gh25053.phpt diff --git a/ext/phar/phar_object.c b/ext/phar/phar_object.c index a1736f221e27..bea61cc0e6a1 100644 --- a/ext/phar/phar_object.c +++ b/ext/phar/phar_object.c @@ -1676,6 +1676,28 @@ static int phar_build(zend_object_iterator *iter, void *puser) /* {{{ */ data->internal_file->offset_abs = data->internal_file->offset = php_stream_tell(p_obj->fp); data->fp = NULL; php_stream_copy_to_stream_ex(fp, p_obj->fp, PHP_STREAM_COPY_ALL, &contents_len); + if (ZEND_SIZE_T_UINT_OVFL(contents_len)) { + zend_throw_exception_ex(spl_ce_UnexpectedValueException, 0, "Iterator %s returned a file that is too large \"%s\"", ZSTR_VAL(ce->name), fname); + phar_entry_delref(data); + + if (save) { + efree(save); + } + + if (opened) { + zend_string_release_ex(opened, 0); + } + + if (temp) { + efree(temp); + } + + if (close_fp) { + php_stream_close(fp); + } + + return ZEND_HASH_APPLY_STOP; + } data->internal_file->uncompressed_filesize = data->internal_file->compressed_filesize = php_stream_tell(p_obj->fp) - data->internal_file->offset; if (php_stream_stat(fp, &ssb) != -1) { @@ -3685,6 +3707,11 @@ static void phar_add_file(phar_archive_data **pphar, zend_string *file_name, con } php_stream_copy_to_stream_ex(contents_file, data->fp, PHP_STREAM_COPY_ALL, &contents_len); } + if (ZEND_SIZE_T_UINT_OVFL(contents_len)) { + zend_throw_exception_ex(spl_ce_BadMethodCallException, 0, "Entry %s is too large to be stored in a phar archive", filename); + phar_entry_delref(data); + goto finish; + } data->internal_file->compressed_filesize = data->internal_file->uncompressed_filesize = contents_len; } diff --git a/ext/phar/stream.c b/ext/phar/stream.c index 19ba48f87500..f99b2bfc9b52 100644 --- a/ext/phar/stream.c +++ b/ext/phar/stream.c @@ -447,6 +447,11 @@ static ssize_t phar_stream_write(php_stream *stream, const char *buf, size_t cou { phar_entry_data *data = (phar_entry_data *) stream->abstract; + if ((uint64_t) data->position + count > UINT32_MAX) { + php_stream_wrapper_log_error(stream->wrapper, stream->flags, "phar error: Could not write %d characters to \"%s\" in phar \"%s\", entry is too large", (int) count, data->internal_file->filename, data->phar->fname); + return -1; + } + php_stream_seek(data->fp, data->position + data->zero, SEEK_SET); if (count != php_stream_write(data->fp, buf, count)) { php_stream_wrapper_log_error(stream->wrapper, stream->flags, "phar error: Could not write %d characters to \"%s\" in phar \"%s\"", (int) count, data->internal_file->filename, data->phar->fname); diff --git a/ext/phar/tests/gh25053.phpt b/ext/phar/tests/gh25053.phpt new file mode 100644 index 000000000000..35b3c7805af7 --- /dev/null +++ b/ext/phar/tests/gh25053.phpt @@ -0,0 +1,42 @@ +--TEST-- +GH-25053 (Phar truncates entries of 4 GiB or more without error/warning) +--EXTENSIONS-- +phar +--SKIPIF-- + +--INI-- +phar.readonly=0 +--FILE-- +addFile($src, 'big'); +} catch (BadMethodCallException $e) { + echo $e->getMessage(), "\n"; +} + +$tar = new PharData(__DIR__ . '/gh25053-2.tar'); +try { + $tar->buildFromIterator(new ArrayIterator(['big' => $src])); +} catch (UnexpectedValueException $e) { + echo str_replace($src, 'gh25053.big', $e->getMessage()), "\n"; +} +?> +--CLEAN-- + +--EXPECT-- +Entry big is too large to be stored in a phar archive +Iterator ArrayIterator returned a file that is too large "gh25053.big" From f8d13590e1eb9bf7bbdfcdc8b3828e149f897503 Mon Sep 17 00:00:00 2001 From: lazerg Date: Fri, 30 Oct 2026 17:24:38 +0500 Subject: [PATCH 2/9] Fix uninitialized fname in phar_build() and warn on too large stream writes --- ext/phar/phar_object.c | 2 +- ext/phar/stream.c | 2 +- ext/phar/tests/gh25053.phpt | 25 +++++++++++++++++++++---- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/ext/phar/phar_object.c b/ext/phar/phar_object.c index bea61cc0e6a1..03d9c3159c51 100644 --- a/ext/phar/phar_object.c +++ b/ext/phar/phar_object.c @@ -1677,7 +1677,7 @@ static int phar_build(zend_object_iterator *iter, void *puser) /* {{{ */ data->fp = NULL; php_stream_copy_to_stream_ex(fp, p_obj->fp, PHP_STREAM_COPY_ALL, &contents_len); if (ZEND_SIZE_T_UINT_OVFL(contents_len)) { - zend_throw_exception_ex(spl_ce_UnexpectedValueException, 0, "Iterator %s returned a file that is too large \"%s\"", ZSTR_VAL(ce->name), fname); + zend_throw_exception_ex(spl_ce_BadMethodCallException, 0, "Entry %s is too large to be stored in a phar archive", str_key); phar_entry_delref(data); if (save) { diff --git a/ext/phar/stream.c b/ext/phar/stream.c index f99b2bfc9b52..0bc1f0f50cd7 100644 --- a/ext/phar/stream.c +++ b/ext/phar/stream.c @@ -448,7 +448,7 @@ static ssize_t phar_stream_write(php_stream *stream, const char *buf, size_t cou phar_entry_data *data = (phar_entry_data *) stream->abstract; if ((uint64_t) data->position + count > UINT32_MAX) { - php_stream_wrapper_log_error(stream->wrapper, stream->flags, "phar error: Could not write %d characters to \"%s\" in phar \"%s\", entry is too large", (int) count, data->internal_file->filename, data->phar->fname); + php_error_docref(NULL, E_WARNING, "phar error: Could not write %d characters to \"%s\" in phar \"%s\", entry is too large", (int) count, data->internal_file->filename, data->phar->fname); return -1; } diff --git a/ext/phar/tests/gh25053.phpt b/ext/phar/tests/gh25053.phpt index 35b3c7805af7..2d473a6d4777 100644 --- a/ext/phar/tests/gh25053.phpt +++ b/ext/phar/tests/gh25053.phpt @@ -27,16 +27,33 @@ try { $tar = new PharData(__DIR__ . '/gh25053-2.tar'); try { $tar->buildFromIterator(new ArrayIterator(['big' => $src])); -} catch (UnexpectedValueException $e) { - echo str_replace($src, 'gh25053.big', $e->getMessage()), "\n"; +} catch (BadMethodCallException $e) { + echo $e->getMessage(), "\n"; } + +$tar = new PharData(__DIR__ . '/gh25053-3.tar'); +try { + $tar->buildFromIterator(new ArrayIterator(['big' => fopen($src, 'rb')])); +} catch (BadMethodCallException $e) { + echo $e->getMessage(), "\n"; +} + +$phar = new Phar(__DIR__ . '/gh25053-4.phar'); +$phar->addFromString('small', 'x'); +var_dump(copy($src, 'phar://' . __DIR__ . '/gh25053-4.phar/big')); ?> --CLEAN-- ---EXPECT-- +--EXPECTF-- +Entry big is too large to be stored in a phar archive Entry big is too large to be stored in a phar archive -Iterator ArrayIterator returned a file that is too large "gh25053.big" +Entry big is too large to be stored in a phar archive + +Warning: copy(): phar error: Could not write %d characters to "big" in phar "%sgh25053-4.phar", entry is too large in %s on line %d +bool(false) From 96565a56e39a8e4edef5890352dfff68d4a60ad0 Mon Sep 17 00:00:00 2001 From: lazerg Date: Fri, 30 Oct 2026 19:36:52 +0500 Subject: [PATCH 3/9] Reject too large entries before creating them and drop failed entries --- ext/phar/phar_object.c | 36 +++++++++++++++++++++++++++++++++++- ext/phar/tests/gh25053.phpt | 6 ++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/ext/phar/phar_object.c b/ext/phar/phar_object.c index 03d9c3159c51..3e96b49059e2 100644 --- a/ext/phar/phar_object.c +++ b/ext/phar/phar_object.c @@ -1640,6 +1640,28 @@ static int phar_build(zend_object_iterator *iter, void *puser) /* {{{ */ return ZEND_HASH_APPLY_KEEP; } + if (php_stream_stat(fp, &ssb) != -1 && (uint64_t) ssb.sb.st_size > UINT32_MAX) { + zend_throw_exception_ex(spl_ce_BadMethodCallException, 0, "Entry %s is too large to be stored in a phar archive", str_key); + + if (save) { + efree(save); + } + + if (opened) { + zend_string_release_ex(opened, 0); + } + + if (temp) { + efree(temp); + } + + if (close_fp) { + php_stream_close(fp); + } + + return ZEND_HASH_APPLY_STOP; + } + if (!(data = phar_get_or_create_entry_data(phar_obj->archive->fname, phar_obj->archive->fname_len, str_key, str_key_len, "w+b", 0, &error, 1))) { zend_throw_exception_ex(spl_ce_BadMethodCallException, 0, "Entry %s cannot be created: %s", str_key, error); efree(error); @@ -1678,6 +1700,7 @@ static int phar_build(zend_object_iterator *iter, void *puser) /* {{{ */ php_stream_copy_to_stream_ex(fp, p_obj->fp, PHP_STREAM_COPY_ALL, &contents_len); if (ZEND_SIZE_T_UINT_OVFL(contents_len)) { zend_throw_exception_ex(spl_ce_BadMethodCallException, 0, "Entry %s is too large to be stored in a phar archive", str_key); + data->internal_file->is_deleted = 1; phar_entry_delref(data); if (save) { @@ -3676,6 +3699,16 @@ static void phar_add_file(phar_archive_data **pphar, zend_string *file_name, con } #endif + if (zresource) { + php_stream_from_zval_no_verify(contents_file, zresource); + } + + if ((content && ZEND_SIZE_T_UINT_OVFL(ZSTR_LEN(content))) + || (contents_file && php_stream_stat(contents_file, &ssb) != -1 && (uint64_t) ssb.sb.st_size > UINT32_MAX)) { + zend_throw_exception_ex(spl_ce_BadMethodCallException, 0, "Entry %s is too large to be stored in a phar archive", filename); + goto finish; + } + if (!(data = phar_get_or_create_entry_data((*pphar)->fname, (*pphar)->fname_len, filename, filename_len, "w+b", 0, &error, 1))) { if (error) { zend_throw_exception_ex(spl_ce_BadMethodCallException, 0, "Entry %s does not exist and cannot be created: %s", filename, error); @@ -3700,7 +3733,7 @@ static void phar_add_file(phar_archive_data **pphar, zend_string *file_name, con goto finish; } } else { - if (!(php_stream_from_zval_no_verify(contents_file, zresource))) { + if (!contents_file) { zend_throw_exception_ex(spl_ce_BadMethodCallException, 0, "Entry %s could not be written to", filename); phar_entry_delref(data); goto finish; @@ -3709,6 +3742,7 @@ static void phar_add_file(phar_archive_data **pphar, zend_string *file_name, con } if (ZEND_SIZE_T_UINT_OVFL(contents_len)) { zend_throw_exception_ex(spl_ce_BadMethodCallException, 0, "Entry %s is too large to be stored in a phar archive", filename); + data->internal_file->is_deleted = 1; phar_entry_delref(data); goto finish; } diff --git a/ext/phar/tests/gh25053.phpt b/ext/phar/tests/gh25053.phpt index 2d473a6d4777..22918bf357b2 100644 --- a/ext/phar/tests/gh25053.phpt +++ b/ext/phar/tests/gh25053.phpt @@ -23,6 +23,7 @@ try { } catch (BadMethodCallException $e) { echo $e->getMessage(), "\n"; } +var_dump(isset($tar['big'])); $tar = new PharData(__DIR__ . '/gh25053-2.tar'); try { @@ -30,6 +31,7 @@ try { } catch (BadMethodCallException $e) { echo $e->getMessage(), "\n"; } +var_dump(isset($tar['big'])); $tar = new PharData(__DIR__ . '/gh25053-3.tar'); try { @@ -37,6 +39,7 @@ try { } catch (BadMethodCallException $e) { echo $e->getMessage(), "\n"; } +var_dump(isset($tar['big'])); $phar = new Phar(__DIR__ . '/gh25053-4.phar'); $phar->addFromString('small', 'x'); @@ -52,8 +55,11 @@ var_dump(copy($src, 'phar://' . __DIR__ . '/gh25053-4.phar/big')); ?> --EXPECTF-- Entry big is too large to be stored in a phar archive +bool(false) Entry big is too large to be stored in a phar archive +bool(false) Entry big is too large to be stored in a phar archive +bool(false) Warning: copy(): phar error: Could not write %d characters to "big" in phar "%sgh25053-4.phar", entry is too large in %s on line %d bool(false) From 70b021315ea544a1f64a3744df891b8a4cb45986 Mon Sep 17 00:00:00 2001 From: lazerg Date: Fri, 30 Oct 2026 21:07:04 +0500 Subject: [PATCH 4/9] Account for stream position, tar padding and mounted files in size checks --- ext/phar/phar_object.c | 5 +++-- ext/phar/tar.c | 6 ++++-- ext/phar/tests/gh25053.phpt | 26 ++++++++++++++++++++++++++ ext/phar/util.c | 5 +++++ 4 files changed, 38 insertions(+), 4 deletions(-) diff --git a/ext/phar/phar_object.c b/ext/phar/phar_object.c index 3e96b49059e2..5321cf04278b 100644 --- a/ext/phar/phar_object.c +++ b/ext/phar/phar_object.c @@ -1640,7 +1640,7 @@ static int phar_build(zend_object_iterator *iter, void *puser) /* {{{ */ return ZEND_HASH_APPLY_KEEP; } - if (php_stream_stat(fp, &ssb) != -1 && (uint64_t) ssb.sb.st_size > UINT32_MAX) { + if (php_stream_stat(fp, &ssb) != -1 && (int64_t) ssb.sb.st_size - php_stream_tell(fp) > (int64_t) UINT32_MAX) { zend_throw_exception_ex(spl_ce_BadMethodCallException, 0, "Entry %s is too large to be stored in a phar archive", str_key); if (save) { @@ -3704,7 +3704,8 @@ static void phar_add_file(phar_archive_data **pphar, zend_string *file_name, con } if ((content && ZEND_SIZE_T_UINT_OVFL(ZSTR_LEN(content))) - || (contents_file && php_stream_stat(contents_file, &ssb) != -1 && (uint64_t) ssb.sb.st_size > UINT32_MAX)) { + || (contents_file && php_stream_stat(contents_file, &ssb) != -1 + && (int64_t) ssb.sb.st_size - php_stream_tell(contents_file) > (int64_t) UINT32_MAX)) { zend_throw_exception_ex(spl_ce_BadMethodCallException, 0, "Entry %s is too large to be stored in a phar archive", filename); goto finish; } diff --git a/ext/phar/tar.c b/ext/phar/tar.c index 198f157ca1fd..52541e62290e 100644 --- a/ext/phar/tar.c +++ b/ext/phar/tar.c @@ -830,9 +830,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 UINT32_MAX - 511, 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", entry->phar->fname, entry->filename); + spprintf(fp->error, 4096, "tar-based phar \"%s\" cannot be created, file \"%s\" is too large for tar file format", entry->phar->fname, entry->filename); } return ZEND_HASH_APPLY_STOP; } diff --git a/ext/phar/tests/gh25053.phpt b/ext/phar/tests/gh25053.phpt index 22918bf357b2..9d56e652ee0f 100644 --- a/ext/phar/tests/gh25053.phpt +++ b/ext/phar/tests/gh25053.phpt @@ -44,6 +44,27 @@ var_dump(isset($tar['big'])); $phar = new Phar(__DIR__ . '/gh25053-4.phar'); $phar->addFromString('small', 'x'); var_dump(copy($src, 'phar://' . __DIR__ . '/gh25053-4.phar/big')); + +try { + Phar::mount('phar://' . __DIR__ . '/gh25053-4.phar/big', $src); +} catch (PharException $e) { + echo "mount failed\n"; +} + +$tar = new PharData(__DIR__ . '/gh25053-5.tar'); +$fp = fopen($src, 'rb'); +fseek($fp, 2**32); +$tar->buildFromIterator(new ArrayIterator(['tail' => $fp])); +var_dump($tar['tail']->getSize()); + +$tar = new PharData(__DIR__ . '/gh25053-6.tar'); +$fp = fopen($src, 'rb'); +fseek($fp, 102); +try { + $tar['big'] = $fp; +} catch (PharException $e) { + echo $e->getMessage(), "\n"; +} ?> --CLEAN-- --EXPECTF-- Entry big is too large to be stored in a phar archive @@ -63,3 +86,6 @@ bool(false) Warning: copy(): phar error: Could not write %d characters to "big" in phar "%sgh25053-4.phar", entry is too large in %s on line %d bool(false) +mount failed +int(1) +tar-based phar "%sgh25053-6.tar" cannot be created, file "big" is too large for tar file format diff --git a/ext/phar/util.c b/ext/phar/util.c index f4de9922f899..35947fc293a4 100644 --- a/ext/phar/util.c +++ b/ext/phar/util.c @@ -276,6 +276,11 @@ zend_result phar_mount_entry(phar_archive_data *phar, char *filename, size_t fil return FAILURE; } } else { + if ((uint64_t) ssb.sb.st_size > UINT32_MAX) { + efree(entry.tmp); + efree(entry.filename); + return FAILURE; + } entry.is_dir = 0; entry.uncompressed_filesize = entry.compressed_filesize = ssb.sb.st_size; } From 142c1d11776840032a62fb89d67fbe3b3f9ec317 Mon Sep 17 00:00:00 2001 From: lazerg Date: Fri, 30 Oct 2026 21:24:49 +0500 Subject: [PATCH 5/9] Reject zip archives whose offsets do not fit in 32 bits --- ext/phar/tests/gh25053.phpt | 11 +++++++++++ ext/phar/zip.c | 8 ++++++++ 2 files changed, 19 insertions(+) diff --git a/ext/phar/tests/gh25053.phpt b/ext/phar/tests/gh25053.phpt index 9d56e652ee0f..07895e1209a5 100644 --- a/ext/phar/tests/gh25053.phpt +++ b/ext/phar/tests/gh25053.phpt @@ -65,6 +65,15 @@ try { } catch (PharException $e) { echo $e->getMessage(), "\n"; } + +$zip = new PharData(__DIR__ . '/gh25053-7.zip'); +$fp = fopen($src, 'rb'); +fseek($fp, 12); +try { + $zip['big'] = $fp; +} catch (PharException $e) { + echo $e->getMessage(), "\n"; +} ?> --CLEAN-- --EXPECTF-- Entry big is too large to be stored in a phar archive @@ -89,3 +99,4 @@ bool(false) mount failed int(1) tar-based phar "%sgh25053-6.tar" cannot be created, file "big" is too large for tar file format +phar zip flush of "%sgh25053-7.zip" failed: archive is too large for zip file format diff --git a/ext/phar/zip.c b/ext/phar/zip.c index f051c35a09a7..46471937fc59 100644 --- a/ext/phar/zip.c +++ b/ext/phar/zip.c @@ -1465,6 +1465,14 @@ int phar_zip_flush(phar_archive_data *phar, zend_string *user_stub, bool is_defa } /* save zip */ + /* zip64 is not supported, so all offsets must fit in 32 bits */ + if ((uint64_t) php_stream_tell(pass.filefp) > UINT32_MAX) { + if (error) { + spprintf(error, 4096, "phar zip flush of \"%s\" failed: archive is too large for zip file format", phar->fname); + } + goto notemperror; + } + cdir_size = php_stream_tell(pass.centralfp); cdir_offset = php_stream_tell(pass.filefp); PHAR_SET_32(eocd.cdir_size, cdir_size); From 7df092c09baf8a1838ac2c3eb5d94ae2694959ab Mon Sep 17 00:00:00 2001 From: lazerg Date: Fri, 30 Oct 2026 21:33:15 +0500 Subject: [PATCH 6/9] Reject zip archives whose central directory size does not fit in 32 bits --- ext/phar/zip.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ext/phar/zip.c b/ext/phar/zip.c index 46471937fc59..b884c68cc9a0 100644 --- a/ext/phar/zip.c +++ b/ext/phar/zip.c @@ -1465,8 +1465,9 @@ int phar_zip_flush(phar_archive_data *phar, zend_string *user_stub, bool is_defa } /* save zip */ - /* zip64 is not supported, so all offsets must fit in 32 bits */ - if ((uint64_t) php_stream_tell(pass.filefp) > UINT32_MAX) { + /* 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", phar->fname); } From 20c979ea4bb4b63c22c49eec1b34c69a0dec0714 Mon Sep 17 00:00:00 2001 From: lazerg Date: Fri, 30 Oct 2026 22:28:47 +0500 Subject: [PATCH 7/9] Skip the early size check for streams with read filters --- ext/phar/phar_object.c | 6 +- ext/phar/tests/gh25053_filtered_stream.phpt | 68 +++++++++++++++++++++ 2 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 ext/phar/tests/gh25053_filtered_stream.phpt diff --git a/ext/phar/phar_object.c b/ext/phar/phar_object.c index 5321cf04278b..37390df05f29 100644 --- a/ext/phar/phar_object.c +++ b/ext/phar/phar_object.c @@ -1640,7 +1640,9 @@ static int phar_build(zend_object_iterator *iter, void *puser) /* {{{ */ return ZEND_HASH_APPLY_KEEP; } - if (php_stream_stat(fp, &ssb) != -1 && (int64_t) ssb.sb.st_size - php_stream_tell(fp) > (int64_t) UINT32_MAX) { + /* read filters change the length, so filtered streams are only checked after the copy */ + if (!fp->readfilters.head && php_stream_stat(fp, &ssb) != -1 + && (int64_t) ssb.sb.st_size - php_stream_tell(fp) > (int64_t) UINT32_MAX) { zend_throw_exception_ex(spl_ce_BadMethodCallException, 0, "Entry %s is too large to be stored in a phar archive", str_key); if (save) { @@ -3704,7 +3706,7 @@ static void phar_add_file(phar_archive_data **pphar, zend_string *file_name, con } if ((content && ZEND_SIZE_T_UINT_OVFL(ZSTR_LEN(content))) - || (contents_file && php_stream_stat(contents_file, &ssb) != -1 + || (contents_file && !contents_file->readfilters.head && php_stream_stat(contents_file, &ssb) != -1 && (int64_t) ssb.sb.st_size - php_stream_tell(contents_file) > (int64_t) UINT32_MAX)) { zend_throw_exception_ex(spl_ce_BadMethodCallException, 0, "Entry %s is too large to be stored in a phar archive", filename); goto finish; diff --git a/ext/phar/tests/gh25053_filtered_stream.phpt b/ext/phar/tests/gh25053_filtered_stream.phpt new file mode 100644 index 000000000000..54e49c50dece --- /dev/null +++ b/ext/phar/tests/gh25053_filtered_stream.phpt @@ -0,0 +1,68 @@ +--TEST-- +GH-25053: Phar size checks use filtered content rather than the underlying file size +--EXTENSIONS-- +phar +zlib +--SKIPIF-- + +--ENV-- +TEST_TIMEOUT=600 +--INI-- +phar.readonly=0 +--FILE-- +buildFromIterator(new ArrayIterator(['data' => $fp])); + } finally { + fclose($fp); + } + } else { + $phar->addFile('php://filter/read=zlib.deflate/resource=' . $src, 'data'); + } + + // Decode in chunks so the round trip does not allocate a 4 GiB string. + $fp = fopen('phar://' . $path . '/data', 'rb'); + stream_filter_append($fp, 'zlib.inflate', STREAM_FILTER_READ); + $length = 0; + $allZero = true; + while (!feof($fp)) { + $chunk = fread($fp, 1024 * 1024); + $length += strlen($chunk); + $allZero = $allZero && strspn($chunk, "\0") === strlen($chunk); + } + fclose($fp); + var_dump($length === $size && $allZero); + } catch (BadMethodCallException $e) { + echo $e->getMessage(), "\n"; + } +} +?> +--CLEAN-- + +--EXPECT-- +buildFromIterator +bool(true) +addFile +bool(true) From 9ffab11ae5a0d959f747d3d40490c0dd8435074c Mon Sep 17 00:00:00 2001 From: lazerg Date: Fri, 30 Oct 2026 22:37:31 +0500 Subject: [PATCH 8/9] Reject compressed entries that do not fit in 32 bits --- ext/phar/phar.c | 12 +++- ext/phar/tests/gh25053_compressed_size.phpt | 69 +++++++++++++++++++++ ext/phar/zip.c | 6 +- 3 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 ext/phar/tests/gh25053_compressed_size.phpt diff --git a/ext/phar/phar.c b/ext/phar/phar.c index fc21692db2c1..fe65e34d1ec0 100644 --- a/ext/phar/phar.c +++ b/ext/phar/phar.c @@ -2819,7 +2819,17 @@ int phar_flush_ex(phar_archive_data *phar, zend_string *user_stub, bool is_defau 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", entry->filename, 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/tests/gh25053_compressed_size.phpt b/ext/phar/tests/gh25053_compressed_size.phpt new file mode 100644 index 000000000000..4ed5717a1f1c --- /dev/null +++ b/ext/phar/tests/gh25053_compressed_size.phpt @@ -0,0 +1,69 @@ +--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/zip.c b/ext/phar/zip.c index b884c68cc9a0..e99327b7e9b1 100644 --- a/ext/phar/zip.c +++ b/ext/phar/zip.c @@ -1026,7 +1026,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", entry->filename, 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 */ From 3916a9158542c1c29499353288f8a58ae32a40e9 Mon Sep 17 00:00:00 2001 From: lazerg Date: Sat, 31 Oct 2026 11:26:07 +0500 Subject: [PATCH 9/9] Skip compressed size test on FreeBSD and keep entries after failed builds --- ext/phar/phar_internal.h | 5 ++ ext/phar/phar_object.c | 55 ++++++++++------ ext/phar/stream.c | 2 +- ext/phar/tar.c | 4 +- ext/phar/tests/gh25053.phpt | 69 ++++++++++++++++++++- ext/phar/tests/gh25053_compressed_size.phpt | 1 + ext/phar/zip.c | 10 ++- 7 files changed, 121 insertions(+), 25 deletions(-) diff --git a/ext/phar/phar_internal.h b/ext/phar/phar_internal.h index 58a30a52b1c1..6df7968b2ae5 100644 --- a/ext/phar/phar_internal.h +++ b/ext/phar/phar_internal.h @@ -293,6 +293,11 @@ struct _phar_archive_data { uint32_t is_persistent:1; }; +static inline uint32_t phar_entry_max_size(const phar_archive_data *phar) +{ + return phar->is_tar ? UINT32_MAX - 511 : UINT32_MAX; +} + typedef struct _phar_entry_fp_info { enum phar_fp_type fp_type; /* offset within fp of the file contents */ diff --git a/ext/phar/phar_object.c b/ext/phar/phar_object.c index 37390df05f29..84e74acbeed7 100644 --- a/ext/phar/phar_object.c +++ b/ext/phar/phar_object.c @@ -1642,7 +1642,7 @@ static int phar_build(zend_object_iterator *iter, void *puser) /* {{{ */ /* read filters change the length, so filtered streams are only checked after the copy */ if (!fp->readfilters.head && php_stream_stat(fp, &ssb) != -1 - && (int64_t) ssb.sb.st_size - php_stream_tell(fp) > (int64_t) UINT32_MAX) { + && (int64_t) ssb.sb.st_size - php_stream_tell(fp) > (int64_t) phar_entry_max_size(phar_obj->archive)) { zend_throw_exception_ex(spl_ce_BadMethodCallException, 0, "Entry %s is too large to be stored in a phar archive", str_key); if (save) { @@ -1664,6 +1664,10 @@ static int phar_build(zend_object_iterator *iter, void *puser) /* {{{ */ return ZEND_HASH_APPLY_STOP; } + if (p_obj->fp == phar_obj->archive->ufp) { + php_stream_seek(p_obj->fp, 0, SEEK_END); + } + if (!(data = phar_get_or_create_entry_data(phar_obj->archive->fname, phar_obj->archive->fname_len, str_key, str_key_len, "w+b", 0, &error, 1))) { zend_throw_exception_ex(spl_ce_BadMethodCallException, 0, "Entry %s cannot be created: %s", str_key, error); efree(error); @@ -1700,7 +1704,7 @@ static int phar_build(zend_object_iterator *iter, void *puser) /* {{{ */ data->internal_file->offset_abs = data->internal_file->offset = php_stream_tell(p_obj->fp); data->fp = NULL; php_stream_copy_to_stream_ex(fp, p_obj->fp, PHP_STREAM_COPY_ALL, &contents_len); - if (ZEND_SIZE_T_UINT_OVFL(contents_len)) { + if (contents_len > phar_entry_max_size(phar_obj->archive)) { zend_throw_exception_ex(spl_ce_BadMethodCallException, 0, "Entry %s is too large to be stored in a phar archive", str_key); data->internal_file->is_deleted = 1; phar_entry_delref(data); @@ -1766,7 +1770,7 @@ static int phar_build(zend_object_iterator *iter, void *puser) /* {{{ */ PHP_METHOD(Phar, buildFromDirectory) { char *error; - bool apply_reg = 0; + bool apply_reg = 0, pass_fp_is_new; zval arg, arg2, iter, iteriter, regexiter; struct _phar_t pass; zend_string *dir, *regex = NULL; @@ -1840,14 +1844,20 @@ PHP_METHOD(Phar, buildFromDirectory) pass.base = dir; pass.count = 0; pass.ret = return_value; - pass.fp = php_stream_fopen_tmpfile(); - if (pass.fp == NULL) { - zval_ptr_dtor(&iteriter); - if (apply_reg) { - zval_ptr_dtor(®exiter); + pass.fp = phar_obj->archive->ufp; + pass_fp_is_new = pass.fp == NULL; + if (pass_fp_is_new) { + pass.fp = php_stream_fopen_tmpfile(); + if (pass.fp == NULL) { + zval_ptr_dtor(&iteriter); + if (apply_reg) { + zval_ptr_dtor(®exiter); + } + zend_throw_exception_ex(phar_ce_PharException, 0, "phar \"%s\" unable to create temporary file", phar_obj->archive->fname); + RETURN_THROWS(); } - zend_throw_exception_ex(phar_ce_PharException, 0, "phar \"%s\" unable to create temporary file", phar_obj->archive->fname); - RETURN_THROWS(); + } else { + php_stream_seek(pass.fp, 0, SEEK_END); } if (phar_obj->archive->is_persistent && FAILURE == phar_copy_on_write(&(phar_obj->archive))) { @@ -1855,7 +1865,9 @@ PHP_METHOD(Phar, buildFromDirectory) if (apply_reg) { zval_ptr_dtor(®exiter); } - php_stream_close(pass.fp); + if (pass_fp_is_new) { + php_stream_close(pass.fp); + } zend_throw_exception_ex(phar_ce_PharException, 0, "phar \"%s\" is persistent, unable to copy on write", phar_obj->archive->fname); RETURN_THROWS(); } @@ -1880,7 +1892,7 @@ PHP_METHOD(Phar, buildFromDirectory) if (apply_reg) { zval_ptr_dtor(®exiter); } - php_stream_close(pass.fp); + phar_obj->archive->ufp = pass.fp; } } /* }}} */ @@ -1925,10 +1937,15 @@ PHP_METHOD(Phar, buildFromIterator) pass.base = base; pass.ret = return_value; pass.count = 0; - pass.fp = php_stream_fopen_tmpfile(); + pass.fp = phar_obj->archive->ufp; if (pass.fp == NULL) { - zend_throw_exception_ex(phar_ce_PharException, 0, "phar \"%s\": unable to create temporary file", phar_obj->archive->fname); - RETURN_THROWS(); + pass.fp = php_stream_fopen_tmpfile(); + if (pass.fp == NULL) { + zend_throw_exception_ex(phar_ce_PharException, 0, "phar \"%s\": unable to create temporary file", phar_obj->archive->fname); + RETURN_THROWS(); + } + } else { + php_stream_seek(pass.fp, 0, SEEK_END); } if (SUCCESS == spl_iterator_apply(obj, (spl_iterator_apply_func_t) phar_build, (void *) &pass)) { @@ -1939,7 +1956,7 @@ PHP_METHOD(Phar, buildFromIterator) efree(error); } } else { - php_stream_close(pass.fp); + phar_obj->archive->ufp = pass.fp; } } /* }}} */ @@ -3705,9 +3722,9 @@ static void phar_add_file(phar_archive_data **pphar, zend_string *file_name, con php_stream_from_zval_no_verify(contents_file, zresource); } - if ((content && ZEND_SIZE_T_UINT_OVFL(ZSTR_LEN(content))) + if ((content && ZSTR_LEN(content) > phar_entry_max_size(*pphar)) || (contents_file && !contents_file->readfilters.head && php_stream_stat(contents_file, &ssb) != -1 - && (int64_t) ssb.sb.st_size - php_stream_tell(contents_file) > (int64_t) UINT32_MAX)) { + && (int64_t) ssb.sb.st_size - php_stream_tell(contents_file) > (int64_t) phar_entry_max_size(*pphar))) { zend_throw_exception_ex(spl_ce_BadMethodCallException, 0, "Entry %s is too large to be stored in a phar archive", filename); goto finish; } @@ -3743,7 +3760,7 @@ static void phar_add_file(phar_archive_data **pphar, zend_string *file_name, con } php_stream_copy_to_stream_ex(contents_file, data->fp, PHP_STREAM_COPY_ALL, &contents_len); } - if (ZEND_SIZE_T_UINT_OVFL(contents_len)) { + if (contents_len > phar_entry_max_size(*pphar)) { zend_throw_exception_ex(spl_ce_BadMethodCallException, 0, "Entry %s is too large to be stored in a phar archive", filename); data->internal_file->is_deleted = 1; phar_entry_delref(data); diff --git a/ext/phar/stream.c b/ext/phar/stream.c index 0bc1f0f50cd7..f7b594f45ff1 100644 --- a/ext/phar/stream.c +++ b/ext/phar/stream.c @@ -447,7 +447,7 @@ static ssize_t phar_stream_write(php_stream *stream, const char *buf, size_t cou { phar_entry_data *data = (phar_entry_data *) stream->abstract; - if ((uint64_t) data->position + count > UINT32_MAX) { + if ((uint64_t) data->position + count > phar_entry_max_size(data->phar)) { php_error_docref(NULL, E_WARNING, "phar error: Could not write %d characters to \"%s\" in phar \"%s\", entry is too large", (int) count, data->internal_file->filename, data->phar->fname); return -1; } diff --git a/ext/phar/tar.c b/ext/phar/tar.c index 52541e62290e..8827e1b469e5 100644 --- a/ext/phar/tar.c +++ b/ext/phar/tar.c @@ -830,8 +830,8 @@ 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); - /* the reader rejects sizes above UINT32_MAX - 511, as they overflow the 512 byte padding */ - if (entry->uncompressed_filesize > UINT32_MAX - 511 + /* the reader rejects sizes above this limit, as they overflow the 512 byte padding */ + if (entry->uncompressed_filesize > phar_entry_max_size(entry->phar) || 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, file \"%s\" is too large for tar file format", entry->phar->fname, entry->filename); diff --git a/ext/phar/tests/gh25053.phpt b/ext/phar/tests/gh25053.phpt index 07895e1209a5..8f5290d1eaf4 100644 --- a/ext/phar/tests/gh25053.phpt +++ b/ext/phar/tests/gh25053.phpt @@ -8,6 +8,8 @@ if (!getenv('RUN_RESOURCE_HEAVY_TESTS')) die('skip resource-heavy test'); if (PHP_INT_SIZE != 8) die('skip this test is for 64bit platforms only'); if (getenv('SKIP_SLOW_TESTS')) die('skip slow tests excluded by request'); ?> +--ENV-- +TEST_TIMEOUT=600 --INI-- phar.readonly=0 --FILE-- @@ -41,6 +43,47 @@ try { } var_dump(isset($tar['big'])); +$tar = new PharData(__DIR__ . '/gh25053-iterator-retry.tar'); +$small = fopen('php://temp', 'w+b'); +fwrite($small, 'small'); +rewind($small); +try { + $tar->buildFromIterator(new ArrayIterator(['small' => $small, 'big' => $src])); +} catch (BadMethodCallException $e) { + echo $e->getMessage(), "\n"; +} +var_dump($tar['small']->getContent()); +$next = fopen('php://temp', 'w+b'); +fwrite($next, 'next'); +rewind($next); +$tar->buildFromIterator(new ArrayIterator(['next' => $next])); +var_dump($tar['small']->getContent()); +var_dump($tar['next']->getContent()); + +$tar = new PharData(__DIR__ . '/gh25053-iterator-read-retry.tar'); +$first = fopen('php://temp', 'w+b'); +fwrite($first, 'first'); +rewind($first); +$second = fopen('php://temp', 'w+b'); +fwrite($second, 'second'); +rewind($second); +try { + $tar->buildFromIterator(new ArrayIterator(['first' => $first, 'second' => $second, 'big' => $src])); +} catch (BadMethodCallException $e) { + echo $e->getMessage(), "\n"; +} +$third = fopen('php://temp', 'w+b'); +fwrite($third, 'third'); +rewind($third); +$iterator = (function () use ($tar, $third) { + var_dump($tar['first']->getContent()); + yield 'third' => $third; +})(); +$tar->buildFromIterator($iterator); +var_dump($tar['first']->getContent()); +var_dump($tar['second']->getContent()); +var_dump($tar['third']->getContent()); + $phar = new Phar(__DIR__ . '/gh25053-4.phar'); $phar->addFromString('small', 'x'); var_dump(copy($src, 'phar://' . __DIR__ . '/gh25053-4.phar/big')); @@ -62,11 +105,14 @@ $fp = fopen($src, 'rb'); fseek($fp, 102); try { $tar['big'] = $fp; -} catch (PharException $e) { +} catch (BadMethodCallException $e) { echo $e->getMessage(), "\n"; } +$tar->addFromString('big', 'small'); +var_dump($tar['big']->getContent()); $zip = new PharData(__DIR__ . '/gh25053-7.zip'); +$zip->addFromString('keep', 'old'); $fp = fopen($src, 'rb'); fseek($fp, 12); try { @@ -74,6 +120,10 @@ try { } 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-- --ENV-- TEST_TIMEOUT=600 diff --git a/ext/phar/zip.c b/ext/phar/zip.c index e99327b7e9b1..d19df472a26b 100644 --- a/ext/phar/zip.c +++ b/ext/phar/zip.c @@ -1475,7 +1475,15 @@ int phar_zip_flush(phar_archive_data *phar, zend_string *user_stub, bool is_defa if (error) { spprintf(error, 4096, "phar zip flush of \"%s\" failed: archive is too large for zip file format", phar->fname); } - goto notemperror; + 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);