Skip to main content

ic_sqlite_vfs/stable/
memory.rs

1//! Byte-addressed SQLite memory wrapper.
2//!
3//! The crate stores SQLite inside a user-provided `VirtualMemory`, so it can
4//! coexist with other stable structures managed by the same MemoryManager.
5
6use crate::config::STABLE_PAGE_SIZE;
7use crate::stable::memory_manager::{MemoryId, MemoryManager};
8use crate::stable::memory_manager::{MemoryIdentity, VirtualMemory};
9use crate::stable::raw_memory::{DefaultMemoryImpl, Memory};
10use std::cell::{Cell, RefCell};
11#[cfg(any(test, feature = "canister-api-test-failpoints"))]
12use std::collections::BTreeMap;
13
14pub type DbMemory = VirtualMemory<DefaultMemoryImpl>;
15
16#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
17pub struct ContextId(u64);
18
19#[derive(Debug, thiserror::Error)]
20pub enum StableMemoryError {
21    #[error("stable memory backend is not initialized")]
22    NotInitialized,
23    #[error("stable memory backend is already initialized")]
24    AlreadyInitialized,
25    #[error("stable memory is already registered with a database handle")]
26    MemoryAlreadyRegistered,
27    #[error(
28        "stable memory grow failed: current_pages={current_pages}, required_pages={required_pages}"
29    )]
30    GrowFailed {
31        current_pages: u64,
32        required_pages: u64,
33    },
34    #[error(
35        "stable memory read out of bounds: offset={offset}, len={len}, size_bytes={size_bytes}"
36    )]
37    ReadOutOfBounds {
38        offset: u64,
39        len: u64,
40        size_bytes: u64,
41    },
42    #[error("offset overflow")]
43    OffsetOverflow,
44    #[error("import session already started")]
45    ImportAlreadyStarted,
46    #[error("import session not started")]
47    ImportNotStarted,
48    #[error("database update already in progress")]
49    UpdateInProgress,
50    #[error("import chunk out of order: offset={offset}, expected={expected}")]
51    ImportOutOfOrder { offset: u64, expected: u64 },
52    #[error("import chunk out of bounds: offset={offset}, len={len}, db_size={db_size}")]
53    ImportOutOfBounds { offset: u64, len: u64, db_size: u64 },
54    #[error("import incomplete: written_until={written_until}, db_size={db_size}")]
55    ImportIncomplete { written_until: u64, db_size: u64 },
56    #[error("checksum mismatch: expected={expected}, actual={actual}")]
57    ChecksumMismatch { expected: u64, actual: u64 },
58    #[error("checksum refresh chunk size must be greater than zero")]
59    ChecksumRefreshChunkEmpty,
60    #[error("stable blob failpoint: {0}")]
61    Failpoint(&'static str),
62    #[error("superblock metadata checksum mismatch")]
63    MetaChecksumMismatch,
64    #[error("unsupported stable layout version: {0}")]
65    UnsupportedLayoutVersion(u64),
66    #[error("non-empty stable memory does not contain an ic-sqlite-vfs image")]
67    ForeignStableMemoryImage,
68    #[error("zero extent limit exceeded: limit={limit}")]
69    ZeroExtentLimitExceeded { limit: usize },
70}
71
72#[cfg(any(test, feature = "canister-api-test-failpoints"))]
73#[derive(Clone, Copy, Debug, Eq, PartialEq)]
74pub enum MemoryFailpoint {
75    GrowFailed { ordinal: u64 },
76    TrapAfterWrite { ordinal: u64 },
77}
78
79#[cfg(any(test, feature = "canister-api-test-failpoints"))]
80thread_local! {
81    static FAILPOINTS: RefCell<BTreeMap<ContextId, MemoryFailpointState>> = const { RefCell::new(BTreeMap::new()) };
82}
83
84thread_local! {
85    static NEXT_CONTEXT_ID: Cell<u64> = const { Cell::new(1) };
86    static DEFAULT_CONTEXT: Cell<Option<ContextId>> = const { Cell::new(None) };
87    static CURRENT_CONTEXT: Cell<Option<ContextId>> = const { Cell::new(None) };
88    static DB_MEMORY: RefCell<Vec<(ContextId, DbMemory)>> = const { RefCell::new(Vec::new()) };
89    static REGISTERED_MEMORY: RefCell<Vec<(ContextId, MemoryIdentity)>> = const { RefCell::new(Vec::new()) };
90}
91
92pub fn init(memory: DbMemory) -> Result<ContextId, StableMemoryError> {
93    DEFAULT_CONTEXT.with(|default| {
94        if default.get().is_some() {
95            return Err(StableMemoryError::AlreadyInitialized);
96        }
97        let context = init_context(memory)?;
98        default.set(Some(context));
99        Ok(context)
100    })
101}
102
103pub fn init_context(memory: DbMemory) -> Result<ContextId, StableMemoryError> {
104    let identity = memory.identity();
105    reject_registered_memory(identity)?;
106    let context = NEXT_CONTEXT_ID.with(|next| {
107        let current = next.get();
108        let context = ContextId(current);
109        next.set(current.checked_add(1).expect("context id overflow"));
110        context
111    });
112    DB_MEMORY.with(|slot| {
113        slot.borrow_mut().push((context, memory));
114    });
115    REGISTERED_MEMORY.with(|slot| {
116        slot.borrow_mut().push((context, identity));
117    });
118    Ok(context)
119}
120
121fn reject_registered_memory(identity: MemoryIdentity) -> Result<(), StableMemoryError> {
122    REGISTERED_MEMORY.with(|slot| {
123        if slot
124            .borrow()
125            .iter()
126            .any(|(_, registered)| *registered == identity)
127        {
128            Err(StableMemoryError::MemoryAlreadyRegistered)
129        } else {
130            Ok(())
131        }
132    })
133}
134
135pub fn default_context() -> Option<ContextId> {
136    DEFAULT_CONTEXT.with(Cell::get)
137}
138
139#[inline(always)]
140pub fn active_context_id() -> Result<ContextId, StableMemoryError> {
141    if let Some(context) = CURRENT_CONTEXT.with(Cell::get) {
142        return Ok(context);
143    }
144    default_context().ok_or(StableMemoryError::NotInitialized)
145}
146
147#[inline(always)]
148pub fn with_context<T>(context: ContextId, f: impl FnOnce() -> T) -> T {
149    let previous = CURRENT_CONTEXT.with(|current| {
150        let previous = current.get();
151        current.set(Some(context));
152        previous
153    });
154    let _guard = ContextGuard { previous };
155    f()
156}
157
158#[cfg(any(test, feature = "canister-api-test-failpoints"))]
159pub fn set_failpoint(failpoint: MemoryFailpoint) {
160    if let Ok(context) = active_context_id() {
161        FAILPOINTS.with(|slot| {
162            slot.borrow_mut().insert(
163                context,
164                MemoryFailpointState {
165                    failpoint: Some(failpoint),
166                    grow_count: 0,
167                    write_count: 0,
168                },
169            );
170        });
171    }
172}
173
174#[cfg(any(test, feature = "canister-api-test-failpoints"))]
175pub fn clear_failpoint() {
176    FAILPOINTS.with(|slot| slot.borrow_mut().clear());
177}
178
179pub fn size_pages() -> u64 {
180    with_memory(|memory| memory.size()).unwrap_or(0)
181}
182
183pub fn ensure_capacity(end_offset: u64) -> Result<(), StableMemoryError> {
184    with_memory(|memory| ensure_memory_capacity(memory, end_offset))?
185}
186
187#[allow(dead_code)]
188pub fn read(offset: u64, dst: &mut [u8]) -> Result<(), StableMemoryError> {
189    if dst.is_empty() {
190        return Ok(());
191    }
192    let len = u64::try_from(dst.len()).map_err(|_| StableMemoryError::OffsetOverflow)?;
193    let end = offset
194        .checked_add(len)
195        .ok_or(StableMemoryError::OffsetOverflow)?;
196    with_memory(|memory| {
197        let size_bytes = memory
198            .size()
199            .checked_mul(STABLE_PAGE_SIZE)
200            .ok_or(StableMemoryError::OffsetOverflow)?;
201        if end > size_bytes {
202            return Err(StableMemoryError::ReadOutOfBounds {
203                offset,
204                len,
205                size_bytes,
206            });
207        }
208        memory.read(offset, dst);
209        Ok(())
210    })?
211}
212
213#[inline(always)]
214pub(crate) fn read_preallocated(offset: u64, dst: &mut [u8]) -> Result<(), StableMemoryError> {
215    checked_end(offset, dst.len())?;
216    with_memory(|memory| {
217        debug_assert_capacity(memory, offset, dst.len(), "read_preallocated");
218        memory.read(offset, dst);
219    })?;
220    Ok(())
221}
222
223pub fn write(offset: u64, bytes: &[u8]) -> Result<(), StableMemoryError> {
224    if bytes.is_empty() {
225        return Ok(());
226    }
227    let end = checked_end(offset, bytes.len())?;
228    with_memory(|memory| {
229        ensure_memory_capacity(memory, end)?;
230        memory.write(offset, bytes);
231        Ok(())
232    })??;
233
234    #[cfg(any(test, debug_assertions, feature = "bench-profile"))]
235    crate::read_metrics::record_stable_data_write(bytes.len());
236
237    #[cfg(any(test, feature = "canister-api-test-failpoints"))]
238    if hit_write_trap_failpoint() {
239        fail_after_stable_write();
240    }
241
242    Ok(())
243}
244
245#[allow(dead_code)]
246pub(crate) fn write_preallocated(offset: u64, bytes: &[u8]) -> Result<(), StableMemoryError> {
247    if bytes.is_empty() {
248        return Ok(());
249    }
250    checked_end(offset, bytes.len())?;
251    with_memory(|memory| {
252        debug_assert_capacity(memory, offset, bytes.len(), "write_preallocated");
253        memory.write(offset, bytes);
254    })?;
255
256    #[cfg(any(test, debug_assertions, feature = "bench-profile"))]
257    crate::read_metrics::record_stable_data_write(bytes.len());
258
259    #[cfg(any(test, feature = "canister-api-test-failpoints"))]
260    if hit_write_trap_failpoint() {
261        fail_after_stable_write();
262    }
263
264    Ok(())
265}
266
267#[inline(always)]
268pub(crate) fn write_prechecked(offset: u64, bytes: &[u8]) -> Result<(), StableMemoryError> {
269    debug_assert!(checked_end(offset, bytes.len()).is_ok());
270    with_memory(|memory| {
271        debug_assert_capacity(memory, offset, bytes.len(), "write_prechecked");
272        memory.write(offset, bytes);
273    })?;
274
275    #[cfg(any(test, debug_assertions, feature = "bench-profile"))]
276    crate::read_metrics::record_stable_data_write(bytes.len());
277
278    #[cfg(any(test, feature = "canister-api-test-failpoints"))]
279    if hit_write_trap_failpoint() {
280        fail_after_stable_write();
281    }
282
283    Ok(())
284}
285
286#[inline(always)]
287pub(crate) fn write_prechecked_unmetered(
288    offset: u64,
289    bytes: &[u8],
290) -> Result<(), StableMemoryError> {
291    debug_assert!(checked_end(offset, bytes.len()).is_ok());
292    with_memory(|memory| {
293        debug_assert_capacity(memory, offset, bytes.len(), "write_prechecked_unmetered");
294        memory.write(offset, bytes);
295    })?;
296
297    #[cfg(any(test, feature = "canister-api-test-failpoints"))]
298    if hit_write_trap_failpoint() {
299        fail_after_stable_write();
300    }
301
302    Ok(())
303}
304
305fn ensure_memory_capacity(memory: &DbMemory, end_offset: u64) -> Result<(), StableMemoryError> {
306    let current_pages = memory.size();
307    let current_bytes = current_pages
308        .checked_mul(STABLE_PAGE_SIZE)
309        .ok_or(StableMemoryError::OffsetOverflow)?;
310    if end_offset <= current_bytes {
311        return Ok(());
312    }
313
314    let missing = end_offset
315        .checked_sub(current_bytes)
316        .ok_or(StableMemoryError::OffsetOverflow)?;
317    let pages = missing.div_ceil(STABLE_PAGE_SIZE);
318
319    #[cfg(any(test, feature = "canister-api-test-failpoints"))]
320    if hit_grow_failpoint() {
321        let required_pages = current_pages
322            .checked_add(pages)
323            .ok_or(StableMemoryError::OffsetOverflow)?;
324        return Err(StableMemoryError::GrowFailed {
325            current_pages,
326            required_pages,
327        });
328    }
329
330    let previous = memory.grow(pages);
331    if previous < 0 {
332        let required_pages = current_pages
333            .checked_add(pages)
334            .ok_or(StableMemoryError::OffsetOverflow)?;
335        return Err(StableMemoryError::GrowFailed {
336            current_pages,
337            required_pages,
338        });
339    }
340
341    #[cfg(any(test, debug_assertions, feature = "bench-profile"))]
342    crate::read_metrics::record_stable_grow(pages);
343    Ok(())
344}
345
346#[inline(always)]
347fn debug_assert_capacity(memory: &DbMemory, offset: u64, len: usize, operation: &str) {
348    #[cfg(debug_assertions)]
349    {
350        let Ok(end) = checked_end(offset, len) else {
351            debug_assert!(false, "{operation} offset overflow");
352            return;
353        };
354        let Some(capacity) = memory.size().checked_mul(STABLE_PAGE_SIZE) else {
355            debug_assert!(false, "{operation} capacity overflow");
356            return;
357        };
358        debug_assert!(
359            end <= capacity,
360            "{operation} requires preallocated capacity: offset={offset}, len={len}, capacity={capacity}"
361        );
362    }
363    #[cfg(not(debug_assertions))]
364    {
365        let _ = (memory, offset, len, operation);
366    }
367}
368
369#[allow(dead_code)]
370pub fn reset_for_tests() {
371    clear_initialization();
372    #[cfg(any(test, feature = "canister-api-test-failpoints"))]
373    clear_failpoint();
374}
375
376#[allow(dead_code)]
377pub fn set_next_context_id_for_tests(value: u64) {
378    NEXT_CONTEXT_ID.with(|next| next.set(value));
379}
380
381#[allow(dead_code)]
382pub(crate) fn clear_initialization() {
383    DB_MEMORY.with(|memory| memory.borrow_mut().clear());
384    REGISTERED_MEMORY.with(|memory| memory.borrow_mut().clear());
385    DEFAULT_CONTEXT.with(|context| context.set(None));
386    CURRENT_CONTEXT.with(|context| context.set(None));
387    NEXT_CONTEXT_ID.with(|next| next.set(1));
388    #[cfg(any(test, feature = "canister-api-test-failpoints"))]
389    clear_failpoint();
390    crate::stable::meta::clear_superblock_cache();
391}
392
393pub(crate) fn clear_failed_initialization(context: ContextId) {
394    DB_MEMORY.with(|memory| {
395        memory
396            .borrow_mut()
397            .retain(|(stored_context, _)| *stored_context != context);
398    });
399    REGISTERED_MEMORY.with(|memory| {
400        memory
401            .borrow_mut()
402            .retain(|(stored_context, _)| *stored_context != context);
403    });
404    DEFAULT_CONTEXT.with(|default| {
405        if default.get() == Some(context) {
406            default.set(None);
407        }
408    });
409    CURRENT_CONTEXT.with(|current| {
410        if current.get() == Some(context) {
411            current.set(None);
412        }
413    });
414    #[cfg(any(test, feature = "canister-api-test-failpoints"))]
415    FAILPOINTS.with(|slot| {
416        slot.borrow_mut().remove(&context);
417    });
418    crate::stable::meta::clear_superblock_cache();
419}
420
421#[allow(dead_code)]
422pub fn snapshot_for_tests() -> Vec<u8> {
423    let len = usize::try_from(size_pages().saturating_mul(STABLE_PAGE_SIZE))
424        .expect("test memory size fits usize");
425    let mut out = vec![0_u8; len];
426    read(0, &mut out).expect("test memory snapshot succeeds");
427    out
428}
429
430#[allow(dead_code)]
431pub fn restore_for_tests(snapshot: Vec<u8>) -> DbMemory {
432    reset_for_tests();
433    let memory = memory_for_tests();
434    let pages = u64::try_from(snapshot.len())
435        .expect("snapshot len fits u64")
436        .div_ceil(STABLE_PAGE_SIZE);
437    if pages > 0 {
438        assert!(memory.grow(pages) >= 0, "snapshot memory grows");
439        memory.write(0, &snapshot);
440    }
441    crate::stable::meta::clear_superblock_cache();
442    memory
443}
444
445#[allow(dead_code)]
446pub fn memory_for_tests() -> DbMemory {
447    MemoryManager::init(DefaultMemoryImpl::default()).get(MemoryId::new(42))
448}
449
450#[inline(always)]
451fn with_memory<T>(f: impl FnOnce(&DbMemory) -> T) -> Result<T, StableMemoryError> {
452    let context = active_context_id()?;
453    DB_MEMORY.with(|slot| {
454        let slot = slot.borrow();
455        if let Some((stored_context, memory)) = slot.first() {
456            if *stored_context == context {
457                return Ok(f(memory));
458            }
459        }
460        for (stored_context, memory) in slot.iter().skip(1) {
461            if *stored_context == context {
462                return Ok(f(memory));
463            }
464        }
465        Err(StableMemoryError::NotInitialized)
466    })
467}
468
469struct ContextGuard {
470    previous: Option<ContextId>,
471}
472
473impl Drop for ContextGuard {
474    fn drop(&mut self) {
475        CURRENT_CONTEXT.with(|current| current.set(self.previous));
476    }
477}
478
479#[cfg(any(test, feature = "canister-api-test-failpoints"))]
480#[derive(Clone, Copy, Debug)]
481struct MemoryFailpointState {
482    failpoint: Option<MemoryFailpoint>,
483    grow_count: u64,
484    write_count: u64,
485}
486
487fn checked_end(offset: u64, len: usize) -> Result<u64, StableMemoryError> {
488    let len = u64::try_from(len).map_err(|_| StableMemoryError::OffsetOverflow)?;
489    offset
490        .checked_add(len)
491        .ok_or(StableMemoryError::OffsetOverflow)
492}
493
494#[cfg(any(test, feature = "canister-api-test-failpoints"))]
495fn hit_grow_failpoint() -> bool {
496    let Ok(context) = active_context_id() else {
497        return false;
498    };
499    FAILPOINTS.with(|slot| {
500        let mut slot = slot.borrow_mut();
501        let Some(state) = slot.get_mut(&context) else {
502            return false;
503        };
504        state.grow_count += 1;
505        if state.failpoint
506            == Some(MemoryFailpoint::GrowFailed {
507                ordinal: state.grow_count,
508            })
509        {
510            state.failpoint = None;
511            true
512        } else {
513            false
514        }
515    })
516}
517
518#[cfg(any(test, feature = "canister-api-test-failpoints"))]
519fn hit_write_trap_failpoint() -> bool {
520    let Ok(context) = active_context_id() else {
521        return false;
522    };
523    FAILPOINTS.with(|slot| {
524        let mut slot = slot.borrow_mut();
525        let Some(state) = slot.get_mut(&context) else {
526            return false;
527        };
528        state.write_count += 1;
529        if state.failpoint
530            == Some(MemoryFailpoint::TrapAfterWrite {
531                ordinal: state.write_count,
532            })
533        {
534            state.failpoint = None;
535            true
536        } else {
537            false
538        }
539    })
540}
541
542#[cfg(all(target_arch = "wasm32", feature = "canister-api-test-failpoints"))]
543fn fail_after_stable_write() -> ! {
544    ic_cdk::trap("stable write failpoint");
545}
546
547#[cfg(all(
548    any(test, feature = "canister-api-test-failpoints"),
549    not(all(target_arch = "wasm32", feature = "canister-api-test-failpoints"))
550))]
551fn fail_after_stable_write() -> ! {
552    panic!("stable write failpoint");
553}