Skip to content

Commit 9466868

Browse files
committed
Fix GH-22885: SEGV when setting the comment of a modified zip entry
1 parent 68d605f commit 9466868

5 files changed

Lines changed: 310 additions & 14 deletions

File tree

NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ PHP NEWS
2323
. Fixed bug GH-15836 (Use-after-free when a user stream filter accesses
2424
$this->stream during the close flush). (iliaal)
2525

26+
- Zip:
27+
. Fixed bug GH-22885 (SEGV when setting the comment of an entry that has been
28+
modified). (Khaled Alam)
29+
2630
30 Jul 2026, PHP 8.4.24
2731

2832
- Calendar:

ext/zip/php_zip.c

Lines changed: 129 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -64,25 +64,106 @@ static int le_zip_entry;
6464
}
6565
/* }}} */
6666

67+
/* {{{ Bookkeeping of the entries libzip has copied the central directory record of.
68+
* Modifying an entry makes libzip copy its central directory record, and the copy
69+
* shares the comment string with the record of the entry. As zip_file_set_comment()
70+
* frees that string unconditionally, setting the comment of a modified entry leaves
71+
* the entry with a dangling comment: a use-after-free, and a double free when the
72+
* archive is closed. The comment of such an entry cannot be replaced from the
73+
* outside either, because libzip only keeps an unchanged comment as the shared
74+
* string, so ext/zip tracks the modified entries and refuses instead.
75+
* Fixed upstream by nih-at/libzip@17f10b9, unreleased as of libzip 1.11.4; this
76+
* can go once that is the minimum supported version. */
77+
static void php_zip_mark_entry_modified(ze_zip_object *obj, zip_int64_t index)
78+
{
79+
if (index < 0) {
80+
return;
81+
}
82+
83+
if (!obj->modified_entries) {
84+
ALLOC_HASHTABLE(obj->modified_entries);
85+
zend_hash_init(obj->modified_entries, 0, NULL, NULL, false);
86+
}
87+
88+
zend_hash_index_add_empty_element(obj->modified_entries, (zend_ulong) index);
89+
}
90+
91+
static void php_zip_unmark_entry_modified(ze_zip_object *obj, zip_int64_t index)
92+
{
93+
if (obj->modified_entries && index >= 0) {
94+
zend_hash_index_del(obj->modified_entries, (zend_ulong) index);
95+
}
96+
}
97+
98+
static void php_zip_forget_modified_entries(ze_zip_object *obj)
99+
{
100+
if (obj->modified_entries) {
101+
zend_hash_destroy(obj->modified_entries);
102+
FREE_HASHTABLE(obj->modified_entries);
103+
obj->modified_entries = NULL;
104+
}
105+
}
106+
107+
/* Whether the comment of the entry is the string shared with the copy of its record. */
108+
static bool php_zip_entry_shares_comment(ze_zip_object *obj, zip_uint64_t index)
109+
{
110+
zip_error_t *err;
111+
int zip_err, sys_err;
112+
zip_uint32_t comment_len = 0;
113+
bool shared;
114+
115+
if (!obj->modified_entries || !zend_hash_index_exists(obj->modified_entries, (zend_ulong) index)) {
116+
return false;
117+
}
118+
119+
/* Only a comment the entry was read with is shared. */
120+
err = zip_get_error(obj->za);
121+
zip_err = zip_error_code_zip(err);
122+
sys_err = zip_error_code_system(err);
123+
124+
shared = zip_file_get_comment(obj->za, index, &comment_len, ZIP_FL_UNCHANGED | ZIP_FL_ENC_RAW) != NULL
125+
&& comment_len > 0;
126+
127+
zip_error_set(err, zip_err, sys_err);
128+
129+
return shared;
130+
}
131+
/* }}} */
132+
67133
/* {{{ php_zip_set_file_comment */
68-
static bool php_zip_set_file_comment(struct zip *za, zip_uint64_t index, const char *comment, size_t comment_len)
134+
static bool php_zip_set_file_comment(ze_zip_object *obj, zip_uint64_t index, const char *comment, size_t comment_len)
69135
{
70136
zip_uint32_t current_comment_len;
71-
const char *current_comment = zip_file_get_comment(za, index, &current_comment_len, ZIP_FL_ENC_RAW);
137+
const char *current_comment = zip_file_get_comment(obj->za, index, &current_comment_len, ZIP_FL_ENC_RAW);
72138

73-
/* Avoid a libzip use-after-free when resetting an unchanged inherited comment. */
139+
/* Setting the comment an entry already has is a no-op. */
74140
if (current_comment && current_comment_len == comment_len
75141
&& memcmp(current_comment, comment, comment_len) == 0) {
76142
return true;
77143
}
78144

145+
if (php_zip_entry_shares_comment(obj, index)) {
146+
php_error_docref(NULL, E_WARNING, "Cannot set the comment of an entry that has been modified, "
147+
"set the comment before modifying the entry");
148+
return false;
149+
}
150+
79151
if (comment_len == 0) {
80152
/* Passing NULL removes the existing comment. */
81-
return zip_file_set_comment(za, index, NULL, 0, 0) == 0;
153+
if (zip_file_set_comment(obj->za, index, NULL, 0, 0) != 0) {
154+
return false;
155+
}
156+
} else {
157+
ZEND_ASSERT(comment_len <= 0xffff);
158+
if (zip_file_set_comment(obj->za, index, comment, (zip_uint16_t) comment_len, 0) != 0) {
159+
return false;
160+
}
82161
}
83162

84-
ZEND_ASSERT(comment_len <= 0xffff);
85-
return zip_file_set_comment(za, index, comment, (zip_uint16_t) comment_len, 0) == 0;
163+
/* The comment of the entry is no longer shared with the copy of its record. */
164+
php_zip_unmark_entry_modified(obj, (zip_int64_t) index);
165+
166+
return true;
86167
}
87168
/* }}} */
88169

@@ -338,6 +419,7 @@ static int php_zip_add_file(ze_zip_object *obj, const char *filename, size_t fil
338419
zip_source_free(zs);
339420
return -1;
340421
}
422+
php_zip_mark_entry_modified(obj, replace);
341423
zip_error_clear(obj->za);
342424
return 1;
343425
}
@@ -347,6 +429,7 @@ static int php_zip_add_file(ze_zip_object *obj, const char *filename, size_t fil
347429
zip_source_free(zs);
348430
return -1;
349431
}
432+
php_zip_mark_entry_modified(obj, obj->last_id);
350433
zip_error_clear(obj->za);
351434
return 1;
352435
}
@@ -1072,6 +1155,9 @@ static void php_zip_object_free_storage(zend_object *object) /* {{{ */
10721155
if (!intern) {
10731156
return;
10741157
}
1158+
1159+
php_zip_forget_modified_entries(intern);
1160+
10751161
if (intern->za) {
10761162
if (zip_close(intern->za) != 0) {
10771163
php_error_docref(NULL, E_WARNING, "Cannot destroy the zip context: %s", zip_strerror(intern->za));
@@ -1509,6 +1595,7 @@ PHP_METHOD(ZipArchive, open)
15091595
RETURN_FALSE;
15101596
}
15111597
ze_obj->za = NULL;
1598+
php_zip_forget_modified_entries(ze_obj);
15121599
}
15131600
if (ze_obj->filename) {
15141601
efree(ze_obj->filename);
@@ -1588,6 +1675,8 @@ PHP_METHOD(ZipArchive, close)
15881675

15891676
ze_obj = Z_ZIP_P(self);
15901677

1678+
php_zip_forget_modified_entries(ze_obj);
1679+
15911680
err = zip_close(intern);
15921681
if (err) {
15931682
php_error_docref(NULL, E_WARNING, "%s", zip_strerror(intern));
@@ -1740,6 +1829,7 @@ PHP_METHOD(ZipArchive, addEmptyDir)
17401829
if ((Z_ZIP_P(self)->last_id = zip_dir_add(intern, (const char *)s, flags)) == -1) {
17411830
RETVAL_FALSE;
17421831
} else {
1832+
php_zip_mark_entry_modified(Z_ZIP_P(self), Z_ZIP_P(self)->last_id);
17431833
zip_error_clear(intern);
17441834
RETVAL_TRUE;
17451835
}
@@ -1999,6 +2089,7 @@ PHP_METHOD(ZipArchive, addFromString)
19992089
zip_source_free(zs);
20002090
RETURN_FALSE;
20012091
} else {
2092+
php_zip_mark_entry_modified(ze_obj, ze_obj->last_id);
20022093
zip_error_clear(intern);
20032094
RETURN_TRUE;
20042095
}
@@ -2217,7 +2308,7 @@ PHP_METHOD(ZipArchive, setCommentName)
22172308
if (idx < 0) {
22182309
RETURN_FALSE;
22192310
}
2220-
RETURN_BOOL(php_zip_set_file_comment(intern, (zip_uint64_t) idx, comment, comment_len));
2311+
RETURN_BOOL(php_zip_set_file_comment(Z_ZIP_P(self), (zip_uint64_t) idx, comment, comment_len));
22212312
}
22222313
/* }}} */
22232314

@@ -2247,7 +2338,7 @@ PHP_METHOD(ZipArchive, setCommentIndex)
22472338
PHP_ZIP_STAT_INDEX(intern, index, 0, sb);
22482339
idx = (zip_uint64_t) index;
22492340

2250-
RETURN_BOOL(php_zip_set_file_comment(intern, idx, comment, comment_len));
2341+
RETURN_BOOL(php_zip_set_file_comment(Z_ZIP_P(self), idx, comment, comment_len));
22512342
}
22522343
/* }}} */
22532344

@@ -2285,6 +2376,8 @@ PHP_METHOD(ZipArchive, setExternalAttributesName)
22852376
(zip_uint8_t)(opsys&0xff), (zip_uint32_t)attr) < 0) {
22862377
RETURN_FALSE;
22872378
}
2379+
2380+
php_zip_mark_entry_modified(Z_ZIP_P(self), idx);
22882381
RETURN_TRUE;
22892382
}
22902383
/* }}} */
@@ -2309,6 +2402,8 @@ PHP_METHOD(ZipArchive, setExternalAttributesIndex)
23092402
(zip_flags_t)flags, (zip_uint8_t)(opsys&0xff), (zip_uint32_t)attr) < 0) {
23102403
RETURN_FALSE;
23112404
}
2405+
2406+
php_zip_mark_entry_modified(Z_ZIP_P(self), (zip_int64_t)index);
23122407
RETURN_TRUE;
23132408
}
23142409
/* }}} */
@@ -2418,6 +2513,8 @@ PHP_METHOD(ZipArchive, setEncryptionName)
24182513
if (zip_file_set_encryption(intern, idx, (zip_uint16_t)method, password)) {
24192514
RETURN_FALSE;
24202515
}
2516+
2517+
php_zip_mark_entry_modified(Z_ZIP_P(self), idx);
24212518
RETURN_TRUE;
24222519
}
24232520
/* }}} */
@@ -2446,6 +2543,8 @@ PHP_METHOD(ZipArchive, setEncryptionIndex)
24462543
if (zip_file_set_encryption(intern, index, (zip_uint16_t)method, password)) {
24472544
RETURN_FALSE;
24482545
}
2546+
2547+
php_zip_mark_entry_modified(Z_ZIP_P(self), (zip_int64_t)index);
24492548
RETURN_TRUE;
24502549
}
24512550
/* }}} */
@@ -2541,6 +2640,8 @@ PHP_METHOD(ZipArchive, setCompressionName)
25412640
(zip_int32_t)comp_method, (zip_uint32_t)comp_flags) != 0) {
25422641
RETURN_FALSE;
25432642
}
2643+
2644+
php_zip_mark_entry_modified(Z_ZIP_P(this), idx);
25442645
RETURN_TRUE;
25452646
}
25462647
/* }}} */
@@ -2564,6 +2665,8 @@ PHP_METHOD(ZipArchive, setCompressionIndex)
25642665
(zip_int32_t)comp_method, (zip_uint32_t)comp_flags) != 0) {
25652666
RETURN_FALSE;
25662667
}
2668+
2669+
php_zip_mark_entry_modified(Z_ZIP_P(this), (zip_int64_t)index);
25672670
RETURN_TRUE;
25682671
}
25692672
/* }}} */
@@ -2601,6 +2704,8 @@ PHP_METHOD(ZipArchive, setMtimeName)
26012704
(time_t)mtime, (zip_uint32_t)flags) != 0) {
26022705
RETURN_FALSE;
26032706
}
2707+
2708+
php_zip_mark_entry_modified(Z_ZIP_P(this), idx);
26042709
RETURN_TRUE;
26052710
}
26062711
/* }}} */
@@ -2624,6 +2729,8 @@ PHP_METHOD(ZipArchive, setMtimeIndex)
26242729
(time_t)mtime, (zip_uint32_t)flags) != 0) {
26252730
RETURN_FALSE;
26262731
}
2732+
2733+
php_zip_mark_entry_modified(Z_ZIP_P(this), (zip_int64_t)index);
26272734
RETURN_TRUE;
26282735
}
26292736
/* }}} */
@@ -2709,6 +2816,8 @@ PHP_METHOD(ZipArchive, renameIndex)
27092816
RETURN_FALSE;
27102817
}
27112818

2819+
php_zip_mark_entry_modified(Z_ZIP_P(self), (zip_int64_t)index);
2820+
27122821
RETURN_TRUE;
27132822
}
27142823
/* }}} */
@@ -2739,6 +2848,8 @@ PHP_METHOD(ZipArchive, renameName)
27392848
RETURN_FALSE;
27402849
}
27412850

2851+
php_zip_mark_entry_modified(Z_ZIP_P(self), (zip_int64_t)sb.index);
2852+
27422853
RETURN_TRUE;
27432854
}
27442855
/* }}} */
@@ -2762,9 +2873,10 @@ PHP_METHOD(ZipArchive, unchangeIndex)
27622873

27632874
if (zip_unchange(intern, index) != 0) {
27642875
RETURN_FALSE;
2765-
} else {
2766-
RETURN_TRUE;
27672876
}
2877+
2878+
php_zip_unmark_entry_modified(Z_ZIP_P(self), (zip_int64_t)index);
2879+
RETURN_TRUE;
27682880
}
27692881
/* }}} */
27702882

@@ -2791,9 +2903,10 @@ PHP_METHOD(ZipArchive, unchangeName)
27912903

27922904
if (zip_unchange(intern, sb.index) != 0) {
27932905
RETURN_FALSE;
2794-
} else {
2795-
RETURN_TRUE;
27962906
}
2907+
2908+
php_zip_unmark_entry_modified(Z_ZIP_P(self), (zip_int64_t)sb.index);
2909+
RETURN_TRUE;
27972910
}
27982911
/* }}} */
27992912

@@ -2811,9 +2924,10 @@ PHP_METHOD(ZipArchive, unchangeAll)
28112924

28122925
if (zip_unchange_all(intern) != 0) {
28132926
RETURN_FALSE;
2814-
} else {
2815-
RETURN_TRUE;
28162927
}
2928+
2929+
php_zip_forget_modified_entries(Z_ZIP_P(self));
2930+
RETURN_TRUE;
28172931
}
28182932
/* }}} */
28192933

@@ -2829,6 +2943,7 @@ PHP_METHOD(ZipArchive, unchangeArchive)
28292943

28302944
ZIP_FROM_OBJECT(intern, self);
28312945

2946+
/* Only the archive level changes are reverted, the entries stay modified. */
28322947
if (zip_unchange_archive(intern) != 0) {
28332948
RETURN_FALSE;
28342949
} else {

ext/zip/php_zip.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,11 @@ typedef struct _ze_zip_object {
7474
int filename_len;
7575
int buffers_cnt;
7676
zip_int64_t last_id;
77+
/* Indices of the entries libzip has copied the central directory record of,
78+
* because they were modified. Such a copy shares its comment with the record
79+
* of the entry, which makes setting the comment unsafe. Lazily allocated,
80+
* see php_zip_set_file_comment(). */
81+
HashTable *modified_entries;
7782
int err_zip;
7883
int err_sys;
7984
#ifdef HAVE_PROGRESS_CALLBACK

0 commit comments

Comments
 (0)