Skip to main content

ic_memory/
stable_cell.rs

1use crate::{LedgerCommitStore, constants::WASM_PAGE_SIZE_BYTES};
2use ic_stable_structures::{Memory, Storable, storable::Bound};
3use serde::{Deserialize, Serialize};
4use std::borrow::Cow;
5use thiserror::Error;
6
7/// Stable-cell magic prefix written by `ic-stable-structures::Cell`.
8pub const STABLE_CELL_MAGIC: &[u8; 3] = b"SCL";
9/// Stable-cell layout version supported by this adapter.
10pub const STABLE_CELL_LAYOUT_VERSION: u8 = 1;
11/// Stable-cell header byte length.
12pub const STABLE_CELL_HEADER_SIZE: usize = 8;
13/// Byte offset where the stable-cell value payload starts.
14pub const STABLE_CELL_VALUE_OFFSET: u64 = 8;
15
16///
17/// StableCellLedgerRecord
18///
19/// `ic-stable-structures::Cell` record containing an `ic-memory` allocation
20/// ledger commit store.
21///
22/// This is a substrate adapter DTO. It owns no framework policy and does not
23/// open application allocations.
24///
25
26#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
27#[serde(deny_unknown_fields)]
28pub struct StableCellLedgerRecord {
29    store: LedgerCommitStore,
30}
31
32impl StableCellLedgerRecord {
33    /// Construct a record from a commit store.
34    #[must_use]
35    pub const fn new(store: LedgerCommitStore) -> Self {
36        Self { store }
37    }
38
39    /// Borrow the embedded commit store.
40    #[must_use]
41    pub const fn store(&self) -> &LedgerCommitStore {
42        &self.store
43    }
44
45    /// Mutably borrow the embedded commit store.
46    pub const fn store_mut(&mut self) -> &mut LedgerCommitStore {
47        &mut self.store
48    }
49
50    /// Consume this record and return the embedded commit store.
51    #[must_use]
52    pub fn into_store(self) -> LedgerCommitStore {
53        self.store
54    }
55}
56
57impl Storable for StableCellLedgerRecord {
58    const BOUND: Bound = Bound::Unbounded;
59
60    fn to_bytes(&self) -> Cow<'_, [u8]> {
61        Cow::Owned(serialize_record(self))
62    }
63
64    fn into_bytes(self) -> Vec<u8> {
65        serialize_record(&self)
66    }
67
68    fn from_bytes(bytes: Cow<'_, [u8]>) -> Self {
69        decode_stable_cell_ledger_record(&bytes).unwrap_or_else(|err| {
70            panic!("StableCellLedgerRecord deserialize failed: {err}");
71        })
72    }
73}
74
75///
76/// StableCellPayloadError
77///
78/// Stable-cell payload decode failure.
79#[non_exhaustive]
80#[derive(Clone, Debug, Eq, Error, PartialEq)]
81pub enum StableCellPayloadError {
82    /// Declared bytes exceed the current recovery ceiling before allocation.
83    #[error("stable-cell ledger payload length {value_len} exceeds recovery limit")]
84    TooLarge { value_len: u64 },
85    /// Memory contents do not start with the stable-cell marker.
86    #[error("memory is not an ic-stable-structures Cell")]
87    NotStableCell,
88    /// Stable-cell layout version does not match the adapter's current shape.
89    #[error("unexpected stable-cell layout version {version}")]
90    UnexpectedLayoutVersion {
91        /// Observed stable-cell version.
92        version: u8,
93    },
94    /// Stable-cell header length does not fit inside the memory.
95    #[error("stable-cell payload length {value_len} exceeds available bytes {available_bytes}")]
96    InvalidLength {
97        /// Encoded value length.
98        value_len: u64,
99        /// Available payload bytes in memory.
100        available_bytes: u64,
101    },
102    /// Stable-cell length cannot be represented on the current host.
103    #[error("stable-cell payload length {value_len} cannot fit in usize")]
104    LengthOverflow {
105        /// Encoded value length.
106        value_len: u64,
107    },
108}
109
110///
111/// StableCellLedgerError
112///
113/// Stable-cell ledger record validation failure.
114#[non_exhaustive]
115#[derive(Debug, Error)]
116pub enum StableCellLedgerError {
117    /// Stable-cell envelope is corrupt or unexpected.
118    #[error(transparent)]
119    Payload(#[from] StableCellPayloadError),
120    /// Stable-cell value bytes are not a valid ledger record.
121    #[error("stable-cell ledger record decode failed")]
122    Record(#[source] ciborium::de::Error<std::io::Error>),
123}
124
125/// Decode the raw value payload from an `ic-stable-structures::Cell` memory.
126///
127/// This helper is intentionally narrow: it recognizes the physical stable-cell
128/// envelope and returns the value bytes. It does not deserialize those bytes or
129/// decide whether they represent a valid allocation ledger.
130pub fn decode_stable_cell_payload<M: Memory>(
131    memory: &M,
132) -> Result<Vec<u8>, StableCellPayloadError> {
133    if memory.size() == 0 {
134        return Err(StableCellPayloadError::NotStableCell);
135    }
136
137    let mut header = [0; STABLE_CELL_HEADER_SIZE];
138    memory.read(0, &mut header);
139    if &header[0..3] != STABLE_CELL_MAGIC {
140        return Err(StableCellPayloadError::NotStableCell);
141    }
142    if header[3] != STABLE_CELL_LAYOUT_VERSION {
143        return Err(StableCellPayloadError::UnexpectedLayoutVersion { version: header[3] });
144    }
145
146    let value_len = u64::from(u32::from_le_bytes([
147        header[4], header[5], header[6], header[7],
148    ]));
149    let available_bytes = memory.size().saturating_mul(WASM_PAGE_SIZE_BYTES);
150    let payload_capacity = available_bytes.saturating_sub(STABLE_CELL_VALUE_OFFSET);
151    if value_len > payload_capacity {
152        return Err(StableCellPayloadError::InvalidLength {
153            value_len,
154            available_bytes: payload_capacity,
155        });
156    }
157    if value_len > crate::constants::MAX_LEDGER_RECORD_BYTES as u64 {
158        return Err(StableCellPayloadError::TooLarge { value_len });
159    }
160    let value_len = usize::try_from(value_len)
161        .map_err(|_| StableCellPayloadError::LengthOverflow { value_len })?;
162
163    let mut bytes = vec![0; value_len];
164    memory.read(STABLE_CELL_VALUE_OFFSET, &mut bytes);
165    Ok(bytes)
166}
167
168/// Decode a `StableCellLedgerRecord` from stable-cell value bytes.
169///
170/// This decodes only the cell value payload, not the enclosing stable-cell
171/// header. Use [`decode_stable_cell_payload`] first when inspecting raw stable
172/// memory.
173///
174/// The returned record is decoded DTO state, not authority. Recover through the
175/// embedded [`LedgerCommitStore`] before trusting any ledger payload.
176pub fn decode_stable_cell_ledger_record(
177    bytes: &[u8],
178) -> Result<StableCellLedgerRecord, ciborium::de::Error<std::io::Error>> {
179    crate::cbor::from_slice_exact(bytes)
180}
181
182pub fn decode_stable_cell_ledger_record_from_memory<M: Memory>(
183    memory: &M,
184) -> Result<StableCellLedgerRecord, StableCellLedgerError> {
185    if memory.size() == 0 {
186        return Ok(StableCellLedgerRecord::default());
187    }
188
189    let payload = decode_stable_cell_payload(memory)?;
190    decode_stable_cell_ledger_record(&payload).map_err(StableCellLedgerError::Record)
191}
192
193/// Validate an existing stable-cell ledger record before opening it with
194/// `ic-stable-structures::Cell`.
195///
196/// `Cell::init` decodes the existing value through [`Storable::from_bytes`].
197/// That trait is panic-based, so the runtime preflights the raw memory with
198/// this fallible helper first. Empty memory is treated as uninitialized and is
199/// safe for `Cell::init` to create.
200pub fn validate_stable_cell_ledger_memory<M: Memory>(
201    memory: &M,
202) -> Result<(), StableCellLedgerError> {
203    decode_stable_cell_ledger_record_from_memory(memory)?;
204    Ok(())
205}
206
207fn serialize_record(record: &StableCellLedgerRecord) -> Vec<u8> {
208    let mut bytes = Vec::new();
209    ciborium::into_writer(record, &mut bytes).unwrap_or_else(|err| {
210        panic!("StableCellLedgerRecord serialize failed: {err}");
211    });
212    bytes
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218    use ic_stable_structures::{Cell, VectorMemory};
219
220    fn hex_fixture(contents: &str) -> Vec<u8> {
221        let hex = contents
222            .chars()
223            .filter(|char| !char.is_whitespace())
224            .collect::<String>();
225        assert_eq!(hex.len() % 2, 0, "fixture hex must have byte pairs");
226        hex.as_bytes()
227            .as_chunks::<2>()
228            .0
229            .iter()
230            .map(|pair| {
231                let pair = std::str::from_utf8(pair).expect("fixture hex is utf8");
232                u8::from_str_radix(pair, 16).expect("fixture hex byte")
233            })
234            .collect()
235    }
236
237    #[test]
238    fn stable_cell_ledger_record_round_trips_through_cell() {
239        let memory = VectorMemory::default();
240        let record = StableCellLedgerRecord::default();
241        let cell = Cell::init(memory.clone(), record.clone());
242
243        assert_eq!(cell.get(), &record);
244        let payload = decode_stable_cell_payload(&memory).expect("decode stable cell payload");
245        let decoded = StableCellLedgerRecord::from_bytes(Cow::Owned(payload));
246        assert_eq!(decoded, record);
247    }
248
249    #[test]
250    fn current_stable_cell_record_fixture_recovers() {
251        let bytes = hex_fixture(include_str!(
252            "../fixtures/current/stable_cell_record.cbor.hex"
253        ));
254        let record = decode_stable_cell_ledger_record(&bytes).expect("stable-cell fixture");
255
256        assert_eq!(
257            bytes,
258            crate::test_cbor::to_vec(&record).expect("re-encoded stable-cell fixture")
259        );
260        assert_eq!(
261            record
262                .store()
263                .recover()
264                .expect("fixture store recovers")
265                .current_generation(),
266            1
267        );
268    }
269
270    #[test]
271    fn malformed_payload_bytes_fail_closed_without_mutating_memory() {
272        let original = hex_fixture(include_str!(
273            "../fixtures/current/stable_cell_record.cbor.hex"
274        ));
275        let start = original
276            .windows(8)
277            .position(|bytes| bytes == b"\x67payload")
278            .unwrap()
279            + 8;
280        let oversized = u32::try_from(crate::constants::MAX_COMMITTED_PAYLOAD_BYTES + 1).unwrap();
281        let mut cases = Vec::new();
282        for replacement in [
283            vec![0x5a, 0xff, 0xff, 0xff, 0xff],
284            vec![0x5f, 0xff],
285            vec![0x41],
286            vec![0xff],
287        ] {
288            let mut bytes = original[..start].to_vec();
289            bytes.extend(replacement);
290            cases.push(bytes);
291        }
292        let mut bytes = original[..start].to_vec();
293        bytes.push(0x5a);
294        bytes.extend_from_slice(&oversized.to_be_bytes());
295        bytes.resize(bytes.len() + oversized as usize, 0);
296        cases.push(bytes);
297        for bytes in cases {
298            let memory = VectorMemory::default();
299            let pages = (bytes.len() + STABLE_CELL_HEADER_SIZE).div_ceil(65_536);
300            memory.grow(pages as u64);
301            memory.write(0, STABLE_CELL_MAGIC);
302            memory.write(3, &[STABLE_CELL_LAYOUT_VERSION]);
303            memory.write(4, &u32::try_from(bytes.len()).unwrap().to_le_bytes());
304            memory.write(STABLE_CELL_VALUE_OFFSET, &bytes);
305            let before = memory.borrow().clone();
306            for _ in 0..2 {
307                assert!(matches!(
308                    decode_stable_cell_ledger_record_from_memory(&memory),
309                    Err(StableCellLedgerError::Record(_))
310                ));
311                assert!(matches!(
312                    validate_stable_cell_ledger_memory(&memory),
313                    Err(StableCellLedgerError::Record(_))
314                ));
315            }
316            assert_eq!(*memory.borrow(), before);
317        }
318    }
319
320    #[test]
321    fn stable_cell_ledger_record_rejects_trailing_bytes() {
322        let mut bytes = serialize_record(&StableCellLedgerRecord::default());
323        bytes.push(0);
324
325        let err =
326            decode_stable_cell_ledger_record(&bytes).expect_err("trailing bytes must fail closed");
327
328        assert!(err.to_string().contains("trailing bytes"));
329    }
330
331    #[test]
332    fn stable_cell_ledger_record_rejects_unknown_top_level_fields() {
333        use crate::test_cbor::Value;
334
335        let mut map = Vec::new();
336        crate::test_cbor::map_insert(
337            &mut map,
338            Value::Text("store".to_string()),
339            crate::test_cbor::to_value(LedgerCommitStore::default()).expect("store value"),
340        );
341        crate::test_cbor::map_insert(
342            &mut map,
343            Value::Text("future_field".to_string()),
344            Value::Bool(true),
345        );
346        let bytes = crate::test_cbor::to_vec(&Value::Map(map)).expect("unknown-field stable cell");
347
348        let err = decode_stable_cell_ledger_record(&bytes)
349            .expect_err("unknown stable-cell record field must fail closed");
350
351        assert!(err.to_string().contains("future_field"));
352    }
353
354    #[test]
355    fn stable_cell_ledger_record_requires_both_commit_slot_fields() {
356        use crate::test_cbor::Value;
357
358        for (missing, present) in [("slot0", "slot1"), ("slot1", "slot0")] {
359            let mut physical = Vec::new();
360            crate::test_cbor::map_insert(
361                &mut physical,
362                Value::Text(present.to_string()),
363                Value::Null,
364            );
365            let mut store = Vec::new();
366            crate::test_cbor::map_insert(
367                &mut store,
368                Value::Text("physical".to_string()),
369                Value::Map(physical),
370            );
371            let mut record = Vec::new();
372            crate::test_cbor::map_insert(
373                &mut record,
374                Value::Text("store".to_string()),
375                Value::Map(store),
376            );
377            let bytes = crate::test_cbor::to_vec(&Value::Map(record)).expect("record bytes");
378
379            let err = decode_stable_cell_ledger_record(&bytes)
380                .expect_err("missing commit slot must fail closed");
381
382            assert!(err.to_string().contains(missing));
383        }
384    }
385
386    #[test]
387    fn stable_cell_payload_rejects_non_cell_memory() {
388        let memory = VectorMemory::default();
389        memory.grow(1);
390        memory.write(0, b"BAD");
391
392        assert_eq!(
393            decode_stable_cell_payload(&memory),
394            Err(StableCellPayloadError::NotStableCell)
395        );
396    }
397
398    #[test]
399    fn stable_cell_payload_rejects_empty_memory_without_panic() {
400        let memory = VectorMemory::default();
401
402        assert_eq!(
403            decode_stable_cell_payload(&memory),
404            Err(StableCellPayloadError::NotStableCell)
405        );
406    }
407
408    #[test]
409    fn stable_cell_ledger_preflight_classifies_bad_record_without_panic() {
410        let memory = VectorMemory::default();
411        memory.grow(1);
412        memory.write(0, STABLE_CELL_MAGIC);
413        memory.write(3, &[STABLE_CELL_LAYOUT_VERSION]);
414        memory.write(4, &1_u32.to_le_bytes());
415        memory.write(STABLE_CELL_VALUE_OFFSET, &[0xff]);
416
417        let err =
418            validate_stable_cell_ledger_memory(&memory).expect_err("bad record must be classified");
419
420        assert!(matches!(err, StableCellLedgerError::Record(_)));
421    }
422    #[test]
423    fn oversized_cell_length_is_rejected_before_payload_read() {
424        struct HeaderOnly;
425        impl Memory for HeaderOnly {
426            fn size(&self) -> u64 {
427                2048
428            }
429            fn grow(&self, _: u64) -> i64 {
430                panic!("must not grow")
431            }
432            fn write(&self, _: u64, _: &[u8]) {
433                panic!("must not write")
434            }
435            fn read(&self, offset: u64, bytes: &mut [u8]) {
436                assert_eq!(offset, 0);
437                assert_eq!(bytes.len(), STABLE_CELL_HEADER_SIZE);
438                bytes[..4].copy_from_slice(b"SCL\x01");
439                bytes[4..8].copy_from_slice(
440                    &u32::try_from(crate::constants::MAX_LEDGER_RECORD_BYTES + 1)
441                        .unwrap()
442                        .to_le_bytes(),
443                );
444            }
445        }
446        assert!(matches!(
447            decode_stable_cell_ledger_record_from_memory(&HeaderOnly),
448            Err(StableCellLedgerError::Payload(
449                StableCellPayloadError::TooLarge { .. }
450            ))
451        ));
452    }
453
454    #[test]
455    fn hostile_cbor_is_rejected_on_production_record_decode() {
456        let huge_array = [0x9b, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff];
457        let huge_string = [0x7b, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff];
458        let mut nested = vec![0x81; crate::constants::MAX_LEDGER_NESTING + 1];
459        nested.push(0);
460        for bytes in [&huge_array[..], &huge_string[..], &nested, &[0xa1], &[0xff]] {
461            assert!(decode_stable_cell_ledger_record(bytes).is_err());
462        }
463    }
464}