diff --git a/ext/phar/phar_object.c b/ext/phar/phar_object.c index a1736f221e27..977bf087f0d8 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) 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 (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 > UINT32_MAX) { + 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) > UINT32_MAX) + || (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; + } + 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 > UINT32_MAX) { + 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..0bc1f0f50cd7 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_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/tests/gh25053.phpt b/ext/phar/tests/gh25053.phpt new file mode 100644 index 000000000000..4ed27377aeb9 --- /dev/null +++ b/ext/phar/tests/gh25053.phpt @@ -0,0 +1,127 @@ +--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')); + +$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()); +?> +--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) +int(1) 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)