Skip to content

Skip data of entries refused by $MAX_FILE_SIZE (fixes #47) - #53

Open
almightyfoon wants to merge 1 commit into
jib:masterfrom
almightyfoon:gh47-skip-refused-entry
Open

almightyfoon wants to merge 1 commit into
jib:masterfrom
almightyfoon:gh47-skip-refused-entry

Conversation

@almightyfoon

Copy link
Copy Markdown

Problem

The CVE-2026-9538 fix (f9af014, released in 3.10) refuses entries whose declared
size exceeds $Archive::Tar::MAX_FILE_SIZE. However, after reporting the error it
does next LOOP without consuming the entry's data. _read_tar() then parses every
following 512-byte data block as a header. Each block fails one of the existing
guards (null-padding check, checksum) and raises its own _error(), and each of
those builds a Carp::longmess.

The work therefore scales with the attacker-controlled size field, with one error
per block. A header declaring 2 GiB costs about 4M loop iterations and errors before
the next real header is reached, which largely defeats the mitigation. The Debian
perl maintainer has held back the bookworm/trixie backport of CVE-2026-9538 for this
reason (Debian #1138861, 2026-08-29).

Fix

Before next LOOP, read past the refused entry's data 64 blocks (32 KiB) at a time.
This is the same chunked-read pattern the existing $skip branch uses for filtered
entries. As a result:

  • each refused entry raises exactly one error and needs one header parse;
  • memory use stays at 32 KiB at most, so the original allocation protection is kept;
  • it works on handles that can't seek (IO::Zlib, IO::Uncompress::Bunzip2/UnXz);
  • if the declared size runs past EOF, the short read ends the loop (last LOOP)
    instead of re-scanning.

The remaining cost is proportional to the bytes actually present in the input, not
to the declared size.

Test

t/04_resolved_issues.t gains a "GH #47" block. With MAX_FILE_SIZE = 1024, it checks
that an archive holding a 64 KiB entry followed by a small entry gives:

  • read and iter return only the small entry;
  • exactly one warning, and it is the size-cap error;
  • a crafted header declaring 2 GiB with a much shorter body returns no entries and
    exactly one warning.

Without the lib change, 4 of these assertions fail (129 warnings instead of 1). With
it, the full suite passes on perl 5.40.2.

For scale, here is a crafted 2 GiB-declared header followed by 32 MiB of real body:

warnings time
before 65,537 4.19 s
after 1 0.02 s

gzip, bzip2 and xz archives behave the same way.

References

When an entry's declared size exceeds $Archive::Tar::MAX_FILE_SIZE,
_read_tar() reported the error and did `next LOOP` without consuming
the entry's data. Every following 512-byte data block was then parsed
as a header and rejected with its own error, so a header declaring
2 GiB cost ~4M loop iterations and _error() calls (each building a
Carp::longmess), largely defeating the CVE-2026-9538 mitigation.

Read past the refused entry's data 64 blocks at a time, as the
existing $skip path does, then continue with the next header. This
never allocates more than 32 KiB, works on non-seekable handles
(IO::Zlib, IO::Uncompress::*), and stops at EOF when the declared size
runs past the end of the archive.

Fixes GH jib#47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant