netget/server/svn/wire.rs
1//! ra_svn framing: reading one tuple item off the wire.
2//!
3//! ra_svn is not a line protocol. A message is one **item**, and an item's extent is decided by
4//! its own structure — matching parentheses and byte-counted strings — never by a newline. A
5//! real `svn` client's very first message ends in a space and contains no newline at all, and a
6//! counted string may contain any byte, newline included (the client's `ANONYMOUS` auth token
7//! ends in one). Reading with `read_line` therefore blocks forever on message one; that is
8//! exactly what it did here until this module existed.
9//!
10//! ```text
11//! item ::= word | number | string | list
12//! word ::= a run of non-space, non-paren bytes
13//! number ::= digits
14//! string ::= <byte-length> ":" <that many raw bytes>
15//! list ::= "(" item* ")"
16//! ```
17//!
18//! # Bounds
19//!
20//! The parser is **iterative** — an explicit stack of partial lists, no recursion — because a
21//! recursive descent over attacker-controlled nesting overflows the stack, and a Rust stack
22//! overflow is a `SIGSEGV` against the guard page rather than a catchable panic: it takes the
23//! whole NetGet process with it, not just the connection's task. This is the sixth wire format
24//! in this repository to need that treatment; `src/utils/bencode.rs` is the precedent.
25//!
26//! Iterative parsing is not enough on its own, because the value it builds is recursive: see
27//! [`MAX_TUPLE_DEPTH`] for what the depth bound protects.
28//!
29//! Three limits, all applied to the number the peer *declared* rather than to what has already
30//! arrived:
31//!
32//! - [`MAX_TUPLE_DEPTH`] — `(` is one byte, so without it ~64 KiB of `(` is ~64 000 frames.
33//! - `max_bytes` (the caller's per-message cap) — counted over the whole item, so a list of a
34//! million empty lists is refused on size as well as depth.
35//! - a declared string length is checked against the remaining budget **before** anything is
36//! allocated for it, so `4294967295:` costs nothing.
37
38use std::fmt;
39use tokio::io::{AsyncBufReadExt, AsyncRead, AsyncReadExt, BufReader};
40
41/// How deeply lists may nest before the message is refused.
42///
43/// Real ra_svn traffic is three or four deep (a command's params, a dirent's date/author
44/// sub-tuples). 64 leaves an order of magnitude of headroom.
45///
46/// The reader's own stack is an explicit `Vec`, so reading a deep tuple cannot overflow the
47/// thread stack — but the `Item` it returns is a recursive enum, and everything downstream walks
48/// it recursively: `Display` and `to_json` in `command_event_data`, and `Drop`. Without this cap
49/// 32 000 closed lists — 64 000 bytes, inside `MAX_COMMAND_BYTES` — become a 32 000-deep `Item`
50/// whose first walk overflows the stack and takes the process with it. That was verified with
51/// `fuzz/fuzz_targets/svn_tuple.rs`: disabling this check makes its `depth_bomb` seed `SIGSEGV`.
52pub const MAX_TUPLE_DEPTH: usize = 64;
53
54/// One ra_svn item.
55#[derive(Debug, Clone, PartialEq, Eq)]
56pub enum Item {
57 /// A bare token: `success`, `get-dir`, `ANONYMOUS`.
58 Word(String),
59 /// A bare integer.
60 Number(u64),
61 /// A counted string, `<len>:<bytes>`. Any byte may appear in it.
62 Str(Vec<u8>),
63 /// `( … )`.
64 List(Vec<Item>),
65}
66
67impl Item {
68 /// The first element, if this is a non-empty list.
69 pub fn head(&self) -> Option<&Item> {
70 match self {
71 Item::List(items) => items.first(),
72 _ => None,
73 }
74 }
75
76 /// This item as JSON, for event data the model reads.
77 ///
78 /// Strings are decoded lossily: event data carries text the model can reason about, never
79 /// raw bytes or base64 (the project's action/event design rule).
80 pub fn to_json(&self) -> serde_json::Value {
81 match self {
82 Item::Word(w) => serde_json::Value::String(w.clone()),
83 Item::Number(n) => serde_json::Value::from(*n),
84 Item::Str(bytes) => serde_json::Value::String(String::from_utf8_lossy(bytes).into()),
85 Item::List(items) => {
86 serde_json::Value::Array(items.iter().map(Item::to_json).collect())
87 }
88 }
89 }
90}
91
92impl fmt::Display for Item {
93 /// Render back into ra_svn's own text, counted strings and all.
94 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95 match self {
96 Item::Word(w) => f.write_str(w),
97 Item::Number(n) => write!(f, "{n}"),
98 Item::Str(bytes) => {
99 write!(f, "{}:{}", bytes.len(), String::from_utf8_lossy(bytes))
100 }
101 Item::List(items) => {
102 f.write_str("(")?;
103 for item in items {
104 write!(f, " {item}")?;
105 }
106 f.write_str(" )")
107 }
108 }
109 }
110}
111
112/// Why a message was refused. Every variant closes the connection; none reaches the peer as
113/// text (the peer gets a [`crate::utils::WireFailure`] category, the log gets this).
114#[derive(Debug)]
115pub enum WireError {
116 /// The peer hung up part-way through an item.
117 UnexpectedEof,
118 /// More than [`MAX_TUPLE_DEPTH`] nested lists.
119 TooDeep,
120 /// The item is larger than the caller's cap, or declares a string that would be.
121 TooLarge { limit: u64 },
122 /// Not ra_svn at all: a stray `)`, a string length that is not a number, and so on.
123 Malformed(&'static str),
124 /// The socket failed.
125 Io(std::io::Error),
126}
127
128impl fmt::Display for WireError {
129 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130 match self {
131 WireError::UnexpectedEof => f.write_str("connection closed mid-tuple"),
132 WireError::TooDeep => {
133 write!(f, "tuple nested deeper than {MAX_TUPLE_DEPTH} lists")
134 }
135 WireError::TooLarge { limit } => {
136 write!(f, "tuple larger than the {limit}-byte message cap")
137 }
138 WireError::Malformed(why) => write!(f, "malformed ra_svn tuple: {why}"),
139 WireError::Io(e) => write!(f, "read error: {e}"),
140 }
141 }
142}
143
144impl From<std::io::Error> for WireError {
145 fn from(e: std::io::Error) -> Self {
146 WireError::Io(e)
147 }
148}
149
150fn is_space(b: u8) -> bool {
151 matches!(b, b' ' | b'\n' | b'\r' | b'\t' | 0x0b | 0x0c)
152}
153
154/// A buffered reader that yields whole ra_svn items.
155pub struct ItemReader<R> {
156 inner: BufReader<R>,
157 /// One byte of push-back: a word or a number ends when the byte *after* it is read, and
158 /// that byte belongs to whatever comes next.
159 pending: Option<u8>,
160}
161
162impl<R: AsyncRead + Unpin> ItemReader<R> {
163 pub fn new(reader: R) -> Self {
164 Self {
165 inner: BufReader::new(reader),
166 pending: None,
167 }
168 }
169
170 async fn byte(&mut self) -> Result<Option<u8>, WireError> {
171 if let Some(b) = self.pending.take() {
172 return Ok(Some(b));
173 }
174 let mut one = [0u8; 1];
175 match self.inner.read_exact(&mut one).await {
176 Ok(_) => Ok(Some(one[0])),
177 Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => Ok(None),
178 Err(e) => Err(WireError::Io(e)),
179 }
180 }
181
182 /// Read one complete item.
183 ///
184 /// Returns `Ok(None)` for a clean end of stream — the peer hung up *between* messages,
185 /// which is how an svn session normally ends. EOF part-way through an item is
186 /// [`WireError::UnexpectedEof`] instead, because those are different events and only one
187 /// of them is an error.
188 ///
189 /// The second half of the tuple is the number of bytes consumed, for connection stats.
190 pub async fn read_item(&mut self, max_bytes: u64) -> Result<Option<(Item, u64)>, WireError> {
191 let mut consumed: u64 = 0;
192 // Partial lists, outermost first. `stack.len()` is the current nesting depth.
193 let mut stack: Vec<Vec<Item>> = Vec::new();
194
195 loop {
196 let Some(b) = self.byte().await? else {
197 return if stack.is_empty() && consumed == 0 {
198 Ok(None)
199 } else {
200 Err(WireError::UnexpectedEof)
201 };
202 };
203 consumed += 1;
204 if consumed > max_bytes {
205 return Err(WireError::TooLarge { limit: max_bytes });
206 }
207
208 // Leading and separating whitespace. ra_svn writes a single space; a newline is
209 // equally legal and NetGet's own replies end with one.
210 if is_space(b) {
211 continue;
212 }
213
214 let item = match b {
215 b'(' => {
216 if stack.len() >= MAX_TUPLE_DEPTH {
217 return Err(WireError::TooDeep);
218 }
219 stack.push(Vec::new());
220 continue;
221 }
222 b')' => {
223 let Some(items) = stack.pop() else {
224 return Err(WireError::Malformed("closing paren with no list open"));
225 };
226 Item::List(items)
227 }
228 b'0'..=b'9' => self.number_or_string(b, &mut consumed, max_bytes).await?,
229 _ => self.word(b, &mut consumed, max_bytes).await?,
230 };
231
232 match stack.last_mut() {
233 Some(open) => open.push(item),
234 // Nothing is open, so this item was the whole message.
235 None => {
236 consumed += self.drain_buffered_whitespace();
237 return Ok(Some((item, consumed)));
238 }
239 }
240 }
241 }
242
243 /// Consume trailing whitespace that has **already arrived**, and report how much.
244 ///
245 /// An item's extent ends at its last byte, so the space or newline a peer writes after it
246 /// belongs to no message and would otherwise be attributed to the next one — leaving the
247 /// connection's byte counter one short for as long as the peer stays quiet. This never
248 /// awaits: it looks only at what the buffer is already holding, so a peer that sends
249 /// nothing more cannot make it block.
250 fn drain_buffered_whitespace(&mut self) -> u64 {
251 let mut drained = 0u64;
252 if let Some(b) = self.pending {
253 if is_space(b) {
254 self.pending = None;
255 drained += 1;
256 } else {
257 return 0;
258 }
259 }
260 loop {
261 let take = self
262 .inner
263 .buffer()
264 .iter()
265 .take_while(|b| is_space(**b))
266 .count();
267 if take == 0 {
268 return drained;
269 }
270 self.inner.consume(take);
271 drained += take as u64;
272 }
273 }
274
275 /// `<digits>` on its own is a number; `<digits>:` introduces a counted string.
276 async fn number_or_string(
277 &mut self,
278 first: u8,
279 consumed: &mut u64,
280 max_bytes: u64,
281 ) -> Result<Item, WireError> {
282 let mut value: u64 = (first - b'0') as u64;
283 let mut digits = 1usize;
284 loop {
285 let Some(b) = self.byte().await? else {
286 // A bare number at end of stream: the caller decides whether that is an error.
287 return Ok(Item::Number(value));
288 };
289 *consumed += 1;
290 if *consumed > max_bytes {
291 return Err(WireError::TooLarge { limit: max_bytes });
292 }
293 match b {
294 b'0'..=b'9' => {
295 digits += 1;
296 // 20 digits is u64::MAX's width; anything longer is not a length or a
297 // revision, it is someone testing what happens.
298 if digits > 20 {
299 return Err(WireError::Malformed("number too long"));
300 }
301 value = value
302 .checked_mul(10)
303 .and_then(|v| v.checked_add((b - b'0') as u64))
304 .ok_or(WireError::Malformed("number overflows 64 bits"))?;
305 }
306 b':' => {
307 // Bound the DECLARED length against what is left of the budget, before
308 // allocating a single byte for it.
309 let remaining = max_bytes.saturating_sub(*consumed);
310 if value > remaining {
311 return Err(WireError::TooLarge { limit: max_bytes });
312 }
313 let len = value as usize;
314 let mut bytes = vec![0u8; len];
315 if len > 0 {
316 // `pending` is always empty here: the ':' was the last byte read.
317 debug_assert!(self.pending.is_none());
318 match self.inner.read_exact(&mut bytes).await {
319 Ok(_) => {}
320 Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
321 return Err(WireError::UnexpectedEof)
322 }
323 Err(e) => return Err(WireError::Io(e)),
324 }
325 }
326 *consumed += value;
327 return Ok(Item::Str(bytes));
328 }
329 other => {
330 self.pending = Some(other);
331 *consumed -= 1;
332 return Ok(Item::Number(value));
333 }
334 }
335 }
336 }
337
338 /// A bare token, ended by whitespace or a paren.
339 ///
340 /// Deliberately lenient about what a word may contain: ra_svn's grammar says
341 /// `[a-zA-Z][a-zA-Z0-9-]*`, but refusing anything else would turn a peer's typo into a
342 /// dropped connection where the model could have answered, and NetGet's own tests speak
343 /// tuples like `( stat /nonexistent )`.
344 async fn word(
345 &mut self,
346 first: u8,
347 consumed: &mut u64,
348 max_bytes: u64,
349 ) -> Result<Item, WireError> {
350 let mut bytes = vec![first];
351 loop {
352 let Some(b) = self.byte().await? else {
353 break;
354 };
355 if is_space(b) || b == b'(' || b == b')' {
356 self.pending = Some(b);
357 break;
358 }
359 *consumed += 1;
360 if *consumed > max_bytes {
361 return Err(WireError::TooLarge { limit: max_bytes });
362 }
363 bytes.push(b);
364 }
365 Ok(Item::Word(String::from_utf8_lossy(&bytes).into_owned()))
366 }
367}