{"id":"GHSA-mx64-mj3q-7prj","aliases":["GO-2026-5048"],"url":"https://o3.security/vulnerability/GHSA-mx64-mj3q-7prj","summary":"iskorotkov/avro: Denial-of-Service Vulnerability in Decoder","details":"# Memory Exhaustion via Unbounded Map Allocations in Avro Decoder\n\n## Summary\n\nThe Avro map decoder accepted attacker-controlled block-element counts from the wire format and grew the destination map without enforcing an upper bound. The slice decoder already had `Config.MaxSliceAllocSize` for the equivalent attack against arrays; the map decoder had no analogous limit, so a producer could declare an arbitrarily large map (in one block, or chunked across many sub-limit blocks) and exhaust process memory until the OOM killer fired.\n\nThe fix introduces `Config.MaxMapAllocSize` with cumulative enforcement across block boundaries. **The new limit is opt-in**: the field defaults to zero, which preserves the previous unbounded behavior for backward compatibility. **Upgrading to `v2.33.0` alone does not mitigate the issue** — consumers of untrusted Avro data must explicitly set `MaxMapAllocSize` on their `avro.Config`.\n\n## Description\n\nAvro maps are encoded as a sequence of blocks; each block declares a `long` element count followed by that many key/value pairs. The decoder uses these counts both to size the destination map and as the loop bound for reading entries.\n\nPre-fix, the map decoder enforced no upper limit at any layer:\n\n- No per-block element-count check.\n- No cumulative across-block element-count check.\n- No memory-budget check before `make(map[...]..., n)` or before growing the map.\n\nThe slice decoder had been hardened via `Config.MaxSliceAllocSize` and tracked cumulatively across blocks; the map decoder was a missing-by-symmetry gap. Even a partial per-block bound on maps would have been insufficient on its own — Avro permits encoding a logical map as many small blocks, so a producer could split a 10 GB map into 10,000 sub-MaxMapAllocSize blocks and still drive total allocation past any single-block threshold. The fix tracks cumulative entry count at block-header boundaries — *before* the block's entries are decoded into the map — and errors out before allocation when the running total would exceed the configured cap.\n\nTwo decoder variants were affected, both in `codec_map.go`:\n\n- `mapDecoder.Decode` — string-keyed maps.\n- `mapDecoderUnmarshaler.Decode` — `encoding.TextUnmarshaler`-keyed maps (e.g. `map[CustomKey]V` where `*CustomKey` implements `UnmarshalText`).\n\n## Affected components\n\n| File | Symbol | Pre-fix behavior | Post-fix behavior |\n|------|--------|------------------|-------------------|\n| `config.go` | `Config.MaxMapAllocSize` | Field did not exist | New `int` field; default zero means unlimited (back-compat) |\n| `codec_map.go` | `mapDecoder.Decode` | Read block count, grew map unbounded | Validates cumulative count against `MaxMapAllocSize` at each block header |\n| `codec_map.go` | `mapDecoderUnmarshaler.Decode` | Same | Same |\n\nPR [#5](https://github.com/iskorotkov/avro/pull/5) (`fix/map-alloc-chunking-bypass`) covers both decoders and adds chunking-attack tests for both. The same PR also adds the previously-missing chunking-attack test coverage for the slice path in `534c7518` — the slice *logic* was already correct, only its test coverage was incomplete.\n\n## Technical details\n\nThe fix mirrors the slice decoder's pattern:\n\n1. At each block header, read the element count as `int64`.\n2. Add it to a running total maintained across the block loop.\n3. If the running total exceeds `Config.MaxMapAllocSize` (when nonzero), return an error before allocating any of that block's entries.\n4. Otherwise, decode the block's entries into the map.\n\nPer-block enforcement alone would be bypassable by chunking; cumulative tracking closes that. The check sits at the block-header read, *before* per-entry allocation, so a single oversized block also cannot allocate first and then fail post-hoc.\n\n`Config.MaxMapAllocSize` semantics match `Config.MaxSliceAllocSize`: zero means unlimited, any positive value is the cumulative cap on element count (not byte size).\n\n## Fixed behavior\n\n`v2.33.0` adds the `MaxMapAllocSize` configuration field and the cumulative-enforcement logic in both map decoders. Both decoders return a descriptive error when the cumulative entry count would exceed the configured cap; no entries are allocated past the limit.\n\nTests added in PR #5 cover, for both `mapDecoder` and `mapDecoderUnmarshaler`:\n\n- Single-block allocation exceeding the limit (rejected before allocation).\n- Chunking attack: multiple sub-limit blocks whose cumulative count exceeds the limit (rejected at the block-header that crosses the threshold).\n- Multi-block under the limit (decoded normally).\n\n## Affected versions\n\n- `github.com/hamba/avro/v2` — all versions up to and including `v2.31.0` (repository is read-only upstream).\n- `github.com/iskorotkov/avro/v2` — all versions prior to `v2.33.0`. Note: `v2.33.0` and later are vulnerable *by default* and only protected when `MaxMapAllocSize` is explicitly configured — see Mitigation.\n\n## Fixed versions\n\n`github.com/iskorotkov/avro/v2` `v2.33.0` and later, **with `Config.MaxMapAllocSize` explicitly set to a non-zero value**.\n\nA bare upgrade to `v2.33.0` without setting `MaxMapAllocSize` leaves the decoder in the same unbounded state as `v2.32.0`. This is a backward-compatibility choice; a future major version may flip the default. Until then, treat this advisory as requiring both an upgrade *and* a configuration change.\n\nThere is no upstream fix for `github.com/hamba/avro/v2` — module path is archived. Migrate to the fork as described under Mitigation.\n\n## Mitigation\n\nMigrate from `github.com/hamba/avro/v2` to `github.com/iskorotkov/avro/v2 >= v2.33.0` **and** configure an allocation cap appropriate for your schema. The recommended approach for processes that decode untrusted input is a dedicated frozen config, used at every relevant call site, rather than mutating `avro.DefaultConfig`:\n\n```go\ncfg := avro.Config{\n    MaxByteSliceSize:  102_400,\n    MaxSliceAllocSize: 10_000,\n    MaxMapAllocSize:   10_000,\n}.Freeze()\n\ndecoder := cfg.NewDecoder(schema, reader)\n```\n\nChoose the values based on the largest legitimate map your schema produces; a value 2–10× that ceiling provides headroom for benign variance while still bounding worst-case memory.\n\nFor consumers that prefer the original import path, a `replace` directive in `go.mod` is supported:\n\n```\nreplace github.com/hamba/avro/v2 => github.com/iskorotkov/avro/v2 v2.33.0\n```\n\n`replace` is honoured only for the **main** module of a build — transitive consumers must add their own `replace`, or migrate the import path directly.\n\nIf you cannot upgrade immediately, the only structural workarounds are out-of-band: run decoders in memory-constrained child processes or cgroups so an OOM is contained, reject inputs from sources without resource controls, and apply per-request decode deadlines so a runaway decode at least times out before the OOM killer fires.\n\n## Proof-of-concept input\n\nTwo attack shapes, both targeting `map[string]int`:\n\n**Single-block, oversize block count.** Emit one block header declaring `n = 2³¹ − 1` (or any value whose `n × averageEntrySize` exceeds available memory) followed by truncated entries. Pre-fix, the decoder pre-allocates `make(map[string]int, n)`, which fails or stalls long before EOF is reached.\n\n**Chunking bypass.** Emit `k` blocks each declaring `n / k` elements, with `n / k` below any plausible per-block threshold but `n` itself well into the GB range. Pre-fix, the decoder happily grows the map block-by-block until the OS kills the process. Post-fix with `MaxMapAllocSize = 10_000`, the decoder rejects whichever block-header read pushes cumulative count past 10,000.\n\nEither shape can be produced by hand-crafting the wire bytes; no `iskorotkov/avro` writer is needed to generate them.\n\n## References\n\n- Fix PR: [iskorotkov/avro#5](https://github.com/iskorotkov/avro/pull/5)\n- Fix commit: [`5192df9`](https://github.com/iskorotkov/avro/commit/5192df96a158999344ac96ebcb1f7461d626f6d7) (`codec_map.go`, `config.go`, tests)\n- Slice-path chunking-attack test coverage added in the same PR: [`534c7518`](https://github.com/iskorotkov/avro/commit/534c7518152a893d8b4dea962669bd1123308a00)\n- Release: [`v2.33.0`](https://github.com/iskorotkov/avro/releases/tag/v2.33.0)\n- Security policy: [`SECURITY.md`](https://github.com/iskorotkov/avro/blob/main/SECURITY.md)\n- Related advisories on this fork: [`GHSA-mc57-h6j3-3hmv`](https://github.com/iskorotkov/avro/security/advisories/GHSA-mc57-h6j3-3hmv) (integer overflow), [`GHSA-w8j3-pq8g-8m7w`](https://github.com/iskorotkov/avro/security/advisories/GHSA-w8j3-pq8g-8m7w) (CPU exhaustion — the same chunked-payload shape may trigger both before allocation pressure kicks in)\n- Cross-module precedent on `hamba/avro`: [`GO-2023-1930`](https://pkg.go.dev/vuln/GO-2023-1930) / `CVE-2023-37475` / `GHSA-9x44-9pgq-cf45`\n- Upstream (read-only): [`hamba/avro`](https://github.com/hamba/avro)\n\n## Credits\n\n- **Fix author** (commit `5192df9`, PR #5 — `MaxMapAllocSize` config field, cumulative enforcement in both map decoders, chunking-attack tests for slices and maps): Ivan Korotkov ([@iskorotkov](https://github.com/iskorotkov))\n- **Review** (commit `a5fbddcb`, \"address review comments\"): Daniel Błażewicz ([@klajok](https://github.com/klajok))\n\n## Timeline\n\n- **2026-04-30** — `MaxMapAllocSize` introduced (`5192df9`); chunking-attack test coverage for slices added (`534c7518`).\n- **2026-05-01** — PR #5 merged into `main`.\n- **2026-05-06** — `v2.33.0` tagged and released.\n- **2026-05-07** — Advisory published.\n- **2026-05-15** — Advisory revised.","published":"2026-05-18T12:59:58Z","modified":"2026-08-05T03:41:56.534792040Z","cvss":{"score":7.5,"severity":"HIGH","vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H"},"epss":null,"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"Go","name":"github.com/iskorotkov/avro/v2","fixedVersion":"2.33.0"}],"fix":null,"references":[{"type":"WEB","url":"https://github.com/iskorotkov/avro/security/advisories/GHSA-mx64-mj3q-7prj"},{"type":"PACKAGE","url":"https://github.com/iskorotkov/avro"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-08-05T03:41:56.534792040Z"}}