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

[email protected] (Weilin Du)
Newsgroups php.cvs
Message-ID <[email protected]>
Author: Weilin Du (LamentXU123)
Date: 2026-08-25T00:45:29+08:00

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

Merge branch 'PHP-8.5'

* PHP-8.5:
  Merge branch 'PHP-8.4' into PHP-8.5

Changed paths:
  A  ext/zip/tests/gh23276.phpt
  A  ext/zip/tests/gh23276_cancel_callback.phpt
  A  ext/zip/tests/gh23276_close_with_open_stream.phpt
  A  ext/zip/tests/gh23276_progress_callback.phpt
  A  ext/zip/tests/oo_addfromstring_reopen_memory.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 022d48973602..497293d89e33 100644
--- a/NEWS
+++ b/NEWS
@@ -75,6 +75,10 @@ PHP                                                                        NEWS
 - Readline:
   . Fixed class constant completion in the interactive shell. (Weilin Du)
 
+- Zip:
+  . Fixed bug GH-23276 (ZipArchive subclass storing its own stream cannot be
+    garbage collected). (Weilin Du, ndossche)
+
 - SAPI:
   . Fixed fuzzer targets failing to build in isolation. (Mrmaxmeier)
   . Fixed returns uninitialized value on LiteSpeed lsapi SAPI (Go Kudo)
diff --git a/ext/zip/php_zip.c b/ext/zip/php_zip.c
index f7a294425e6d..a7a3e340ecf1 100644
--- a/ext/zip/php_zip.c
+++ b/ext/zip/php_zip.c
@@ -339,6 +339,7 @@ static zend_result php_zip_add_file(ze_zip_object *obj, const char *filename, si
 	zip_flags_t flags
 ) /* {{{ */
 {
+	struct zip *za = php_zip_object_za(obj);
 	struct zip_source *zs;
 	char resolved_path[MAXPATHLEN];
 	php_stream_statbuf ssb;
@@ -364,33 +365,33 @@ static zend_result php_zip_add_file(ze_zip_object *obj, const char *filename, si
 			return FAILURE;
 		}
 		flags ^= ZIP_FL_OPEN_FILE_NOW;
-		zs = zip_source_filep(obj->za, fd, offset_start, offset_len);
+		zs = zip_source_filep(za, fd, offset_start, offset_len);
 		if (!zs) {
 			fclose(fd);
 			return FAILURE;
 		}
 	} else {
-		zs = zip_source_file(obj->za, resolved_path, offset_start, offset_len);
+		zs = zip_source_file(za, resolved_path, offset_start, offset_len);
 		if (!zs) {
 			return FAILURE;
 		}
 	}
 	/* Replace */
 	if (replace >= 0) {
-		if (zip_file_replace(obj->za, replace, zs, flags) < 0) {
+		if (zip_file_replace(za, replace, zs, flags) < 0) {
 			zip_source_free(zs);
 			return FAILURE;
 		}
-		zip_error_clear(obj->za);
+		zip_error_clear(za);
 		return SUCCESS;
 	}
 	/* Add */
-	obj->last_id = zip_file_add(obj->za, entry_name, zs, flags);
+	obj->last_id = zip_file_add(za, entry_name, zs, flags);
 	if (obj->last_id < 0) {
 		zip_source_free(zs);
 		return FAILURE;
 	}
-	zip_error_clear(obj->za);
+	zip_error_clear(za);
 	return SUCCESS;
 }
 /* }}} */
@@ -526,7 +527,7 @@ static zend_result php_zip_parse_options(HashTable *options, zip_options *opts)
 #define ZIP_FROM_OBJECT(intern, object) \
 	{ \
 		ze_zip_object *obj = Z_ZIP_P(object); \
-		intern = obj->za; \
+		intern = php_zip_object_za(obj); \
 		if (!intern) { \
 			zend_value_error("Invalid or uninitialized Zip object"); \
 			RETURN_THROWS(); \
@@ -565,12 +566,13 @@ static zend_result php_zip_parse_options(HashTable *options, zip_options *opts)
 
 static zend_long php_zip_status(ze_zip_object *obj) /* {{{ */
 {
+	struct zip *za = php_zip_object_za(obj);
 	zend_long zep = (zend_long)obj->err_zip; /* saved err if closed */
 
-	if (obj->za) {
+	if (za) {
 		zip_error_t *err;
 
-		err = zip_get_error(obj->za);
+		err = zip_get_error(za);
 		zep = (zend_long)zip_error_code_zip(err);
 		zip_error_fini(err);
 	}
@@ -586,12 +588,13 @@ static zend_long php_zip_last_id(ze_zip_object *obj) /* {{{ */
 
 static zend_long php_zip_status_sys(ze_zip_object *obj) /* {{{ */
 {
+	struct zip *za = php_zip_object_za(obj);
 	zend_long syp = (zend_long)obj->err_sys;  /* saved err if closed */
 
-	if (obj->za) {
+	if (za) {
 		zip_error_t *err;
 
-		err = zip_get_error(obj->za);
+		err = zip_get_error(za);
 		syp = (zend_long)zip_error_code_system(err);
 		zip_error_fini(err);
 	}
@@ -601,8 +604,10 @@ static zend_long php_zip_status_sys(ze_zip_object *obj) /* {{{ */
 
 static zend_long php_zip_get_num_files(ze_zip_object *obj) /* {{{ */
 {
-	if (obj->za) {
-		zip_int64_t num = zip_get_num_entries(obj->za, 0);
+	struct zip *za = php_zip_object_za(obj);
+
+	if (za) {
+		zip_int64_t num = zip_get_num_entries(za, 0);
 		return MIN(num, ZEND_LONG_MAX);
 	}
 	return 0;
@@ -621,8 +626,10 @@ static char * php_zipobj_get_filename(ze_zip_object *obj, int *len) /* {{{ */
 
 static char * php_zipobj_get_zip_comment(ze_zip_object *obj, int *len) /* {{{ */
 {
-	if (obj->za) {
-		return (char *)zip_get_archive_comment(obj->za, len, 0);
+	struct zip *za = php_zip_object_za(obj);
+
+	if (za) {
+		return (char *)zip_get_archive_comment(za, len, 0);
 	}
 	return NULL;
 }
@@ -635,7 +642,9 @@ static char * php_zipobj_get_zip_comment(ze_zip_object *obj, int *len) /* {{{ */
  * If out_str is NULL, the final string contents, if any, will be discarded. */
 static bool php_zipobj_close(ze_zip_object *obj, zend_string **out_str) /* {{{ */
 {
-	struct zip *intern = obj->za;
+	php_zip_archive *archive = obj->archive;
+	struct zip *intern = archive ? archive->za : NULL;
+	bool bailout = false;
 	bool success = false;
 
 	if (intern) {
@@ -665,22 +674,26 @@ static bool php_zipobj_close(ze_zip_object *obj, zend_string **out_str) /* {{{ *
 		obj->filename_len = 0;
 	}
 
-	if (obj->out_str) {
+	if (archive && archive->out_str) {
 		if (out_str) {
-			*out_str = obj->out_str;
+			*out_str = archive->out_str;
 		} else {
-			zend_string_release(obj->out_str);
+			zend_string_release(archive->out_str);
 		}
-		obj->out_str = NULL;
+		archive->out_str = NULL;
 	} else {
 		ZEND_ASSERT(!out_str);
 	}
 
-	obj->za = NULL;
-	obj->from_string = false;
+	if (archive) {
+		archive->za = NULL;
+		bailout = archive->bailout_callback;
+		archive->bailout_callback = false;
+		obj->archive = NULL;
+		bailout |= php_zip_archive_release(archive);
+	}
 
-	if (obj->bailout_callback) {
-		obj->bailout_callback = false;
+	if (bailout) {
 		zend_bailout();
 	}
 
@@ -1102,10 +1115,10 @@ static HashTable *php_zip_get_properties(zend_object *object)/* {{{ */
 #ifdef HAVE_PROGRESS_CALLBACK
 static void php_zip_progress_callback_free(void *ptr)
 {
-	ze_zip_object *obj = ptr;
+	php_zip_archive *archive = ptr;
 
-	if (ZEND_FCC_INITIALIZED(obj->progress_callback)) {
-		zend_fcc_dtor(&obj->progress_callback);
+	if (ZEND_FCC_INITIALIZED(archive->progress_callback)) {
+		zend_fcc_dtor(&archive->progress_callback);
 	}
 }
 #endif
@@ -1113,30 +1126,76 @@ static void php_zip_progress_callback_free(void *ptr)
 #ifdef HAVE_CANCEL_CALLBACK
 static void php_zip_cancel_callback_free(void *ptr)
 {
-	ze_zip_object *obj = ptr;
+	php_zip_archive *archive = ptr;
 
-	if (ZEND_FCC_INITIALIZED(obj->cancel_callback)) {
-		zend_fcc_dtor(&obj->cancel_callback);
+	if (ZEND_FCC_INITIALIZED(archive->cancel_callback)) {
+		zend_fcc_dtor(&archive->cancel_callback);
 	}
 }
 #endif
 
+static php_zip_archive *php_zip_archive_create(struct zip *za)
+{
+	php_zip_archive *archive = ecalloc(1, sizeof(php_zip_archive));
+
+	archive->za = za;
+	archive->refcount = 1;
+
+	return archive;
+}
+
+void php_zip_archive_addref(php_zip_archive *archive)
+{
+	ZEND_ASSERT(archive->refcount > 0);
+	archive->refcount++;
+}
+
+bool php_zip_archive_release(php_zip_archive *archive)
+{
+	ZEND_ASSERT(archive->refcount > 0);
+	if (--archive->refcount != 0) {
+		return false;
+	}
+
+	if (archive->za) {
+		if (zip_close(archive->za) != 0) {
+			if (!archive->bailout_callback) {
+				php_error_docref(NULL, E_WARNING, "Cannot destroy the zip context: %s", zip_strerror(archive->za));
+			}
+			zip_discard(archive->za);
+		}
+		archive->za = NULL;
+	}
+
+#ifdef HAVE_PROGRESS_CALLBACK
+	/* In case libzip did not invoke the callback state destructor. */
+	php_zip_progress_callback_free(archive);
+#endif
+
+#ifdef HAVE_CANCEL_CALLBACK
+	/* In case libzip did not invoke the callback state destructor. */
+	php_zip_cancel_callback_free(archive);
+#endif
+
+	if (archive->out_str) {
+		zend_string_release(archive->out_str);
+	}
+
+	bool bailout = archive->bailout_callback;
+	efree(archive);
+	return bailout;
+}
+
 static void php_zip_object_dtor(zend_object *object)
 {
 	zend_objects_destroy_object(object);
 
 	ze_zip_object *intern = php_zip_fetch_object(object);
 
-	if (intern->za) {
-		if (zip_close(intern->za) != 0) {
-			if (!intern->bailout_callback) {
-				php_error_docref(NULL, E_WARNING, "Cannot destroy the zip context: %s", zip_strerror(intern->za));
-			}
-			zip_discard(intern->za);
-		}
-		intern->za = NULL;
-		if (intern->bailout_callback) {
-			intern->bailout_callback = false;
+	if (intern->archive) {
+		bool bailout = php_zip_archive_release(intern->archive);
+		intern->archive = NULL;
+		if (bailout) {
 			zend_bailout();
 		}
 	}
@@ -1147,17 +1206,6 @@ static void php_zip_object_free_storage(zend_object *object) /* {{{ */
 	ze_zip_object * intern = php_zip_fetch_object(object);
 
 	php_zipobj_close(intern, NULL);
-
-#ifdef HAVE_PROGRESS_CALLBACK
-	/* if not properly called by libzip */
-	php_zip_progress_callback_free(intern);
-#endif
-
-#ifdef HAVE_CANCEL_CALLBACK
-	/* if not properly called by libzip */
-	php_zip_cancel_callback_free(intern);
-#endif
-
 	zend_object_std_dtor(&intern->zo);
 }
 /* }}} */
@@ -1576,8 +1624,7 @@ PHP_METHOD(ZipArchive, open)
 	}
 	ze_obj->filename = resolved_path;
 	ze_obj->filename_len = strlen(resolved_path);
-	ze_obj->za = intern;
-	ze_obj->from_string = false;
+	ze_obj->archive = php_zip_archive_create(intern);
 	RETURN_TRUE;
 }
 /* }}} */
@@ -1598,18 +1645,21 @@ PHP_METHOD(ZipArchive, openString)
 	}
 
 	ze_zip_object *ze_obj = Z_ZIP_P(self);
+	php_zip_archive *archive;
 
 	php_zipobj_close(ze_obj, NULL);
 
 	zip_error_t err;
 	zip_error_init(&err);
 
-	zip_source_t * zip_source = php_zip_create_string_source(buffer, &ze_obj->out_str, &err);
+	archive = php_zip_archive_create(NULL);
+	zip_source_t * zip_source = php_zip_create_string_source(buffer, &archive->out_str, &err);
 
 	if (!zip_source) {
 		ze_obj->err_zip = zip_error_code_zip(&err);
 		ze_obj->err_sys = zip_error_code_system(&err);
 		zip_error_fini(&err);
+		php_zip_archive_release(archive);
 		RETURN_LONG(ze_obj->err_zip);
 	}
 
@@ -1619,11 +1669,13 @@ PHP_METHOD(ZipArchive, openString)
 		ze_obj->err_sys = zip_error_code_system(&err);
 		zip_error_fini(&err);
 		zip_source_free(zip_source);
+		php_zip_archive_release(archive);
 		RETURN_LONG(ze_obj->err_zip);
 	}
 
-	ze_obj->from_string = true;
-	ze_obj->za = intern;
+	archive->za = intern;
+	archive->from_string = true;
+	ze_obj->archive = archive;
 	zip_error_fini(&err);
 	RETURN_TRUE;
 }
@@ -1675,7 +1727,7 @@ PHP_METHOD(ZipArchive, closeString)
 
 	ZIP_FROM_OBJECT(intern, self);
 
-	if (!Z_ZIP_P(self)->from_string) {
+	if (!Z_ZIP_P(self)->archive->from_string) {
 		zend_throw_error(NULL, "ZipArchive::closeString can only be called on "
 				"an archive opened with ZipArchive::openString");
 		RETURN_THROWS();
@@ -1737,12 +1789,14 @@ PHP_METHOD(ZipArchive, clearError)
 {
 	zval *self = ZEND_THIS;
 	ze_zip_object *ze_obj;
+	struct zip *za;
 
 	ZEND_PARSE_PARAMETERS_NONE();
 
 	ze_obj = Z_ZIP_P(self); /* not ZIP_FROM_OBJECT as we can use saved error after close */
-	if (ze_obj->za) {
-		zip_error_clear(ze_obj->za);
+	za = php_zip_object_za(ze_obj);
+	if (za) {
+		zip_error_clear(za);
 	} else {
 		ze_obj->err_zip = 0;
 		ze_obj->err_sys = 0;
@@ -1755,15 +1809,16 @@ PHP_METHOD(ZipArchive, getStatusString)
 {
 	zval *self = ZEND_THIS;
 	ze_zip_object *ze_obj;
+	struct zip *za;
 
 	ZEND_PARSE_PARAMETERS_NONE();
 
 	ze_obj = Z_ZIP_P(self); /* not ZIP_FROM_OBJECT as we can use saved error after close */
-
-	if (ze_obj->za) {
+	za = php_zip_object_za(ze_obj);
+	if (za) {
 		zip_error_t *err;
 
-		err = zip_get_error(ze_obj->za);
+		err = zip_get_error(za);
 		RETVAL_STRING(zip_error_strerror(err));
 		zip_error_fini(err);
 	} else {
@@ -1858,6 +1913,7 @@ static void php_zip_add_from_pattern(INTERNAL_FUNCTION_PARAMETERS, int type) /*
 	if (found > 0) {
 		zval *zval_file;
 		ze_zip_object *ze_obj = Z_ZIP_P(self);
+		struct zip *za = php_zip_object_za(ze_obj);
 
 		for (int i = 0; i < found; i++) {
 			zend_string *basename = NULL;
@@ -1920,14 +1976,14 @@ static void php_zip_add_from_pattern(INTERNAL_FUNCTION_PARAMETERS, int type) /*
 					RETURN_FALSE;
 				}
 				if (opts.comp_method >= 0) {
-					if (zip_set_file_compression(ze_obj->za, ze_obj->last_id, opts.comp_method, opts.comp_flags)) {
+					if (zip_set_file_compression(za, ze_obj->last_id, opts.comp_method, opts.comp_flags)) {
 						zend_array_destroy(Z_ARR_P(return_value));
 						RETURN_FALSE;
 					}
 				}
 #ifdef HAVE_ENCRYPTION
 				if (opts.enc_method >= 0) {
-					if (!php_zip_file_set_encryption(ze_obj->za, ze_obj->last_id, opts.enc_method, opts.enc_password)) {
+					if (!php_zip_file_set_encryption(za, ze_obj->last_id, opts.enc_method, opts.enc_password)) {
 						zend_array_destroy(Z_ARR_P(return_value));
 						RETURN_FALSE;
 					}
@@ -3084,9 +3140,9 @@ PHP_METHOD(ZipArchive, getStream)
 #ifdef HAVE_PROGRESS_CALLBACK
 static void php_zip_progress_callback(zip_t *arch, double state, void *ptr)
 {
-	ze_zip_object *obj = ptr;
+	php_zip_archive *archive = ptr;
 
-	if (UNEXPECTED(!EG(active) || obj->bailout_callback)) {
+	if (UNEXPECTED(!EG(active) || archive->bailout_callback)) {
 		return;
 	}
 
@@ -3095,9 +3151,9 @@ static void php_zip_progress_callback(zip_t *arch, double state, void *ptr)
 	ZVAL_DOUBLE(&cb_args[0], state);
 
 	zend_try {
-		zend_call_known_fcc(&obj->progress_callback, NULL, 1, cb_args, NULL);
+		zend_call_known_fcc(&archive->progress_callback, NULL, 1, cb_args, NULL);
 	} zend_catch {
-		obj->bailout_callback = true;
+		archive->bailout_callback = true;
 	} zend_end_try();
 }
 
@@ -3108,27 +3164,28 @@ PHP_METHOD(ZipArchive, registerProgressCallback)
 	double rate;
 	zend_fcall_info dummy_fci;
 	zend_fcall_info_cache fcc;
+	php_zip_archive *archive;
 	ze_zip_object *obj;
 
 	if (zend_parse_parameters(ZEND_NUM_ARGS(), "dF", &rate, &dummy_fci, &fcc) == FAILURE) {
 		RETURN_THROWS();
 	}
-
 	/* Inline ZIP_FROM_OBJECT(intern, self); */
 	obj = Z_ZIP_P(ZEND_THIS);
-	intern = obj->za;
-	if (!intern) { \
+	intern = php_zip_object_za(obj);
+	if (!intern) {
 		zend_value_error("Invalid or uninitialized Zip object");
 		zend_release_fcall_info_cache(&fcc);
 		RETURN_THROWS();
 	}
+	archive = obj->archive;
 
 	/* register */
-	if (zip_register_progress_callback_with_state(intern, rate, php_zip_progress_callback, php_zip_progress_callback_free, obj)) {
+	if (zip_register_progress_callback_with_state(intern, rate, php_zip_progress_callback, php_zip_progress_callback_free, archive)) {
 		zend_release_fcall_info_cache(&fcc);
 		RETURN_FALSE;
 	}
-	zend_fcc_dup(&obj->progress_callback, &fcc);
+	zend_fcc_dup(&archive->progress_callback, &fcc);
 
 	RETURN_TRUE;
 }
@@ -3139,16 +3196,16 @@ PHP_METHOD(ZipArchive, registerProgressCallback)
 static int php_zip_cancel_callback(zip_t *arch, void *ptr)
 {
 	zval cb_retval;
-	ze_zip_object *obj = ptr;
+	php_zip_archive *archive = ptr;
 
-	if (UNEXPECTED(!EG(active) || obj->bailout_callback)) {
+	if (UNEXPECTED(!EG(active) || archive->bailout_callback)) {
 		return 0;
 	}
 
 	zend_try {
-		zend_call_known_fcc(&obj->cancel_callback, &cb_retval, 0, NULL, NULL);
+		zend_call_known_fcc(&archive->cancel_callback, &cb_retval, 0, NULL, NULL);
 	} zend_catch {
-		obj->bailout_callback = true;
+		archive->bailout_callback = true;
 		/* Cancel if a bailout occurs to allow cleanup to happen */
 		return -1;
 	} zend_end_try();
@@ -3176,6 +3233,7 @@ PHP_METHOD(ZipArchive, registerCancelCallback)
 	struct zip *intern;
 	zend_fcall_info dummy_fci;
 	zend_fcall_info_cache fcc;
+	php_zip_archive *archive;
 	ze_zip_object *obj;
 	if (zend_parse_parameters(ZEND_NUM_ARGS(), "F", &dummy_fci, &fcc) == FAILURE) {
 		RETURN_THROWS();
@@ -3183,19 +3241,20 @@ PHP_METHOD(ZipArchive, registerCancelCallback)
 
 	/* Inline ZIP_FROM_OBJECT(intern, self); */
 	obj = Z_ZIP_P(ZEND_THIS);
-	intern = obj->za;
-	if (!intern) { \
+	intern = php_zip_object_za(obj);
+	if (!intern) {
 		zend_value_error("Invalid or uninitialized Zip object");
 		zend_release_fcall_info_cache(&fcc);
 		RETURN_THROWS();
 	}
+	archive = obj->archive;
 
 	/* register */
-	if (zip_register_cancel_callback_with_state(intern, php_zip_cancel_callback, php_zip_cancel_callback_free, obj)) {
+	if (zip_register_cancel_callback_with_state(intern, php_zip_cancel_callback, php_zip_cancel_callback_free, archive)) {
 		zend_release_fcall_info_cache(&fcc);
 		RETURN_FALSE;
 	}
-	zend_fcc_dup(&obj->cancel_callback, &fcc);
+	zend_fcc_dup(&archive->cancel_callback, &fcc);
 
 	RETURN_TRUE;
 }
diff --git a/ext/zip/php_zip.h b/ext/zip/php_zip.h
index a10b1910f2ad..a67d2042d7ca 100644
--- a/ext/zip/php_zip.h
+++ b/ext/zip/php_zip.h
@@ -64,17 +64,13 @@ typedef struct _ze_zip_read_rsrc {
 	zend_long zip_rsrc_handle;
 } zip_read_rsrc;
 
-/* Extends zend object */
-typedef struct _ze_zip_object {
+/* Refcounted holder for the native archive state.
+ * Owned by a ZipArchive object and streams opened from it. */
+typedef struct _php_zip_archive {
 	struct zip *za;
-	HashTable *prop_handler;
-	char *filename;
-	size_t filename_len;
+	uint32_t refcount;
 	zend_string *out_str;
 	bool from_string;
-	zip_int64_t last_id;
-	int err_zip;
-	int err_sys;
 	bool bailout_callback;
 #ifdef HAVE_PROGRESS_CALLBACK
 	zend_fcall_info_cache progress_callback;
@@ -82,16 +78,37 @@ typedef struct _ze_zip_object {
 #ifdef HAVE_CANCEL_CALLBACK
 	zend_fcall_info_cache cancel_callback;
 #endif
+} php_zip_archive;
+
+/* Extends zend object */
+typedef struct _ze_zip_object {
+	/* NULL when there is no open archive, non-NULL otherwise.
+	 * Owns one ref to the struct. */
+	php_zip_archive *archive;
+	HashTable *prop_handler;
+	char *filename;
+	size_t filename_len;
+	zip_int64_t last_id;
+	int err_zip;
+	int err_sys;
 	zend_object zo;
 } ze_zip_object;
 
 #define php_zip_fetch_object(obj) ZEND_CONTAINER_OF(obj, ze_zip_object, zo)
 
+/* The archive an object currently has open, or NULL. */
+static zend_always_inline struct zip *php_zip_object_za(const ze_zip_object *obj) {
+	return obj->archive ? obj->archive->za : NULL;
+}
+
 #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(ze_zip_object *obj, struct zip_stat *sb, const char *mode, zip_flags_t flags STREAMS_DC);
 
+void php_zip_archive_addref(php_zip_archive *archive);
+bool php_zip_archive_release(php_zip_archive *archive);
+
 extern const php_stream_wrapper php_stream_zip_wrapper;
 
 zip_source_t * php_zip_create_string_source(zend_string *str, zend_string **dest, zip_error_t *err);
diff --git a/ext/zip/tests/gh23276.phpt b/ext/zip/tests/gh23276.phpt
new file mode 100644
index 000000000000..ac005cd86354
--- /dev/null
+++ b/ext/zip/tests/gh23276.phpt
@@ -0,0 +1,95 @@
+--TEST--
+GH-23276 (ZipArchive subclass storing its own stream is collectable)
+--CREDITS--
+Eyüp Can Akman
+--EXTENSIONS--
+zip
+--FILE--
+<?php
+class Holder extends ZipArchive {
+    public $stream;
+    public $bag = [];
+}
+
+class ResurrectingHolder extends Holder {
+    public function __destruct() {
+        $GLOBALS['resurrected'] = $this;
+    }
+}
+
+function getEntryStream(ZipArchive $zip) {
+    $stream = $zip->getStream('entry.txt');
+    if (!is_resource($stream)) {
+        throw new Exception('Failed to open entry stream');
+    }
+    return $stream;
+}
+
+$filename = __DIR__ . '/gh23276.zip';
+
+$zip = new Holder;
+$zip->open($filename, ZipArchive::CREATE | ZipArchive::OVERWRITE);
+$zip->addFromString('entry.txt', 'contents');
+$zip->close();
+
+// An archive holding its own stream in a property must be destroyed.
+$zip->open($filename, ZipArchive::RDONLY);
+$zip->stream = getEntryStream($zip);
+$weakRef = WeakReference::create($zip);
+unset($zip);
+var_dump($weakRef->get());
+
+// Same through an indirect edge (property -> array -> resource).
+$zip = new Holder;
+$zip->open($filename, ZipArchive::RDONLY);
+$zip->bag[] = getEntryStream($zip);
+$weakRef = WeakReference::create($zip);
+unset($zip);
+var_dump($weakRef->get());
+
+// Two archives cross-holding each other's streams.
+$a = new Holder;
+$b = new Holder;
+$a->open($filename, ZipArchive::RDONLY);
+$b->open($filename, ZipArchive::RDONLY);
+$a->stream = getEntryStream($b);
+$b->stream = getEntryStream($a);
+$weakRef = WeakReference::create($a);
+unset($a, $b);
+var_dump($weakRef->get());
+
+// A resurrected object must retain a usable stream.
+$zip = new ResurrectingHolder;
+$zip->open($filename, ZipArchive::RDONLY);
+$zip->stream = getEntryStream($zip);
+unset($zip);
+var_dump($resurrected instanceof ResurrectingHolder);
+var_dump(stream_get_contents($resurrected->stream));
+fclose($resurrected->stream);
+unset($resurrected);
+
+// Externally held streams no longer keep the object alive. Closing one stream
+// must not close the archive while another stream still uses it.
+$zip = new Holder;
+$zip->open($filename, ZipArchive::RDONLY);
+$stream1 = getEntryStream($zip);
+$stream2 = getEntryStream($zip);
+$weakRef = WeakReference::create($zip);
+unset($zip);
+var_dump($weakRef->get());
+fclose($stream1);
+var_dump(stream_get_contents($stream2));
+fclose($stream2);
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . '/gh23276.zip');
+?>
+--EXPECT--
+NULL
+NULL
+NULL
+bool(true)
+string(8) "contents"
+NULL
+string(8) "contents"
diff --git a/ext/zip/tests/gh23276_cancel_callback.phpt b/ext/zip/tests/gh23276_cancel_callback.phpt
new file mode 100644
index 000000000000..5aa9057e6f39
--- /dev/null
+++ b/ext/zip/tests/gh23276_cancel_callback.phpt
@@ -0,0 +1,49 @@
+--TEST--
+GH-23276 (ZipArchive cancel callback outlives the object while a stream holds the archive)
+--EXTENSIONS--
+zip
+--SKIPIF--
+<?php
+if (!method_exists(ZipArchive::class, 'registerCancelCallback')) {
+    die('skip cancel callbacks are not supported');
+}
+?>
+--FILE--
+<?php
+$filename = __DIR__ . '/gh23276_cancel_callback.zip';
+
+$zip = new ZipArchive;
+$zip->open($filename, ZipArchive::CREATE | ZipArchive::OVERWRITE);
+$zip->addFromString('entry.txt', 'contents');
+$zip->close();
+
+$zip->open($filename);
+$callbackState = new stdClass;
+$callbackStateRef = WeakReference::create($callbackState);
+var_dump($zip->registerCancelCallback(
+    static function () use ($callbackState): int { return 0; },
+));
+$zip->addFromString('cancel.txt', 'late');
+$stream = $zip->getStream('entry.txt');
+if (!is_resource($stream)) {
+    throw new Exception('Failed to open entry stream');
+}
+$weakRef = WeakReference::create($zip);
+unset($callbackState, $zip);
+
+var_dump($weakRef->get());
+var_dump($callbackStateRef->get() !== null);
+var_dump(stream_get_contents($stream));
+fclose($stream);
+var_dump($callbackStateRef->get());
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . '/gh23276_cancel_callback.zip');
+?>
+--EXPECT--
+bool(true)
+NULL
+bool(true)
+string(8) "contents"
+NULL
diff --git a/ext/zip/tests/gh23276_close_with_open_stream.phpt b/ext/zip/tests/gh23276_close_with_open_stream.phpt
new file mode 100644
index 000000000000..e216c95f4eb9
--- /dev/null
+++ b/ext/zip/tests/gh23276_close_with_open_stream.phpt
@@ -0,0 +1,47 @@
+--TEST--
+GH-23276 (ZipArchive dropping its archive while a stream is open leaves the object collectable)
+--EXTENSIONS--
+zip
+--FILE--
+<?php
+$filename = __DIR__ . '/gh23276_close_with_open_stream.zip';
+
+$zip = new ZipArchive;
+$zip->open($filename, ZipArchive::CREATE | ZipArchive::OVERWRITE);
+$zip->addFromString('entry.txt', 'contents');
+$zip->close();
+
+$zip->open($filename, ZipArchive::RDONLY);
+$stream = $zip->getStream('entry.txt');
+var_dump($zip->close());
+$weakRef = WeakReference::create($zip);
+unset($zip);
+var_dump($weakRef->get());
+var_dump(stream_get_contents($stream));
+fclose($stream);
+
+$zip = new ZipArchive;
+$zip->open($filename, ZipArchive::RDONLY);
+$stream = $zip->getStream('entry.txt');
+var_dump($zip->open($filename, ZipArchive::RDONLY));
+$weakRef = WeakReference::create($zip);
+unset($zip);
+var_dump($weakRef->get());
+var_dump(stream_get_contents($stream));
+fclose($stream);
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . '/gh23276_close_with_open_stream.zip');
+?>
+--EXPECTF--
+bool(true)
+NULL
+
+Warning: stream_get_contents(): Zip stream error: %s in %s on line %d
+string(0) ""
+bool(true)
+NULL
+
+Warning: stream_get_contents(): Zip stream error: %s in %s on line %d
+string(0) ""
diff --git a/ext/zip/tests/gh23276_progress_callback.phpt b/ext/zip/tests/gh23276_progress_callback.phpt
new file mode 100644
index 000000000000..ff37b6233e5a
--- /dev/null
+++ b/ext/zip/tests/gh23276_progress_callback.phpt
@@ -0,0 +1,50 @@
+--TEST--
+GH-23276 (ZipArchive progress callback outlives the object while a stream holds the archive)
+--EXTENSIONS--
+zip
+--SKIPIF--
+<?php
+if (!method_exists(ZipArchive::class, 'registerProgressCallback')) {
+    die('skip progress callbacks are not supported');
+}
+?>
+--FILE--
+<?php
+$filename = __DIR__ . '/gh23276_progress_callback.zip';
+
+$zip = new ZipArchive;
+$zip->open($filename, ZipArchive::CREATE | ZipArchive::OVERWRITE);
+$zip->addFromString('entry.txt', 'contents');
+$zip->close();
+
+$zip->open($filename);
+$callbackState = new stdClass;
+$callbackStateRef = WeakReference::create($callbackState);
+var_dump($zip->registerProgressCallback(
+    0.5,
+    static function (float $rate) use ($callbackState): void {},
+));
+$zip->addFromString('progress.txt', 'late');
+$stream = $zip->getStream('entry.txt');
+if (!is_resource($stream)) {
+    throw new Exception('Failed to open entry stream');
+}
+$weakRef = WeakReference::create($zip);
+unset($callbackState, $zip);
+
+var_dump($weakRef->get());
+var_dump($callbackStateRef->get() !== null);
+var_dump(stream_get_contents($stream));
+fclose($stream);
+var_dump($callbackStateRef->get());
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . '/gh23276_progress_callback.zip');
+?>
+--EXPECT--
+bool(true)
+NULL
+bool(true)
+string(8) "contents"
+NULL
diff --git a/ext/zip/tests/oo_addfromstring_reopen_memory.phpt b/ext/zip/tests/oo_addfromstring_reopen_memory.phpt
new file mode 100644
index 000000000000..fb721c2d6273
--- /dev/null
+++ b/ext/zip/tests/oo_addfromstring_reopen_memory.phpt
@@ -0,0 +1,26 @@
+--TEST--
+ZipArchive::addFromString() buffers are released when the archive is closed
+--EXTENSIONS--
+zip
+--FILE--
+<?php
+$filename = __DIR__ . '/oo_addfromstring_reopen_memory.zip';
+$blob = str_repeat('q', 200000);
+
+$zip = new ZipArchive;
+$start = memory_get_usage();
+
+for ($i = 0; $i < 50; $i++) {
+    $zip->open($filename, ZipArchive::CREATE | ZipArchive::OVERWRITE);
+    $zip->addFromString('entry.txt', $blob);
+    $zip->close();
+}
+
+var_dump(memory_get_usage() - $start < 1000000);
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . '/oo_addfromstring_reopen_memory.zip');
+?>
+--EXPECT--
+bool(true)
diff --git a/ext/zip/zip_stream.c b/ext/zip/zip_stream.c
index 89c6a46e653d..a6665630e350 100644
--- a/ext/zip/zip_stream.c
+++ b/ext/zip/zip_stream.c
@@ -32,7 +32,7 @@ struct php_zip_stream_data_t {
 	struct zip_file *zf;
 	size_t cursor;
 	php_stream *stream;
-	ze_zip_object *owner;
+	php_zip_archive *archive;
 };
 
 #define STREAM_DATA_FROM_STREAM() \
@@ -82,6 +82,7 @@ static ssize_t php_zip_ops_write(php_stream *stream, const char *buf, size_t cou
 static int php_zip_ops_close(php_stream *stream, int close_handle)
 {
 	STREAM_DATA_FROM_STREAM();
+	bool bailout = false;
 	if (close_handle) {
 		if (self->zf) {
 			zip_fclose(self->zf);
@@ -94,13 +95,16 @@ static int php_zip_ops_close(php_stream *stream, int close_handle)
 		}
 	}
 
-	/* 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;
+	/* the archive ref is tied to self, so release it regardless of close_handle */
+	if (self->archive) {
+		bailout = php_zip_archive_release(self->archive);
+		self->archive = NULL;
 	}
 	efree(self);
 	stream->abstract = NULL;
+	if (bailout) {
+		zend_bailout();
+	}
 	return EOF;
 }
 /* }}} */
@@ -236,7 +240,7 @@ const php_stream_ops php_stream_zipio_ops = {
 /* {{{ php_stream_zip_open */
 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 *arch = php_zip_object_za(obj);
 	struct zip_file *zf = NULL;
 
 	php_stream *stream = NULL;
@@ -255,9 +259,9 @@ php_stream *php_stream_zip_open(ze_zip_object *obj, struct zip_stat *sb, const c
 			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);
+			/* keep the zip_t alive while the stream borrows it */
+			self->archive = obj->archive;
+			php_zip_archive_addref(self->archive);
 #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);
@@ -343,7 +347,7 @@ php_stream *php_stream_zip_opener(php_stream_wrapper *wrapper,
 			self->zf = zf;
 			self->stream = NULL;
 			self->cursor = 0;
-			self->owner = NULL;
+			self->archive = 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.