@@ -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 {
0 commit comments