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}