From 6f512803d27153d6502f0a30613fc829c8d20f15 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Fri, 14 Aug 2026 10:28:20 -0400 Subject: [PATCH] chore(lint): catch mholt/archives decompressors too The decompression rule only matched the stdlib constructors, so the two cataloger sites that decompress through mholt/archives went unreported while being exactly as unbounded. Matches OpenReader on an archives.Decompressor, which picks up both. Also notes in the report text that a byte limit bounds the input and not what the consumer retains. Measured on the mtree parser: 16MB of newlines, well inside a 64MB byte cap, cost 8GB of peak heap, because the parser keeps an object per line. A site can satisfy this rule and still OOM. Worth knowing for the next rule: a type filter that fails to resolve takes the whole rule set down with "used Run() with an empty rule set" rather than just skipping that rule. Type.Is on an interface does that; Type.Implements with an m.Import is what works. Signed-off-by: Alex Goodman --- test/rules/rules.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/test/rules/rules.go b/test/rules/rules.go index 2145b967a..881ed0547 100644 --- a/test/rules/rules.go +++ b/test/rules/rules.go @@ -55,6 +55,8 @@ func noUnboundedAllocations(m dsl.Matcher) { // nolint:unused func noUnboundedDecompression(m dsl.Matcher) { + m.Import("github.com/mholt/archives") + // compression ratios are unbounded (gzip does ~1032:1), so a decompressed stream must be capped // independently of the compressed input, which is already bounded by the file it came from. // The constructor is flagged rather than the consumer because the consumer is often a @@ -68,7 +70,14 @@ func noUnboundedDecompression(m dsl.Matcher) { `xz.NewReader($_)`, ). Where(m.File().PkgPath.Matches(`/cataloger/`)). - Report("unbounded decompression in a cataloger: wrap the decompressed stream in io.LimitReader before anything consumes it, or nolint with the bound that already applies") + Report("unbounded decompression in a cataloger: wrap the decompressed stream in io.LimitReader before anything consumes it, or nolint with the bound that already applies. Note that a byte limit bounds the input, not what the consumer retains: a parser that keeps an object per line still needs a count bound") + + // the mholt/archives decompressors reach the same stdlib readers a layer down, so a site using them + // is exactly as unbounded while matching none of the constructors above + m.Match(`$d.OpenReader($_)`). + Where(m.File().PkgPath.Matches(`/cataloger/`) && + m["d"].Type.Implements(`archives.Decompressor`)). + Report("unbounded decompression in a cataloger: $d.OpenReader returns a stream with no ceiling. bound it before anything consumes it, or nolint with the bound that already applies") } // nolint:unused