[php-src] PHP-8.5: Merge branch 'PHP-8.4' into PHP-8.5
| From: | Weilin Du | Date: | Thu, 08 Oct 2026 10:03:17 +0000 |
| Subject: | [php-src] PHP-8.5: Merge branch 'PHP-8.4' into PHP-8.5 | ||
| Groups: | php.cvs | ||
| Request: | Send a blank email to php-cvs+get-142080@lists.php.net to get a copy of this message | ||
Author: Weilin Du (LamentXU123)
Date: 2026-10-08T17:35:57+08:00
Commit: https://github.com/php/php-src/commit/d5710aefdf95f119555264d3bb2d866ce60e9278
Raw diff: https://github.com/php/php-src/commit/d5710aefdf95f119555264d3bb2d866ce60e9278.diff
Merge branch 'PHP-8.4' into PHP-8.5
* PHP-8.4:
ext/zip: Fix use-after-free in the archive destructor path (#23779)
Changed paths:
A ext/zip/tests/gh23747_close_error.phpt
A ext/zip/tests/gh23747_dtor.phpt
M NEWS
M ext/zip/php_zip.c
Diff:
diff --git a/NEWS b/NEWS
index 061ecfdf74ce..9b485ee45596 100644
--- a/NEWS
+++ b/NEWS
@@ -7,6 +7,10 @@ PHP NEWS
root trace at the opcache.jit_max_root_traces limit, causing spurious
"Too few arguments" errors and crashes). (RV7PR)
+- Zip:
+ . Fixed use-after-free when re-entering ZipArchive during destruction or
+ a close warning, and rejected opening streams while closing. (jvoisin)
+
22 Oct 2026, PHP 8.5.12
- BCMath:
diff --git a/ext/zip/php_zip.c b/ext/zip/php_zip.c
index 12d004cc27d8..8fce5d4a2c28 100644
--- a/ext/zip/php_zip.c
+++ b/ext/zip/php_zip.c
@@ -1046,6 +1046,10 @@ static void php_zip_progress_callback_free(void *ptr)
{
php_zip_archive *archive = ptr;
+ if (UNEXPECTED(!EG(active))) {
+ return;
+ }
+
if (ZEND_FCC_INITIALIZED(archive->progress_callback)) {
zend_fcc_dtor(&archive->progress_callback);
}
@@ -1057,6 +1061,10 @@ static void php_zip_cancel_callback_free(void *ptr)
{
php_zip_archive *archive = ptr;
+ if (UNEXPECTED(!EG(active))) {
+ return;
+ }
+
if (ZEND_FCC_INITIALIZED(archive->cancel_callback)) {
zend_fcc_dtor(&archive->cancel_callback);
}
@@ -1087,7 +1095,12 @@ void php_zip_archive_release(php_zip_archive *archive)
}
if (archive->za) {
- if (zip_close(archive->za) != 0) {
+ /* Guard against a re-entrant close() or open() from a progress/cancel
+ * callback fired during zip_close(), which would run a nested zip_close()
+ * on the same archive (see ZipArchive::close()). */
+ archive->close = true;
+ int err = zip_close(archive->za);
+ if (err != 0) {
php_error_docref(NULL, E_WARNING, "Cannot destroy the zip context: %s",
zip_strerror(archive->za));
zip_discard(archive->za);
}
@@ -1119,6 +1132,7 @@ static void php_zip_object_detach_archive(ze_zip_object *ze_obj, struct zip
*rel
ZEND_ASSERT(ze_obj->archive != NULL);
ZEND_ASSERT(ze_obj->archive->za == released_za);
ze_obj->archive->za = NULL;
+ ze_obj->archive->close = false;
php_zip_archive_release(ze_obj->archive);
ze_obj->archive = NULL;
}
@@ -1638,7 +1652,6 @@ PHP_METHOD(ZipArchive, close)
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 */
@@ -3115,6 +3128,11 @@ static void php_zip_get_stream(INTERNAL_FUNCTION_PARAMETERS, int type, bool
acce
ZIP_FROM_OBJECT(intern, self);
+ if (Z_ZIP_P(self)->archive->close) {
+ zend_throw_error(NULL, "Already being closed");
+ RETURN_THROWS();
+ }
+
if (type) {
PHP_ZIP_STAT_PATH(intern, ZSTR_VAL(filename), ZSTR_LEN(filename), flags, sb);
} else {
diff --git a/ext/zip/tests/gh23747_close_error.phpt b/ext/zip/tests/gh23747_close_error.phpt
new file mode 100644
index 000000000000..2af621867e85
--- /dev/null
+++ b/ext/zip/tests/gh23747_close_error.phpt
@@ -0,0 +1,48 @@
+--TEST--
+GH-23747 (Re-entrant close from a ZipArchive close warning is rejected)
+--EXTENSIONS--
+zip
+--FILE--
+<?php
+$filename = __DIR__ . '/gh23747_close_error.zip';
+$source = __DIR__ . '/gh23747_close_error.txt';
+
+foreach (['close', 'destruct'] as $operation) {
+ echo $operation, ":\n";
+ file_put_contents($source, 'contents');
+ $zip = new ZipArchive();
+ $zip->open($filename, ZipArchive::CREATE | ZipArchive::OVERWRITE);
+ $zip->addFile($source, 'file.txt');
+ unlink($source);
+
+ $weak = WeakReference::create($zip);
+ set_error_handler(static function (int $errno, string $message) use ($weak): bool {
+ try {
+ $weak->get()->close();
+ } catch (Error $error) {
+ echo $error::class, ': ', $error->getMessage(), "\n";
+ }
+ return true;
+ });
+
+ if ($operation === 'close') {
+ var_dump($zip->close());
+ } else {
+ unset($zip);
+ echo "destroyed\n";
+ }
+ restore_error_handler();
+}
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . '/gh23747_close_error.zip');
+@unlink(__DIR__ . '/gh23747_close_error.txt');
+?>
+--EXPECT--
+close:
+Error: Already being closed
+bool(false)
+destruct:
+Error: Already being closed
+destroyed
diff --git a/ext/zip/tests/gh23747_dtor.phpt b/ext/zip/tests/gh23747_dtor.phpt
new file mode 100644
index 000000000000..5732eb98383f
--- /dev/null
+++ b/ext/zip/tests/gh23747_dtor.phpt
@@ -0,0 +1,58 @@
+--TEST--
+GH-23747 (Re-entrant operations during ZipArchive destruction are rejected)
+--EXTENSIONS--
+zip
+--SKIPIF--
+<?php
+if (!method_exists(ZipArchive::class, 'registerProgressCallback')) {
+ die('skip progress callbacks are not supported');
+}
+?>
+--FILE--
+<?php
+$filename = __DIR__ . '/gh23747_dtor.zip';
+
+foreach ([
+ ['close', []],
+ ['getStream', ['f0.txt']],
+ ['getStreamName', ['f0.txt']],
+ ['getStreamIndex', [0]],
+] as [$method, $arguments]) {
+ $zip = new ZipArchive();
+ $zip->open($filename, ZipArchive::CREATE | ZipArchive::OVERWRITE);
+ for ($index = 0; $index < 64; $index++) {
+ $zip->addFromString("f$index.txt", str_repeat('x', 2000));
+ }
+
+ $weak = WeakReference::create($zip);
+ $callback = static function (float $rate) use ($weak, $method, $arguments): void {
+ static $done = false;
+ if ($done) {
+ return;
+ }
+ $done = true;
+
+ try {
+ $weak->get()->$method(...$arguments);
+ } catch (Error $error) {
+ echo $method, ': ', $error->getMessage(), "\n";
+ }
+ };
+ $zip->registerProgressCallback(0.0, $callback);
+ unset($zip);
+ echo "destroyed\n";
+}
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . '/gh23747_dtor.zip');
+?>
+--EXPECT--
+close: Already being closed
+destroyed
+getStream: Already being closed
+destroyed
+getStreamName: Already being closed
+destroyed
+getStreamIndex: Already being closed
+destroyed