Skip to main content

netget/utils/
bencode.rs

1//! A bound on bencode nesting, applied before a decoder ever sees the bytes.
2//!
3//! `serde_bencode` has no depth limit. Its `Deserializer::deserialize_any` recurses into
4//! `visit_seq`/`visit_map` for every `l` or `d` it meets, and serde's derived struct
5//! deserialisers reach the same code through `IgnoredAny` when they skip an unknown field,
6//! so a *typed* decode is no safer than decoding into [`serde_bencode::value::Value`].
7//! Nothing in either path counts how deep it has gone.
8//!
9//! Bencode is nested by construction and one `l` byte opens a level, so the cost to an
10//! attacker is one byte per stack frame. Measured against 0.2.4, decoding `l` repeated N
11//! times on a thread of a given stack size:
12//!
13//! | nesting | stack | result |
14//! |---|---|---|
15//! | 800 | 2 MiB (a tokio worker) | returns `Err("End of stream")` |
16//! | 1,000 | 2 MiB | **`fatal runtime error: stack overflow, aborting`** |
17//! | 9,215 | 8 MiB (a main thread) | **`fatal runtime error: stack overflow, aborting`** |
18//!
19//! Roughly **one kilobyte on the wire** is enough to kill a worker, and the largest UDP
20//! datagram macOS will send (`net.inet.udp.maxdgram` = 9216) overflows even the main thread.
21//! **This is not a panic.** A Rust stack overflow raises `SIGSEGV` against the guard page and
22//! the runtime aborts; `tokio::spawn` cannot contain it and there is no `catch_unwind` that
23//! sees it. One datagram from any host that can reach the socket takes the whole of netget
24//! down with it.
25//!
26//! So the bytes are walked *iteratively* first. [`check_bencode_structure`] uses a depth
27//! counter and an index, allocates nothing, and reads each byte once; only input it accepts
28//! is handed to `serde_bencode`. It scans exactly one top-level value and ignores whatever
29//! follows, which is what `serde_bencode::from_bytes` does too — being stricter about
30//! trailing bytes would reject input the decoder would have accepted.
31//!
32//! It also bounds a declared byte-string length against the bytes actually present rather
33//! than trusting it, so `99999999999:` cannot be used to make the caller reserve on a
34//! promise the datagram does not keep.
35
36use std::fmt;
37
38/// Deepest nesting a bencode value may use before it is refused.
39///
40/// Far above anything the protocols that use bencode define. KRPC's deepest legitimate shape
41/// is the outer dict, the `a`/`r` dict inside it, and at most a list of dicts below that —
42/// four. A tracker announce reply is `d` → `peers` list → peer dict — three. 32 leaves room
43/// for an extension nobody has written yet while still being ~2000x short of the frame count
44/// that overflows a worker thread.
45pub const MAX_BENCODE_DEPTH: usize = 32;
46
47/// Why a bencode byte string was refused before decoding.
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub enum BencodeStructureError {
50    /// Nesting exceeded the limit. This is the one that matters: it is refused here because
51    /// letting it reach `serde_bencode` aborts the process.
52    TooDeep { limit: usize },
53    /// The value ends mid-way through: an unterminated container, integer or byte string.
54    Truncated,
55    /// An `e` with no container open.
56    UnbalancedEnd,
57    /// A byte-string or integer length that is not a number, or a byte-string length longer
58    /// than the input that is supposed to contain it.
59    BadLength,
60    /// A byte where a value could not begin.
61    InvalidByte(u8),
62    /// No value at all.
63    Empty,
64}
65
66impl fmt::Display for BencodeStructureError {
67    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68        match self {
69            Self::TooDeep { limit } => {
70                write!(f, "bencode nesting deeper than the {} level limit", limit)
71            }
72            Self::Truncated => write!(f, "bencode value is truncated"),
73            Self::UnbalancedEnd => write!(f, "bencode 'e' with no open container"),
74            Self::BadLength => write!(f, "bencode length prefix is invalid or exceeds the input"),
75            Self::InvalidByte(b) => {
76                write!(f, "byte 0x{:02x} cannot begin a bencode value", b)
77            }
78            Self::Empty => write!(f, "bencode input is empty"),
79        }
80    }
81}
82
83impl std::error::Error for BencodeStructureError {}
84
85/// Walk one bencode value iteratively, refusing anything nested deeper than
86/// [`MAX_BENCODE_DEPTH`].
87///
88/// Call this on every byte string that arrives from the network before handing it to
89/// `serde_bencode`. It is O(n), allocates nothing, and never recurses.
90pub fn check_bencode_structure(data: &[u8]) -> Result<(), BencodeStructureError> {
91    check_bencode_structure_with_limit(data, MAX_BENCODE_DEPTH)
92}
93
94/// [`check_bencode_structure`] with an explicit limit, for tests.
95pub fn check_bencode_structure_with_limit(
96    data: &[u8],
97    max_depth: usize,
98) -> Result<(), BencodeStructureError> {
99    if data.is_empty() {
100        return Err(BencodeStructureError::Empty);
101    }
102
103    let mut i = 0usize;
104    let mut depth = 0usize;
105
106    loop {
107        let Some(&byte) = data.get(i) else {
108            // Ran off the end with a container still open, or before any value began.
109            return Err(BencodeStructureError::Truncated);
110        };
111
112        match byte {
113            // A container opens a level. Depth is now at least 1, so the "finished the
114            // top-level value" check below is unreachable from here.
115            b'd' | b'l' => {
116                depth += 1;
117                if depth > max_depth {
118                    return Err(BencodeStructureError::TooDeep { limit: max_depth });
119                }
120                i += 1;
121            }
122            b'e' => {
123                if depth == 0 {
124                    return Err(BencodeStructureError::UnbalancedEnd);
125                }
126                depth -= 1;
127                i += 1;
128                if depth == 0 {
129                    return Ok(());
130                }
131            }
132            b'i' => {
133                i = scan_integer(data, i)?;
134                if depth == 0 {
135                    return Ok(());
136                }
137            }
138            b'0'..=b'9' => {
139                i = scan_byte_string(data, i)?;
140                if depth == 0 {
141                    return Ok(());
142                }
143            }
144            other => return Err(BencodeStructureError::InvalidByte(other)),
145        }
146    }
147}
148
149/// `i<digits>e` starting at `start`. Returns the index just past the `e`.
150fn scan_integer(data: &[u8], start: usize) -> Result<usize, BencodeStructureError> {
151    debug_assert_eq!(data[start], b'i');
152    let mut i = start + 1;
153    let mut digits = 0usize;
154    // A leading '-' is legal; anything else non-digit before the 'e' is not.
155    if data.get(i) == Some(&b'-') {
156        i += 1;
157    }
158    while let Some(&b) = data.get(i) {
159        match b {
160            b'0'..=b'9' => {
161                digits += 1;
162                i += 1;
163            }
164            b'e' => {
165                if digits == 0 {
166                    return Err(BencodeStructureError::BadLength);
167                }
168                return Ok(i + 1);
169            }
170            _ => return Err(BencodeStructureError::BadLength),
171        }
172    }
173    Err(BencodeStructureError::Truncated)
174}
175
176/// `<digits>:<len bytes>` starting at `start`. Returns the index just past the payload.
177///
178/// The declared length is checked against the bytes actually remaining, not merely parsed —
179/// the other half of the pair of defects this module exists for.
180fn scan_byte_string(data: &[u8], start: usize) -> Result<usize, BencodeStructureError> {
181    let mut i = start;
182    let mut len: usize = 0;
183    let mut digits = 0usize;
184
185    while let Some(&b) = data.get(i) {
186        match b {
187            b'0'..=b'9' => {
188                // A length that cannot fit in a usize cannot fit in the input either.
189                len = len
190                    .checked_mul(10)
191                    .and_then(|v| v.checked_add((b - b'0') as usize))
192                    .ok_or(BencodeStructureError::BadLength)?;
193                digits += 1;
194                i += 1;
195                if len > data.len() {
196                    // Bounded against the whole input, so an arbitrarily long run of digits
197                    // cannot spin here either.
198                    return Err(BencodeStructureError::BadLength);
199                }
200            }
201            b':' => {
202                if digits == 0 {
203                    return Err(BencodeStructureError::BadLength);
204                }
205                let payload_start = i + 1;
206                let end = payload_start
207                    .checked_add(len)
208                    .ok_or(BencodeStructureError::BadLength)?;
209                if end > data.len() {
210                    return Err(BencodeStructureError::Truncated);
211                }
212                return Ok(end);
213            }
214            _ => return Err(BencodeStructureError::BadLength),
215        }
216    }
217    Err(BencodeStructureError::Truncated)
218}