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/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 a1736f221e27..84e74acbeed7 100644 --- a/ext/phar/phar_object.c +++ b/ext/phar/phar_object.c @@ -1640,6 +1640,34 @@ static int phar_build(zend_object_iterator *iter, void *puser) /* {{{ */ return ZEND_HASH_APPLY_KEEP; } + /* 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) 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) { + 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 (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); @@ -1676,6 +1704,29 @@ 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 (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); + + 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) { @@ -1719,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; @@ -1793,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))) { @@ -1808,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(); } @@ -1833,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; } } /* }}} */ @@ -1878,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)) { @@ -1892,7 +1956,7 @@ PHP_METHOD(Phar, buildFromIterator) efree(error); } } else { - php_stream_close(pass.fp); + phar_obj->archive->ufp = pass.fp; } } /* }}} */ @@ -3654,6 +3718,17 @@ 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 && 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) 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; + } + 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); @@ -3678,13 +3753,19 @@ 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; } php_stream_copy_to_stream_ex(contents_file, data->fp, PHP_STREAM_COPY_ALL, &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); + 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..f7b594f45ff1 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 > 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; + } + 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/tar.c b/ext/phar/tar.c index 198f157ca1fd..8827e1b469e5 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 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, 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 new file mode 100644 index 000000000000..8f5290d1eaf4 --- /dev/null +++ b/ext/phar/tests/gh25053.phpt @@ -0,0 +1,167 @@ +--TEST-- +GH-25053 (Phar truncates entries of 4 GiB or more without error/warning) +--EXTENSIONS-- +phar +--SKIPIF-- + +--ENV-- +TEST_TIMEOUT=600 +--INI-- +phar.readonly=0 +--FILE-- +addFile($src, 'big'); +} catch (BadMethodCallException $e) { + echo $e->getMessage(), "\n"; +} +var_dump(isset($tar['big'])); + +$tar = new PharData(__DIR__ . '/gh25053-2.tar'); +try { + $tar->buildFromIterator(new ArrayIterator(['big' => $src])); +} catch (BadMethodCallException $e) { + echo $e->getMessage(), "\n"; +} +var_dump(isset($tar['big'])); + +$tar = new PharData(__DIR__ . '/gh25053-3.tar'); +try { + $tar->buildFromIterator(new ArrayIterator(['big' => fopen($src, 'rb')])); +} catch (BadMethodCallException $e) { + echo $e->getMessage(), "\n"; +} +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')); + +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 (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 { + $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-- +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) +Entry big is too large to be stored in a phar archive +string(5) "small" +string(5) "small" +string(4) "next" +Entry big is too large to be stored in a phar archive +string(5) "first" +string(5) "first" +string(6) "second" +string(5) "third" + +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) +Entry big is too large to be stored in a phar archive +string(5) "small" +phar zip flush of "%sgh25053-7.zip" failed: archive is too large for zip file format +string(3) "old" +string(3) "old" +string(5) "small" 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_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) 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; } diff --git a/ext/phar/zip.c b/ext/phar/zip.c index f051c35a09a7..d19df472a26b 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 */ @@ -1465,6 +1469,23 @@ int phar_zip_flush(phar_archive_data *phar, zend_string *user_stub, bool is_defa } /* 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", 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);