Skip to main content

alloy_provider/provider/ccip_read/
bounds.rs

1//! Allocation-free bounds checks before decoding contract-controlled dynamic ABI values.
2//!
3//! ABI offsets may alias the same tail data, so the decoded size can exceed the encoded size by a
4//! large factor. These checks walk the encoding without allocating and charge the decoded sizes
5//! against [`CcipReadConfig::max_revert_data_size`].
6
7use super::{CcipReadConfig, CcipReadError};
8use alloy_primitives::U256;
9use alloy_sol_types::Error;
10
11/// Checks an `OffchainLookup` revert: the URL count and the total decoded dynamic data, counting
12/// overlapping references separately.
13pub(super) fn offchain_lookup(data: &[u8], config: &CcipReadConfig) -> Result<(), CcipReadError> {
14    offchain_lookup_fields(data, &mut Limits::new(config)).map_err(|error| match error {
15        CheckError::Abi(error) => CcipReadError::InvalidOffchainLookup(error),
16        CheckError::Limit(message) => CcipReadError::ResourceLimit(message.into()),
17    })
18}
19
20/// Checks every request of an ENSIP-21 `query` batch before allocating it, including nested
21/// batches.
22pub(super) fn batch(data: &[u8], config: &CcipReadConfig) -> Result<(), CcipReadError> {
23    batch_requests(data, &mut Limits::new(config)).map_err(|error| match error {
24        CheckError::Abi(error) => CcipReadError::InvalidBatch(error.to_string()),
25        CheckError::Limit(message) => CcipReadError::ResourceLimit(message.into()),
26    })
27}
28
29fn offchain_lookup_fields(data: &[u8], limits: &mut Limits<'_>) -> Result<(), CheckError> {
30    let data = data.get(4..).ok_or(Error::Overrun)?;
31    let mut head = data;
32    word(&mut head)?; // sender
33    limits.urls(indirect(&mut head, data)?)?;
34    limits.bytes(indirect(&mut head, data)?, false)?; // callData
35    word(&mut head)?; // callbackFunction
36    limits.bytes(indirect(&mut head, data)?, false) // extraData
37}
38
39fn batch_requests(data: &[u8], limits: &mut Limits<'_>) -> Result<(), CheckError> {
40    let config = limits.config;
41    if data.len() > config.max_revert_data_size {
42        return Err(CheckError::Limit("batch data exceeds revert data size limit"));
43    }
44    let data = data.get(4..).ok_or(Error::Overrun)?;
45    let mut head = data;
46    let (count, array) = limits.array(
47        indirect(&mut head, data)?,
48        config.max_batch_size,
49        "batch request count exceeds configured limit",
50    )?;
51    let mut head = array;
52    for _ in 0..count {
53        let request = indirect(&mut head, array)?;
54        let mut fields = request;
55        word(&mut fields)?; // sender
56        limits.urls(indirect(&mut fields, request)?)?;
57        limits.bytes(indirect(&mut fields, request)?, false)?; // data
58    }
59    Ok(())
60}
61
62enum CheckError {
63    Abi(Error),
64    Limit(&'static str),
65}
66
67impl From<Error> for CheckError {
68    fn from(error: Error) -> Self {
69        Self::Abi(error)
70    }
71}
72
73struct Limits<'a> {
74    config: &'a CcipReadConfig,
75    remaining: usize,
76}
77
78impl<'a> Limits<'a> {
79    const fn new(config: &'a CcipReadConfig) -> Self {
80        Self { config, remaining: config.max_revert_data_size }
81    }
82
83    fn charge(&mut self, size: usize) -> Result<(), CheckError> {
84        self.remaining = self
85            .remaining
86            .checked_sub(size)
87            .ok_or(CheckError::Limit("decoded ABI data exceeds revert data size limit"))?;
88        Ok(())
89    }
90
91    fn array<'b>(
92        &mut self,
93        mut data: &'b [u8],
94        max: usize,
95        message: &'static str,
96    ) -> Result<(usize, &'b [u8]), CheckError> {
97        let count = offset(&mut data)?;
98        if count > max {
99            return Err(CheckError::Limit(message));
100        }
101        let size = count.checked_mul(32).ok_or(Error::Overrun)?;
102        data.get(..size).ok_or(Error::Overrun)?;
103        self.charge(size)?;
104        // Element offsets are relative to the word after the array length.
105        Ok((count, data))
106    }
107
108    fn urls(&mut self, data: &[u8]) -> Result<(), CheckError> {
109        let (count, array) = self.array(
110            data,
111            self.config.max_gateway_urls,
112            "gateway URL count exceeds configured limit",
113        )?;
114        let mut head = array;
115        for _ in 0..count {
116            self.bytes(indirect(&mut head, array)?, true)?;
117        }
118        Ok(())
119    }
120
121    fn bytes(&mut self, mut data: &[u8], string: bool) -> Result<(), CheckError> {
122        let len = offset(&mut data)?;
123        let bytes = data.get(..len).ok_or(Error::Overrun)?;
124        self.charge(len)?;
125        if string {
126            // The ABI decoder replaces invalid UTF-8 with U+FFFD. Charge any extra bytes
127            // before it allocates the owned String, without allocating a lossy copy here.
128            for chunk in bytes.utf8_chunks() {
129                if !chunk.invalid().is_empty() {
130                    self.charge(3 - chunk.invalid().len())?;
131                }
132            }
133        }
134        Ok(())
135    }
136}
137
138fn word<'a>(head: &mut &'a [u8]) -> Result<&'a [u8; 32], Error> {
139    let (word, rest) = head.split_first_chunk::<32>().ok_or(Error::Overrun)?;
140    *head = rest;
141    Ok(word)
142}
143
144fn offset(head: &mut &[u8]) -> Result<usize, Error> {
145    U256::from_be_bytes(*word(head)?).try_into().map_err(|_| Error::Overrun)
146}
147
148fn indirect<'a>(head: &mut &[u8], base: &'a [u8]) -> Result<&'a [u8], Error> {
149    base.get(offset(head)?..).ok_or(Error::Overrun)
150}