Fix GH-17787: ZipArchive stream truncates when the archive is freed - #22555
Fix GH-17787: ZipArchive stream truncates when the archive is freed#22555eyupcanakman wants to merge 3 commits into
Conversation
…e is freed getStream(), getStreamIndex() and getStreamName() return a stream that reads from the ZipArchive's underlying zip_t but keeps no reference to it. When the object is freed while the stream is still open, the destructor closes the zip_t and reads stop partway through. Keep the archive object alive for the stream's lifetime so the borrowed handle stays valid until the stream is closed.
|
is this PR still needed? current master running the test from this PR: which is the correct error. |
|
That output is the bug the PR fixes. gh17787.phpt fails on current master the same way for me and passes with the patch applied, and cmb69 opened the issue back up because the zip_t shouldn't be closed while a stream is still open. |
LamentXU123
left a comment
There was a problem hiding this comment.
Could we have a test for getStreamName?
|
|
||
| php_stream *php_stream_zip_opener(php_stream_wrapper *wrapper, const char *path, const char *mode, int options, zend_string **opened_path, php_stream_context *context STREAMS_DC); | ||
| php_stream *php_stream_zip_open(struct zip *arch, struct zip_stat *sb, const char *mode, zip_flags_t flags STREAMS_DC); | ||
| php_stream *php_stream_zip_open(ze_zip_object *obj, struct zip_stat *sb, const char *mode, zip_flags_t flags STREAMS_DC); |
There was a problem hiding this comment.
I would say it is probably possible to avoid this change in a stable branch.
There was a problem hiding this comment.
php_stream_zip_open has a single caller in php_zip.c, the symbol isn't exported (no PHPAPI, hidden visibility) and php_zip.h isn't installed, so the prototype change isn't visible outside the tree. Can keep the old signature and add a second function that takes the object if you'd rather avoid it on 8.4.
There was a problem hiding this comment.
No you re right.
Would it be possible however to update the test as follow ?
diff --git a/ext/zip/tests/gh17787.phpt b/ext/zip/tests/gh17787.phpt
index c05558191cf..f82cfa1145b 100644
--- a/ext/zip/tests/gh17787.phpt
+++ b/ext/zip/tests/gh17787.phpt
@@ -30,11 +30,51 @@
var_dump(stream_get_contents($stream) === $data);
fclose($stream);
+
+// Same with getStream()
+$zip = new ZipArchive;
+$zip->open($name, ZipArchive::RDONLY);
+$stream = $zip->getStream('entry.txt');
+$zip = null;
+
+var_dump(stream_get_contents($stream) === $data);
+fclose($stream);
+
+// Pending changes are still committed once the last stream is closed
+$name = __DIR__ . '/gh17787_write.zip';
+
+$zip = new ZipArchive;
+var_dump($zip->open($name, ZipArchive::CREATE | ZipArchive::OVERWRITE));
+$zip->addFromString('first.txt', 'first');
+$zip->close();
+
+$zip = new ZipArchive;
+var_dump($zip->open($name));
+$zip->addFromString('second.txt', 'second');
+$stream = $zip->getStreamName('first.txt', ZipArchive::FL_UNCHANGED);
+$zip = null;
+
+var_dump(stream_get_contents($stream));
+fclose($stream);
+
+$zip = new ZipArchive;
+var_dump($zip->open($name, ZipArchive::RDONLY));
+var_dump($zip->numFiles);
+var_dump($zip->getFromName('second.txt'));
+$zip->close();
?>
--CLEAN--
<?php
@unlink(__DIR__ . '/gh17787.zip');
+@unlink(__DIR__ . '/gh17787_write.zip');
?>
--EXPECT--
bool(true)
bool(true)
+bool(true)
+bool(true)
+bool(true)
+string(5) "first"
+bool(true)
+int(2)
+string(6) "second"There was a problem hiding this comment.
Added both cases. The pending-changes one is a better test than I expected, since unpatched it does not just fail the read, it drops the queued write (numFiles 1, getFromName false).
One thing I hit while stress testing the patch. Pinning the object makes a subclass that stores its own stream uncollectable, because php_zip_get_gc cannot see the GC_ADDREF at zip_stream.c:266.
class Z extends ZipArchive { public $stream; }
$z = new Z; $z->open($f, ZipArchive::RDONLY);
$z->stream = $z->getStreamIndex(1, ZipArchive::FL_UNCHANGED);
$w = WeakReference::create($z); unset($z);
var_dump(gc_collect_cycles(), $w->get() instanceof Z); // int(0), bool(true)Without the fix it prints bool(false). Over 500 iterations that is 700KB and one leaked fd each time. It needs the stream reachable from the object and never closed, and fclose recovers everything.
I tried refcounting the zip_t instead so the stream never points back. That fixes the leak and breaks the fix, because addFromString hands libzip an object owned buffer via zip_source_buffer(..., 0) which free_storage frees, so the queued entry writes back garbage.
The same prototype with addFile is fine, so it is the buffer ownership. Looks like the pin is needed here and a real fix means moving that ownership. Want me to open a separate issue for it?
|
Added a |
|
The circleCI failure is unrelated. @devnexen Do you think this is good to merge? |
* PHP-8.4: Fix phpGH-17787: ZipArchive stream truncates when the archive is freed (php#22555)
* PHP-8.4: [skip ci] Fix NEWS entry in #22555
* PHP-8.5: [skip ci] Fix NEWS entry in #22555
ZipArchive::getStreamIndex()(andgetStream/getStreamName) hands back a stream that reads from the archive's underlyingzip_twithout keeping a reference to it. When theZipArchiveobject is freed while the stream is still open, as in the report where$zgets reassigned inside the read loop, the object destructor closes thezip_tand the stream stops reading early. cmb69 diagnosed this on the issue. The fix keeps the archive object alive for the stream's lifetime, so the borrowed handle stays valid until the stream is closed.One case this does not cover is calling
$zip->close()or reopening the same object while a stream is open, which hits the same truncation by a different route. I can extend the fix to those paths if you prefer.