fix: bound total decompression under each top-level archive

the limiter bounds what's held *at once*, and each archive gives its bytes back when it's released. So a recursive bomb (42.zip shape) never trips a limit: every level fits on its own, and the scan just runs for effectively forever. Capping each archive at N× its own size wouldn't help either, since every level of such a bomb is high-ratio, so the work still grows about N-fold per level.

- each top-level archive gets one decompression budget of `max(100 × its size, 1 GiB)`, spent by every archive nested under it at any depth
- the budget is attached to the context when the walk enters a top-level archive, so `Extract` picks it up without a signature change
- once it's spent, the archive being extracted is truncated with a "possible decompression bomb" unknown and the walk moves on. The next top-level archive starts fresh

real trees (a war of jars, a layer of tarballs) expand a few times over, well under the ratio.

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
Alex Goodman
2026-09-24 13:23:32 -04:00
parent d27b6ebb1b
commit 81ee189bf4
7 changed files with 114 additions and 1 deletions
+3 -1
View File
@@ -35,13 +35,14 @@ type Resolver struct {
Digests []file.Digest
// Truncated reports that the resolver covers only part of the archive, and TruncatedReason says why:
// the disk limit stopped extraction early, or an entry was too large to store.
// the disk limit or the decompression budget stopped extraction early, or an entry was too large.
Truncated bool
TruncatedReason string
fileSystemID string
archivePath string
charge *charge
budget *Budget
// content is held in memory while the memory limit admits it and in one spill file once it does
// not. The file is created on first use, so an archive that stays in memory never touches the
@@ -92,6 +93,7 @@ func newResolver(ctx context.Context, fileSystemID, archivePath string, charge *
fileSystemID: fileSystemID,
archivePath: archivePath,
charge: charge,
budget: budgetFromContext(ctx),
tempDir: tmpdir.FromContext(ctx),
byPath: map[string]*node{"/": {path: "/", isDir: true}},
}