Skip to main content

netget/utils/
bson_depth.rs

1//! A bound on BSON nesting, applied before `bson` ever sees the bytes.
2//!
3//! `bson` 3.0 has no depth limit. `Document::from_reader` builds a `RawDocumentBuf` and converts
4//! it with `TryFrom<&RawDocument> for Document` (`src/raw/document.rs:580`), which turns each
5//! element into a `Bson` through `TryFrom<RawBsonRef> for Bson` (`src/raw/bson_ref.rs:412`) and
6//! `TryFrom<RawBson> for Bson` (`src/raw/bson.rs:447`) — and an embedded document, an array or a
7//! code-with-scope's scope goes straight back into the first of those. One Rust call chain per
8//! level, nothing counting. Each level also copies the rest of the bytes (`RawBson::from` owns
9//! the sub-document), so the work is quadratic in the depth as well.
10//!
11//! An embedded document costs the peer **seven bytes per level** (type, a one-byte key and its
12//! NUL, a length, a terminator). Measured against 3.0.0, `Document::from_reader` on a thread of
13//! a given stack size:
14//!
15//! | build | stack | survives | overflows at | bytes |
16//! |---|---|---|---|---|
17//! | debug | 2 MiB (a tokio worker) | 283 levels | **284** | 2 277 |
18//! | release | 2 MiB | 1 219 levels | **1 220** | 9 765 |
19//! | release | 8 MiB (a main thread) | 4 860 levels | **4 861** | 38 893 |
20//!
21//! Ten kilobytes in one unauthenticated `OP_MSG` — the MongoDB server's body cap is 48 MB — and
22//! the process is gone. **This is not a panic**: a stack overflow is a `SIGSEGV` against the
23//! guard page, and `tokio::spawn` cannot contain it.
24//!
25//! So the bytes are walked *iteratively* first. [`scan_bson_document`] keeps the end offset of
26//! each open document in a fixed array of [`MAX_BSON_DEPTH`] slots, allocates nothing, never
27//! recurses, and bounds-checks every length against the document that contains it. Only a
28//! document it reports [`BsonScan::Complete`] may be handed to `bson`.
29//!
30//! **It is at least as strict as `bson` about structure, never stricter about anything `bson`
31//! would accept.** Every element is sized exactly as `RawIter::get_next_kvp` sizes it, so the
32//! scan finds each nested document at the same offset the decoder will; an element that runs
33//! into its parent's terminator, which `bson` yields and only then rejects, is `Malformed` here
34//! at once. What it does not check — UTF-8 in keys and strings, boolean bytes, the old binary
35//! subtype's inner length — cannot open a level, and `bson` still rejects it.
36//!
37//! **The declared top-level length.** `bson`'s `reader_to_vec` (`src/raw.rs:322`) reserves
38//! `Vec::with_capacity(length)` from the document's own first four bytes — up to 2 GiB from a
39//! 21-byte message — before discovering the bytes are not there. A document whose declared
40//! length exceeds the bytes present is `Malformed` here, so nothing downstream reserves on the
41//! peer's number.
42
43/// Deepest nesting a BSON document may use before it is refused. The top-level document is
44/// level 1; each embedded document, array or code-with-scope scope inside it adds one.
45///
46/// MongoDB itself allows 100 levels in a stored document, and a command wraps a document in
47/// two more (the command document and its `documents` array). That is not the number here,
48/// because the table above says what 100 costs: at ~7.4 KB of stack per level in a debug
49/// build, 102 levels spends ~750 KB of a 2 MiB worker on decoding alone, and the MongoDB server
50/// then walks the same document recursively three more times (relaxed extended JSON for the
51/// model's event, `Debug` in a `trace!`, and `Drop`). 64 keeps the whole of that well inside
52/// one worker in every build, and no driver-generated command comes near it; the cost is that
53/// a document nested 63 to 100 levels deep, which real MongoDB would store, is refused.
54pub const MAX_BSON_DEPTH: usize = 64;
55
56/// The verdict on a BSON document at the front of a buffer.
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum BsonScan {
59    /// One whole, structurally sound document occupies the first `consumed` bytes.
60    Complete { consumed: usize },
61    /// Nested deeper than the limit. Refused as soon as the first level past it opens.
62    TooDeep { limit: usize },
63    /// Not a BSON document: a length that is negative, too small or larger than what contains
64    /// it, a missing terminator, an unknown element type, or an element that runs past its
65    /// document. `bson` would reject it too, but possibly only after recursing into it.
66    Malformed,
67}
68
69/// Walk one BSON document iteratively, refusing anything nested deeper than [`MAX_BSON_DEPTH`].
70///
71/// Call it on every document that arrives from the network before `bson` decodes it. It is
72/// O(n), allocates nothing, and never recurses. Bytes after the document are ignored, as
73/// `Document::from_reader` ignores them.
74pub fn scan_bson_document(data: &[u8]) -> BsonScan {
75    scan_bson_document_with_limit(data, MAX_BSON_DEPTH)
76}
77
78/// [`scan_bson_document`] with an explicit limit, clamped to [`MAX_BSON_DEPTH`] (which sizes
79/// the fixed array of open documents).
80pub fn scan_bson_document_with_limit(data: &[u8], max_depth: usize) -> BsonScan {
81    let max_depth = max_depth.min(MAX_BSON_DEPTH);
82    // `ends[d]` is one past the last byte (the terminator) of the document open at depth d+1.
83    let mut ends = [0usize; MAX_BSON_DEPTH];
84    let mut depth = 0usize;
85
86    let Some(end) = document_end(data, 0, data.len()) else {
87        return BsonScan::Malformed;
88    };
89    if max_depth == 0 {
90        return BsonScan::TooDeep { limit: max_depth };
91    }
92    ends[0] = end;
93    depth += 1;
94    let mut pos = 4usize;
95
96    loop {
97        let end = ends[depth - 1];
98        // The terminator: `document_end` already checked it is a zero byte.
99        if pos == end - 1 {
100            pos = end;
101            depth -= 1;
102            if depth == 0 {
103                return BsonScan::Complete { consumed: end };
104            }
105            continue;
106        }
107        // Elements occupy [4, end - 1); anything reaching the terminator is malformed.
108        let limit = end - 1;
109        if pos > limit {
110            return BsonScan::Malformed;
111        }
112
113        let kind = data[pos];
114        pos += 1;
115        // The key: a NUL-terminated string that must end before the terminator.
116        let Some(nul) = data[pos..limit].iter().position(|&b| b == 0) else {
117            return BsonScan::Malformed;
118        };
119        pos += nul + 1;
120
121        let size = match kind {
122            // Fixed-size values.
123            0x08 => 1,                      // boolean
124            0x10 => 4,                      // int32
125            0x01 | 0x09 | 0x11 | 0x12 => 8, // double, datetime, timestamp, int64
126            0x07 => 12,                     // ObjectId
127            0x13 => 16,                     // decimal128
128            0x06 | 0x0A | 0x7F | 0xFF => 0, // undefined, null, max key, min key
129            // Length-prefixed strings: int32 length (including the NUL) >= 1, then the bytes.
130            0x02 | 0x0D | 0x0E => match string_size(data, pos, limit) {
131                Some(n) => n,
132                None => return BsonScan::Malformed,
133            },
134            // DBPointer: a string, then an ObjectId.
135            0x0C => match string_size(data, pos, limit) {
136                Some(n) => n + 12,
137                None => return BsonScan::Malformed,
138            },
139            // Binary: int32 length >= 0, a subtype byte, then the bytes.
140            0x05 => match read_i32(data, pos, limit).and_then(|n| usize::try_from(n).ok()) {
141                Some(n) => match n.checked_add(5) {
142                    Some(n) => n,
143                    None => return BsonScan::Malformed,
144                },
145                None => return BsonScan::Malformed,
146            },
147            // Regular expression: two C strings.
148            0x0B => {
149                let Some(p) = data[pos..limit].iter().position(|&b| b == 0) else {
150                    return BsonScan::Malformed;
151                };
152                let Some(o) = data[pos + p + 1..limit].iter().position(|&b| b == 0) else {
153                    return BsonScan::Malformed;
154                };
155                p + 1 + o + 1
156            }
157            // Embedded document or array: a nested level.
158            0x03 | 0x04 => {
159                let Some(child_end) = document_end(data, pos, limit) else {
160                    return BsonScan::Malformed;
161                };
162                if depth >= max_depth {
163                    return BsonScan::TooDeep { limit: max_depth };
164                }
165                ends[depth] = child_end;
166                depth += 1;
167                pos += 4;
168                continue;
169            }
170            // Code with scope: int32 total, a string, then a document that must end exactly
171            // where the total says — `bson` parses the scope with `RawDocument::from_bytes` on
172            // the rest of the element, which demands that. The scope is a nested level.
173            0x0F => {
174                let Some(total) = read_i32(data, pos, limit).and_then(|n| usize::try_from(n).ok())
175                else {
176                    return BsonScan::Malformed;
177                };
178                // 4 (total) + 5 (the shortest string) + 5 (the shortest document).
179                if total < 14 {
180                    return BsonScan::Malformed;
181                }
182                let element_end = match pos.checked_add(total) {
183                    Some(e) if e <= limit => e,
184                    _ => return BsonScan::Malformed,
185                };
186                let Some(code) = string_size(data, pos + 4, element_end) else {
187                    return BsonScan::Malformed;
188                };
189                let scope = pos + 4 + code;
190                match document_end(data, scope, element_end) {
191                    Some(scope_end) if scope_end == element_end => {}
192                    _ => return BsonScan::Malformed,
193                }
194                if depth >= max_depth {
195                    return BsonScan::TooDeep { limit: max_depth };
196                }
197                ends[depth] = element_end;
198                depth += 1;
199                pos = scope + 4;
200                continue;
201            }
202            _ => return BsonScan::Malformed,
203        };
204
205        match pos.checked_add(size) {
206            Some(next) if next <= limit => pos = next,
207            _ => return BsonScan::Malformed,
208        }
209    }
210}
211
212/// The end of the document starting at `start`, which must lie wholly before `limit`: its
213/// declared length is at least 5, fits, and its last byte is the zero terminator.
214fn document_end(data: &[u8], start: usize, limit: usize) -> Option<usize> {
215    let len = usize::try_from(read_i32(data, start, limit)?).ok()?;
216    if len < 5 {
217        return None;
218    }
219    let end = start.checked_add(len)?;
220    if end > limit || data[end - 1] != 0 {
221        return None;
222    }
223    Some(end)
224}
225
226/// The size of a length-prefixed BSON string at `start` — the four length bytes plus the
227/// declared length — if the declared length is at least 1 and it fits before `limit`.
228fn string_size(data: &[u8], start: usize, limit: usize) -> Option<usize> {
229    let len = usize::try_from(read_i32(data, start, limit)?).ok()?;
230    if len < 1 {
231        return None;
232    }
233    let size = len.checked_add(4)?;
234    if start.checked_add(size)? > limit {
235        return None;
236    }
237    Some(size)
238}
239
240/// A little-endian i32 at `start`, if all four bytes lie before `limit`.
241fn read_i32(data: &[u8], start: usize, limit: usize) -> Option<i32> {
242    let bytes = data.get(start..start.checked_add(4)?)?;
243    if start + 4 > limit {
244        return None;
245    }
246    Some(i32::from_le_bytes(bytes.try_into().ok()?))
247}