Skip to main content

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}