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);