Skip to content

fix: reject an empty archive whose central directory offset is past the EOCD - #1025

Merged
Pr0methean merged 4 commits into
zip-rs:masterfrom
yuxi-liu-wired:fix/empty-archive-cd-offset
Oct 9, 2026
Merged

Pr0methean merged 4 commits into
zip-rs:masterfrom
yuxi-liu-wired:fix/empty-archive-cd-offset

Conversation

@yuxi-liu-wired

Copy link
Copy Markdown
Contributor

Problem

find_central_directory_end checks that the central directory starts before the EOCD, but it
returns early for an archive with zero entries, before that check. So a 22-byte EOCD that says
"0 entries, central directory at 0x7fffffff" opens as a valid empty archive, with
central_directory_start() = 2147483647 in a 22-byte file.

ZipWriter::new_append then takes that offset as the place to write the next entry:

// 22-byte EOCD: 0 entries, cd size 0, cd offset 0x7fffffff
let mut w = ZipWriter::new_append(file)?;
w.start_file("hello.txt", SimpleFileOptions::default())?;
w.write_all(b"hello")?;
w.finish()?; // Ok

finish returns Ok, and the file is now 2,147,483,770 bytes, with the new local header at
0x7fffffff. With offset 0xffffff00 it is 4 GiB. On ext4 the file is sparse, so it takes only a few
KiB on disk, but anything that copies or uploads it moves 2–4 GiB. With a Cursor<Vec<u8>>,
new_append + finish tries to allocate the whole 2–4 GiB. Any program that appends to an
archive it got from somewhere else is affected.

Info-ZIP unzip rejects such a file. The ZIP64 path already rejects the same shape, because it
checks the file count against the EOCD64 position.

Change

In the zero-entry branch, apply the same consistency check as for non-empty archives: the
central directory offset must not be past the EOCD. An offset equal to the EOCD position is still
accepted, because that is exactly where an empty central directory sits (APPNOTE 4.4.24: the
offset is where the central directory starts; with no entries it ends where it starts, right
before the EOCD). On failure the search goes on to the next EOCD candidate, as for the other
checks.

Test

tests/empty_archive_cd_offset.rs:

  • empty_archive_with_cd_offset_past_eocd_is_rejected: offsets 23, 0x7fffffff and 0xffffff00 in
    a 22-byte EOCD. Both ZipArchive::new and ZipWriter::new_append must fail. Fails on master
    (offset 23 is accepted).
  • empty_archive_still_opens_and_appends: the normal empty archive (offset 0), with and without a
    prepended prefix, still opens, takes an appended entry, and the result is small and reads back.

The suite passes on 1.88, stable and nightly with default features, --no-default-features,
--all-features, and the two single-deflate feature sets; cargo clippy --all-targets -- -D warnings and cargo doc --no-deps are clean with default, --no-default-features and
--all-features; cargo fmt --check --all is clean.

Found with a fuzzer that appends to and merges generated archives and checks that existing
entries keep their bytes.

…he EOCD

An end-of-central-directory record with zero entries skipped every consistency
check, including the one that the central directory starts before the EOCD.
A 22-byte EOCD claiming the central directory starts at 0x7fffffff was
accepted with archive_offset 0, and `ZipWriter::new_append` then wrote the next
entry at that offset: `finish` returned Ok and the output grew to 2 GiB
(0xffffff00: 4 GiB), or tried to allocate that much in a `Cursor<Vec<u8>>`.

APPNOTE 4.4.24: the offset of the start of the central directory is relative
to the starting disk number. An empty central directory ends where it starts,
directly before the EOCD, so its offset cannot be past the EOCD. Apply the same
check as non-empty archives, except that an offset equal to the EOCD position
(empty central directory immediately before the EOCD) stays valid.

Info-ZIP's unzip rejects such a file. The ZIP64 path already rejects it through
the file-count check against the EOCD64 position.
@Pr0methean Pr0methean added this to the 9.0.1 milestone Oct 8, 2026
Comment thread tests/empty_archive_cd_offset.rs Resolved
Signed-off-by: Chris Hennick <4961925+Pr0methean@users.noreply.github.com>
Pr0methean
Pr0methean previously approved these changes Oct 8, 2026
Signed-off-by: Chris Hennick <4961925+Pr0methean@users.noreply.github.com>
Pr0methean
Pr0methean previously approved these changes Oct 9, 2026
@Pr0methean
Pr0methean enabled auto-merge October 9, 2026 03:29
@Pr0methean
Pr0methean added this pull request to the merge queue Oct 9, 2026
Merged via the queue into zip-rs:master with commit 25bb3de Oct 9, 2026
134 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants