Commit 5a326b9a432 for php.net
commit 5a326b9a432a8d7df6f814692391bbda15eeff4c
Author: Eyüp Can Akman <eyupcanakman@gmail.com>
Date: Fri Aug 14 00:56:49 2026 +0800
Fix GH-17787: ZipArchive stream truncates when the archive is freed (#22555)
ZipArchive stream methods return streams that borrow the archive zip_t. Keep the archive object alive until the stream closes so reads can complete and pending changes are committed when the object is destroyed.
Closes #22555
diff --git a/NEWS b/NEWS
index 0d9e4279207..8ddc64181e4 100644
--- a/NEWS
+++ b/NEWS
@@ -63,6 +63,10 @@ PHP NEWS
- Readline:
. Fixed class constant completion in the interactive shell. (Weilin Du)
+- Zip:
+ . Fixed bug GH-17787 (ZipArchive stream stops reading early when the archive
+ is freed while the stream is still open). (Eyüp Can Akman)
+
- Session:
. Fix corruption in mod_mm. (ndossche)
. Fixed bug GH-23043 (broken session id code can cause zend_mm_heap
diff --git a/ext/zip/php_zip.c b/ext/zip/php_zip.c
index fecb9396ace..712468509cb 100644
--- a/ext/zip/php_zip.c
+++ b/ext/zip/php_zip.c
@@ -3035,7 +3035,7 @@ static void php_zip_get_stream(INTERNAL_FUNCTION_PARAMETERS, int type, bool acce
PHP_ZIP_STAT_INDEX(intern, index, flags, sb);
}
- stream = php_stream_zip_open(intern, &sb, mode, flags STREAMS_CC);
+ stream = php_stream_zip_open(Z_ZIP_P(self), &sb, mode, flags STREAMS_CC);
if (stream) {
php_stream_to_zval(stream, return_value);
} else {
diff --git a/ext/zip/php_zip.h b/ext/zip/php_zip.h
index 18f5c4eddd1..84fdd21e347 100644
--- a/ext/zip/php_zip.h
+++ b/ext/zip/php_zip.h
@@ -92,7 +92,7 @@ static inline ze_zip_object *php_zip_fetch_object(zend_object *obj) {
#define Z_ZIP_P(zv) php_zip_fetch_object(Z_OBJ_P((zv)))
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);
extern const php_stream_wrapper php_stream_zip_wrapper;
diff --git a/ext/zip/tests/gh17787.phpt b/ext/zip/tests/gh17787.phpt
new file mode 100644
index 00000000000..f82cfa1145b
--- /dev/null
+++ b/ext/zip/tests/gh17787.phpt
@@ -0,0 +1,80 @@
+--TEST--
+GH-17787 (ZipArchive stream stops reading early when the archive is freed while the stream is open)
+--EXTENSIONS--
+zip
+--FILE--
+<?php
+$name = __DIR__ . '/gh17787.zip';
+$data = str_repeat("The quick brown fox jumps over the lazy dog.\n", 4000);
+
+$zip = new ZipArchive;
+$zip->open($name, ZipArchive::CREATE | ZipArchive::OVERWRITE);
+$zip->addFromString('entry.txt', $data);
+$zip->close();
+
+$zip = new ZipArchive;
+$zip->open($name, ZipArchive::RDONLY);
+$stream = $zip->getStreamIndex(0, ZipArchive::FL_UNCHANGED);
+
+// Free the archive while the stream is still open
+$zip = null;
+
+var_dump(stream_get_contents($stream) === $data);
+fclose($stream);
+
+// Same with getStreamName()
+$zip = new ZipArchive;
+$zip->open($name, ZipArchive::RDONLY);
+$stream = $zip->getStreamName('entry.txt', ZipArchive::FL_UNCHANGED);
+$zip = null;
+
+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"
diff --git a/ext/zip/zip_stream.c b/ext/zip/zip_stream.c
index 496fd9cdfeb..7f6990962d0 100644
--- a/ext/zip/zip_stream.c
+++ b/ext/zip/zip_stream.c
@@ -34,6 +34,7 @@ struct php_zip_stream_data_t {
struct zip_file *zf;
size_t cursor;
php_stream *stream;
+ ze_zip_object *owner;
};
#define STREAM_DATA_FROM_STREAM() \
@@ -101,6 +102,12 @@ static int php_zip_ops_close(php_stream *stream, int close_handle)
self->za = NULL;
}
}
+
+ /* the pinned object ref is tied to self, so release it regardless of close_handle */
+ if (self->owner) {
+ OBJ_RELEASE(&self->owner->zo);
+ self->owner = NULL;
+ }
efree(self);
stream->abstract = NULL;
return EOF;
@@ -234,8 +241,9 @@ const php_stream_ops php_stream_zipio_ops = {
};
/* {{{ php_stream_zip_open */
-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)
{
+ struct zip *arch = obj->za;
struct zip_file *zf = NULL;
php_stream *stream = NULL;
@@ -254,6 +262,9 @@ php_stream *php_stream_zip_open(struct zip *arch, struct zip_stat *sb, const cha
self->zf = zf;
self->stream = NULL;
self->cursor = 0;
+ /* keep the archive object alive while the stream borrows its zip_t */
+ self->owner = obj;
+ GC_ADDREF(&obj->zo);
#if LIBZIP_ATLEAST(1,9,1)
if (zip_file_is_seekable(zf) > 0) {
stream = php_stream_alloc(&php_stream_zipio_seek_ops, self, NULL, mode);
@@ -339,6 +350,7 @@ php_stream *php_stream_zip_opener(php_stream_wrapper *wrapper,
self->zf = zf;
self->stream = NULL;
self->cursor = 0;
+ self->owner = NULL;
#if LIBZIP_ATLEAST(1,9,1)
if (zip_file_is_seekable(zf) > 0) {
stream = php_stream_alloc(&php_stream_zipio_seek_ops, self, NULL, mode);