Skip to content

security: validate QCOW2 ClusterBits to prevent OOM from crafted headers#1995

Open
adilburaksen wants to merge 1 commit intogoogle:mainfrom
adilburaksen:fix/qcow2-clusterbits-bounds-check
Open

security: validate QCOW2 ClusterBits to prevent OOM from crafted headers#1995
adilburaksen wants to merge 1 commit intogoogle:mainfrom
adilburaksen:fix/qcow2-clusterbits-bounds-check

Conversation

@adilburaksen
Copy link
Copy Markdown

Summary

parseHeader in extractor/filesystem/embeddedfs/qcow2/format.go does not validate the ClusterBits field from the QCOW2 header. Every code path that computes clusterSize = uint64(1) << header.ClusterBits then passes that value directly to make() or bytes.Repeat() without bounds checking.

Impact: A 116-byte crafted .qcow2 file with ClusterBits=33 causes scalibr to attempt an 8 GB heap allocation inside readL2Table, crashing the process with runtime: out of memory. Values up to 62 produce allocations up to 4 EB.

Root Cause

// format.go:218 — no bounds check on ClusterBits before here
clusterSize := uint64(1) << header.ClusterBits
buf := make([]byte, clusterSize)   // ← OOM with attacker-controlled value

Same pattern at: readRefcountBlock:316, readCluster:232, writeRawImage:340.

Fix

Validate ClusterBits against the QCOW2 spec (9–21) in parseHeader, before any clusterSize is computed:

if h.ClusterBits < 9 || h.ClusterBits > 21 {
    return nil, nil, fmt.Errorf("ClusterBits %d out of valid range [9, 21]", h.ClusterBits)
}

Test

TestConvertQCOW2ClusterBitsRejected in security_regression_test.go:

  • Pre-fix: 116-byte input → runtime: out of memory at readL2Table:format.go:218
  • Post-fix: 116-byte input → immediate parse error, TotalAlloc delta = 0 MB

All existing qcow2 tests pass (18/18).

@google-cla
Copy link
Copy Markdown

google-cla Bot commented Apr 20, 2026

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@adilburaksen
Copy link
Copy Markdown
Author

Updated with two additional fixes per code review:

  • L1Size bounds check (prevents 4 GB alloc via make([]uint64, L1Size))
  • RefcountTableClusters bounds check (prevents ~4 GB alloc via uint32 overflow in readRefcountTable)

All 38 tests pass.

Three header fields are used as allocation sizes without validation:

1. ClusterBits — used as shift exponent: 1<<ClusterBits. Value 33 → 8 GB
   make([]byte, 8GB) in readL2Table (format.go:218). Confirmed live:
   116-byte file causes "runtime: out of memory".

2. L1Size — used directly: make([]uint64, L1Size). Value 0x1FFFFFFF
   → 4 GB allocation in readL1Table.

3. RefcountTableClusters — multiplied with uint32(clusterSize) in
   readRefcountTable. With valid ClusterBits=9, value 0x7FFFFF yields
   a ~4 GB uint32 result without overflow.

Fix: reject all three out-of-range values in parseHeader before any
clusterSize or allocation is computed.
- ClusterBits: spec range [9, 21]
- L1Size: cap at 2M entries (covers ≥64 TiB at minimum cluster size)
- RefcountTableClusters: cap at 64 (typical images use 1–3)

Regression test: TestConvertQCOW2ClusterBitsRejected confirms
116-byte malicious input is rejected with 0 MB TotalAlloc delta.
All 38 existing tests pass.
@adilburaksen adilburaksen force-pushed the fix/qcow2-clusterbits-bounds-check branch from 36260d7 to c3a62c2 Compare April 20, 2026 23:25
@adilburaksen
Copy link
Copy Markdown
Author

Hi! Just a friendly ping — all CI checks are passing. Happy to address any review feedback whenever you get a chance. Thanks for your time!

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