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