Skip to main content

eth_valkyoth_codec/
lib.rs

1#![no_std]
2#![forbid(unsafe_code)]
3//! Bounded decoding policy for untrusted Ethereum wire inputs.
4
5use core::cmp::Ordering;
6
7/// Resource limits required by every untrusted decoder.
8#[derive(Clone, Copy, Debug, Eq, PartialEq)]
9pub struct DecodeLimits {
10    /// Maximum accepted input length in bytes.
11    pub max_input_bytes: usize,
12    /// Maximum items accepted in any decoded list.
13    pub max_list_items: usize,
14    /// Maximum nested list depth.
15    pub max_nesting_depth: usize,
16    /// Maximum total allocation a decoder may request.
17    pub max_total_allocation: usize,
18}
19
20impl DecodeLimits {
21    /// Conservative defaults for small unit and conformance fixtures.
22    pub const STRICT: Self = Self {
23        max_input_bytes: 1 << 20,
24        max_list_items: 4096,
25        max_nesting_depth: 64,
26        max_total_allocation: 1 << 20,
27    };
28
29    /// Validates the input length before parsing starts.
30    pub fn check_input_len(self, len: usize) -> Result<(), DecodeError> {
31        if len > self.max_input_bytes {
32            return Err(DecodeError::InputTooLarge);
33        }
34        Ok(())
35    }
36
37    /// Validates a decoded list item count.
38    pub fn check_list_count(self, count: usize) -> Result<(), DecodeError> {
39        if count > self.max_list_items {
40            return Err(DecodeError::ListTooLong);
41        }
42        Ok(())
43    }
44
45    /// Validates the current nesting depth.
46    pub fn check_nesting_depth(self, depth: usize) -> Result<(), DecodeError> {
47        if depth > self.max_nesting_depth {
48            return Err(DecodeError::NestingTooDeep);
49        }
50        Ok(())
51    }
52
53    /// Validates one requested allocation against the allocation budget.
54    ///
55    /// This helper is for single-allocation checks only. Decoders that can make
56    /// more than one allocation must use [`DecodeAccumulator`] to enforce the
57    /// cumulative budget.
58    pub fn check_allocation(self, size: usize) -> Result<(), DecodeError> {
59        if size > self.max_total_allocation {
60            return Err(DecodeError::AllocationExceeded);
61        }
62        Ok(())
63    }
64
65    /// Starts stateful budget accounting for a decoder invocation.
66    #[must_use]
67    pub const fn accumulator(self) -> DecodeAccumulator {
68        DecodeAccumulator {
69            limits: self,
70            total_allocated: 0,
71        }
72    }
73}
74
75/// Stateful budget accounting for one decoder invocation.
76#[derive(Debug, Eq, PartialEq)]
77pub struct DecodeAccumulator {
78    limits: DecodeLimits,
79    total_allocated: usize,
80}
81
82impl DecodeAccumulator {
83    /// Returns the active decode limits.
84    #[must_use]
85    pub const fn limits(&self) -> DecodeLimits {
86        self.limits
87    }
88
89    /// Returns the cumulative allocation accounted so far.
90    #[must_use]
91    pub const fn total_allocated(&self) -> usize {
92        self.total_allocated
93    }
94
95    /// Validates the input length before parsing starts.
96    pub fn check_input_len(&self, len: usize) -> Result<(), DecodeError> {
97        self.limits.check_input_len(len)
98    }
99
100    /// Validates a decoded list item count.
101    pub fn check_list_count(&self, count: usize) -> Result<(), DecodeError> {
102        self.limits.check_list_count(count)
103    }
104
105    /// Validates the current nesting depth.
106    pub fn check_nesting_depth(&self, depth: usize) -> Result<(), DecodeError> {
107        self.limits.check_nesting_depth(depth)
108    }
109
110    /// Accounts for one allocation against the cumulative allocation budget.
111    pub fn check_allocation(&mut self, size: usize) -> Result<(), DecodeError> {
112        let new_total = self
113            .total_allocated
114            .checked_add(size)
115            .ok_or(DecodeError::AllocationExceeded)?;
116        if new_total > self.limits.max_total_allocation {
117            return Err(DecodeError::AllocationExceeded);
118        }
119        self.total_allocated = new_total;
120        Ok(())
121    }
122}
123
124/// Shared decode failure categories.
125#[derive(Clone, Copy, Debug, Eq, PartialEq)]
126pub enum DecodeError {
127    /// The byte input is larger than the active decode budget.
128    InputTooLarge,
129    /// The input contains trailing bytes after a decoded value.
130    TrailingBytes,
131    /// A decoder reported consuming more bytes than the input contains.
132    DecoderOverread,
133    /// The input is malformed for the selected wire format.
134    Malformed,
135    /// A decoded list contains more items than the active decode budget.
136    ListTooLong,
137    /// Decoding exceeded the active nesting-depth budget.
138    NestingTooDeep,
139    /// A decoder requested allocation beyond the active allocation budget.
140    AllocationExceeded,
141}
142
143/// Ensures a decoder consumed the whole input.
144pub fn require_exact_consumption(consumed: usize, input_len: usize) -> Result<(), DecodeError> {
145    match consumed.cmp(&input_len) {
146        Ordering::Equal => Ok(()),
147        Ordering::Less => Err(DecodeError::TrailingBytes),
148        Ordering::Greater => Err(DecodeError::DecoderOverread),
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155
156    #[test]
157    fn rejects_oversized_input() {
158        let limits = DecodeLimits {
159            max_input_bytes: 2,
160            ..DecodeLimits::STRICT
161        };
162        assert_eq!(limits.check_input_len(3), Err(DecodeError::InputTooLarge));
163    }
164
165    #[test]
166    fn rejects_oversized_list() {
167        let limits = DecodeLimits {
168            max_list_items: 2,
169            ..DecodeLimits::STRICT
170        };
171        assert_eq!(limits.check_list_count(3), Err(DecodeError::ListTooLong));
172    }
173
174    #[test]
175    fn rejects_excessive_nesting_depth() {
176        let limits = DecodeLimits {
177            max_nesting_depth: 2,
178            ..DecodeLimits::STRICT
179        };
180        assert_eq!(
181            limits.check_nesting_depth(3),
182            Err(DecodeError::NestingTooDeep)
183        );
184    }
185
186    #[test]
187    fn rejects_excessive_allocation() {
188        let limits = DecodeLimits {
189            max_total_allocation: 2,
190            ..DecodeLimits::STRICT
191        };
192        assert_eq!(
193            limits.check_allocation(3),
194            Err(DecodeError::AllocationExceeded)
195        );
196    }
197
198    #[test]
199    fn accumulator_rejects_cumulative_allocation_over_budget() {
200        let limits = DecodeLimits {
201            max_total_allocation: 4,
202            ..DecodeLimits::STRICT
203        };
204        let mut accumulator = limits.accumulator();
205
206        assert_eq!(accumulator.check_allocation(3), Ok(()));
207        assert_eq!(accumulator.total_allocated(), 3);
208        assert_eq!(
209            accumulator.check_allocation(2),
210            Err(DecodeError::AllocationExceeded)
211        );
212        assert_eq!(accumulator.total_allocated(), 3);
213    }
214
215    #[test]
216    fn accumulator_rejects_allocation_counter_overflow() {
217        let limits = DecodeLimits {
218            max_total_allocation: usize::MAX,
219            ..DecodeLimits::STRICT
220        };
221        let mut accumulator = limits.accumulator();
222
223        assert_eq!(accumulator.check_allocation(usize::MAX), Ok(()));
224        assert_eq!(
225            accumulator.check_allocation(1),
226            Err(DecodeError::AllocationExceeded)
227        );
228    }
229
230    #[test]
231    fn detects_trailing_bytes() {
232        assert_eq!(
233            require_exact_consumption(1, 2),
234            Err(DecodeError::TrailingBytes)
235        );
236    }
237
238    #[test]
239    fn detects_decoder_overread() {
240        assert_eq!(
241            require_exact_consumption(3, 2),
242            Err(DecodeError::DecoderOverread)
243        );
244    }
245}