Skip to main content

ic_storage/
memory.rs

1//! Page-addressed backing IO for Storage infrastructure.
2//!
3//! A backing implements byte access, not allocation authority. Applications use
4//! admitted Storage handles rather than treating this trait as a grant of access.
5
6use std::{cell::RefCell, rc::Rc};
7
8use crate::WASM_PAGE_SIZE_BYTES;
9
10///
11/// Memory
12///
13/// Page-addressed storage with initialized reads and nonshrinking capacity.
14/// Implementations validate complete access ranges before touching backing bytes.
15///
16pub trait Memory {
17    /// Returns the number of complete accessible Wasm pages.
18    fn size(&self) -> u64;
19
20    /// Adds zero-filled pages, returning the previous size or `-1` on refusal.
21    /// Refusal leaves the existing bytes and extent unchanged.
22    fn grow(&self, pages: u64) -> i64;
23
24    /// Copies the selected backing bytes into an initialized destination.
25    ///
26    /// # Panics
27    ///
28    /// Panics or traps if the complete range is outside the backing.
29    fn read(&self, offset: u64, bytes: &mut [u8]);
30
31    /// Initializes the destination from the selected backing bytes.
32    ///
33    /// The default implementation admits the entire source range, then reads
34    /// through a bounded, initialized scratch buffer. Custom backings pay an
35    /// additional copy per chunk; direct implementations can override this.
36    /// Failure may leave an initialized prefix in the destination.
37    ///
38    /// # Safety
39    ///
40    /// `dst` must satisfy the validity, alignment, and exclusive writable access
41    /// requirements of a Rust slice of `count` bytes, including for zero length.
42    /// That destination must not overlap the source or the backing's own state.
43    ///
44    /// # Panics
45    ///
46    /// Panics or traps if the complete source range is outside the backing or
47    /// if the backing's safe read fails.
48    #[expect(
49        unsafe_code,
50        reason = "The raw destination is an explicit caller-owned boundary."
51    )]
52    unsafe fn read_unsafe(&self, offset: u64, dst: *mut u8, count: usize) {
53        admit_range(offset, count, self.size());
54        let mut scratch = [0_u8; 1024];
55        let mut copied = 0;
56        while copied < count {
57            let chunk = (count - copied).min(scratch.len());
58            let source_offset = offset + u64::try_from(copied).expect("admitted offset");
59            self.read(source_offset, &mut scratch[..chunk]);
60            // SAFETY: The caller supplies the complete exclusive destination;
61            // admission and the loop bounds keep this copy inside it. Scratch
62            // contains initialized bytes and cannot overlap that destination.
63            unsafe {
64                std::ptr::copy_nonoverlapping(scratch.as_ptr(), dst.add(copied), chunk);
65            }
66            copied += chunk;
67        }
68    }
69
70    /// Replaces the selected backing bytes.
71    ///
72    /// # Panics
73    ///
74    /// Panics or traps if the complete range is outside the backing.
75    fn write(&self, offset: u64, bytes: &[u8]);
76}
77
78impl<M: Memory + ?Sized> Memory for Rc<M> {
79    fn size(&self) -> u64 {
80        (**self).size()
81    }
82
83    fn grow(&self, pages: u64) -> i64 {
84        (**self).grow(pages)
85    }
86
87    fn read(&self, offset: u64, bytes: &mut [u8]) {
88        (**self).read(offset, bytes);
89    }
90
91    #[expect(
92        unsafe_code,
93        reason = "Forwarding preserves the caller's raw-read obligations."
94    )]
95    unsafe fn read_unsafe(&self, offset: u64, dst: *mut u8, count: usize) {
96        // SAFETY: The destination and source obligations are unchanged.
97        unsafe { (**self).read_unsafe(offset, dst, count) };
98    }
99
100    fn write(&self, offset: u64, bytes: &[u8]) {
101        (**self).write(offset, bytes);
102    }
103}
104
105impl<M: Memory + ?Sized> Memory for &M {
106    fn size(&self) -> u64 {
107        (**self).size()
108    }
109
110    fn grow(&self, pages: u64) -> i64 {
111        (**self).grow(pages)
112    }
113
114    fn read(&self, offset: u64, bytes: &mut [u8]) {
115        (**self).read(offset, bytes);
116    }
117
118    #[expect(
119        unsafe_code,
120        reason = "Forwarding preserves the caller's raw-read obligations."
121    )]
122    unsafe fn read_unsafe(&self, offset: u64, dst: *mut u8, count: usize) {
123        // SAFETY: The destination and source obligations are unchanged.
124        unsafe { (**self).read_unsafe(offset, dst, count) };
125    }
126
127    fn write(&self, offset: u64, bytes: &[u8]) {
128        (**self).write(offset, bytes);
129    }
130}
131
132///
133/// `VectorMemory`
134///
135/// Shared native byte backing. Clones refer to the same physical bytes.
136///
137pub type VectorMemory = Rc<RefCell<Vec<u8>>>;
138
139///
140/// `DefaultMemoryImpl`
141///
142/// Native default backing, shared through reference-counted ownership.
143///
144#[cfg(not(target_arch = "wasm32"))]
145pub type DefaultMemoryImpl = VectorMemory;
146
147impl Memory for RefCell<Vec<u8>> {
148    fn size(&self) -> u64 {
149        u64::try_from(self.borrow().len()).expect("native byte length fits u64")
150            / WASM_PAGE_SIZE_BYTES
151    }
152
153    fn grow(&self, pages: u64) -> i64 {
154        let mut bytes = self.borrow_mut();
155        let length = u64::try_from(bytes.len()).expect("native byte length fits u64");
156        let old_pages = length / WASM_PAGE_SIZE_BYTES;
157        let Ok(previous) = i64::try_from(old_pages) else {
158            return -1;
159        };
160        if pages == 0 {
161            return previous;
162        }
163        // An externally supplied partial page is not advertised or completed
164        // implicitly: its tail remains the caller's data, outside this extent.
165        if !length.is_multiple_of(WASM_PAGE_SIZE_BYTES) {
166            return -1;
167        }
168        let Some(new_length) = old_pages
169            .checked_add(pages)
170            .and_then(|total| total.checked_mul(WASM_PAGE_SIZE_BYTES))
171            .and_then(|total| usize::try_from(total).ok())
172        else {
173            return -1;
174        };
175        let additional = new_length - bytes.len();
176        if bytes.try_reserve_exact(additional).is_err() {
177            return -1;
178        }
179        bytes.resize(new_length, 0);
180        previous
181    }
182
183    fn read(&self, offset: u64, bytes: &mut [u8]) {
184        let backing = self.borrow();
185        let range = native_range(offset, bytes.len(), backing.len());
186        bytes.copy_from_slice(&backing[range]);
187    }
188
189    #[expect(
190        unsafe_code,
191        reason = "Direct copying initializes caller-owned raw output without a temporary."
192    )]
193    unsafe fn read_unsafe(&self, offset: u64, dst: *mut u8, count: usize) {
194        let backing = self.borrow();
195        let range = native_range(offset, count, backing.len());
196        // SAFETY: The admitted source slice contains count initialized bytes;
197        // the caller guarantees a valid, exclusive, nonoverlapping destination.
198        unsafe {
199            std::ptr::copy_nonoverlapping(backing[range].as_ptr(), dst, count);
200        }
201    }
202
203    fn write(&self, offset: u64, bytes: &[u8]) {
204        let mut backing = self.borrow_mut();
205        let range = native_range(offset, bytes.len(), backing.len());
206        backing[range].copy_from_slice(bytes);
207    }
208}
209
210// Admission precedes every copy, so overflow and out-of-range failures cannot
211// leave a partially modified native destination or backing.
212fn admit_range(offset: u64, count: usize, pages: u64) {
213    let extent = pages
214        .checked_mul(WASM_PAGE_SIZE_BYTES)
215        .expect("memory extent overflow");
216    let count = u64::try_from(count).expect("memory access length overflow");
217    let end = offset.checked_add(count).expect("memory access overflow");
218    assert!(end <= extent, "memory access outside complete pages");
219}
220
221fn native_range(offset: u64, count: usize, length: usize) -> std::ops::Range<usize> {
222    let pages = u64::try_from(length).expect("native byte length fits u64") / WASM_PAGE_SIZE_BYTES;
223    admit_range(offset, count, pages);
224    let start = usize::try_from(offset).expect("admitted native offset");
225    start..start + count
226}
227
228///
229/// `DefaultMemoryImpl`
230///
231/// The IC stable-memory backing accessed through Platform Runtime's safe IC0 API.
232///
233#[cfg(target_arch = "wasm32")]
234#[derive(Clone, Copy, Default)]
235pub struct DefaultMemoryImpl {
236    _private: (),
237}
238
239#[cfg(target_arch = "wasm32")]
240impl Memory for DefaultMemoryImpl {
241    fn size(&self) -> u64 {
242        ic_platform_runtime::ic0::stable64_size()
243    }
244
245    fn grow(&self, pages: u64) -> i64 {
246        let old_pages = self.size();
247        let Ok(previous) = i64::try_from(old_pages) else {
248            return -1;
249        };
250        if pages == 0 {
251            return previous;
252        }
253        if old_pages
254            .checked_add(pages)
255            .and_then(|total| total.checked_mul(WASM_PAGE_SIZE_BYTES))
256            .is_none()
257        {
258            return -1;
259        }
260        i64::try_from(ic_platform_runtime::ic0::stable64_grow(pages)).unwrap_or(-1)
261    }
262
263    fn read(&self, offset: u64, bytes: &mut [u8]) {
264        // Empty syscalls need not check their offset. Admit that case here;
265        // nonempty syscalls own complete range and overflow admission.
266        if bytes.is_empty() {
267            admit_range(offset, 0, self.size());
268            return;
269        }
270        ic_platform_runtime::ic0::stable64_read(bytes, offset);
271    }
272
273    #[expect(
274        unsafe_code,
275        reason = "MaybeUninit exposes raw writable bytes to the safe Runtime boundary."
276    )]
277    unsafe fn read_unsafe(&self, offset: u64, dst: *mut u8, count: usize) {
278        if count == 0 {
279            admit_range(offset, 0, self.size());
280            return;
281        }
282        // SAFETY: The caller guarantees slice validity and exclusive access.
283        // MaybeUninit permits the bytes to be uninitialized on entry; Runtime
284        // admits the complete nonempty stable range and initializes the entire
285        // slice on success without a temporary buffer.
286        let bytes = unsafe {
287            std::slice::from_raw_parts_mut(dst.cast::<std::mem::MaybeUninit<u8>>(), count)
288        };
289        ic_platform_runtime::ic0::stable64_read_uninit(bytes, offset);
290    }
291
292    fn write(&self, offset: u64, bytes: &[u8]) {
293        // Empty writes require explicit admission; nonempty syscalls check the
294        // complete stable range before writing.
295        if bytes.is_empty() {
296            admit_range(offset, 0, self.size());
297            return;
298        }
299        ic_platform_runtime::ic0::stable64_write(bytes, offset);
300    }
301}
302
303#[cfg(test)]
304mod contract_tests;