Commit 0c3799a4cc0 for php.net

commit 0c3799a4cc07dfd768320a186bc6ed815fd95db9
Merge: 98b02fff3de f49b94e96b0
Author: Weilin Du <weilindu@php.net>
Date:   Tue Aug 25 00:43:29 2026 +0800

    Merge branch 'PHP-8.4' into PHP-8.5

    * PHP-8.4:
      Fix GH-23276: Collect ZipArchive subclasses holding their own streams (#23282)

diff --cc NEWS
index bb7c34323ce,5607f31081b..247fcb340ef
--- a/NEWS
+++ b/NEWS
@@@ -41,13 -45,15 +41,15 @@@ PH
    . Fixed a leak when a persistent connection failed a liveness check
      with no other live PDO handle. (iliaal)

 -- Standard:
 -  . Fixed a memory leak in array_merge_recursive() when the recursive merge of
 -    an object converted to an array fails. (David Carlier)
 +- Readline:
 +  . 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)
+   . Fixed bug GH-23276 (ZipArchive subclass storing its own stream cannot be
+     garbage collected). (Weilin Du, ndossche)
 -  . Fixed ZipArchive::extractTo() and ZipArchive::getFrom*() reporting success
 -    on corrupted entries. (David Carlier)

  - SAPI:
    . Fixed fuzzer targets failing to build in isolation. (Mrmaxmeier)
diff --cc ext/zip/php_zip.c
index 6071f065f51,5e640df9a10..74c8db3fefd
--- a/ext/zip/php_zip.c
+++ b/ext/zip/php_zip.c
@@@ -1035,23 -1085,25 +1042,23 @@@ static HashTable *php_zip_get_propertie
  /* }}} */

  #ifdef HAVE_PROGRESS_CALLBACK
 -static void _php_zip_progress_callback_free(void *ptr)
 +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 (!Z_ISUNDEF(archive->progress_callback)) {
 -		zval_ptr_dtor(&archive->progress_callback);
 -		ZVAL_UNDEF(&archive->progress_callback);
++	if (ZEND_FCC_INITIALIZED(archive->progress_callback)) {
++		zend_fcc_dtor(&archive->progress_callback);
  	}
  }
  #endif

  #ifdef HAVE_CANCEL_CALLBACK
 -static void _php_zip_cancel_callback_free(void *ptr)
 +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 (!Z_ISUNDEF(archive->cancel_callback)) {
 -		zval_ptr_dtor(&archive->cancel_callback);
 -		ZVAL_UNDEF(&archive->cancel_callback);
++	if (ZEND_FCC_INITIALIZED(archive->cancel_callback)) {
++		zend_fcc_dtor(&archive->cancel_callback);
  	}
  }
  #endif
@@@ -1075,17 -1139,43 +1094,43 @@@ void php_zip_archive_release(php_zip_ar
  	}

  #ifdef HAVE_PROGRESS_CALLBACK
- 	/* if not properly called by libzip */
- 	php_zip_progress_callback_free(intern);
+ 	/* In case libzip did not invoke the callback state destructor. */
 -	_php_zip_progress_callback_free(archive);
++	php_zip_progress_callback_free(archive);
  #endif

  #ifdef HAVE_CANCEL_CALLBACK
- 	/* if not properly called by libzip */
- 	php_zip_cancel_callback_free(intern);
+ 	/* In case libzip did not invoke the callback state destructor. */
 -	_php_zip_cancel_callback_free(archive);
++	php_zip_cancel_callback_free(archive);
  #endif

- 	intern->za = NULL;
+ 	if (archive->buffers) {
+ 		for (int i = 0; i < archive->buffers_cnt; i++) {
+ 			efree(archive->buffers[i]);
+ 		}
+ 		efree(archive->buffers);
+ 	}
+
+ 	efree(archive);
+ }
+
+ /* The caller must close or discard released_za before detaching it. */
+ static void php_zip_object_detach_archive(ze_zip_object *ze_obj, struct zip *released_za)
+ {
+ 	ZEND_ASSERT(ze_obj->archive != NULL);
+ 	ZEND_ASSERT(ze_obj->archive->za == released_za);
+ 	ze_obj->archive->za = NULL;
+ 	php_zip_archive_release(ze_obj->archive);
+ 	ze_obj->archive = NULL;
+ }
+
+ static void php_zip_object_free_storage(zend_object *object) /* {{{ */
+ {
+ 	ze_zip_object * intern = php_zip_fetch_object(object);
+
+ 	if (intern->archive) {
+ 		php_zip_archive_release(intern->archive);
+ 		intern->archive = NULL;
+ 	}
  	zend_object_std_dtor(&intern->zo);

  	if (intern->filename) {
@@@ -3081,43 -3180,41 +3135,44 @@@ PHP_METHOD(ZipArchive, getStream
  }

  #ifdef HAVE_PROGRESS_CALLBACK
 -static void _php_zip_progress_callback(zip_t *arch, double state, void *ptr)
 +static void php_zip_progress_callback(zip_t *arch, double state, void *ptr)
  {
  	zval cb_args[1];
- 	ze_zip_object *obj = ptr;
 -	zval cb_retval;
+ 	php_zip_archive *archive = ptr;

  	ZVAL_DOUBLE(&cb_args[0], state);
- 	zend_call_known_fcc(&obj->progress_callback, NULL, 1, cb_args, NULL);
 -	if (call_user_function(EG(function_table), NULL, &archive->progress_callback, &cb_retval, 1, cb_args) == SUCCESS && !Z_ISUNDEF(cb_retval)) {
 -		zval_ptr_dtor(&cb_retval);
 -	}
++	zend_call_known_fcc(&archive->progress_callback, NULL, 1, cb_args, NULL);
  }

  /* {{{ register a progression callback: void callback(double state); */
  PHP_METHOD(ZipArchive, registerProgressCallback)
  {
  	struct zip *intern;
 -	zval *self = ZEND_THIS;
  	double rate;
 -	zend_fcall_info fci;
 +	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, &fci, &fcc) == FAILURE) {
 +	if (zend_parse_parameters(ZEND_NUM_ARGS(), "dF", &rate, &dummy_fci, &fcc) == FAILURE) {
  		RETURN_THROWS();
  	}
 -
 -	ZIP_FROM_OBJECT(intern, self);
--
 -	archive = Z_ZIP_P(self)->archive;
 +	/* 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)) {
++	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);
 -	ZVAL_COPY(&archive->progress_callback, &fci.function_name);
++	zend_fcc_dup(&archive->progress_callback, &fcc);

  	RETURN_TRUE;
  }
@@@ -3125,55 -3222,41 +3180,57 @@@
  #endif

  #ifdef HAVE_CANCEL_CALLBACK
 -static int _php_zip_cancel_callback(zip_t *arch, void *ptr)
 +static int php_zip_cancel_callback(zip_t *arch, void *ptr)
  {
  	zval cb_retval;
- 	ze_zip_object *obj = ptr;
 -	int retval = 0;
+ 	php_zip_archive *archive = ptr;

- 	zend_call_known_fcc(&obj->cancel_callback, &cb_retval, 0, NULL, NULL);
 -	if (call_user_function(EG(function_table), NULL, &archive->cancel_callback, &cb_retval, 0, NULL) == SUCCESS && !Z_ISUNDEF(cb_retval)) {
 -		retval = zval_get_long(&cb_retval);
++	zend_call_known_fcc(&archive->cancel_callback, &cb_retval, 0, NULL, NULL);
 +	if (Z_ISUNDEF(cb_retval)) {
 +		/* Cancel if an exception has been thrown */
 +		return -1;
 +	}
 +	bool failed;
 +	zend_long retval = zval_try_get_long(&cb_retval, &failed);
 +	if (failed) {
 +		zend_type_error("Return value of callback provided to ZipArchive::registerCancelCallback()"
 +			" must be of type int, %s returned", zend_zval_value_name(&cb_retval));
  		zval_ptr_dtor(&cb_retval);
 +		return -1;
  	}
 +	zval_ptr_dtor(&cb_retval);

 -	return retval;
 +	return (int) retval;
  }

  /* {{{ register a progression callback: int callback(double state); */
  PHP_METHOD(ZipArchive, registerCancelCallback)
  {
  	struct zip *intern;
 -	zval *self = ZEND_THIS;
 -	zend_fcall_info fci;
 +	zend_fcall_info dummy_fci;
  	zend_fcall_info_cache fcc;
+ 	php_zip_archive *archive;
 -	if (zend_parse_parameters(ZEND_NUM_ARGS(), "f", &fci, &fcc) == FAILURE) {
 +	ze_zip_object *obj;
 +	if (zend_parse_parameters(ZEND_NUM_ARGS(), "F", &dummy_fci, &fcc) == FAILURE) {
  		RETURN_THROWS();
  	}

 -	ZIP_FROM_OBJECT(intern, self);
 -
 -	archive = Z_ZIP_P(self)->archive;
 +	/* 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)) {
++	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);
 -	ZVAL_COPY(&archive->cancel_callback, &fci.function_name);
++	zend_fcc_dup(&archive->cancel_callback, &fcc);

  	RETURN_TRUE;
  }
diff --cc ext/zip/php_zip.h
index 382e4a70b98,8385674a1cf..e761364d0cc
--- a/ext/zip/php_zip.h
+++ b/ext/zip/php_zip.h
@@@ -65,23 -65,33 +65,33 @@@ 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;
+ 	uint32_t refcount;
+ 	/* libzip reads buffers until the archive is closed, can outlive the object. */
  	char **buffers;
- 	HashTable *prop_handler;
- 	char *filename;
- 	int filename_len;
  	int buffers_cnt;
- 	zip_int64_t last_id;
- 	int err_zip;
- 	int err_sys;
  #ifdef HAVE_PROGRESS_CALLBACK
 -	zval progress_callback;
 +	zend_fcall_info_cache progress_callback;
  #endif
  #ifdef HAVE_CANCEL_CALLBACK
 -	zval 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;
+ 	int filename_len;
+ 	zip_int64_t last_id;
+ 	int err_zip;
+ 	int err_sys;
  	zend_object zo;
  } ze_zip_object;