Commit ebed41a03b8 for php.net
commit ebed41a03b8b1f9135874d9aac1320d17eb37e95
Author: Weilin Du <weilindu@php.net>
Date: Fri Jul 10 16:37:52 2026 +0800
Fix GH-22649: avoid ZipArchive comment reset crash (#22652)
Rewrite the PHP_ZIP_SET_FILE_COMMENT macro to solve this uaf that occurs after
overwriting an existing entry and setting its inherited unchanged comment
again. My solution is to simply add a if branch.
I also add some defensive checks in the function by the way
(ZEND_ASSERT(comment_len <= 0xffff);) just in case someone will re-use this
helper in the future.
Fixes #22649
diff --git a/NEWS b/NEWS
index d64ce32ab99..7aada8213bb 100644
--- a/NEWS
+++ b/NEWS
@@ -85,6 +85,9 @@ PHP NEWS
when parsing an IFF chunk with a size of INT_MAX. (David Carlier, Weilin Du)
- Zip:
+ . Fixed bug GH-22649 (ZipArchive::setCommentName() and setCommentIndex()
+ could crash after overwriting an entry and resetting its inherited
+ unchanged comment). (Weilin Du)
. Fixed bug GH-21705 (ZipArchive::getFromIndex() ignores
ZipArchive::FL_UNCHANGED for deleted entries). (Weilin Du)
diff --git a/ext/zip/php_zip.c b/ext/zip/php_zip.c
index 16df403dcda..ccc474bc715 100644
--- a/ext/zip/php_zip.c
+++ b/ext/zip/php_zip.c
@@ -64,17 +64,26 @@ static int le_zip_entry;
}
/* }}} */
-/* {{{ PHP_ZIP_SET_FILE_COMMENT(za, index, comment, comment_len) */
-#define PHP_ZIP_SET_FILE_COMMENT(za, index, comment, comment_len) \
- if (comment_len == 0) { \
- /* Passing NULL remove the existing comment */ \
- if (zip_file_set_comment(za, index, NULL, 0, 0) < 0) { \
- RETURN_FALSE; \
- } \
- } else if (zip_file_set_comment(za, index, comment, comment_len, 0) < 0) { \
- RETURN_FALSE; \
- } \
- RETURN_TRUE;
+/* {{{ php_zip_set_file_comment */
+static bool php_zip_set_file_comment(struct zip *za, zip_uint64_t index, const char *comment, size_t comment_len)
+{
+ zip_uint32_t current_comment_len;
+ const char *current_comment = zip_file_get_comment(za, index, ¤t_comment_len, ZIP_FL_ENC_RAW);
+
+ /* Avoid a libzip use-after-free when resetting an unchanged inherited comment. */
+ if (current_comment && current_comment_len == comment_len
+ && memcmp(current_comment, comment, comment_len) == 0) {
+ return true;
+ }
+
+ if (comment_len == 0) {
+ /* Passing NULL removes the existing comment. */
+ return zip_file_set_comment(za, index, NULL, 0, 0) == 0;
+ }
+
+ ZEND_ASSERT(comment_len <= 0xffff);
+ return zip_file_set_comment(za, index, comment, (zip_uint16_t) comment_len, 0) == 0;
+}
/* }}} */
# define add_ascii_assoc_string add_assoc_string
@@ -2185,7 +2194,7 @@ PHP_METHOD(ZipArchive, setCommentName)
zval *self = ZEND_THIS;
size_t comment_len, name_len;
char * comment, *name;
- int idx;
+ zip_int64_t idx;
if (zend_parse_parameters(ZEND_NUM_ARGS(), "ss",
&name, &name_len, &comment, &comment_len) == FAILURE) {
@@ -2208,7 +2217,7 @@ PHP_METHOD(ZipArchive, setCommentName)
if (idx < 0) {
RETURN_FALSE;
}
- PHP_ZIP_SET_FILE_COMMENT(intern, idx, comment, comment_len);
+ RETURN_BOOL(php_zip_set_file_comment(intern, (zip_uint64_t) idx, comment, comment_len));
}
/* }}} */
@@ -2221,6 +2230,7 @@ PHP_METHOD(ZipArchive, setCommentIndex)
size_t comment_len;
char * comment;
struct zip_stat sb;
+ zip_uint64_t idx;
if (zend_parse_parameters(ZEND_NUM_ARGS(), "ls",
&index, &comment, &comment_len) == FAILURE) {
@@ -2235,7 +2245,9 @@ PHP_METHOD(ZipArchive, setCommentIndex)
}
PHP_ZIP_STAT_INDEX(intern, index, 0, sb);
- PHP_ZIP_SET_FILE_COMMENT(intern, index, comment, comment_len);
+ idx = (zip_uint64_t) index;
+
+ RETURN_BOOL(php_zip_set_file_comment(intern, idx, comment, comment_len));
}
/* }}} */
diff --git a/ext/zip/tests/gh22649.phpt b/ext/zip/tests/gh22649.phpt
new file mode 100644
index 00000000000..d6d0a092818
--- /dev/null
+++ b/ext/zip/tests/gh22649.phpt
@@ -0,0 +1,55 @@
+--TEST--
+GH-22649 (setCommentName/Index after addFromString should not segfault)
+--EXTENSIONS--
+zip
+--FILE--
+<?php
+$file = __DIR__ . '/gh22649.zip';
+
+@unlink($file);
+
+$zip = new ZipArchive;
+if (!$zip->open($file, ZipArchive::CREATE)) {
+ exit('failed');
+}
+
+$zip->addFromString('dir/entry2d.txt', 'entry #2');
+var_dump($zip->setCommentName('dir/entry2d.txt', 'dir/entry2d.txt'));
+
+$zip->addFromString('dir/entry3d.txt', 'entry #3');
+var_dump($zip->setCommentIndex($zip->lastId, 'dir/entry3d.txt'));
+
+$zip->close();
+
+if (!$zip->open($file, ZipArchive::CREATE)) {
+ exit('failed');
+}
+
+$zip->addFromString('dir/entry2d.txt', 'updated entry #2');
+var_dump($zip->setCommentName('dir/entry2d.txt', 'dir/entry2d.txt'));
+
+$zip->addFromString('dir/entry3d.txt', 'updated entry #3');
+var_dump($zip->setCommentIndex($zip->lastId, 'dir/entry3d.txt'));
+
+$zip->close();
+
+if (!$zip->open($file)) {
+ exit('failed');
+}
+
+var_dump($zip->getCommentName('dir/entry2d.txt'));
+var_dump($zip->getCommentName('dir/entry3d.txt'));
+
+$zip->close();
+?>
+--EXPECT--
+bool(true)
+bool(true)
+bool(true)
+bool(true)
+string(15) "dir/entry2d.txt"
+string(15) "dir/entry3d.txt"
+--CLEAN--
+<?php
+@unlink(__DIR__ . '/gh22649.zip');
+?>