Layer extraction/gunzip has no decompression size cap (io.Copy //nolint:gosec "trusted layer content")
Assessment
This issue has not been assessed yet.
Description
Summary
Three copy loops move layer bytes without any size bound, each suppressed from gosec with a comment asserting the content is trusted:
- core/internal/artifact/blobs.go:427 (
gunzipToFile) - core/internal/artifact/blobs.go:484 (
extractGzTar) - core/internal/runtime/launch.go:508 (
extractTar)
if _, err := io.Copy(out, tr); err != nil { //nolint:gosec // trusted layer content
The premise of the suppression does not hold: layer content is publisher-supplied. A runnable image's layers are whatever the publisher pushed to the registry; the staging pipeline verifies integrity (digests) but not size. A gzip bomb or sparse-tar bomb inside a digest-verified layer therefore decompresses without limit into the node's staging root (/tmp/brewlet-runnable/...) or the bundle's classpath dirs — ENOSPC on a shared node, which is exactly the outcome the rest of the staging design works to bound.
For the gzip paths this is the decompression-bomb class gosec's G110 check exists for; the right handling is a bounded copy, not a suppression.
Repro
Any highly-compressible payload inside a layer, published as a legitimate digest-verified image:
$ dd if=/dev/zero bs=1M count=100 | gzip > payload.bin
# pack payload.bin into the image layer, publish, start the workload
$ df -h /tmp # before vs during extraction
The io.Copy in gunzipToFile/extractGzTar writes the full decompressed size with no cap; extractTar likewise copies uncompressed entry bodies with no per-entry or total bound.
Suggested fix
A bounded-copy helper used by all three sites:
// generous absolute ceiling; per-layer limits can ride the manifest's
// declared sizes where available
const maxLayerDecompressedBytes = 8 << 30
func copyBounded(dst io.Writer, src io.Reader) (int64, error) {
n, err := io.Copy(dst, io.LimitReader(src, maxLayerDecompressedBytes+1))
if err == nil && n > maxLayerDecompressedBytes {
return n, fmt.Errorf("layer exceeds decompression cap (%d bytes)", maxLayerDecompressedBytes)
}
return n, err
}
Fail the stage loudly on cap exceed rather than half-writing, then remove the nolint — the finding it suppresses is actually addressed.
Impact framing
Availability hardening, same class as node disk exhaustion from staging retention: publisher-controlled bytes can exhaust a shared node resource the publisher does not own. It does not cross the tenant boundary (a tenant can already fill its own persistent volumes); the gap is the shared staging root and bundle dirs.
- Dominant language
- Go
- Stars
- 0
- Forks
- 0
- Avg merge
- 9h 20m
- Merged PRs (30d)
- 90
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from microsoft/brewlet
-
Difficulty 5/5 Over a week Newbie friendliness 38/100
-
dependencies go
Difficulty 4/5 3-5 days Newbie friendliness 42/100
-
Difficulty 5/5 Over a week Newbie friendliness 35/100
All issues in microsoft/brewlet
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
crossplane/crossplane#7859 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
bazel-contrib/rules_go#4721 · 2 comments ·
-
needs-triage
Difficulty 1/5 Under an hour Newbie friendliness 92/100
-
bug carvel-triage
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
carvel-dev/kapp-controller#1861 ·
-
area/logging kind/bug
Difficulty 2/5 1-3 hours Newbie friendliness 86/100