Commit d975bbb6c48 for php.net

commit d975bbb6c48df0f49b4fc85165beedca81b3ca42
Merge: 6db10b644cc 8ceef7f8129
Author: David Carlier <devnexen@gmail.com>
Date:   Fri Sep 18 22:19:38 2026 +0100

    Merge branch 'PHP-8.5'

    * PHP-8.5:
      ext/zip: ZipArchive::close() use-after-free from a progress or cancel callback.

    # Conflicts:
    #       ext/zip/php_zip.c
    #       ext/zip/php_zip.h

diff --cc ext/zip/php_zip.c
index 93004531dac,12d004cc27d..88fdcaa9b03
--- a/ext/zip/php_zip.c
+++ b/ext/zip/php_zip.c
@@@ -634,73 -632,7 +634,85 @@@ static char * php_zipobj_get_zip_commen
  }
  /* }}} */

 -int php_zip_glob(char *pattern, int pattern_len, zend_long flags, zval *return_value) /* {{{ */
++static bool php_zipobj_closing(ze_zip_object *obj) /* {{{ */
++{
++	if (obj->archive && obj->archive->close) {
++		zend_throw_error(NULL, "Already being closed");
++		return true;
++	}
++	return false;
++}
++/* }}} */
++
 +/* Close and free the zip_t. If the archive was opened as a string, the
 + * final contents of the archive will be assigned to *out_str and that
 + * string will afterwards be owned by the caller.
 + *
 + * 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) /* {{{ */
 +{
 +	php_zip_archive *archive = obj->archive;
 +	struct zip *intern = archive ? archive->za : NULL;
 +	bool bailout = false;
 +	bool success = false;
 +
 +	if (intern) {
++		archive->close = true;
 +		int err = zip_close(intern);
++		archive->close = false;
 +		if (err) {
 +			php_error_docref(NULL, E_WARNING, "%s", zip_strerror(intern));
 +			/* Save error for property reader */
 +			zip_error_t *ziperr = zip_get_error(intern);
 +			obj->err_zip = zip_error_code_zip(ziperr);
 +			obj->err_sys = zip_error_code_system(ziperr);
 +			zip_error_fini(ziperr);
 +			zip_discard(intern);
 +		} else {
 +			obj->err_zip = 0;
 +			obj->err_sys = 0;
 +		}
 +		success = !err;
 +	}
 +
 +	/* if we have a filename, we need to free it */
 +	if (obj->filename) {
 +		/* clear cache as empty zip are not created but deleted */
 +		php_clear_stat_cache(1, obj->filename, obj->filename_len);
 +
 +		efree(obj->filename);
 +		obj->filename = NULL;
 +		obj->filename_len = 0;
 +	}
 +
 +	if (archive && archive->out_str) {
 +		if (out_str) {
 +			*out_str = archive->out_str;
 +		} else {
 +			zend_string_release(archive->out_str);
 +		}
 +		archive->out_str = NULL;
 +	} else {
 +		ZEND_ASSERT(!out_str);
 +	}
 +
 +	if (archive) {
 +		archive->za = NULL;
 +		bailout = archive->bailout_callback;
 +		archive->bailout_callback = false;
 +		obj->archive = NULL;
 +		bailout |= php_zip_archive_release(archive);
 +	}
 +
 +	if (bailout) {
 +		zend_bailout();
 +	}
 +
 +	return success;
 +}
 +/* }}} */
 +
 +static int php_zip_glob(zend_string *spattern, zend_long flags, zval *return_value) /* {{{ */
  {
  	int cwd_skip = 0;
  #ifdef ZTS
@@@ -1599,11 -1535,35 +1611,16 @@@ PHP_METHOD(ZipArchive, open
  		RETURN_FALSE;
  	}

 -	if (ze_obj->archive) {
 -		/* we already have an opened zip, free it */
 -		if (ze_obj->archive->close) {
 -			efree(resolved_path);
 -			zend_throw_error(NULL, "Already being closed");
 -			RETURN_THROWS();
 -		}
 -		intern = ze_obj->archive->za;
 -		ze_obj->archive->close = true;
 -		err = zip_close(intern);
 -		ze_obj->archive->close = false;
 -		if (err != 0) {
 -			php_error_docref(NULL, E_WARNING, "Empty string as source");
 -			efree(resolved_path);
 -			RETURN_FALSE;
 -		}
 -		php_zip_object_detach_archive(ze_obj, intern);
 -	}
 -	if (ze_obj->filename) {
 -		efree(ze_obj->filename);
 -		ze_obj->filename = NULL;
++	if (php_zipobj_closing(ze_obj)) {
++		efree(resolved_path);
++		RETURN_THROWS();
+ 	}
+
 +	/* If we already have an opened zip, free it */
 +	php_zipobj_close(ze_obj, NULL);
 +
  	/* open for write without option to empty the archive */
 -#ifdef ZIP_RDONLY
  	if ((flags & (ZIP_TRUNCATE | ZIP_RDONLY)) == 0) {
 -#else
 -	if ((flags & ZIP_TRUNCATE) == 0) {
 -#endif
  		zend_stat_t st = {0};

  		/* exists and is empty */
@@@ -1628,58 -1588,6 +1645,62 @@@
  }
  /* }}} */

 +/* {{{ Create new zip from a string, or a create an empty zip to be saved to a string */
 +PHP_METHOD(ZipArchive, openString)
 +{
 +	zend_string *buffer = NULL;
 +	zend_long flags = 0;
 +	zval *self = ZEND_THIS;
 +
 +	if (zend_parse_parameters(ZEND_NUM_ARGS(), "|Sl", &buffer, &flags) == FAILURE) {
 +		RETURN_THROWS();
 +	}
 +
 +	if (!buffer) {
 +		buffer = ZSTR_EMPTY_ALLOC();
 +	}
 +
 +	ze_zip_object *ze_obj = Z_ZIP_P(self);
 +	php_zip_archive *archive;
 +
++	if (php_zipobj_closing(ze_obj)) {
++		RETURN_THROWS();
++	}
++
 +	php_zipobj_close(ze_obj, NULL);
 +
 +	zip_error_t err;
 +	zip_error_init(&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);
 +	}
 +
 +	struct zip *intern = zip_open_from_source(zip_source, flags, &err);
 +	if (!intern) {
 +		ze_obj->err_zip = zip_error_code_zip(&err);
 +		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);
 +	}
 +
 +	archive->za = intern;
 +	archive->from_string = true;
 +	ze_obj->archive = archive;
 +	zip_error_fini(&err);
 +	RETURN_TRUE;
 +}
 +/* }}} */
 +
  /* {{{ Set the password for the active archive */
  PHP_METHOD(ZipArchive, setPassword)
  {
@@@ -1712,34 -1629,50 +1733,42 @@@ PHP_METHOD(ZipArchive, close

  	ZIP_FROM_OBJECT(intern, self);

 -	ze_obj = Z_ZIP_P(self);
 -
 -	if (ze_obj->archive->close) {
 -		zend_throw_error(NULL, "Already being closed");
++	if (php_zipobj_closing(Z_ZIP_P(self))) {
+ 		RETURN_THROWS();
+ 	}
+
 -	ze_obj->archive->close = true;
 -	err = zip_close(intern);
 -	ze_obj->archive->close = false;
 -	if (err) {
 -		php_error_docref(NULL, E_WARNING, "%s", zip_strerror(intern));
 -		/* Save error for property reader */
 -		#if LIBZIP_VERSION_MAJOR < 1
 -			zip_error_get(intern, &ze_obj->err_zip, &ze_obj->err_sys);
 -		#else
 -			{
 -			zip_error_t *ziperr;
 +	RETURN_BOOL(php_zipobj_close(Z_ZIP_P(self), NULL));
 +}
 +/* }}} */

 -			ziperr = zip_get_error(intern);
 -			ze_obj->err_zip = zip_error_code_zip(ziperr);
 -			ze_obj->err_sys = zip_error_code_system(ziperr);
 -			zip_error_fini(ziperr);
 -			}
 -		#endif
 -		zip_discard(intern);
 -	} else {
 -		ze_obj->err_zip = 0;
 -		ze_obj->err_sys = 0;
 -	}
 +/* {{{ close the zip archive and get the result as a string */
 +PHP_METHOD(ZipArchive, closeString)
 +{
 +	struct zip *intern;
 +	zval *self = ZEND_THIS;

 -	/* clear cache as empty zip are not created but deleted */
 -	php_clear_stat_cache(1, ze_obj->filename, ze_obj->filename_len);
 +	ZEND_PARSE_PARAMETERS_NONE();

 -	efree(ze_obj->filename);
 -	ze_obj->filename = NULL;
 -	ze_obj->filename_len = 0;
 -	php_zip_object_detach_archive(ze_obj, intern);
 +	ZIP_FROM_OBJECT(intern, self);

 -	if (!err) {
 -		RETURN_TRUE;
 -	} else {
 -		RETURN_FALSE;
 +	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();
 +	}
 +
++	if (php_zipobj_closing(Z_ZIP_P(self))) {
++		RETURN_THROWS();
++	}
++
 +	zend_string *ret = NULL;
 +	bool success = php_zipobj_close(Z_ZIP_P(self), &ret);
 +	ZEND_ASSERT(ret);
 +	if (success) {
 +		RETURN_STR(ret);
  	}
 +	zend_string_release(ret);
 +	RETURN_FALSE;
  }
  /* }}} */

diff --cc ext/zip/php_zip.h
index a67d2042d7c,38773659012..18f09b20850
--- a/ext/zip/php_zip.h
+++ b/ext/zip/php_zip.h
@@@ -69,9 -70,10 +69,10 @@@ typedef struct _ze_zip_read_rsrc
  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;
 -	int buffers_cnt;
 +	zend_string *out_str;
 +	bool from_string;
 +	bool bailout_callback;
+ 	bool close;
  #ifdef HAVE_PROGRESS_CALLBACK
  	zend_fcall_info_cache progress_callback;
  #endif