Commit 132403de396 for php.net
commit 132403de39604a7e7a3fb04276dde54d6326c9aa
Author: David Carlier <devnexen@gmail.com>
Date: Fri Sep 18 05:04:30 2026 +0100
ext/zip: ZipArchive::close() use-after-free from a progress or cancel callback.
Fix #23747
ZipArchive::close() called from a progress or cancel callback ran a
nested zip_close() that failed, then zip_discard() freed the archive
while the outer zip_close() from close() or open() was still using it.
Track the close in progress and throw an Error from close() and open()
meanwhile.
Close GH-23749
diff --git a/NEWS b/NEWS
index c38d108d840..c8eb609cc2a 100644
--- a/NEWS
+++ b/NEWS
@@ -47,6 +47,8 @@ PHP NEWS
- Zip:
. Fixed ZipArchive::extractTo() ignoring files given in a non-list array.
(David Carlier)
+ . Fixed bug GH-23747 (ZipArchive::close() use-after-free from a progress or
+ cancel callback). (David Carlier)
24 Sep 2026, PHP 8.4.26
diff --git a/ext/zip/php_zip.c b/ext/zip/php_zip.c
index 682736d4fe7..42a5a527cfc 100644
--- a/ext/zip/php_zip.c
+++ b/ext/zip/php_zip.c
@@ -1582,8 +1582,16 @@ PHP_METHOD(ZipArchive, open)
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;
- if (zip_close(intern) != 0) {
+ 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;
@@ -1668,7 +1676,14 @@ PHP_METHOD(ZipArchive, close)
ze_obj = Z_ZIP_P(self);
+ if (ze_obj->archive->close) {
+ zend_throw_error(NULL, "Already being closed");
+ 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 */
diff --git a/ext/zip/php_zip.h b/ext/zip/php_zip.h
index 8385674a1cf..e5930c87aad 100644
--- a/ext/zip/php_zip.h
+++ b/ext/zip/php_zip.h
@@ -73,6 +73,7 @@ typedef struct _php_zip_archive {
/* libzip reads buffers until the archive is closed, can outlive the object. */
char **buffers;
int buffers_cnt;
+ bool close;
#ifdef HAVE_PROGRESS_CALLBACK
zval progress_callback;
#endif
diff --git a/ext/zip/tests/gh23747.phpt b/ext/zip/tests/gh23747.phpt
new file mode 100644
index 00000000000..24e977410e1
--- /dev/null
+++ b/ext/zip/tests/gh23747.phpt
@@ -0,0 +1,102 @@
+--TEST--
+GH-23747 (ZipArchive::close() from inside a progress or cancel callback causes segv)
+--CREDITS--
+djarfluka
+--EXTENSIONS--
+zip
+--SKIPIF--
+<?php
+if (!method_exists(ZipArchive::class, 'registerProgressCallback')) {
+ die('skip progress callbacks are not supported');
+}
+if (!method_exists(ZipArchive::class, 'registerCancelCallback')) {
+ die('skip cancel callbacks are not supported');
+}
+?>
+--FILE--
+<?php
+function populate(ZipArchive $zip, string $filename): void {
+ $zip->open($filename, ZipArchive::CREATE | ZipArchive::OVERWRITE);
+ for ($i = 0; $i < 64; $i++) {
+ $zip->addFromString("f$i.txt", str_repeat('x', 2000));
+ }
+}
+
+$filename = __DIR__ . '/gh23747.zip';
+
+$zip = new ZipArchive();
+populate($zip, $filename);
+$zip->registerProgressCallback(0.0, function ($rate) use ($zip) {
+ static $done = false;
+ if (!$done) {
+ $done = true;
+ try {
+ $zip->close();
+ } catch (Error $e) {
+ echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+ }
+ }
+});
+var_dump($zip->close());
+
+$zip = new ZipArchive();
+populate($zip, $filename);
+$zip->registerCancelCallback(function () use ($zip) {
+ static $done = false;
+ if (!$done) {
+ $done = true;
+ try {
+ $zip->close();
+ } catch (Error $e) {
+ echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+ }
+ }
+ return 0;
+});
+var_dump($zip->close());
+
+$zip = new ZipArchive();
+populate($zip, $filename);
+$zip->registerProgressCallback(0.0, function ($rate) use ($zip) {
+ static $done = false;
+ if (!$done) {
+ $done = true;
+ try {
+ $zip->close();
+ } catch (Error $e) {
+ echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+ }
+ }
+});
+var_dump($zip->open($filename));
+var_dump($zip->count());
+
+$zip = new ZipArchive();
+populate($zip, $filename);
+$zip->registerProgressCallback(0.0, function ($rate) use ($zip, $filename) {
+ static $done = false;
+ if (!$done) {
+ $done = true;
+ try {
+ $zip->open($filename);
+ } catch (Error $e) {
+ echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+ }
+ }
+});
+var_dump($zip->close());
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . '/gh23747.zip');
+?>
+--EXPECT--
+Error: Already being closed
+bool(true)
+Error: Already being closed
+bool(true)
+Error: Already being closed
+bool(true)
+int(64)
+Error: Already being closed
+bool(true)