Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion ext/phar/phar.c
Original file line number Diff line number Diff line change
Expand Up @@ -2786,7 +2786,15 @@ 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);
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;
Expand Down
6 changes: 4 additions & 2 deletions ext/phar/tar.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
70 changes: 70 additions & 0 deletions ext/phar/tests/gh25053_compressed_size.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
--TEST--
GH-25053: Phar rejects compressed entries whose size exceeds the 32-bit size field
--EXTENSIONS--
phar
zlib
hash
--SKIPIF--
<?php
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');
if (PHP_OS === 'FreeBSD') die('skip exceeds FreeBSD CI hard memory limit');
?>
--ENV--
TEST_TIMEOUT=600
--INI--
phar.readonly=0
--FILE--
<?php
// Deterministic incompressible data. The repeated block is larger than the
// DEFLATE history window, so repeating it does not make it compressible.
$block = '';
for ($i = 0; $i < 32768; $i++) {
$block .= hash('sha256', pack('V', $i), true);
}

$src = __DIR__ . '/gh25053_compressed_size.bin';
$fp = fopen($src, 'wb');
// Leave room for ZIP headers so adding the uncompressed entry is valid.
// DEFLATE overhead then pushes the compressed size above UINT32_MAX.
$remaining = 2**32 - 65536;
while ($remaining > 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--
<?php
@unlink(__DIR__ . '/gh25053_compressed_size.bin');
@unlink(__DIR__ . '/gh25053_compressed_size.phar');
@unlink(__DIR__ . '/gh25053_compressed_size.zip');
?>
--EXPECT--
phar
PharException
zip
PharException
66 changes: 66 additions & 0 deletions ext/phar/tests/gh25053_format_limits.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
--TEST--
GH-25053: Phar rejects mounts, tar entries and zip archives beyond the 32-bit format limits
--EXTENSIONS--
phar
--SKIPIF--
<?php
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--
<?php
$src = __DIR__ . '/gh25053_format_limits.big';
$f = fopen($src, 'w');
ftruncate($f, 2**32 + 1);
fclose($f);

$phar = new Phar(__DIR__ . '/gh25053_format_limits.phar');
$phar->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--
<?php
@unlink(__DIR__ . '/gh25053_format_limits.big');
@unlink(__DIR__ . '/gh25053_format_limits.phar');
@unlink(__DIR__ . '/gh25053_format_limits.tar');
@unlink(__DIR__ . '/gh25053_format_limits.zip');
?>
--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"
5 changes: 5 additions & 0 deletions ext/phar/util.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
21 changes: 20 additions & 1 deletion ext/phar/zip.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -1441,6 +1445,21 @@ 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) {
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);
Expand Down
Loading