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}