Skip to content

Commit dd200bb

Browse files
committed
ext/zip: Reject ZipArchive mutators during close()
A progress or cancel callback runs inside zip_close() after libzip has fixed the list of entries it writes. Deleting or unchanging an entry from the callback frees a dirent that zip_close() still uses, and other changes are either dropped or make the close fail and lose the archive. Throw the Error that close() and open() already raise in that state from every method that modifies the archive.
1 parent 75d77cb commit dd200bb

5 files changed

Lines changed: 397 additions & 4 deletions

File tree

‎NEWS‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,9 @@ PHP NEWS
191191
(David Carlier)
192192
. Fixed bug GH-23747 (ZipArchive::close() use-after-free from a progress or
193193
cancel callback). (David Carlier)
194+
. Fixed a use-after-free when a ZipArchive method that modifies the archive
195+
is called from a progress or cancel callback during close().
196+
(Ilia Alshanetsky)
194197

195198

196199
24 Sep 2026, PHP 8.4.26

‎ext/zip/php_zip.c‎

Lines changed: 123 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -638,6 +638,15 @@ static char * php_zipobj_get_zip_comment(ze_zip_object *obj, int *len) /* {{{ */
638638
}
639639
/* }}} */
640640

641+
static bool php_zipobj_closing(ze_zip_object *obj)
642+
{
643+
if (obj->archive && obj->archive->close) {
644+
zend_throw_error(NULL, "Already being closed");
645+
return true;
646+
}
647+
return false;
648+
}
649+
641650
#ifdef HAVE_GLOB /* {{{ */
642651
#ifndef GLOB_ONLYDIR
643652
#define GLOB_ONLYDIR (1<<30)
@@ -1582,9 +1591,8 @@ PHP_METHOD(ZipArchive, open)
15821591

15831592
if (ze_obj->archive) {
15841593
/* we already have an opened zip, free it */
1585-
if (ze_obj->archive->close) {
1594+
if (php_zipobj_closing(ze_obj)) {
15861595
efree(resolved_path);
1587-
zend_throw_error(NULL, "Already being closed");
15881596
RETURN_THROWS();
15891597
}
15901598
intern = ze_obj->archive->za;
@@ -1647,6 +1655,10 @@ PHP_METHOD(ZipArchive, setPassword)
16471655

16481656
ZIP_FROM_OBJECT(intern, self);
16491657

1658+
if (php_zipobj_closing(Z_ZIP_P(self))) {
1659+
RETURN_THROWS();
1660+
}
1661+
16501662
if (password_len < 1) {
16511663
RETURN_FALSE;
16521664
}
@@ -1676,8 +1688,7 @@ PHP_METHOD(ZipArchive, close)
16761688

16771689
ze_obj = Z_ZIP_P(self);
16781690

1679-
if (ze_obj->archive->close) {
1680-
zend_throw_error(NULL, "Already being closed");
1691+
if (php_zipobj_closing(ze_obj)) {
16811692
RETURN_THROWS();
16821693
}
16831694

@@ -1823,6 +1834,10 @@ PHP_METHOD(ZipArchive, addEmptyDir)
18231834

18241835
ZIP_FROM_OBJECT(intern, self);
18251836

1837+
if (php_zipobj_closing(Z_ZIP_P(self))) {
1838+
RETURN_THROWS();
1839+
}
1840+
18261841
if (dirname_len<1) {
18271842
RETURN_FALSE;
18281843
}
@@ -1881,6 +1896,10 @@ static void php_zip_add_from_pattern(INTERNAL_FUNCTION_PARAMETERS, int type) /*
18811896
RETURN_THROWS();
18821897
}
18831898

1899+
if (php_zipobj_closing(Z_ZIP_P(self))) {
1900+
RETURN_THROWS();
1901+
}
1902+
18841903
if (type == 1) {
18851904
found = php_zip_glob(ZSTR_VAL(pattern), ZSTR_LEN(pattern), glob_flags, return_value);
18861905
} else {
@@ -2015,6 +2034,10 @@ PHP_METHOD(ZipArchive, addFile)
20152034
entry_name_len = ZSTR_LEN(filename);
20162035
}
20172036

2037+
if (php_zipobj_closing(Z_ZIP_P(self))) {
2038+
RETURN_THROWS();
2039+
}
2040+
20182041
if (php_zip_add_file(Z_ZIP_P(self), ZSTR_VAL(filename), ZSTR_LEN(filename),
20192042
entry_name, entry_name_len, offset_start, offset_len, -1, flags) < 0) {
20202043
RETURN_FALSE;
@@ -2048,6 +2071,10 @@ PHP_METHOD(ZipArchive, replaceFile)
20482071
RETURN_THROWS();
20492072
}
20502073

2074+
if (php_zipobj_closing(Z_ZIP_P(self))) {
2075+
RETURN_THROWS();
2076+
}
2077+
20512078
if (php_zip_add_file(Z_ZIP_P(self), ZSTR_VAL(filename), ZSTR_LEN(filename),
20522079
NULL, 0, offset_start, offset_len, index, flags) < 0) {
20532080
RETURN_FALSE;
@@ -2078,6 +2105,10 @@ PHP_METHOD(ZipArchive, addFromString)
20782105

20792106
ZIP_FROM_OBJECT(intern, self);
20802107

2108+
if (php_zipobj_closing(Z_ZIP_P(self))) {
2109+
RETURN_THROWS();
2110+
}
2111+
20812112
ze_obj = Z_ZIP_P(self);
20822113
archive = ze_obj->archive;
20832114
if (archive->buffers_cnt) {
@@ -2220,6 +2251,10 @@ PHP_METHOD(ZipArchive, setArchiveComment)
22202251

22212252
ZIP_FROM_OBJECT(intern, self);
22222253

2254+
if (php_zipobj_closing(Z_ZIP_P(self))) {
2255+
RETURN_THROWS();
2256+
}
2257+
22232258
if (comment_len > 0xffff) {
22242259
zend_argument_value_error(1, "must be less than 65535 bytes");
22252260
RETURN_THROWS();
@@ -2268,6 +2303,10 @@ PHP_METHOD(ZipArchive, setArchiveFlag)
22682303

22692304
ZIP_FROM_OBJECT(intern, self);
22702305

2306+
if (php_zipobj_closing(Z_ZIP_P(self))) {
2307+
RETURN_THROWS();
2308+
}
2309+
22712310
if (zip_set_archive_flag(intern, flag, (int)value)) {
22722311
RETURN_FALSE;
22732312
} else {
@@ -2311,6 +2350,10 @@ PHP_METHOD(ZipArchive, setCommentName)
23112350

23122351
ZIP_FROM_OBJECT(intern, self);
23132352

2353+
if (php_zipobj_closing(Z_ZIP_P(self))) {
2354+
RETURN_THROWS();
2355+
}
2356+
23142357
if (comment_len > 0xffff) {
23152358
zend_argument_value_error(2, "must be less than 65535 bytes");
23162359
RETURN_THROWS();
@@ -2342,6 +2385,10 @@ PHP_METHOD(ZipArchive, setCommentIndex)
23422385

23432386
ZIP_FROM_OBJECT(intern, self);
23442387

2388+
if (php_zipobj_closing(Z_ZIP_P(self))) {
2389+
RETURN_THROWS();
2390+
}
2391+
23452392
if (comment_len > 0xffff) {
23462393
zend_argument_value_error(2, "must be less than 65535 bytes");
23472394
RETURN_THROWS();
@@ -2374,6 +2421,10 @@ PHP_METHOD(ZipArchive, setExternalAttributesName)
23742421

23752422
ZIP_FROM_OBJECT(intern, self);
23762423

2424+
if (php_zipobj_closing(Z_ZIP_P(self))) {
2425+
RETURN_THROWS();
2426+
}
2427+
23772428
if (name_len == 0) {
23782429
zend_argument_must_not_be_empty_error(1);
23792430
RETURN_THROWS();
@@ -2407,6 +2458,10 @@ PHP_METHOD(ZipArchive, setExternalAttributesIndex)
24072458

24082459
ZIP_FROM_OBJECT(intern, self);
24092460

2461+
if (php_zipobj_closing(Z_ZIP_P(self))) {
2462+
RETURN_THROWS();
2463+
}
2464+
24102465
PHP_ZIP_STAT_INDEX(intern, index, 0, sb);
24112466
if (zip_file_set_external_attributes(intern, (zip_uint64_t)index,
24122467
(zip_flags_t)flags, (zip_uint8_t)(opsys&0xff), (zip_uint32_t)attr) < 0) {
@@ -2502,6 +2557,10 @@ PHP_METHOD(ZipArchive, setEncryptionName)
25022557

25032558
ZIP_FROM_OBJECT(intern, self);
25042559

2560+
if (php_zipobj_closing(Z_ZIP_P(self))) {
2561+
RETURN_THROWS();
2562+
}
2563+
25052564
if (name_len == 0) {
25062565
zend_argument_must_not_be_empty_error(1);
25072566
RETURN_THROWS();
@@ -2541,6 +2600,10 @@ PHP_METHOD(ZipArchive, setEncryptionIndex)
25412600

25422601
ZIP_FROM_OBJECT(intern, self);
25432602

2603+
if (php_zipobj_closing(Z_ZIP_P(self))) {
2604+
RETURN_THROWS();
2605+
}
2606+
25442607
if (UNEXPECTED(zip_file_set_encryption(intern, index, ZIP_EM_NONE, NULL) < 0)) {
25452608
php_error_docref(NULL, E_WARNING, "password reset failed");
25462609
RETURN_FALSE;
@@ -2629,6 +2692,10 @@ PHP_METHOD(ZipArchive, setCompressionName)
26292692

26302693
ZIP_FROM_OBJECT(intern, this);
26312694

2695+
if (php_zipobj_closing(Z_ZIP_P(this))) {
2696+
RETURN_THROWS();
2697+
}
2698+
26322699
if (name_len == 0) {
26332700
zend_argument_must_not_be_empty_error(1);
26342701
RETURN_THROWS();
@@ -2663,6 +2730,10 @@ PHP_METHOD(ZipArchive, setCompressionIndex)
26632730

26642731
ZIP_FROM_OBJECT(intern, this);
26652732

2733+
if (php_zipobj_closing(Z_ZIP_P(this))) {
2734+
RETURN_THROWS();
2735+
}
2736+
26662737
if (zip_set_file_compression(intern, (zip_uint64_t)index,
26672738
(zip_int32_t)comp_method, (zip_uint32_t)comp_flags) != 0) {
26682739
RETURN_FALSE;
@@ -2689,6 +2760,10 @@ PHP_METHOD(ZipArchive, setMtimeName)
26892760

26902761
ZIP_FROM_OBJECT(intern, this);
26912762

2763+
if (php_zipobj_closing(Z_ZIP_P(this))) {
2764+
RETURN_THROWS();
2765+
}
2766+
26922767
if (name_len == 0) {
26932768
zend_argument_must_not_be_empty_error(1);
26942769
RETURN_THROWS();
@@ -2723,6 +2798,10 @@ PHP_METHOD(ZipArchive, setMtimeIndex)
27232798

27242799
ZIP_FROM_OBJECT(intern, this);
27252800

2801+
if (php_zipobj_closing(Z_ZIP_P(this))) {
2802+
RETURN_THROWS();
2803+
}
2804+
27262805
if (zip_file_set_mtime(intern, (zip_uint64_t)index,
27272806
(time_t)mtime, (zip_uint32_t)flags) != 0) {
27282807
RETURN_FALSE;
@@ -2745,6 +2824,10 @@ PHP_METHOD(ZipArchive, deleteIndex)
27452824

27462825
ZIP_FROM_OBJECT(intern, self);
27472826

2827+
if (php_zipobj_closing(Z_ZIP_P(self))) {
2828+
RETURN_THROWS();
2829+
}
2830+
27482831
if (index < 0) {
27492832
RETURN_FALSE;
27502833
}
@@ -2772,6 +2855,10 @@ PHP_METHOD(ZipArchive, deleteName)
27722855

27732856
ZIP_FROM_OBJECT(intern, self);
27742857

2858+
if (php_zipobj_closing(Z_ZIP_P(self))) {
2859+
RETURN_THROWS();
2860+
}
2861+
27752862
if (name_len < 1) {
27762863
RETURN_FALSE;
27772864
}
@@ -2803,6 +2890,10 @@ PHP_METHOD(ZipArchive, renameIndex)
28032890

28042891
ZIP_FROM_OBJECT(intern, self);
28052892

2893+
if (php_zipobj_closing(Z_ZIP_P(self))) {
2894+
RETURN_THROWS();
2895+
}
2896+
28062897
if (new_name_len == 0) {
28072898
zend_argument_must_not_be_empty_error(2);
28082899
RETURN_THROWS();
@@ -2831,6 +2922,10 @@ PHP_METHOD(ZipArchive, renameName)
28312922

28322923
ZIP_FROM_OBJECT(intern, self);
28332924

2925+
if (php_zipobj_closing(Z_ZIP_P(self))) {
2926+
RETURN_THROWS();
2927+
}
2928+
28342929
if (new_name_len == 0) {
28352930
zend_argument_must_not_be_empty_error(2);
28362931
RETURN_THROWS();
@@ -2859,6 +2954,10 @@ PHP_METHOD(ZipArchive, unchangeIndex)
28592954

28602955
ZIP_FROM_OBJECT(intern, self);
28612956

2957+
if (php_zipobj_closing(Z_ZIP_P(self))) {
2958+
RETURN_THROWS();
2959+
}
2960+
28622961
if (index < 0) {
28632962
RETURN_FALSE;
28642963
}
@@ -2886,6 +2985,10 @@ PHP_METHOD(ZipArchive, unchangeName)
28862985

28872986
ZIP_FROM_OBJECT(intern, self);
28882987

2988+
if (php_zipobj_closing(Z_ZIP_P(self))) {
2989+
RETURN_THROWS();
2990+
}
2991+
28892992
if (name_len < 1) {
28902993
RETURN_FALSE;
28912994
}
@@ -2912,6 +3015,10 @@ PHP_METHOD(ZipArchive, unchangeAll)
29123015

29133016
ZIP_FROM_OBJECT(intern, self);
29143017

3018+
if (php_zipobj_closing(Z_ZIP_P(self))) {
3019+
RETURN_THROWS();
3020+
}
3021+
29153022
if (zip_unchange_all(intern) != 0) {
29163023
RETURN_FALSE;
29173024
} else {
@@ -2932,6 +3039,10 @@ PHP_METHOD(ZipArchive, unchangeArchive)
29323039

29333040
ZIP_FROM_OBJECT(intern, self);
29343041

3042+
if (php_zipobj_closing(Z_ZIP_P(self))) {
3043+
RETURN_THROWS();
3044+
}
3045+
29353046
if (zip_unchange_archive(intern) != 0) {
29363047
RETURN_FALSE;
29373048
} else {
@@ -3222,6 +3333,10 @@ PHP_METHOD(ZipArchive, registerProgressCallback)
32223333

32233334
ZIP_FROM_OBJECT(intern, self);
32243335

3336+
if (php_zipobj_closing(Z_ZIP_P(self))) {
3337+
RETURN_THROWS();
3338+
}
3339+
32253340
archive = Z_ZIP_P(self)->archive;
32263341

32273342
/* register */
@@ -3264,6 +3379,10 @@ PHP_METHOD(ZipArchive, registerCancelCallback)
32643379

32653380
ZIP_FROM_OBJECT(intern, self);
32663381

3382+
if (php_zipobj_closing(Z_ZIP_P(self))) {
3383+
RETURN_THROWS();
3384+
}
3385+
32673386
archive = Z_ZIP_P(self)->archive;
32683387

32693388
/* register */

0 commit comments

Comments
 (0)