Skip to main content

netget/utils/
resp.rs

1//! A bound on RESP2 nesting and declared lengths, applied before a decoder ever sees the bytes.
2//!
3//! `redis-protocol` 6.0 has no depth limit. `resp2::decode` walks a frame with
4//! `d_parse_frame` → `d_parse_array` → `nom::multi::count(d_parse_frame, len)`, one level of
5//! Rust recursion per nested array, and `build_owned_frame` then recurses over the result a
6//! second time. Nothing in either path counts how deep it has gone.
7//!
8//! `*1\r\n` opens a level in **four bytes**, so a few hundred kilobytes overflow a tokio
9//! worker's 2 MiB stack, and the Redis server's only size bound is its 64 MiB
10//! incomplete-frame buffer. **This is not a panic.** A Rust stack overflow raises `SIGSEGV`
11//! against the guard page and the runtime aborts; `tokio::spawn` cannot contain it. One
12//! unauthenticated connection takes the whole of netget down with it. Confirmed by sending
13//! 100 000 levels to the unguarded server: the test binary died with
14//! `fatal runtime error: stack overflow`.
15//!
16//! So the bytes are walked *iteratively* first. [`scan_resp2_frame`] keeps one counter per
17//! open aggregate in a fixed array of [`MAX_RESP_DEPTH`] slots, allocates nothing, never
18//! recurses, and reads each byte at most twice (once to find a line's CRLF, once to parse it).
19//! Only a frame it reports as [`RespScan::Complete`] is handed to the decoder.
20//!
21//! It follows the decoder's grammar exactly where the two could otherwise disagree — a length
22//! is parsed with the same `str::parse::<isize>` the crate uses, a bulk string's two trailing
23//! bytes are skipped without being checked for CRLF just as `nom_take(2)` does — because a
24//! guard that rejected something the decoder would have accepted is a behaviour change, and one
25//! that accepted something the decoder would then recurse into is no guard at all.
26//!
27//! **Declared lengths.** `nom` 7.1.3's `count` caps its initial `Vec::with_capacity` at
28//! 64 KiB (`MAX_INITIAL_CAPACITY_BYTES`, `src/multi/mod.rs:577`), so `*4000000000\r\n` is not
29//! an allocation bomb by itself. With depth bounded, the most the decoder can pre-allocate at
30//! once is [`MAX_RESP_DEPTH`] × 64 KiB. What a declared length *is* is a promise about bytes
31//! that have not arrived, and one the caller's buffer cap guarantees can never be kept should
32//! be refused on the header rather than after the peer has filled the buffer.
33//! [`RespLimits::max_bulk_len`] does that for `$N`, and [`RespLimits::max_elements`] bounds
34//! both any single `*N` and the total number of values in one frame, which is what bounds the
35//! decoder's per-value allocation (a `RangeFrame` and then an `OwnedFrame` for every three-byte
36//! `+\r\n` the peer sends).
37
38/// Deepest nesting of arrays a RESP2 frame may use before it is refused.
39///
40/// A Redis *request* never nests at all — real Redis rejects an array inside a command with
41/// `Protocol error: expected '$', got '*'` — and the deepest reply shapes in common use (an
42/// `EXEC` of `XREAD`s) are four or five levels. 32 is far above both and several thousand
43/// levels short of what overflows a worker thread.
44pub const MAX_RESP_DEPTH: usize = 32;
45
46/// Largest declared array length, and the most values one frame may contain in total.
47///
48/// 2^20 values is far past any command a model is asked to answer (NetGet flattens a command
49/// into one string for the event), and bounds what `decode` allocates for one frame to a few
50/// tens of megabytes — the same order as the 64 MiB buffer cap the Redis server applies to the
51/// bytes.
52pub const MAX_RESP_ELEMENTS: usize = 1 << 20;
53
54/// The bounds [`scan_resp2_frame`] enforces.
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub struct RespLimits {
57    /// Deepest array nesting accepted. Clamped to [`MAX_RESP_DEPTH`], which sizes the scan's
58    /// fixed counter array.
59    pub max_depth: usize,
60    /// Largest declared `*N`, and the most values in one frame.
61    pub max_elements: usize,
62    /// Largest declared `$N`. Set it to the caller's buffer cap: a bulk string longer than the
63    /// buffer can never complete.
64    pub max_bulk_len: usize,
65}
66
67impl RespLimits {
68    /// [`MAX_RESP_DEPTH`] and [`MAX_RESP_ELEMENTS`], with the caller's bulk-length cap.
69    pub const fn with_max_bulk_len(max_bulk_len: usize) -> Self {
70        Self {
71            max_depth: MAX_RESP_DEPTH,
72            max_elements: MAX_RESP_ELEMENTS,
73            max_bulk_len,
74        }
75    }
76}
77
78/// Which declared length was refused.
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub enum RespLength {
81    /// An `*N` longer than [`RespLimits::max_elements`], or one that would take the frame's
82    /// total past it.
83    Aggregate,
84    /// A `$N` longer than [`RespLimits::max_bulk_len`].
85    Bulk,
86}
87
88/// The verdict on the front of a buffer.
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90pub enum RespScan {
91    /// One whole frame occupies the first `consumed` bytes. `resp2::decode` will return the
92    /// same count for it.
93    Complete { consumed: usize },
94    /// Nothing wrong so far and the frame is not finished; read more.
95    Incomplete,
96    /// Arrays nested deeper than the limit. Refused as soon as the first level past it is
97    /// opened, whether or not the rest of the frame has arrived.
98    TooDeep { limit: usize },
99    /// A declared length past its limit. Refused on the header line.
100    TooLong(RespLength),
101    /// Not RESP2: a type byte the decoder does not know, or a length or integer it cannot
102    /// parse. The decoder would return an error at the same point.
103    Malformed,
104}
105
106/// Scan the RESP2 frame at the front of `data` without recursing or allocating.
107///
108/// Call it on the unconsumed buffer before every `resp2::decode`, and decode only on
109/// [`RespScan::Complete`]. It is O(n) in the bytes up to the end of the frame or the point of
110/// refusal.
111pub fn scan_resp2_frame(data: &[u8], limits: &RespLimits) -> RespScan {
112    let max_depth = limits.max_depth.min(MAX_RESP_DEPTH);
113    // `remaining[d]` is how many values the array open at depth `d + 1` still expects.
114    let mut remaining = [0usize; MAX_RESP_DEPTH];
115    let mut depth = 0usize;
116    let mut values = 0usize;
117    let mut i = 0usize;
118
119    loop {
120        let Some(&kind) = data.get(i) else {
121            return RespScan::Incomplete;
122        };
123        i += 1;
124        values += 1;
125        if values > limits.max_elements {
126            return RespScan::TooLong(RespLength::Aggregate);
127        }
128
129        match kind {
130            b'+' | b'-' => match find_crlf(data, i) {
131                Some(end) => i = end + 2,
132                None => return RespScan::Incomplete,
133            },
134            b':' => {
135                let Some(end) = find_crlf(data, i) else {
136                    return RespScan::Incomplete;
137                };
138                if parse_i64(&data[i..end]).is_none() {
139                    return RespScan::Malformed;
140                }
141                i = end + 2;
142            }
143            b'$' => {
144                let Some(end) = find_crlf(data, i) else {
145                    return RespScan::Incomplete;
146                };
147                let Some(len) = parse_isize(&data[i..end]) else {
148                    return RespScan::Malformed;
149                };
150                i = end + 2;
151                if len != -1 {
152                    let Ok(len) = usize::try_from(len) else {
153                        return RespScan::Malformed;
154                    };
155                    if len > limits.max_bulk_len {
156                        return RespScan::TooLong(RespLength::Bulk);
157                    }
158                    // The payload and the two bytes after it, which the decoder takes
159                    // without checking that they are CRLF.
160                    let Some(next) = i.checked_add(len).and_then(|v| v.checked_add(2)) else {
161                        return RespScan::TooLong(RespLength::Bulk);
162                    };
163                    if next > data.len() {
164                        return RespScan::Incomplete;
165                    }
166                    i = next;
167                }
168            }
169            b'*' => {
170                let Some(end) = find_crlf(data, i) else {
171                    return RespScan::Incomplete;
172                };
173                let Some(len) = parse_isize(&data[i..end]) else {
174                    return RespScan::Malformed;
175                };
176                i = end + 2;
177                if len != -1 {
178                    let Ok(len) = usize::try_from(len) else {
179                        return RespScan::Malformed;
180                    };
181                    // Refused on the header: the values this promises, on top of those
182                    // already counted, would pass the per-frame total.
183                    if len > limits.max_elements.saturating_sub(values) {
184                        return RespScan::TooLong(RespLength::Aggregate);
185                    }
186                    // An empty array still costs the decoder a level of recursion, so it
187                    // counts against depth too.
188                    if depth + 1 > max_depth {
189                        return RespScan::TooDeep { limit: max_depth };
190                    }
191                    if len > 0 {
192                        remaining[depth] = len;
193                        depth += 1;
194                        continue;
195                    }
196                }
197            }
198            _ => return RespScan::Malformed,
199        }
200
201        // A value finished. Close every array it was the last element of.
202        loop {
203            if depth == 0 {
204                return RespScan::Complete { consumed: i };
205            }
206            remaining[depth - 1] -= 1;
207            if remaining[depth - 1] > 0 {
208                break;
209            }
210            depth -= 1;
211        }
212    }
213}
214
215/// Index of the first `\r\n` at or after `from`, as `nom`'s `take_until("\r\n")` finds it.
216fn find_crlf(data: &[u8], from: usize) -> Option<usize> {
217    data.get(from..)?
218        .windows(2)
219        .position(|w| w == b"\r\n")
220        .map(|p| from + p)
221}
222
223/// The decoder's own `str::from_utf8(s)?.parse::<isize>()`, so the two agree on every input.
224fn parse_isize(line: &[u8]) -> Option<isize> {
225    std::str::from_utf8(line).ok()?.parse::<isize>().ok()
226}
227
228/// The decoder's `to_i64`.
229fn parse_i64(line: &[u8]) -> Option<i64> {
230    std::str::from_utf8(line).ok()?.parse::<i64>().ok()
231}