[PHP-CVS] [php-src] PHP-8.5: Fix GH-17787: ZipArchive stream truncates when the archive is freed (#22555)

[email protected] (Eyüp Can Akman via Weilin Du)
Newsgroups php.cvs
Message-ID <[email protected]>
Author: Eyüp Can Akman (eyupcanakman)
Committer: Weilin Du (LamentXU123)
Date: 2026-08-14T01:06:39+08:00

Commit: https://github.com/php/php-src/commit/5a326b9a432a8d7df6f814692391bbda15eeff4c
Raw diff: https://github.com/php/php-src/commit/5a326b9a432a8d7df6f814692391bbda15eeff4c.diff

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

Changed paths:
  A  ext/zip/tests/gh17787.phpt
  M  NEWS
  M  ext/zip/php_zip.c
  M  ext/zip/php_zip.h
  M  ext/zip/zip_stream.c


Diff:

diff --git a/NEWS b/NEWS
index 0d9e42792078..8ddc64181e45 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 fecb9396ace9..712468509cbc 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 18f5c4eddd1e..84fdd21e3476 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 000000000000..f82cfa1145bd
--- /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 496fd9cdfeb8..7f6990962d00 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);
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.