GitHub Advisoryadvisory · Go
GHSA-w8j3-pq8g-8m7w iskorotkov/avro: CPU Exhaustion in Decoder
HIGH
Official page ↗
- Aliases
- CVE-2026-46385
- Ecosystem
- Go
- Packages
- github.com/iskorotkov/avro/v2
- Severity
- High
- CVSS vector
- CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
- Weaknesses
- CWE-1284, CWE-400, CWE-835
- Reviewed
- 2026-05-18
Affected packages
• Go github.com/iskorotkov/avro/v2: < 2.33.0 → fixed in 2.33.0
Description
CPU Exhaustion in Avro Decoder via Unbounded Block-Count Iteration
Summary
The 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`).
Description
Avro 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`: 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` |
| `codec_skip.go` | `mapSkipDecoder.Decode` | — | `b124caa` |
| `reader_generic.go` | `Reader.ReadArrayCB` | #4 | `2ce4242` |
| `reader_generic.go` | `Reader.ReadMapCB` | #4 | `2ce4242` |
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 = v2.33.0`. Replace the import path and run `go mod tidy`:
```bash
go get github.com/iskorotkov/avro/v2@latest
```
Or, for consumers that prefer the original import path, a `replace` directive in `go.mod`:
```
replace github.com/hamba/avro/v2 => github.com/iskorotkov/avro/v2 v2.33.0
```
`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:
```go
cfg := avro.Config{
MaxByteSliceSize: 102_400,
MaxSliceAllocSize: 10_000,
MaxMapAllocSize: 10_000,
}.Freeze()
decoder := cfg.NewDecoder(schema, reader)
```
`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` 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 input
A minimal payload that triggers the bug for an array of `int`:
```
zigzag-encoded long: math.MaxInt64 (block element count)
EOF (no further bytes)
```
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 bl…
Related
Alias · CVE (1)
- CVE-2026-46385 iskorotkov/avro is a fast Go Avro codec. vulnerability