@@ -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 ));
@@ -1501,6 +1587,8 @@ PHP_METHOD(ZipArchive, open)
15011587 RETURN_FALSE ;
15021588 }
15031589
1590+ php_zip_forget_modified_entries (ze_obj );
1591+
15041592 if (ze_obj -> za ) {
15051593 /* we already have an opened zip, free it */
15061594 if (zip_close (ze_obj -> za ) != 0 ) {
@@ -1588,6 +1676,8 @@ PHP_METHOD(ZipArchive, close)
15881676
15891677 ze_obj = Z_ZIP_P (self );
15901678
1679+ php_zip_forget_modified_entries (ze_obj );
1680+
15911681 err = zip_close (intern );
15921682 if (err ) {
15931683 php_error_docref (NULL , E_WARNING , "%s" , zip_strerror (intern ));
@@ -1740,6 +1830,7 @@ PHP_METHOD(ZipArchive, addEmptyDir)
17401830 if ((Z_ZIP_P (self )-> last_id = zip_dir_add (intern , (const char * )s , flags )) == -1 ) {
17411831 RETVAL_FALSE ;
17421832 } else {
1833+ php_zip_mark_entry_modified (Z_ZIP_P (self ), Z_ZIP_P (self )-> last_id );
17431834 zip_error_clear (intern );
17441835 RETVAL_TRUE ;
17451836 }
@@ -1999,6 +2090,7 @@ PHP_METHOD(ZipArchive, addFromString)
19992090 zip_source_free (zs );
20002091 RETURN_FALSE ;
20012092 } else {
2093+ php_zip_mark_entry_modified (ze_obj , ze_obj -> last_id );
20022094 zip_error_clear (intern );
20032095 RETURN_TRUE ;
20042096 }
@@ -2217,7 +2309,7 @@ PHP_METHOD(ZipArchive, setCommentName)
22172309 if (idx < 0 ) {
22182310 RETURN_FALSE ;
22192311 }
2220- RETURN_BOOL (php_zip_set_file_comment (intern , (zip_uint64_t ) idx , comment , comment_len ));
2312+ RETURN_BOOL (php_zip_set_file_comment (Z_ZIP_P ( self ) , (zip_uint64_t ) idx , comment , comment_len ));
22212313}
22222314/* }}} */
22232315
@@ -2247,7 +2339,7 @@ PHP_METHOD(ZipArchive, setCommentIndex)
22472339 PHP_ZIP_STAT_INDEX (intern , index , 0 , sb );
22482340 idx = (zip_uint64_t ) index ;
22492341
2250- RETURN_BOOL (php_zip_set_file_comment (intern , idx , comment , comment_len ));
2342+ RETURN_BOOL (php_zip_set_file_comment (Z_ZIP_P ( self ) , idx , comment , comment_len ));
22512343}
22522344/* }}} */
22532345
@@ -2285,6 +2377,8 @@ PHP_METHOD(ZipArchive, setExternalAttributesName)
22852377 (zip_uint8_t )(opsys & 0xff ), (zip_uint32_t )attr ) < 0 ) {
22862378 RETURN_FALSE ;
22872379 }
2380+
2381+ php_zip_mark_entry_modified (Z_ZIP_P (self ), idx );
22882382 RETURN_TRUE ;
22892383}
22902384/* }}} */
@@ -2309,6 +2403,8 @@ PHP_METHOD(ZipArchive, setExternalAttributesIndex)
23092403 (zip_flags_t )flags , (zip_uint8_t )(opsys & 0xff ), (zip_uint32_t )attr ) < 0 ) {
23102404 RETURN_FALSE ;
23112405 }
2406+
2407+ php_zip_mark_entry_modified (Z_ZIP_P (self ), (zip_int64_t )index );
23122408 RETURN_TRUE ;
23132409}
23142410/* }}} */
@@ -2418,6 +2514,8 @@ PHP_METHOD(ZipArchive, setEncryptionName)
24182514 if (zip_file_set_encryption (intern , idx , (zip_uint16_t )method , password )) {
24192515 RETURN_FALSE ;
24202516 }
2517+
2518+ php_zip_mark_entry_modified (Z_ZIP_P (self ), idx );
24212519 RETURN_TRUE ;
24222520}
24232521/* }}} */
@@ -2446,6 +2544,8 @@ PHP_METHOD(ZipArchive, setEncryptionIndex)
24462544 if (zip_file_set_encryption (intern , index , (zip_uint16_t )method , password )) {
24472545 RETURN_FALSE ;
24482546 }
2547+
2548+ php_zip_mark_entry_modified (Z_ZIP_P (self ), (zip_int64_t )index );
24492549 RETURN_TRUE ;
24502550}
24512551/* }}} */
@@ -2541,6 +2641,8 @@ PHP_METHOD(ZipArchive, setCompressionName)
25412641 (zip_int32_t )comp_method , (zip_uint32_t )comp_flags ) != 0 ) {
25422642 RETURN_FALSE ;
25432643 }
2644+
2645+ php_zip_mark_entry_modified (Z_ZIP_P (this ), idx );
25442646 RETURN_TRUE ;
25452647}
25462648/* }}} */
@@ -2564,6 +2666,8 @@ PHP_METHOD(ZipArchive, setCompressionIndex)
25642666 (zip_int32_t )comp_method , (zip_uint32_t )comp_flags ) != 0 ) {
25652667 RETURN_FALSE ;
25662668 }
2669+
2670+ php_zip_mark_entry_modified (Z_ZIP_P (this ), (zip_int64_t )index );
25672671 RETURN_TRUE ;
25682672}
25692673/* }}} */
@@ -2601,6 +2705,8 @@ PHP_METHOD(ZipArchive, setMtimeName)
26012705 (time_t )mtime , (zip_uint32_t )flags ) != 0 ) {
26022706 RETURN_FALSE ;
26032707 }
2708+
2709+ php_zip_mark_entry_modified (Z_ZIP_P (this ), idx );
26042710 RETURN_TRUE ;
26052711}
26062712/* }}} */
@@ -2624,6 +2730,8 @@ PHP_METHOD(ZipArchive, setMtimeIndex)
26242730 (time_t )mtime , (zip_uint32_t )flags ) != 0 ) {
26252731 RETURN_FALSE ;
26262732 }
2733+
2734+ php_zip_mark_entry_modified (Z_ZIP_P (this ), (zip_int64_t )index );
26272735 RETURN_TRUE ;
26282736}
26292737/* }}} */
@@ -2709,6 +2817,8 @@ PHP_METHOD(ZipArchive, renameIndex)
27092817 RETURN_FALSE ;
27102818 }
27112819
2820+ php_zip_mark_entry_modified (Z_ZIP_P (self ), (zip_int64_t )index );
2821+
27122822 RETURN_TRUE ;
27132823}
27142824/* }}} */
@@ -2739,6 +2849,8 @@ PHP_METHOD(ZipArchive, renameName)
27392849 RETURN_FALSE ;
27402850 }
27412851
2852+ php_zip_mark_entry_modified (Z_ZIP_P (self ), (zip_int64_t )sb .index );
2853+
27422854 RETURN_TRUE ;
27432855}
27442856/* }}} */
@@ -2760,6 +2872,8 @@ PHP_METHOD(ZipArchive, unchangeIndex)
27602872 RETURN_FALSE ;
27612873 }
27622874
2875+ php_zip_unmark_entry_modified (Z_ZIP_P (self ), (zip_int64_t )index );
2876+
27632877 if (zip_unchange (intern , index ) != 0 ) {
27642878 RETURN_FALSE ;
27652879 } else {
@@ -2789,6 +2903,8 @@ PHP_METHOD(ZipArchive, unchangeName)
27892903
27902904 PHP_ZIP_STAT_PATH (intern , name , name_len , 0 , sb );
27912905
2906+ php_zip_unmark_entry_modified (Z_ZIP_P (self ), (zip_int64_t )sb .index );
2907+
27922908 if (zip_unchange (intern , sb .index ) != 0 ) {
27932909 RETURN_FALSE ;
27942910 } else {
@@ -2809,6 +2925,8 @@ PHP_METHOD(ZipArchive, unchangeAll)
28092925
28102926 ZIP_FROM_OBJECT (intern , self );
28112927
2928+ php_zip_forget_modified_entries (Z_ZIP_P (self ));
2929+
28122930 if (zip_unchange_all (intern ) != 0 ) {
28132931 RETURN_FALSE ;
28142932 } else {
@@ -2829,6 +2947,8 @@ PHP_METHOD(ZipArchive, unchangeArchive)
28292947
28302948 ZIP_FROM_OBJECT (intern , self );
28312949
2950+ php_zip_forget_modified_entries (Z_ZIP_P (self ));
2951+
28322952 if (zip_unchange_archive (intern ) != 0 ) {
28332953 RETURN_FALSE ;
28342954 } else {
0 commit comments