Skip to main content

cachekit_core/
msgpack_bounds.rs

1//! Structural pre-scan for untrusted MessagePack.
2//!
3//! `ByteStorage::retrieve` runs it before the envelope decode; it is exported
4//! for SDK bindings that decode untrusted MessagePack themselves. The bounds are
5//! `spec/interop-mode.md` → Decode bounds, pinned by
6//! `tests/vectors/decode-bounds.json`.
7
8/// Why [`check_msgpack_structure`] or [`msgpack_document_len`] rejected its
9/// input.
10///
11/// `Display` is the bare reason with no prefix (for example
12/// `nests deeper than 100 levels`), so each caller adds its own. The wording is
13/// stable within a minor version: callers may match on its leading words.
14#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
15#[error("{0}")]
16pub struct MsgpackStructureError(String);
17
18fn reject(reason: impl Into<String>) -> MsgpackStructureError {
19    MsgpackStructureError(reason.into())
20}
21
22/// Checks that `bytes` begin with one MessagePack document whose declared structure
23/// the input can actually back, before any decoder sees them.
24///
25/// A header-only walk: str/bin/ext payloads are skipped by offset, never read,
26/// and nothing is decoded. It keeps one `u64` per open non-empty collection, so
27/// at most `min(max_depth, bytes.len())` of them (8 KiB at a depth of 1024), and
28/// allocates nothing proportional to any declared length.
29///
30/// Bytes after the document are neither read nor rejected. To find where the
31/// document ends, call [`msgpack_document_len`], which runs this same walk.
32///
33/// `max_depth` is the caller's bound. The protocol (`spec/interop-mode.md` →
34/// Decode bounds) requires one in `32..=1024`; this function does not enforce
35/// that range and applies whatever value it is given.
36///
37/// # Errors
38///
39/// Returns a [`MsgpackStructureError`] naming the violated bound. It rejects:
40/// - nesting deeper than `max_depth`, counting every array or map header on
41///   the path (an empty one included);
42/// - a header declaring more payload bytes than the input holds;
43/// - more pending elements (across every open collection) than the remaining
44///   bytes can back. Every element costs at least one byte, so a decoder's
45///   total container pre-allocation is bounded by the input length rather
46///   than by `depth × declared length`;
47/// - the reserved marker `0xc1`, and input that ends mid-document.
48///
49/// # Examples
50///
51/// ```
52/// use cachekit_core::check_msgpack_structure;
53///
54/// // [[[]]]: three levels, because the empty innermost array counts as one.
55/// let doc = [0x91, 0x91, 0x90];
56/// assert_eq!(check_msgpack_structure(&doc, 3), Ok(()));
57/// assert_eq!(
58///     check_msgpack_structure(&doc, 2).unwrap_err().to_string(),
59///     "nests deeper than 2 levels"
60/// );
61///
62/// // An array32 header claiming 2^32 - 1 elements, in five bytes.
63/// assert_eq!(
64///     check_msgpack_structure(&[0xdd, 0xff, 0xff, 0xff, 0xff], 100)
65///         .unwrap_err()
66///         .to_string(),
67///     "declares more elements than the input can back"
68/// );
69/// ```
70pub fn check_msgpack_structure(
71    bytes: &[u8],
72    max_depth: usize,
73) -> Result<(), MsgpackStructureError> {
74    msgpack_document_len(bytes, max_depth).map(|_| ())
75}
76
77/// Runs the walk [`check_msgpack_structure`] describes and returns the length,
78/// in bytes, of the document at the start of `bytes`.
79///
80/// It accepts and rejects exactly what [`check_msgpack_structure`] does. The
81/// walk stops where the document ends, so the length is at least 1 and at most
82/// `bytes.len()`, and a length below `bytes.len()` means trailing bytes follow.
83/// A caller that requires the input to be exactly one document compares the
84/// two instead of walking the input a second time.
85///
86/// # Errors
87///
88/// The same [`MsgpackStructureError`], for the same inputs, as
89/// [`check_msgpack_structure`].
90///
91/// # Examples
92///
93/// ```
94/// use cachekit_core::msgpack_document_len;
95///
96/// // nil, then two bytes the walk never reads (so 0xc1 is not rejected here).
97/// let input = [0xc0, 0xc1, 0xff];
98/// assert_eq!(msgpack_document_len(&input, 100), Ok(1));
99///
100/// // [[nil], "x"]: five bytes, the whole input.
101/// let doc = [0x92, 0x91, 0xc0, 0xa1, b'x'];
102/// assert_eq!(msgpack_document_len(&doc, 100), Ok(doc.len()));
103/// ```
104pub fn msgpack_document_len(
105    bytes: &[u8],
106    max_depth: usize,
107) -> Result<usize, MsgpackStructureError> {
108    fn be(bytes: &[u8], pos: usize, width: usize) -> Result<u64, MsgpackStructureError> {
109        let end = pos
110            .checked_add(width)
111            .filter(|e| *e <= bytes.len())
112            .ok_or_else(|| reject("ends inside a length prefix"))?;
113        Ok(bytes[pos..end]
114            .iter()
115            .fold(0u64, |acc, b| (acc << 8) | u64::from(*b)))
116    }
117
118    let mut pos = 0usize;
119    let mut pending: u64 = 1; // elements owed across all open collections (the root is one)
120    let mut open: Vec<u64> = Vec::new(); // elements still owed per open collection = depth
121    while pending > 0 {
122        while open.last() == Some(&0) {
123            open.pop();
124        }
125        let marker = *bytes
126            .get(pos)
127            .ok_or_else(|| reject("ends before the document is complete"))?;
128        pos += 1;
129        pending -= 1;
130        if let Some(innermost) = open.last_mut() {
131            *innermost -= 1;
132        }
133        // (length-prefix bytes, payload bytes after the prefix, child elements)
134        let (prefix, payload, children): (usize, u64, u64) = match marker {
135            0x00..=0x7f | 0xc0 | 0xc2 | 0xc3 | 0xe0..=0xff => (0, 0, 0),
136            0x80..=0x8f => (0, 0, 2 * u64::from(marker & 0x0f)),
137            0x90..=0x9f => (0, 0, u64::from(marker & 0x0f)),
138            0xa0..=0xbf => (0, u64::from(marker & 0x1f), 0),
139            0xc1 => return Err(reject("contains the reserved marker 0xc1")),
140            0xc4 | 0xd9 => (1, be(bytes, pos, 1)?, 0),
141            0xc5 | 0xda => (2, be(bytes, pos, 2)?, 0),
142            0xc6 | 0xdb => (4, be(bytes, pos, 4)?, 0),
143            0xc7 => (1, be(bytes, pos, 1)? + 1, 0), // ext: length prefix, then type byte + data
144            0xc8 => (2, be(bytes, pos, 2)? + 1, 0),
145            0xc9 => (4, be(bytes, pos, 4)? + 1, 0),
146            0xca..=0xd3 => (0, 1u64 << (marker & 0x03), 0), // f32/f64/u8..u64/i8..i64: 4,8,1,2,4,8,1,2,4,8
147            0xd4..=0xd8 => (0, 1 + (1u64 << (marker - 0xd4)), 0), // fixext: type byte + 1/2/4/8/16
148            0xdc => (2, 0, be(bytes, pos, 2)?),
149            0xdd => (4, 0, be(bytes, pos, 4)?),
150            0xde => (2, 0, 2 * be(bytes, pos, 2)?),
151            0xdf => (4, 0, 2 * be(bytes, pos, 4)?),
152        };
153        // An empty collection is still a level (spec: depth counts collection
154        // headers), so the bound is checked before the `children > 0` push.
155        if matches!(marker, 0x80..=0x9f | 0xdc..=0xdf) && open.len() >= max_depth {
156            return Err(reject(format!("nests deeper than {max_depth} levels")));
157        }
158        pos += prefix;
159        let remaining = (bytes.len() - pos) as u64;
160        if payload > remaining {
161            return Err(reject("declares more bytes than the input holds"));
162        }
163        // Unreachable while the check above holds (`remaining` came from a
164        // usize); checked rather than cast so a future edit cannot truncate.
165        pos += usize::try_from(payload)
166            .map_err(|_| reject("declares more bytes than the input holds"))?;
167        if children > 0 {
168            open.push(children);
169        }
170        pending += children;
171        if pending > remaining - payload {
172            return Err(reject("declares more elements than the input can back"));
173        }
174    }
175    Ok(pos)
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181
182    /// The envelope's bound; any bound in the protocol's range would do here.
183    const MAX_DEPTH: usize = 100;
184
185    #[test]
186    fn the_error_is_a_std_error() {
187        fn is_error<E: std::error::Error + Send + Sync + 'static>() {}
188        is_error::<MsgpackStructureError>();
189    }
190
191    fn check(bytes: &[u8]) -> Result<(), String> {
192        check_msgpack_structure(bytes, MAX_DEPTH).map_err(|e| e.to_string())
193    }
194
195    fn doc_len(bytes: &[u8]) -> Result<usize, String> {
196        msgpack_document_len(bytes, MAX_DEPTH).map_err(|e| e.to_string())
197    }
198
199    /// `count` copies of a collection header, then `tail`.
200    fn nested(header: &[u8], count: usize, tail: &[u8]) -> Vec<u8> {
201        [header.repeat(count), tail.to_vec()].concat()
202    }
203
204    /// (one level of nesting: fixarray(1) or fixmap(1) with key "", its empty form)
205    const LEVELS: [(&[u8], u8); 2] = [(&[0x91], 0x90), (&[0x81, 0xa0], 0x80)];
206
207    #[test]
208    fn accepts_every_scalar_marker_class() {
209        let docs: &[&[u8]] = &[
210            &[0x00],
211            &[0x7f],
212            &[0xe0],
213            &[0xff],
214            &[0xc0],
215            &[0xc2],
216            &[0xc3],
217            &[0xa3, b'a', b'b', b'c'],
218            &[0xd9, 0x01, b'x'],
219            &[0xda, 0x00, 0x01, b'x'],
220            &[0xdb, 0x00, 0x00, 0x00, 0x01, b'x'],
221            &[0xc4, 0x02, 0x01, 0x02],
222            &[0xc5, 0x00, 0x00],
223            &[0xc6, 0x00, 0x00, 0x00, 0x00],
224            &[0xc7, 0x01, 0x05, 0xaa],
225            &[0xc8, 0x00, 0x00, 0x05],
226            &[0xc9, 0x00, 0x00, 0x00, 0x00, 0x05],
227            &[0xca, 0, 0, 0, 0],
228            &[0xcb, 0, 0, 0, 0, 0, 0, 0, 0],
229            &[0xcc, 0],
230            &[0xcd, 0, 0],
231            &[0xce, 0, 0, 0, 0],
232            &[0xcf, 0, 0, 0, 0, 0, 0, 0, 0],
233            &[0xd0, 0],
234            &[0xd1, 0, 0],
235            &[0xd2, 0, 0, 0, 0],
236            &[0xd3, 0, 0, 0, 0, 0, 0, 0, 0],
237            &[0xd4, 0x05, 0],
238            &[0xd8, 0x05, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
239            &[0xdc, 0x00, 0x01, 0xc0],
240            &[0xdd, 0x00, 0x00, 0x00, 0x01, 0xc0],
241            &[0xde, 0x00, 0x01, 0xa0, 0xc0],
242            &[0xdf, 0x00, 0x00, 0x00, 0x01, 0xa0, 0xc0],
243        ];
244        for doc in docs {
245            assert_eq!(check(doc), Ok(()), "{doc:02x?}");
246            assert_eq!(doc_len(doc), Ok(doc.len()), "{doc:02x?}");
247        }
248    }
249
250    #[test]
251    fn rejects_every_truncation_of_a_fixed_width_marker() {
252        let docs: &[&[u8]] = &[
253            &[0xd9],
254            &[0xda, 0x00],
255            &[0xdb, 0x00, 0x00, 0x00],
256            &[0xc7],
257            &[0xcb, 0, 0, 0, 0, 0, 0, 0],
258            &[0xd8, 0x05, 0],
259            &[0xdd, 0x00, 0x00],
260            &[0xa3, b'a', b'b'],
261            &[],
262        ];
263        for doc in docs {
264            assert!(check(doc).is_err(), "{doc:02x?}");
265        }
266    }
267
268    #[test]
269    fn rejects_the_reserved_marker() {
270        assert_eq!(
271            check(&[0x91, 0xc1]),
272            Err("contains the reserved marker 0xc1".to_owned())
273        );
274    }
275
276    #[test]
277    fn stops_where_the_document_ends_and_reports_its_length() {
278        // The trailing 0xc1 is never read, so it is not the reserved-marker error.
279        assert_eq!(check(&[0xc0, 0xc1, 0xff]), Ok(()));
280        assert_eq!(doc_len(&[0xc0, 0xc1, 0xff]), Ok(1));
281        // {"a": [nil, bin8(2)]}, then a trailing fixmap(1) the walk never opens.
282        let doc = [0x81, 0xa1, b'a', 0x92, 0xc0, 0xc4, 0x02, 0xff, 0xff];
283        assert_eq!(doc_len(&doc), Ok(doc.len()));
284        assert_eq!(doc_len(&[&doc[..], &[0x81]].concat()), Ok(doc.len()));
285    }
286
287    #[test]
288    fn both_entry_points_reject_alike() {
289        let too_deep = nested(&[0x91], MAX_DEPTH + 1, &[0xc0]);
290        let cases: &[&[u8]] = &[
291            &[],
292            &[0x91, 0xc1],
293            &[0xdd, 0xff, 0xff, 0xff, 0xff],
294            &too_deep,
295        ];
296        for bytes in cases {
297            assert!(check(bytes).is_err(), "{bytes:02x?}");
298            assert_eq!(doc_len(bytes).map(|_| ()), check(bytes), "{bytes:02x?}");
299        }
300    }
301
302    #[test]
303    fn depth_bound_is_inclusive_for_arrays_and_maps() {
304        let too_deep = Err(format!("nests deeper than {MAX_DEPTH} levels"));
305        for (header, _) in LEVELS {
306            assert_eq!(check(&nested(header, MAX_DEPTH, &[0xc0])), Ok(()));
307            assert_eq!(check(&nested(header, MAX_DEPTH + 1, &[0xc0])), too_deep);
308        }
309    }
310
311    #[test]
312    fn an_empty_innermost_collection_counts_as_a_level() {
313        let too_deep = Err(format!("nests deeper than {MAX_DEPTH} levels"));
314        for (header, empty) in LEVELS {
315            assert_eq!(check(&nested(header, MAX_DEPTH - 1, &[empty])), Ok(()));
316            assert_eq!(check(&nested(header, MAX_DEPTH, &[empty])), too_deep);
317        }
318    }
319
320    #[test]
321    fn map_pairs_cost_two_slots() {
322        assert_eq!(check(&[0x81, 0xa0, 0xc0]), Ok(()));
323        assert!(check(&[0x81, 0xa0]).is_err());
324    }
325
326    #[test]
327    fn widest_claims_do_not_overflow() {
328        // 2^32 − 1 pairs (2 × (2^32 − 1) slots) and a u32::MAX ext length,
329        // computed in u64.
330        assert_eq!(
331            check(&[0xdf, 0xff, 0xff, 0xff, 0xff]),
332            Err("declares more elements than the input can back".to_owned())
333        );
334        assert_eq!(
335            check(&[0xc9, 0xff, 0xff, 0xff, 0xff, 0x05]),
336            Err("declares more bytes than the input holds".to_owned())
337        );
338    }
339
340    #[cfg(all(feature = "compression", feature = "checksum", feature = "messagepack"))]
341    #[test]
342    fn every_real_envelope_and_every_strict_prefix_of_one() {
343        // The walk admits what writers emit, and nothing that ends early:
344        // every strict prefix of a complete document is incomplete.
345        let storage = crate::ByteStorage::new(None);
346        let bin = storage
347            .store(b"pre-scan admits real envelopes", None)
348            .unwrap();
349        let e: crate::StorageEnvelope = rmp_serde::from_slice(&bin).unwrap();
350        // Legacy writers encoded `compressed_data` as an array of ints.
351        let legacy =
352            rmp_serde::to_vec(&(&e.compressed_data, e.checksum, e.original_size, &e.format))
353                .unwrap();
354        assert!(storage.retrieve(&legacy).is_ok());
355        for doc in [bin, legacy] {
356            assert_eq!(check(&doc), Ok(()));
357            assert_eq!(doc_len(&doc), Ok(doc.len()));
358            assert_eq!(doc_len(&[&doc[..], &doc[..]].concat()), Ok(doc.len()));
359            for end in 0..doc.len() {
360                assert!(
361                    check(&doc[..end]).is_err(),
362                    "prefix of {end} bytes admitted"
363                );
364            }
365        }
366    }
367}