Severity by source
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X
Primary rating from Vendor (https://github.com/iskorotkov/avro).
CVSS VectorVendor: https://github.com/iskorotkov/avro
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X
Lifecycle Timeline
6DescriptionCVE.org
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.gosliceSkipDecoder.Decode- skip helper for arrays.codec_skip.gomapSkipDecoder.Decode- skip helper for maps.reader_generic.goReader.ReadArrayCBandReader.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 < int(l); i++ near Reader method calls and confirm an in-loop error check.
Technical details
Vulnerable pattern:
for range l {
d.decoder.Decode(nil, r)
// r.Error may have been set by Decode; loop continues regardless.
}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, 2ce4242):
for range l {
d.decoder.Decode(nil, r)
if r.Error != nil {
break
}
}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 behavior
The 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 includingv2.31.0(repository is read-only upstream).github.com/iskorotkov/avro/v2- all versions prior tov2.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.
Mitigation
Migrate from github.com/hamba/avro/v2 to github.com/iskorotkov/avro/v2 >= v2.33.0. Replace the import path and run go mod tidy:
go get github.com/iskorotkov/avro/v2@latestOr, 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.0replace 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:
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 block with an explicit byte length); the same iteration pattern applies once the count is negated.
References
- Fix PR: iskorotkov/avro#4 (callback path)
- Fix commits:
b124caa(skip helpers),2ce4242(callback path) - Release:
v2.33.0 - Security policy:
SECURITY.md - Related advisories on this fork:
GHSA-mc57-h6j3-3hmv(integer overflow - same large-block-count payload also triggers cumulative-arithmetic overflow there),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/CVE-2023-37475/GHSA-9x44-9pgq-cf45 - Upstream (read-only):
hamba/avro
Credits
- Discovery and fixes (commits
b124caaskip helpers and2ce4242callback path, PR #4): Daniel Błażewicz (@klajok) - Release authorship: Ivan Korotkov (@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.0tagged and released. - 2026-05-11 - Advisory published.
- 2026-05-15 - Advisory revised.
AnalysisAI
Remote unauthenticated denial-of-service in the iskorotkov/avro Go Avro decoder (and the archived upstream hamba/avro/v2) allows a malicious producer to pin a CPU core indefinitely by declaring an Avro array or map block with an attacker-controlled element count up to math.MaxInt64. The array/map decoders in codec_skip.go and reader_generic.go iterate the declared count without checking the Reader's accumulated error state, so a truncated payload triggers ~9.2×10^18 no-op iterations. No public exploit identified at time of analysis beyond the advisory's documented proof-of-concept payload; CVSS 4.0 scores this 8.7 (high) reflecting network reachability with high availability impact.
Technical ContextAI
Apache Avro encodes arrays and maps as a sequence of blocks; each block begins with a zigzag-encoded long declaring how many elements follow. The affected decoders - sliceSkipDecoder.Decode and mapSkipDecoder.Decode in codec_skip.go, and Reader.ReadArrayCB / Reader.ReadMapCB in reader_generic.go - read that count via Reader.ReadBlockHeader, which returns Go int (64-bit on amd64/arm64), and then loop that many times invoking an inner Decode. Once the underlying Reader sets r.Error (e.g., from EOF on a truncated payload), inner Decode calls short-circuit and become no-ops, but the loop counter continues to the original bound. The package CPE pkg:go/github.com_iskorotkov_avro_v2 identifies the fork, and the same defect existed in the read-only upstream github.com/hamba/avro/v2. The root cause maps to CWE-400 (Uncontrolled Resource Consumption): an external input directly determines the upper bound of a CPU-bound loop without validation against the actual stream length or any policy cap.
RemediationAI
Vendor-released patch: github.com/iskorotkov/avro/v2 v2.33.0, applied via 'go get github.com/iskorotkov/avro/v2@latest' followed by 'go mod tidy', with fix commits b124caa (skip helpers) and 2ce4242 (callback path via PR #4). Consumers wanting to keep the original hamba import path can add a go.mod replace directive 'replace github.com/hamba/avro/v2 => github.com/iskorotkov/avro/v2 v2.33.0', noting that replace directives only apply to the main module so transitive consumers must add their own. For defense-in-depth against well-formed but oversized payloads - which the error-propagation fix does not address - configure avro.Config with explicit MaxSliceAllocSize, MaxMapAllocSize (new in v2.33.0, default zero meaning unbounded), and MaxByteSliceSize caps; the trade-off is that legitimate large arrays or maps will be rejected. If upgrading is not immediately possible, apply per-request decode timeouts, run decoder workers under CPU quotas, and reject payloads whose advertised block count exceeds a schema-specific sane bound; these add latency and require knowledge of expected payload shapes. Full details at https://github.com/iskorotkov/avro/security/advisories/GHSA-w8j3-pq8g-8m7w.
Same weakness CWE-400 – Uncontrolled Resource Consumption
View allSame technique Denial Of Service
View allVendor StatusVendor
Share
External POC / Exploit Code
Leaving vuln.today
EUVD-2026-33444
GHSA-w8j3-pq8g-8m7w