Skip to content

size limits are enforced post-hoc for an archive that understates its central directory: the bytes are already on disk #157

Description

@kiki830621

Split out of #130 (PR #154), from cross-model review.

What #130 actually bounds

ZipHelper.unzip checks the archive's declarations before extracting, and measures the extracted tree afterwards. Both refuse. But they refuse at different moments, and only the first one is a bound:

archive when it is refused bytes written first
declares more than the limit before extraction none
understates its entries after extraction, from the walk all of them

The central directory can lie — ZipHelper.swift says so in its own comment — so a package that declares 1 KB and writes 4 MB gets its 4 MB onto disk, and is only then refused. Scale that: a package can consume the volume, or the 1.7 GB from #130's own PoC, and the refusal arrives afterwards. That is post-hoc validation, not "how far a package may expand".

The actual compression ratio is not checked at all. An entry declaring a modest size under the 500× declared-ratio limit can still emit 200 MiB from a tiny compressed stream, pass the 256 MiB actual entry limit, and have a real ratio well over 500×.

What it needs

Count bytes as the decompressor produces them and abort the extraction as soon as a per-entry or aggregate limit is crossed. The post-extraction walk can stay as defence in depth; it just cannot be the bound.

The easy route does not work — measured

ZIPFoundation's unzipItem(at:to:…, progress: Progress?) looks sufficient: its helpers check progress?.isCancelled and throw ArchiveError.cancelledOperation, and Archive+Helpers.swift adds actual bytes (progress?.completedUnitCount += Int64(data.count)).

But unzipItem builds totalUnitCount from archive.totalUnitCountForReading(_:) — the declared sizes — and aggregates per-entry children with addChild(_:withPendingUnitCount:). For an archive that understates, each child saturates against its own declared total, so the parent's count stops rising exactly in the case that matters. Observing the parent progress therefore cannot detect the overflow it exists to detect.

So this needs per-entry Archive.extract(_:bufferSize:consumer:) with our own counter, which means re-doing the extraction path that carries #137#139's hardening (symlink refusal, owner-only descriptors, the private copy, the strict walk). That is why it is not folded into #130 — it is a rewrite of security-critical code, and #130's own history is twelve fix rounds of exactly that kind of change going wrong one round at a time.

Meanwhile

PR #154's CHANGELOG now states the boundary rather than implying a bound: a declaration over the limit is refused before any byte is written; an understating archive is caught after those bytes have landed.

Refs #130

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions