[php-src] master: Merge branch 'PHP-8.4' into PHP-8.5

Weilin Du <[email protected]>
Newsgroups gmane.comp.php.cvs.general
Message-ID <[email protected]>
Author: Weilin Du (LamentXU123)
Date: 2026-08-14T01:07:08+08:00

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

Merge branch 'PHP-8.4' into PHP-8.5

* PHP-8.4:
  Fix GH-17787: ZipArchive stream truncates when the archive is freed (#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 ab010e5a0ec7..6eebf9d3d37d 100644
--- a/NEWS
+++ b/NEWS
@@ -10,6 +10,10 @@ PHP                                                                        NEWS
   . Fixed the interactive shell not waiting for the pager process to exit.
     (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)
+
 
 27 Aug 2026, PHP 8.5.10
 
diff --git a/ext/zip/php_zip.c b/ext/zip/php_zip.c
index d2f1f0326eee..2a9318b6ee40 100644
--- a/ext/zip/php_zip.c
+++ b/ext/zip/php_zip.c
@@ -2987,7 +2987,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 cccdc6d1c4d0..382e4a70b984 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.