Skip to main content

ic_memory/runtime/
mod.rs

1mod default;
2mod diagnostics;
3mod error;
4mod policy;
5
6#[cfg(test)]
7mod tests;
8
9pub use default::{
10    bootstrap_default_memory_manager, bootstrap_default_memory_manager_with_policy,
11    committed_allocations, default_memory_manager_commit_recovery_diagnostic,
12    default_memory_manager_diagnostic_export, default_memory_manager_doctor_report,
13    is_default_memory_manager_bootstrapped, open_default_memory_manager_memory,
14};
15pub use error::{
16    RuntimeBootstrapError, RuntimeConstructionError, RuntimeDiagnosticError, RuntimeOpenError,
17    RuntimePolicyError, RuntimeStateError,
18};
19
20use self::policy::{RuntimeMemoryManagerPolicy, runtime_bootstrap_error_from_bootstrap};
21use crate::{
22    AllocationBootstrap, AllocationHistory, AllocationLedger, AllocationPolicy,
23    CommittedAllocations, RuntimeBootstrapPolicy, STABLE_CELL_VALUE_OFFSET, StableCellLedgerError,
24    StableCellLedgerRecord, StableKey, registry::SealedDeclarationSnapshot,
25    slot::MEMORY_MANAGER_LEDGER_ID, stable_cell::decode_stable_cell_ledger_record_from_memory,
26};
27use ic_stable_structures::{
28    Cell, Memory, Storable,
29    memory_manager::{MemoryId, MemoryManager, VirtualMemory},
30};
31
32type LedgerCell<M> = Cell<StableCellLedgerRecord, VirtualMemory<M>>;
33
34// `ic-stable-structures` 0.7.2 documents this four-byte prefix for its V1
35// `MemoryManager` layout but does not expose a fallible constructor or these
36// constants. Keep this preflight coupled to the pinned dependency version.
37const MEMORY_MANAGER_MAGIC: [u8; 3] = *b"MGR";
38const MEMORY_MANAGER_LAYOUT_VERSION: u8 = 1;
39
40enum RuntimeLifecycle {
41    Unbootstrapped,
42    Bootstrapped {
43        committed_allocations: CommittedAllocations,
44        binding: RuntimeBootstrapBinding,
45    },
46}
47
48struct RuntimeBootstrapBinding {
49    declarations: SealedDeclarationSnapshot,
50    policy_identity: &'static str,
51}
52
53///
54/// MemoryRuntime
55///
56/// Canonical owner of allocation bootstrap state for one backing memory.
57///
58/// The runtime owns its `MemoryManager`, allocation-ledger cell, bootstrap
59/// lifecycle, committed allocation capability, opens, and diagnostics. Static
60/// linked-program declarations are supplied separately as one immutable
61/// [`SealedDeclarationSnapshot`].
62///
63/// `M` needs only [`Memory`]. The runtime does not require the backing memory
64/// to be `Send`, `Sync`, `Clone`, or `'static`.
65///
66
67pub struct MemoryRuntime<M: Memory> {
68    memory_manager: MemoryManager<M>,
69    ledger_cell: Option<LedgerCell<M>>,
70    lifecycle: RuntimeLifecycle,
71}
72
73impl<M: Memory> MemoryRuntime<M> {
74    /// Construct an unbootstrapped runtime without overwriting foreign memory.
75    ///
76    /// Empty backing memory is initialized as an
77    /// `ic_stable_structures::MemoryManager`. Nonempty memory must already
78    /// contain the current `MemoryManager` magic and layout version; otherwise
79    /// construction returns a typed error before `MemoryManager::init` can
80    /// write its header or allocation table. A pre-grown blank memory is
81    /// nonempty and is therefore rejected rather than assumed disposable.
82    ///
83    /// # Errors
84    ///
85    /// Returns [`RuntimeConstructionError::ForeignMemory`] for nonempty memory
86    /// without `MemoryManager` magic, or
87    /// [`RuntimeConstructionError::UnsupportedMemoryManagerVersion`] when the
88    /// magic is recognized but the layout version is not current.
89    pub fn new(memory: M) -> Result<Self, RuntimeConstructionError> {
90        preflight_memory_manager_backing(&memory)?;
91        Ok(Self {
92            memory_manager: MemoryManager::init(memory),
93            ledger_cell: None,
94            lifecycle: RuntimeLifecycle::Unbootstrapped,
95        })
96    }
97
98    /// Return whether this runtime has published committed allocation authority.
99    #[must_use]
100    pub const fn is_bootstrapped(&self) -> bool {
101        matches!(self.lifecycle, RuntimeLifecycle::Bootstrapped { .. })
102    }
103
104    /// Bootstrap this backing memory from one immutable declaration snapshot.
105    ///
106    /// Recovery, policy evaluation, staging, persistence, and capability
107    /// publication are local to this runtime. A repeated call is idempotent
108    /// only when the sealed declaration snapshot and
109    /// [`RuntimeBootstrapPolicy::runtime_bootstrap_identity`] match the
110    /// successful bootstrap. A mismatch returns a typed error without
111    /// advancing the durable generation or re-evaluating policy.
112    pub fn bootstrap<P: RuntimeBootstrapPolicy>(
113        &mut self,
114        declarations: &SealedDeclarationSnapshot,
115        policy: &P,
116    ) -> Result<&CommittedAllocations, RuntimeBootstrapError<P::Error>> {
117        let policy_identity = policy.runtime_bootstrap_identity();
118        if policy_identity.is_empty() {
119            return Err(RuntimeBootstrapError::EmptyPolicyIdentity);
120        }
121        let already_bootstrapped = match &self.lifecycle {
122            RuntimeLifecycle::Unbootstrapped => false,
123            RuntimeLifecycle::Bootstrapped { binding, .. } => {
124                binding.validate(declarations, policy_identity)?;
125                true
126            }
127        };
128        if !already_bootstrapped {
129            self.bootstrap_unbootstrapped(declarations, policy, policy_identity)?;
130        }
131        match &self.lifecycle {
132            RuntimeLifecycle::Bootstrapped {
133                committed_allocations,
134                ..
135            } => Ok(committed_allocations),
136            RuntimeLifecycle::Unbootstrapped => Err(RuntimeBootstrapError::State(
137                RuntimeStateError::InconsistentLifecycle,
138            )),
139        }
140    }
141
142    fn bootstrap_unbootstrapped<P: AllocationPolicy>(
143        &mut self,
144        declarations: &SealedDeclarationSnapshot,
145        policy: &P,
146        policy_identity: &'static str,
147    ) -> Result<(), RuntimeBootstrapError<P::Error>> {
148        self.initialize_ledger_cell()?;
149        let mut record = self
150            .ledger_cell
151            .as_ref()
152            .map(|cell| cell.get().clone())
153            .ok_or(RuntimeStateError::InconsistentLifecycle)?;
154        let runtime_policy = RuntimeMemoryManagerPolicy {
155            declarations,
156            custom_policy: policy,
157        };
158        let genesis = AllocationLedger::new(0, AllocationHistory::default())?;
159        let commit = AllocationBootstrap::new(record.store_mut())
160            .initialize_validate_and_commit(
161                &genesis,
162                declarations.allocation_snapshot().clone(),
163                &runtime_policy,
164                None,
165            )
166            .map_err(runtime_bootstrap_error_from_bootstrap)?;
167        let (ledger, validated) = commit.into_parts();
168
169        self.persist_ledger_record(record)?;
170        let committed =
171            external_runtime_allocations(validated.confirm_persisted(ledger.current_generation()));
172        self.lifecycle = RuntimeLifecycle::Bootstrapped {
173            committed_allocations: committed,
174            binding: RuntimeBootstrapBinding {
175                declarations: declarations.clone(),
176                policy_identity,
177            },
178        };
179        Ok(())
180    }
181
182    /// Borrow this runtime's committed allocation-open capability.
183    pub const fn committed_allocations(&self) -> Result<&CommittedAllocations, RuntimeOpenError> {
184        match &self.lifecycle {
185            RuntimeLifecycle::Unbootstrapped => Err(RuntimeOpenError::NotBootstrapped),
186            RuntimeLifecycle::Bootstrapped {
187                committed_allocations,
188                ..
189            } => Ok(committed_allocations),
190        }
191    }
192
193    /// Open this runtime's committed memory by stable key and expected ID.
194    pub fn open_memory(
195        &self,
196        stable_key: &str,
197        expected_id: u8,
198    ) -> Result<VirtualMemory<M>, RuntimeOpenError> {
199        let key = StableKey::parse(stable_key)?;
200        if crate::is_ic_memory_stable_key(key.as_str()) {
201            return Err(RuntimeOpenError::ReservedStableKey {
202                stable_key: stable_key.to_string(),
203            });
204        }
205        let committed = self.committed_allocations()?;
206        let slot = committed
207            .slot_for(&key)
208            .ok_or_else(|| RuntimeOpenError::StableKeyNotCommitted(stable_key.to_string()))?;
209        let committed_id = slot.memory_manager_id()?;
210        if committed_id != expected_id {
211            return Err(RuntimeOpenError::MemoryIdMismatch {
212                stable_key: stable_key.to_string(),
213                committed_id,
214                requested_id: expected_id,
215            });
216        }
217        Ok(self.memory(expected_id))
218    }
219
220    fn initialize_ledger_cell<P>(&mut self) -> Result<(), RuntimeBootstrapError<P>> {
221        if self.ledger_cell.is_some() {
222            return Ok(());
223        }
224        let memory = self.memory(MEMORY_MANAGER_LEDGER_ID);
225        crate::validate_stable_cell_ledger_memory(&memory)?;
226        ensure_ledger_cell_capacity(&memory, &StableCellLedgerRecord::default())?;
227        self.ledger_cell = Some(Cell::init(memory, StableCellLedgerRecord::default()));
228        Ok(())
229    }
230
231    fn persist_ledger_record<P>(
232        &mut self,
233        record: StableCellLedgerRecord,
234    ) -> Result<(), RuntimeBootstrapError<P>> {
235        let memory = self.memory(MEMORY_MANAGER_LEDGER_ID);
236        ensure_ledger_cell_capacity(&memory, &record)?;
237        let cell = self
238            .ledger_cell
239            .as_mut()
240            .ok_or(RuntimeStateError::InconsistentLifecycle)?;
241        let _previous = cell.set(record);
242        Ok(())
243    }
244
245    fn memory(&self, id: u8) -> VirtualMemory<M> {
246        self.memory_manager.get(MemoryId::new(id))
247    }
248
249    fn ledger_record_from_memory(&self) -> Result<StableCellLedgerRecord, StableCellLedgerError> {
250        decode_stable_cell_ledger_record_from_memory(&self.memory(MEMORY_MANAGER_LEDGER_ID))
251    }
252}
253
254fn preflight_memory_manager_backing<M: Memory>(memory: &M) -> Result<(), RuntimeConstructionError> {
255    if memory.size() == 0 {
256        return Ok(());
257    }
258
259    let mut prefix = [0_u8; 4];
260    memory.read(0, &mut prefix);
261    let observed_magic = [prefix[0], prefix[1], prefix[2]];
262    if observed_magic != MEMORY_MANAGER_MAGIC {
263        return Err(RuntimeConstructionError::ForeignMemory { observed_magic });
264    }
265    let observed = prefix[3];
266    if observed != MEMORY_MANAGER_LAYOUT_VERSION {
267        return Err(RuntimeConstructionError::UnsupportedMemoryManagerVersion {
268            observed,
269            supported: MEMORY_MANAGER_LAYOUT_VERSION,
270        });
271    }
272    Ok(())
273}
274
275impl RuntimeBootstrapBinding {
276    fn validate<P>(
277        &self,
278        declarations: &SealedDeclarationSnapshot,
279        policy_identity: &'static str,
280    ) -> Result<(), RuntimeBootstrapError<P>> {
281        if !self.declarations.shares_storage_with(declarations) {
282            return Err(RuntimeBootstrapError::DeclarationSnapshotMismatch);
283        }
284        if self.policy_identity != policy_identity {
285            return Err(RuntimeBootstrapError::PolicyIdentityMismatch {
286                established: self.policy_identity,
287                requested: policy_identity,
288            });
289        }
290        Ok(())
291    }
292}
293
294fn ensure_ledger_cell_capacity<M: Memory, P>(
295    memory: &VirtualMemory<M>,
296    record: &StableCellLedgerRecord,
297) -> Result<(), RuntimeBootstrapError<P>> {
298    let value_size = record.to_bytes().len();
299    let value_size_u32 = u32::try_from(value_size)
300        .map_err(|_| RuntimeBootstrapError::StableCellLedgerWriteTooLarge { value_size })?;
301    let required_bytes = STABLE_CELL_VALUE_OFFSET
302        .checked_add(u64::from(value_size_u32))
303        .ok_or(RuntimeBootstrapError::StableCellLedgerWriteTooLarge { value_size })?;
304    let available_bytes = memory.size().saturating_mul(crate::WASM_PAGE_SIZE_BYTES);
305    if required_bytes <= available_bytes {
306        return Ok(());
307    }
308    let grow_by = required_bytes
309        .saturating_sub(available_bytes)
310        .div_ceil(crate::WASM_PAGE_SIZE_BYTES);
311    if memory.grow(grow_by) < 0 {
312        return Err(RuntimeBootstrapError::StableCellLedgerWriteTooLarge { value_size });
313    }
314    Ok(())
315}
316
317fn external_runtime_allocations(committed: CommittedAllocations) -> CommittedAllocations {
318    committed.without_stable_key_prefix(crate::IC_MEMORY_STABLE_KEY_PREFIX)
319}