Skip to content

Commit e90b48c

Browse files
committed
Fix bug #74154: Phar extractTo creates empty files
The current code causes the phar entry to remain in the fname cache. This would be fine for uncompressed phars, but is a problem for compressed phars when they try to reopen the file pointer. The reopen code will try to use the compressed file pointer as if it were an uncompressed file pointer. In that case, for the given test, the file offsets are out of bounds for the compressed file pointer because they are the uncompressed offsets. This results in empty files. In other cases, it's possible to read compressed parts of the file that don't belong to that particular file. To solve this, we simply remove the phar entry from the fname cache if the file pointer was closed but the phar is compressed. This will make sure that reopening the phar will not go through the cache and instead opens up a fresh file pointer with the right decompression settings. Closes GH-20754.
1 parent 10bbd95 commit e90b48c

File tree

4 files changed

+57
-9
lines changed

4 files changed

+57
-9
lines changed

NEWS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ PHP NEWS
3737
(ndossche)
3838
. Fix SplFileInfo::openFile() in write mode. (ndossche)
3939
. Fix build on legacy OpenSSL 1.1.0 systems. (Giovanni Giacobbi)
40+
. Fixed bug #74154 (Phar extractTo creates empty files). (ndossche)
4041

4142
- SPL:
4243
. Fixed bug GH-20678 (resource created by GlobIterator crashes with fclose()).

ext/phar/phar.c

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -260,20 +260,26 @@ bool phar_archive_delref(phar_archive_data *phar) /* {{{ */
260260
PHAR_G(last_phar) = NULL;
261261
PHAR_G(last_phar_name) = PHAR_G(last_alias) = NULL;
262262

263+
/* This is a new phar that has perhaps had an alias/metadata set, but has never been flushed. */
264+
bool remove_fname_cache = !zend_hash_num_elements(&phar->manifest);
265+
263266
if (phar->fp && (!(phar->flags & PHAR_FILE_COMPRESSION_MASK) || !phar->alias)) {
264267
/* close open file handle - allows removal or rename of
265268
the file on windows, which has greedy locking
266-
only close if the archive was not already compressed. If it
267-
was compressed, then the fp does not refer to the original file.
268-
We're also closing compressed files to save resources,
269-
but only if the archive isn't aliased. */
269+
only close if the archive was not already compressed.
270+
We're also closing compressed files to save resources, but only if the archive isn't aliased.
271+
If it was compressed, then the fp does not refer to the original compressed file:
272+
it refers to the **uncompressed** filtered file stream.
273+
Therefore, upon closing a compressed file we need to invalidate the phar archive such
274+
that the code that reopens the phar will not try to use the **compressed** file as if it was uncompressed.
275+
That would result in treating compressed file data as if it were compressed and using uncompressed file offsets
276+
on the compressed file. */
270277
php_stream_close(phar->fp);
271278
phar->fp = NULL;
279+
remove_fname_cache |= phar->flags & PHAR_FILE_COMPRESSION_MASK;
272280
}
273281

274-
if (!zend_hash_num_elements(&phar->manifest)) {
275-
/* this is a new phar that has perhaps had an alias/metadata set, but has never
276-
been flushed */
282+
if (remove_fname_cache) {
277283
if (zend_hash_str_del(&(PHAR_G(phar_fname_map)), phar->fname, phar->fname_len) != SUCCESS) {
278284
phar_destroy_phar_data(phar);
279285
}

ext/phar/tests/bug74154.phpt

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
--TEST--
2+
Bug #74154 (Phar extractTo creates empty files)
3+
--EXTENSIONS--
4+
phar
5+
--FILE--
6+
<?php
7+
8+
$dir = __DIR__.'/bug74154';
9+
mkdir($dir);
10+
file_put_contents("$dir/1.txt", str_repeat('h', 64));
11+
file_put_contents("$dir/2.txt", str_repeat('i', 64));
12+
$phar = new PharData(__DIR__.'/bug74154.tar');
13+
$phar->buildFromDirectory($dir);
14+
15+
$compPhar = $phar->compress(Phar::GZ);
16+
unset($phar); //make sure that test.tar is closed
17+
unlink(__DIR__.'/bug74154.tar');
18+
unset($compPhar); //make sure that test.tar.gz is closed
19+
$extractingPhar = new PharData(__DIR__.'/bug74154.tar.gz');
20+
$extractingPhar->extractTo($dir.'_out');
21+
22+
var_dump(file_get_contents($dir.'_out/1.txt'));
23+
var_dump(file_get_contents($dir.'_out/2.txt'));
24+
25+
?>
26+
--CLEAN--
27+
<?php
28+
$dir = __DIR__.'/bug74154';
29+
@unlink("$dir/1.txt");
30+
@unlink("$dir/2.txt");
31+
@rmdir($dir);
32+
@unlink($dir.'_out/1.txt');
33+
@unlink($dir.'_out/2.txt');
34+
@rmdir($dir.'_out');
35+
@unlink(__DIR__.'/bug74154.tar');
36+
@unlink(__DIR__.'/bug74154.tar.gz');
37+
?>
38+
--EXPECT--
39+
string(64) "hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh"
40+
string(64) "iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii"

ext/phar/tests/tar/bug70417.phpt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,11 @@ $filename = __DIR__ . '/bug70417.tar';
1010
$resBefore = count(get_resources());
1111
$arch = new PharData($filename);
1212
$arch->addFromString('foo', 'bar');
13+
$arch->addFromString('foo2', 'baz');
1314
$arch->compress(Phar::GZ);
1415
unset($arch);
1516
$resAfter = count(get_resources());
16-
var_dump($resBefore === $resAfter);
17+
var_dump($resAfter - $resBefore);
1718
?>
1819
--CLEAN--
1920
<?php
@@ -22,4 +23,4 @@ $filename = __DIR__ . '/bug70417.tar';
2223
@unlink("$filename.gz");
2324
?>
2425
--EXPECT--
25-
bool(true)
26+
int(0)

0 commit comments

Comments
 (0)