GHSA-w8j3-pq8g-8m7w iskorotkov/avro: CPU Exhaustion in Decoder
CPU Exhaustion in Avro Decoder via Unbounded Block-Count IterationSummaryThe Avro array and map decoders looped over an attacker-controlled block-count value without checking the underlying reader's error state inside the loop body. `Reader.ReadBlockHeader` returns the count as a Go `int`, which is 64-bit on `amd64` / `arm64` targets — so a producer can declare a block of up to `math.MaxInt64` (~9.2 × 10¹⁸) elements followed by EOF (or any truncated payload), and the decoder will attempt that many no-op iterations before propagating the error. The realistic ceiling is "indefinite until the worker is killed externally" — a single hostile payload pins a CPU core until the process is OOM-killed, deadline-cancelled, or terminated. Remote, unauthenticated denial-of-service.The fix exits the loop on the first inner-decode error. It does not bound the loop length itself; for full coverage on untrusted inputs, also configure `Config.MaxSliceAllocSize` and `Config.MaxMapAllocSize` (the latter introduced in `v2.33.0`).DescriptionAvro arrays and maps are encoded as one or more blocks; each block declares an element count followed by that many encoded elements. The decoder reads the block count as a zigzag-encoded `long`, then iterates that many times calling an inner decoder.Three iteration sites trusted the block count without checking the reader's accumulated error state between iterations:`codec_skip.go` `sliceSkipDecoder.Decode` — skip helper for arrays.`codec_skip.go` `mapSkipDecoder.Decode` — skip helper for maps.`reader_generic.go` `Reader.ReadArrayCB` and `Reader.ReadMapCB` — callback-based decoders used by generic and unmarshaling code paths.Because the inner `Decode(nil, r)` call is a no-op when `r` has already errored (it returns immediately without consuming bytes), the loop would run to completion even after the first iteration's EOF. On `amd64` / `arm64`, `Reader.ReadBlockHeader` returns the count as `int` (= `int64`), so the loop bound is whatever the wire payload specified, up to `math.MaxInt64`. A modest 200-million-count payload (well under 2³¹) already burns several seconds; a `math.MaxInt − 2` payload (the value used in the regression test `TestDecoder_ArrayMultiBlockExceedsMaxInt` from PR #9) effectively pins the goroutine until external kill.This overlaps with [`GHSA-mc57-h6j3-3hmv`](https://github.com/iskorotkov/avro/security/advisories/GHSA-mc57-h6j3-3hmv): the same large-block-count payload that drives the unbounded loop here also drives the cumulative-arithmetic overflow there (cross-platform), and on a 32-bit target additionally triggers the union-index / byte-slice narrowing.Affected components| File | Function | PR | Fix commit |
|------|----------|----|------------|
| `codec_skip.go` | `sliceSkipDecoder.Decode` | — | [`b124caa`](https://github.com/iskorotkov/avro/commit/b124caa58a821f68f100d86f045f9753b88881e8) |
| `codec_skip.go` | `mapSkipDecoder.Decode` | — | [`b124caa`](https://github.com/iskorotkov/avro/commit/b124caa58a821f68f100d86f045f9753b88881e8) |
| `reader_generic.go` | `Reader.ReadArrayCB` | [#4](https://github.com/iskorotkov/avro/pull/4) | [`2ce4242`](https://github.com/iskorotkov/avro/commit/2ce4242e6095d93470ab3b37ed6082b0596f325c) |
| `reader_generic.go` | `Reader.ReadMapCB` | [#4](https://github.com/iskorotkov/avro/pull/4) | [`2ce4242`](https://github.com/iskorotkov/avro/commit/2ce4242e6095d93470ab3b37ed6082b0596f325c) |These are the audited and patched sites. Any other code path that iterates over an attacker-controlled count while calling a `Reader`-style decoder is structurally susceptible to the same pattern; reviewers of consumer code should grep for `for range l` / `for i := 0; i < int(l); i++` near `Reader` method calls and confirm an in-loop error check.Technical details**Vulnerable pattern:**After `r.Error != nil`, subsequent `Decode` calls short-circuit and return without consuming bytes or doing useful work, but the loop control variable still runs to `l`. With `l = math.MaxInt64`, the loop body executes ~9.2 × 10¹⁸ times — effectively infinite for any realistic timeout.**Fixed pattern** ([`b124caa`](https://github.com/iskorotkov/avro/commit/b124caa58a821f68f100d86f045f9753b88881e8), [`2ce4242`](https://github.com/iskorotkov/avro/commit/2ce4242e6095d93470ab3b37ed6082b0596f325c)):The fix terminates the loop on the first inner error. It does **not** bound `l` itself — a well-formed payload that actually contains `N` encoded `null` elements still iterates `N` times. The `MaxSliceAllocSize` / `MaxMapAllocSize` caps are the policy-level bound on that case (see Mitigation).Fixed behaviorThe reader's accumulated error is checked after every inner `Decode` in the four affected loops. Decoder errors now surface in O(1) iterations instead of O(blockCount) when the underlying read fails mid-stream.Affected versions`github.com/hamba/avro/v2` — all versions up to and including `v2.31.0` (repository is read-only upstream).`github.com/iskorotkov/avro/v2` — all versions prior to `v2.33.0`.Fixed versions`github.com/iskorotkov/avro/v2` `v2.33.0` and later. There is no upstream fix for `github.com/hamba/avro/v2` — module path is archived. Migrate to the fork as described under Mitigation.MitigationMigrate from `github.com/hamba/avro/v2` to `github.com/iskorotkov/avro/v2 >= v2.33.0`. Replace the import path and run `go mod tidy`:Or, for consumers that prefer the original import path, a `replace` directive in `go.mod`:`replace` is honoured only for the **main** module of a build — transitive consumers must add their own `replace`, or migrate the import path directly.The error-propagation fix runs on the existing decode path and requires no configuration.For defense-in-depth against well-formed but oversized payloads (where the fix above does not help, because no error fires), set explicit allocation caps:`MaxMapAllocSize` is new in `v2.33.0` and opt-in (default zero, which leaves the previous unbounded behavior). Without setting it, a producer that ships a `math.MaxInt64`-count block still consumes the corresponding memory and CPU; see [`GHSA-mx64-mj3q-7prj`](https://github.com/iskorotkov/avro/security/advisories/GHSA-mx64-mj3q-7prj) for the cumulative-allocation enforcement details.If you cannot upgrade immediately, the structural workarounds are application-level: per-request decode timeouts, isolated decoder workers under CPU quotas, and rejection of payloads whose advertised block count exceeds a known sane bound for your schema.Proof-of-concept inputA minimal payload that triggers the bug for an array of `int`:The decoder reads the block-count header, enters the loop, fails to read the first element (EOF), records the error, and then iterates `math.MaxInt64 − 1` further times calling the inner decoder as a no-op. Wall-clock cost on commodity hardware: indefinite — the goroutine pins one CPU core until the process is OOM-killed, deadline-cancelled, or terminated externally. The classic *"a few seconds per request"* characterisation applies only to small-but-still-pathological block counts in the 10⁸–10⁹ range (e.g. `200_999_000` in `TestDecoder_SkipArrayEOF`); the architectural ceiling is `math.MaxInt64`.A negative block count (`-N`) is also legal in Avro (signals an N-element block with an explicit byte length); the same iteration pattern applies once the count is negated.ReferencesFix PR: [iskorotkov/avro#4](https://github.com/iskorotkov/avro/pull/4) (callback path)Fix commits: [`b124caa`](https://github.com/iskorotkov/avro/commit/b124caa58a821f68f100d86f045f9753b88881e8) (skip helpers), [`2ce4242`](https://github.com/iskorotkov/avro/commit/2ce4242e6095d93470ab3b37ed6082b0596f325c) (callback path)Release: [`v2.33.0`](https://github.com/iskorotkov/avro/releases/tag/v2.33.0)Security policy: [`SECURITY.md`](https://github.com/iskorotkov/avro/blob/main/SECURITY.md)Related advisories on this fork: [`GHSA-mc57-h6j3-3hmv`](https://github.com/iskorotkov/avro/security/advisories/GHSA-mc57-h6j3-3hmv) (integer overflow — same large-block-count payload also triggers cumulative-arithmetic overflow there), [`GHSA-mx64-mj3q-7prj`](https://github.com/iskorotkov/avro/security/advisories/GHSA-mx64-mj3q-7prj) (unbounded map allocation — the policy-level bound on well-formed huge inputs)Cross-module precedent on `hamba/avro`: [`GO-2023-1930`](https://pkg.go.dev/vuln/GO-2023-1930) / `CVE-2023-37475` / `GHSA-9x44-9pgq-cf45`Upstream (read-only): [`hamba/avro`](https://github.com/hamba/avro)Credits**Discovery and fixes** (commits `b124caa` skip helpers and `2ce4242` callback path, PR #4): Daniel Błażewicz ([@klajok](https://github.com/klajok))**Release authorship**: Ivan Korotkov ([@iskorotkov](https://github.com/iskorotkov))Timeline**2026-04-28** — Skip-decoder fix (`b124caa`) merged.**2026-04-30** — Callback-decoder fix (PR #4, `2ce4242`) merged.**2026-05-06** — `v2.33.0` tagged and released.**2026-05-11** — Advisory published.**2026-05-15** — Advisory revised.
CVSS v4.0 score: 8.7
CVSS v3.1 score: 7.5