cachekit/serializer/mod.rs
1use rmp_serde::decode::ReadReader;
2use serde::{de::DeserializeOwned, Serialize};
3
4use crate::error::CachekitError;
5
6/// Maximum MessagePack nesting depth accepted from an untrusted payload.
7///
8/// Every cache read decodes bytes the backend controls, and `rmp-serde`
9/// recurses once per nesting level, so the failure mode of unbounded depth is
10/// a **stack overflow** — an uncatchable process abort. `rmp-serde`'s own
11/// default (1024) is not a safe bound: measured (rmp-serde 1.3.1, 2026-09) on
12/// a 2 MiB thread stack — the tokio worker default — a debug build overflows
13/// between 512 and 768 nested arrays, i.e. a ~700-byte forged entry; a release
14/// build fits 1024 with little margin, and frame size grows with the target
15/// type. 100 matches cachekit-ts (`DEFAULT_MAX_DEPTH`), leaves a wide margin on
16/// every profile and target (wasm32 included), and sits inside the protocol's
17/// required `32..=1024` window (`spec/interop-mode.md` → Decode bounds).
18///
19/// Enforced by `check_structure`, which defines how depth is counted.
20pub const MAX_DECODE_DEPTH: usize = 100;
21
22/// Header-only structural walk over one MessagePack document.
23///
24/// Proves that a complete document lies within `bytes`, that it nests no deeper
25/// than [`MAX_DECODE_DEPTH`], and that no header declares more elements or bytes
26/// than the remaining input can back — every element costs at least one input
27/// byte, so `pending ≤ remaining` at every step means Σ declared ≤ input.
28/// `pending` sums what every open header still owes, so nested headers that each
29/// fit the input after them but together overclaim are rejected too. All counts
30/// are `u64`: before each header is added `pending` is at most the remaining
31/// input, and one header adds less than 2³³ (a map32 declares 2 × (2³² − 1)
32/// children), so no sum can overflow. Depth counts every array and map header
33/// on the deepest path, empty ones included; scalars, str, bin and ext are
34/// leaves. Exactly `MAX_DECODE_DEPTH` levels are accepted, one more is rejected
35/// with a `decode bound:` error before `rmp-serde` recurses. Memory is fixed: a
36/// `[u64; MAX_DECODE_DEPTH]` stack of children still owed by each open
37/// collection, whatever the input. Fails closed on the reserved marker,
38/// truncation, excess depth, and length overflow, always with a message
39/// starting `decode bound:`.
40///
41/// Why depth is checked here and not only by `rmp-serde`: a guard that admits
42/// a document deeper than the bound leaves the rejection to the decoder,
43/// mid-recursion, after it has materialised the outer levels.
44///
45/// Why it is needed even though `rmp-serde` reads str/bin lazily: serde's
46/// sequence visitors (`Vec<T>`, and the `Content` buffer that
47/// `#[serde(untagged)]` targets decode through) reserve
48/// `min(declared_len × size_of::<Element>(), 1 MiB)` bytes per collection from
49/// `size_hint`, so without this walk a 500-byte payload of nested
50/// `array32(0xFFFFFFFF)` headers requests `MAX_DECODE_DEPTH` MiB before the
51/// first EOF error — an OOM kill on a Cloudflare Workers isolate. Trailing
52/// bytes are left to the caller (auto mode ignores them; interop rejects them).
53pub(crate) fn check_structure(bytes: &[u8]) -> Result<(), CachekitError> {
54 fn reject(what: &str) -> CachekitError {
55 CachekitError::Serialization(format!("decode bound: {what}"))
56 }
57 fn be(bytes: &[u8], pos: usize, width: usize) -> Result<u64, CachekitError> {
58 let end = pos.checked_add(width).filter(|e| *e <= bytes.len());
59 let field = &bytes[pos..end.ok_or_else(|| reject("input ends inside a length prefix"))?];
60 Ok(field.iter().fold(0u64, |acc, b| (acc << 8) | u64::from(*b)))
61 }
62
63 // Children still owed by each open collection, innermost last. The top frame
64 // is non-zero between elements; an ancestor may reach 0 while its last child
65 // is still open, so the pop loop drains every finished frame, and an empty
66 // collection never stays pushed.
67 let mut open = [0u64; MAX_DECODE_DEPTH];
68 let mut depth = 0usize;
69 let mut pos = 0usize;
70 let mut pending: u64 = 1; // elements still owed by open headers (the root counts as one)
71 while pending > 0 {
72 let marker = *bytes
73 .get(pos)
74 .ok_or_else(|| reject("input ends before the document is complete"))?;
75 pos += 1;
76 pending -= 1;
77 if let Some(parent) = depth.checked_sub(1) {
78 open[parent] -= 1;
79 }
80 // (prefix bytes, payload bytes after the prefix, child elements)
81 let (prefix, payload, children): (usize, u64, u64) = match marker {
82 0x00..=0x7f | 0xc0 | 0xc2 | 0xc3 | 0xe0..=0xff => (0, 0, 0),
83 0x80..=0x8f => (0, 0, 2 * u64::from(marker & 0x0f)),
84 0x90..=0x9f => (0, 0, u64::from(marker & 0x0f)),
85 0xa0..=0xbf => (0, u64::from(marker & 0x1f), 0),
86 0xc1 => return Err(reject("reserved marker 0xc1")),
87 0xc4 | 0xd9 => (1, be(bytes, pos, 1)?, 0),
88 0xc5 | 0xda => (2, be(bytes, pos, 2)?, 0),
89 0xc6 | 0xdb => (4, be(bytes, pos, 4)?, 0),
90 0xc7 => (1, be(bytes, pos, 1)? + 1, 0), // ext: length prefix, then type byte + data
91 0xc8 => (2, be(bytes, pos, 2)? + 1, 0),
92 0xc9 => (4, be(bytes, pos, 4)? + 1, 0),
93 0xca..=0xd3 => (0, 1u64 << (marker & 0x03), 0), // f32/f64/u8..u64/i8..i64: 4,8,1,2,4,8,1,2,4,8
94 0xd4..=0xd8 => (0, 1 + (1u64 << (marker - 0xd4)), 0), // fixext: type byte + 1/2/4/8/16
95 0xdc => (2, 0, be(bytes, pos, 2)?),
96 0xdd => (4, 0, be(bytes, pos, 4)?),
97 0xde => (2, 0, 2 * be(bytes, pos, 2)?),
98 0xdf => (4, 0, 2 * be(bytes, pos, 4)?),
99 };
100 pos += prefix;
101 let remaining = (bytes.len() - pos) as u64;
102 if payload > remaining {
103 return Err(reject("header declares more bytes than the input holds"));
104 }
105 pos += usize::try_from(payload)
106 .map_err(|_| reject("header declares more bytes than the input holds"))?;
107 pending += children;
108 if pending > remaining - payload {
109 return Err(reject(
110 "header declares more elements than the input can back",
111 ));
112 }
113 if matches!(marker, 0x80..=0x9f | 0xdc..=0xdf) {
114 // Every collection header is a level, an empty one included.
115 if depth == MAX_DECODE_DEPTH {
116 return Err(reject(&format!(
117 "nesting deeper than {MAX_DECODE_DEPTH} levels"
118 )));
119 }
120 open[depth] = children;
121 depth += 1;
122 }
123 while depth > 0 && open[depth - 1] == 0 {
124 depth -= 1;
125 }
126 }
127 Ok(())
128}
129
130/// Build a depth- and allocation-bounded `rmp_serde::Deserializer` over `bytes`.
131///
132/// Every decode of backend-supplied bytes (auto-mode [`deserialize`] and
133/// [`crate::interop::deserialize`]) MUST go through here so the bounds cannot
134/// drift between paths. Runs `check_structure` first, which enforces both the
135/// depth and the allocation bound, then sets `rmp-serde`'s own depth limit as a
136/// backstop just above [`MAX_DECODE_DEPTH`]: its default (1024) is deep enough
137/// to overflow a debug-build thread stack.
138pub(crate) fn bounded_deserializer(
139 bytes: &[u8],
140) -> Result<rmp_serde::Deserializer<ReadReader<&[u8]>>, CachekitError> {
141 check_structure(bytes)?;
142 let mut de = rmp_serde::Deserializer::new(bytes);
143 // rmp-serde decrements its counter on entry and errors when it reaches 0, so
144 // `set_max_depth(n)` admits n - 1 levels, and it counts an ext value as a
145 // level where the walk (and the spec) count it as a leaf. Ext is always a
146 // leaf, so +2 admits every document the walk admits, including an ext at the
147 // bound, and the walk's error is the one callers see for anything deeper
148 // (both pinned by tests/decode_bounds_tests.rs).
149 de.set_max_depth(MAX_DECODE_DEPTH + 2);
150 Ok(de)
151}
152
153/// Serialize `value` to MessagePack bytes using named fields (map format).
154pub fn serialize<T: Serialize>(value: &T) -> Result<Vec<u8>, CachekitError> {
155 rmp_serde::to_vec_named(value).map_err(|e| CachekitError::Serialization(e.to_string()))
156}
157
158/// Deserialize `bytes` from MessagePack into `T` under the decode bounds: the
159/// `check_structure` walk (depth ≤ [`MAX_DECODE_DEPTH`], no overclaim) runs
160/// before anything is decoded. Trailing bytes are ignored
161/// (auto mode is SDK-internal; interop mode's strict single-document read is
162/// [`crate::interop::deserialize`]).
163pub fn deserialize<T: DeserializeOwned>(bytes: &[u8]) -> Result<T, CachekitError> {
164 T::deserialize(&mut bounded_deserializer(bytes)?)
165 .map_err(|e| CachekitError::Serialization(e.to_string()))
166}