From be41aefcd9a4e1f9ee7214b646789e5ca6749776 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ey=C3=BCp=20Can=20Akman?= Date: Thu, 2 Jul 2026 14:57:07 +0300 Subject: [PATCH] ext/zip: Fix GH-17787: ZipArchive stream truncates when the archive 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. --- NEWS | 4 ++++ ext/zip/php_zip.c | 2 +- ext/zip/php_zip.h | 2 +- ext/zip/tests/gh17787.phpt | 30 ++++++++++++++++++++++++++++++ ext/zip/zip_stream.c | 14 +++++++++++++- 5 files changed, 49 insertions(+), 3 deletions(-) create mode 100644 ext/zip/tests/gh17787.phpt diff --git a/NEWS b/NEWS index 6ac3e8db3939..94a6c9f8a157 100644 --- a/NEWS +++ b/NEWS @@ -63,6 +63,10 @@ PHP NEWS . Fixed bug GH-22395 (base_convert() outputs at most 64 characters). (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) + 02 Jul 2026, PHP 8.4.23 - Core: diff --git a/ext/zip/php_zip.c b/ext/zip/php_zip.c index b29ba2c370b8..d9ba931d4ae2 100644 --- a/ext/zip/php_zip.c +++ b/ext/zip/php_zip.c @@ -3024,7 +3024,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..06331e81acc2 --- /dev/null +++ b/ext/zip/tests/gh17787.phpt @@ -0,0 +1,30 @@ +--TEST-- +GH-17787 (ZipArchive stream stops reading early when the archive is freed while the stream is open) +--EXTENSIONS-- +zip +--FILE-- +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); +?> +--CLEAN-- + +--EXPECT-- +bool(true) 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);