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

From: Date: Thu, 08 Oct 2026 16:56:58 +0000
Subject: [php-src] master: Merge branch 'PHP-8.5' into PHP-8.6
Groups: php.cvs 
Request: Send a blank email to php-cvs+get-142097@lists.php.net to get a copy of this message
Author: Weilin Du (LamentXU123) Date: 2026-10-09T00:55:01+08:00 Commit: https://github.com/php/php-src/commit/4985676d96ef33acd413b9fe595747676b73cd33 Raw diff: https://github.com/php/php-src/commit/4985676d96ef33acd413b9fe595747676b73cd33.diff Merge branch 'PHP-8.5' into PHP-8.6 * PHP-8.5: ext/zip: Reject ZipArchive mutators during close() (#24025) Changed paths: A ext/zip/tests/zip_close_mutator_reentry.phpt A ext/zip/tests/zip_close_mutator_reentry_cancel.phpt A ext/zip/tests/zip_close_mutator_reentry_open.phpt M NEWS M ext/zip/php_zip.c Diff: diff --git a/NEWS b/NEWS index cce4f4a0ee44..c895aa7b1dd0 100644 --- a/NEWS +++ b/NEWS @@ -25,6 +25,9 @@ PHP NEWS - Zip: . Fixed use-after-free when re-entering ZipArchive during destruction or a close warning, and rejected opening streams while closing. (jvoisin) + . Fixed a use-after-free when a ZipArchive method that modifies the archive + is called from a progress or cancel callback during close(). + (Ilia Alshanetsky) 08 Oct 2026, PHP 8.6.0RC3 diff --git a/ext/zip/php_zip.c b/ext/zip/php_zip.c index fc2998a7ca3f..5baecfc18933 100644 --- a/ext/zip/php_zip.c +++ b/ext/zip/php_zip.c @@ -1728,6 +1728,10 @@ PHP_METHOD(ZipArchive, setPassword) ZIP_FROM_OBJECT(intern, self); + if (php_zipobj_closing(Z_ZIP_P(self))) { + RETURN_THROWS(); + } + if (password_len < 1) { RETURN_FALSE; } @@ -1890,6 +1894,10 @@ PHP_METHOD(ZipArchive, addEmptyDir) ZIP_FROM_OBJECT(intern, self); + if (php_zipobj_closing(Z_ZIP_P(self))) { + RETURN_THROWS(); + } + if (dirname_len<1) { RETURN_FALSE; } @@ -1945,6 +1953,10 @@ static void php_zip_add_from_pattern(INTERNAL_FUNCTION_PARAMETERS, int type) /* RETURN_THROWS(); } + if (php_zipobj_closing(Z_ZIP_P(self))) { + RETURN_THROWS(); + } + if (type == 1) { found = php_zip_glob(pattern, glob_flags, return_value); } else { @@ -2079,6 +2091,10 @@ PHP_METHOD(ZipArchive, addFile) entry_name_len = ZSTR_LEN(filename); } + if (php_zipobj_closing(Z_ZIP_P(self))) { + RETURN_THROWS(); + } + RETURN_BOOL(php_zip_add_file(Z_ZIP_P(self), ZSTR_VAL(filename), ZSTR_LEN(filename), entry_name, entry_name_len, offset_start, offset_len, -1, flags) == SUCCESS); } @@ -2108,6 +2124,10 @@ PHP_METHOD(ZipArchive, replaceFile) RETURN_THROWS(); } + if (php_zipobj_closing(Z_ZIP_P(self))) { + RETURN_THROWS(); + } + RETURN_BOOL(php_zip_add_file(Z_ZIP_P(self), ZSTR_VAL(filename), ZSTR_LEN(filename), NULL, 0, offset_start, offset_len, index, flags) == SUCCESS); } @@ -2132,6 +2152,10 @@ PHP_METHOD(ZipArchive, addFromString) ZIP_FROM_OBJECT(intern, self); + if (php_zipobj_closing(Z_ZIP_P(self))) { + RETURN_THROWS(); + } + ze_obj = Z_ZIP_P(self); zs = php_zip_create_string_source(buffer, NULL, NULL); @@ -2262,6 +2286,10 @@ PHP_METHOD(ZipArchive, setArchiveComment) ZIP_FROM_OBJECT(intern, self); + if (php_zipobj_closing(Z_ZIP_P(self))) { + RETURN_THROWS(); + } + if (comment_len > 0xffff) { zend_argument_value_error(1, "must be less than 65535 bytes"); RETURN_THROWS(); @@ -2306,6 +2334,10 @@ PHP_METHOD(ZipArchive, setArchiveFlag) ZIP_FROM_OBJECT(intern, self); + if (php_zipobj_closing(Z_ZIP_P(self))) { + RETURN_THROWS(); + } + RETURN_BOOL(zip_set_archive_flag(intern, flag, (int)value) == 0); } @@ -2345,6 +2377,10 @@ PHP_METHOD(ZipArchive, setCommentName) ZIP_FROM_OBJECT(intern, self); + if (php_zipobj_closing(Z_ZIP_P(self))) { + RETURN_THROWS(); + } + if (comment_len > 0xffff) { zend_argument_value_error(2, "must be less than 65535 bytes"); RETURN_THROWS(); @@ -2376,6 +2412,10 @@ PHP_METHOD(ZipArchive, setCommentIndex) ZIP_FROM_OBJECT(intern, self); + if (php_zipobj_closing(Z_ZIP_P(self))) { + RETURN_THROWS(); + } + if (comment_len > 0xffff) { zend_argument_value_error(2, "must be less than 65535 bytes"); RETURN_THROWS(); @@ -2408,6 +2448,10 @@ PHP_METHOD(ZipArchive, setExternalAttributesName) ZIP_FROM_OBJECT(intern, self); + if (php_zipobj_closing(Z_ZIP_P(self))) { + RETURN_THROWS(); + } + if (name_len == 0) { zend_argument_must_not_be_empty_error(1); RETURN_THROWS(); @@ -2438,6 +2482,10 @@ PHP_METHOD(ZipArchive, setExternalAttributesIndex) ZIP_FROM_OBJECT(intern, self); + if (php_zipobj_closing(Z_ZIP_P(self))) { + RETURN_THROWS(); + } + PHP_ZIP_STAT_INDEX(intern, index, 0, sb); RETURN_BOOL(zip_file_set_external_attributes(intern, (zip_uint64_t)index, (zip_flags_t)flags, (zip_uint8_t)(opsys&0xff), (zip_uint32_t)attr) == 0); @@ -2527,6 +2575,10 @@ PHP_METHOD(ZipArchive, setEncryptionName) ZIP_FROM_OBJECT(intern, self); + if (php_zipobj_closing(Z_ZIP_P(self))) { + RETURN_THROWS(); + } + if (name_len == 0) { zend_argument_must_not_be_empty_error(1); RETURN_THROWS(); @@ -2558,6 +2610,10 @@ PHP_METHOD(ZipArchive, setEncryptionIndex) ZIP_FROM_OBJECT(intern, self); + if (php_zipobj_closing(Z_ZIP_P(self))) { + RETURN_THROWS(); + } + RETURN_BOOL(php_zip_file_set_encryption(intern, index, method, password)); } /* }}} */ @@ -2653,6 +2709,11 @@ PHP_METHOD(ZipArchive, setCompressionName) } ZIP_FROM_OBJECT(intern, this); + + if (php_zipobj_closing(Z_ZIP_P(this))) { + RETURN_THROWS(); + } + idx = zip_name_locate(intern, name, 0); if (idx < 0) { @@ -2694,6 +2755,10 @@ PHP_METHOD(ZipArchive, setCompressionIndex) ZIP_FROM_OBJECT(intern, this); + if (php_zipobj_closing(Z_ZIP_P(this))) { + RETURN_THROWS(); + } + RETURN_BOOL(zip_set_file_compression(intern, (zip_uint64_t)index, (zip_int32_t)comp_method, (zip_uint32_t)comp_flags) == 0); } @@ -2721,6 +2786,10 @@ PHP_METHOD(ZipArchive, setMtimeName) ZIP_FROM_OBJECT(intern, this); + if (php_zipobj_closing(Z_ZIP_P(this))) { + RETURN_THROWS(); + } + idx = zip_name_locate(intern, name, 0); if (idx < 0) { @@ -2747,6 +2816,10 @@ PHP_METHOD(ZipArchive, setMtimeIndex) ZIP_FROM_OBJECT(intern, this); + if (php_zipobj_closing(Z_ZIP_P(this))) { + RETURN_THROWS(); + } + RETURN_BOOL(zip_file_set_mtime(intern, (zip_uint64_t)index, (time_t)mtime, (zip_uint32_t)flags) == 0); } @@ -2765,6 +2838,10 @@ PHP_METHOD(ZipArchive, deleteIndex) ZIP_FROM_OBJECT(intern, self); + if (php_zipobj_closing(Z_ZIP_P(self))) { + RETURN_THROWS(); + } + if (index < 0) { RETURN_FALSE; } @@ -2792,6 +2869,10 @@ PHP_METHOD(ZipArchive, deleteName) ZIP_FROM_OBJECT(intern, self); + if (php_zipobj_closing(Z_ZIP_P(self))) { + RETURN_THROWS(); + } + PHP_ZIP_STAT_PATH(intern, name, name_len, 0, sb); RETURN_BOOL(zip_delete(intern, sb.index) == 0); @@ -2822,6 +2903,10 @@ PHP_METHOD(ZipArchive, renameIndex) ZIP_FROM_OBJECT(intern, self); + if (php_zipobj_closing(Z_ZIP_P(self))) { + RETURN_THROWS(); + } + RETURN_BOOL(zip_file_rename(intern, index, (const char *)new_name, 0) == 0); } /* }}} */ @@ -2841,6 +2926,10 @@ PHP_METHOD(ZipArchive, renameName) ZIP_FROM_OBJECT(intern, self); + if (php_zipobj_closing(Z_ZIP_P(self))) { + RETURN_THROWS(); + } + if (new_name_len == 0) { zend_argument_must_not_be_empty_error(2); RETURN_THROWS(); @@ -2869,6 +2958,10 @@ PHP_METHOD(ZipArchive, unchangeIndex) ZIP_FROM_OBJECT(intern, self); + if (php_zipobj_closing(Z_ZIP_P(self))) { + RETURN_THROWS(); + } + RETURN_BOOL(zip_unchange(intern, index) == 0); } /* }}} */ @@ -2892,6 +2985,10 @@ PHP_METHOD(ZipArchive, unchangeName) ZIP_FROM_OBJECT(intern, self); + if (php_zipobj_closing(Z_ZIP_P(self))) { + RETURN_THROWS(); + } + PHP_ZIP_STAT_PATH(intern, name, name_len, 0, sb); RETURN_BOOL(zip_unchange(intern, sb.index) == 0); @@ -2908,6 +3005,10 @@ PHP_METHOD(ZipArchive, unchangeAll) ZIP_FROM_OBJECT(intern, self); + if (php_zipobj_closing(Z_ZIP_P(self))) { + RETURN_THROWS(); + } + RETURN_BOOL(zip_unchange_all(intern) == 0); } /* }}} */ @@ -2922,6 +3023,10 @@ PHP_METHOD(ZipArchive, unchangeArchive) ZIP_FROM_OBJECT(intern, self); + if (php_zipobj_closing(Z_ZIP_P(self))) { + RETURN_THROWS(); + } + RETURN_BOOL(zip_unchange_archive(intern) == 0); } /* }}} */ @@ -3222,6 +3327,10 @@ PHP_METHOD(ZipArchive, registerProgressCallback) zend_release_fcall_info_cache(&fcc); RETURN_THROWS(); } + if (php_zipobj_closing(obj)) { + zend_release_fcall_info_cache(&fcc); + RETURN_THROWS(); + } archive = obj->archive; /* register */ @@ -3297,6 +3406,10 @@ PHP_METHOD(ZipArchive, registerCancelCallback) zend_release_fcall_info_cache(&fcc); RETURN_THROWS(); } + if (php_zipobj_closing(obj)) { + zend_release_fcall_info_cache(&fcc); + RETURN_THROWS(); + } archive = obj->archive; /* register */ diff --git a/ext/zip/tests/zip_close_mutator_reentry.phpt b/ext/zip/tests/zip_close_mutator_reentry.phpt new file mode 100644 index 000000000000..98636d9202f0 --- /dev/null +++ b/ext/zip/tests/zip_close_mutator_reentry.phpt @@ -0,0 +1,145 @@ +--TEST-- +ZipArchive mutators throw when called from a progress callback during close() +--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'); +} +if (!method_exists(ZipArchive::class, 'setEncryptionName')) { + die('skip encryption not supported'); +} +if (!method_exists(ZipArchive::class, 'setMtimeName')) { + die('skip libzip too old'); +} +?> +--FILE-- +<?php +$filename = __DIR__ . '/zip_close_mutator_reentry.zip'; + +function populate(ZipArchive $zip, string $filename): void { + $zip->open($filename, ZipArchive::CREATE | ZipArchive::OVERWRITE); + $zip->addFromString('a.txt', str_repeat('a', 100000)); + $zip->addFromString('b.txt', str_repeat('b', 100000)); +} + +$zip = new ZipArchive(); +populate($zip, $filename); +$zip->registerProgressCallback(0.0, function ($rate) use ($zip) { + static $done = false; + if ($done || $rate <= 0) { + return; + } + $done = true; + try { + var_dump($zip->deleteIndex(0)); + } catch (Error $e) { + echo $e::class, ': ', $e->getMessage(), PHP_EOL; + } +}); +var_dump($zip->close()); + +$zip = new ZipArchive(); +populate($zip, $filename); +$mutators = [ + 'deleteIndex' => fn() => $zip->deleteIndex(0), + 'deleteName' => fn() => $zip->deleteName('a.txt'), + 'unchangeIndex' => fn() => $zip->unchangeIndex(0), + 'unchangeName' => fn() => $zip->unchangeName('a.txt'), + 'unchangeAll' => fn() => $zip->unchangeAll(), + 'unchangeArchive' => fn() => $zip->unchangeArchive(), + 'addEmptyDir' => fn() => $zip->addEmptyDir('dir'), + 'addFile' => fn() => $zip->addFile(__FILE__, 'file.phpt'), + 'addFromString' => fn() => $zip->addFromString('c.txt', 'c'), + 'addGlob' => fn() => $zip->addGlob(__FILE__), + 'addPattern' => fn() => $zip->addPattern('/\.phpt$/', __DIR__), + 'replaceFile' => fn() => $zip->replaceFile(__FILE__, 0), + 'renameIndex' => fn() => $zip->renameIndex(0, 'x.txt'), + 'renameName' => fn() => $zip->renameName('a.txt', 'x.txt'), + 'setArchiveComment' => fn() => $zip->setArchiveComment('comment'), + 'setArchiveFlag' => fn() => $zip->setArchiveFlag(ZipArchive::AFL_RDONLY, 1), + 'setCommentIndex' => fn() => $zip->setCommentIndex(0, 'comment'), + 'setCommentName' => fn() => $zip->setCommentName('a.txt', 'comment'), + 'setCompressionIndex' => fn() => $zip->setCompressionIndex(0, ZipArchive::CM_STORE), + 'setCompressionName' => fn() => $zip->setCompressionName('a.txt', ZipArchive::CM_STORE), + 'setEncryptionIndex' => fn() => $zip->setEncryptionIndex(0, ZipArchive::EM_AES_256, 'secret'), + 'setEncryptionName' => fn() => $zip->setEncryptionName('a.txt', ZipArchive::EM_AES_256, 'secret'), + 'setExternalAttributesIndex' => fn() => $zip->setExternalAttributesIndex(0, ZipArchive::OPSYS_UNIX, 0), + 'setExternalAttributesName' => fn() => $zip->setExternalAttributesName('a.txt', ZipArchive::OPSYS_UNIX, 0), + 'setMtimeIndex' => fn() => $zip->setMtimeIndex(0, 0), + 'setMtimeName' => fn() => $zip->setMtimeName('a.txt', 0), + 'setPassword' => fn() => $zip->setPassword('secret'), + 'registerCancelCallback' => fn() => $zip->registerCancelCallback(fn() => 0), + 'registerProgressCallback' => fn() => $zip->registerProgressCallback(0.5, function () {}), +]; + +$zip->registerProgressCallback(0.0, function ($rate) use ($mutators) { + static $done = false; + if ($done || $rate <= 0) { + return; + } + $done = true; + foreach ($mutators as $name => $mutator) { + try { + var_dump($mutator()); + } catch (Error $e) { + echo $name, ': ', $e::class, ': ', $e->getMessage(), PHP_EOL; + } + } +}); +var_dump($zip->close()); + +$zip = new ZipArchive(); +var_dump($zip->open($filename, ZipArchive::CHECKCONS)); +var_dump($zip->numFiles); +var_dump($zip->getFromName('a.txt') === str_repeat('a', 100000)); +var_dump($zip->getFromName('b.txt') === str_repeat('b', 100000)); +var_dump($zip->getArchiveComment()); +$zip->close(); +?> +--CLEAN-- +<?php +@unlink(__DIR__ . '/zip_close_mutator_reentry.zip'); +?> +--EXPECT-- +Error: Already being closed +bool(true) +deleteIndex: Error: Already being closed +deleteName: Error: Already being closed +unchangeIndex: Error: Already being closed +unchangeName: Error: Already being closed +unchangeAll: Error: Already being closed +unchangeArchive: Error: Already being closed +addEmptyDir: Error: Already being closed +addFile: Error: Already being closed +addFromString: Error: Already being closed +addGlob: Error: Already being closed +addPattern: Error: Already being closed +replaceFile: Error: Already being closed +renameIndex: Error: Already being closed +renameName: Error: Already being closed +setArchiveComment: Error: Already being closed +setArchiveFlag: Error: Already being closed +setCommentIndex: Error: Already being closed +setCommentName: Error: Already being closed +setCompressionIndex: Error: Already being closed +setCompressionName: Error: Already being closed +setEncryptionIndex: Error: Already being closed +setEncryptionName: Error: Already being closed +setExternalAttributesIndex: Error: Already being closed +setExternalAttributesName: Error: Already being closed +setMtimeIndex: Error: Already being closed +setMtimeName: Error: Already being closed +setPassword: Error: Already being closed +registerCancelCallback: Error: Already being closed +registerProgressCallback: Error: Already being closed +bool(true) +bool(true) +int(2) +bool(true) +bool(true) +string(0) "" diff --git a/ext/zip/tests/zip_close_mutator_reentry_cancel.phpt b/ext/zip/tests/zip_close_mutator_reentry_cancel.phpt new file mode 100644 index 000000000000..4826c8b6a71c --- /dev/null +++ b/ext/zip/tests/zip_close_mutator_reentry_cancel.phpt @@ -0,0 +1,57 @@ +--TEST-- +ZipArchive mutators throw when called from a cancel callback during close() +--EXTENSIONS-- +zip +--SKIPIF-- +<?php +if (!method_exists(ZipArchive::class, 'registerCancelCallback')) { + die('skip cancel callbacks are not supported'); +} +?> +--FILE-- +<?php +$filename = __DIR__ . '/zip_close_mutator_reentry_cancel.zip'; +$zip = new ZipArchive(); +$zip->open($filename, ZipArchive::CREATE | ZipArchive::OVERWRITE); +$zip->addFromString('a.txt', str_repeat('a', 100000)); +$zip->addFromString('b.txt', str_repeat('b', 100000)); +$zip->registerCancelCallback(function () use ($zip) { + static $done = false; + if ($done) { + return 0; + } + $done = true; + $mutators = [ + 'deleteIndex' => fn() => $zip->deleteIndex(0), + 'addFromString' => fn() => $zip->addFromString('c.txt', 'c'), + 'registerCancelCallback' => fn() => $zip->registerCancelCallback(fn() => 0), + ]; + foreach ($mutators as $name => $mutator) { + try { + var_dump($mutator()); + } catch (Error $e) { + echo $name, ': ', $e::class, ': ', $e->getMessage(), PHP_EOL; + } + } + return 0; +}); +var_dump($zip->close()); +var_dump($zip->open($filename, ZipArchive::CHECKCONS)); +var_dump($zip->numFiles); +var_dump($zip->getFromName('a.txt') === str_repeat('a', 100000)); +var_dump($zip->getFromName('b.txt') === str_repeat('b', 100000)); +$zip->close(); +?> +--CLEAN-- +<?php +@unlink(__DIR__ . '/zip_close_mutator_reentry_cancel.zip'); +?> +--EXPECT-- +deleteIndex: Error: Already being closed +addFromString: Error: Already being closed +registerCancelCallback: Error: Already being closed +bool(true) +bool(true) +int(2) +bool(true) +bool(true) diff --git a/ext/zip/tests/zip_close_mutator_reentry_open.phpt b/ext/zip/tests/zip_close_mutator_reentry_open.phpt new file mode 100644 index 000000000000..c169ea9a02d4 --- /dev/null +++ b/ext/zip/tests/zip_close_mutator_reentry_open.phpt @@ -0,0 +1,69 @@ +--TEST-- +ZipArchive mutators throw when open() implicitly closes the previous archive +--EXTENSIONS-- +zip +--SKIPIF-- +<?php +if (!method_exists(ZipArchive::class, 'registerProgressCallback')) { + die('skip progress callbacks are not supported'); +} +?> +--FILE-- +<?php +$filename = __DIR__ . '/zip_close_mutator_reentry_open.zip'; +$nextFilename = __DIR__ . '/zip_close_mutator_reentry_open_next.zip'; +$zip = new ZipArchive(); +$zip->open($filename, ZipArchive::CREATE | ZipArchive::OVERWRITE); +$zip->addFromString('a.txt', str_repeat('a', 100000)); +$zip->addFromString('b.txt', str_repeat('b', 100000)); +$zip->registerProgressCallback(0.0, function ($rate) use ($zip) { + static $done = false; + if ($done || $rate <= 0) { + return; + } + $done = true; + $mutators = [ + 'deleteIndex' => fn() => $zip->deleteIndex(0), + 'addFromString' => fn() => $zip->addFromString('c.txt', 'c'), + 'registerProgressCallback' => fn() => $zip->registerProgressCallback(0.5, function () {}), + ]; + foreach ($mutators as $name => $mutator) { + try { + var_dump($mutator()); + } catch (Error $e) { + echo $name, ': ', $e::class, ': ', $e->getMessage(), PHP_EOL; + } + } +}); +var_dump($zip->open($nextFilename, ZipArchive::CREATE | ZipArchive::OVERWRITE)); +var_dump($zip->addFromString('new.txt', 'new contents')); +var_dump($zip->close()); +var_dump($zip->open($filename, ZipArchive::CHECKCONS)); +var_dump($zip->numFiles); +var_dump($zip->getFromName('a.txt') === str_repeat('a', 100000)); +var_dump($zip->getFromName('b.txt') === str_repeat('b', 100000)); +$zip->close(); +var_dump($zip->open($nextFilename, ZipArchive::CHECKCONS)); +var_dump($zip->numFiles); +var_dump($zip->getFromName('new.txt') === 'new contents'); +$zip->close(); +?> +--CLEAN-- +<?php +@unlink(__DIR__ . '/zip_close_mutator_reentry_open.zip'); +@unlink(__DIR__ . '/zip_close_mutator_reentry_open_next.zip'); +?> +--EXPECT-- +deleteIndex: Error: Already being closed +addFromString: Error: Already being closed +registerProgressCallback: Error: Already being closed +bool(true) +bool(true) +bool(true) +bool(true) +int(2) +bool(true) +bool(true) +bool(true) +int(1) +bool(true)

« previous php.cvs (#142097) next »