Skip to main content

ic_memory/runtime/
mod.rs

1mod admission;
2#[cfg(test)]
3mod admission_tests;
4mod adoption;
5#[cfg(test)]
6mod adoption_tests;
7mod allocations;
8mod backing;
9mod config;
10mod custody;
11mod diagnostics;
12mod error;
13#[cfg(any(test, feature = "test-support"))]
14#[path = "../testing/diagnostics.rs"]
15pub mod fixture_diagnostics;
16#[cfg(test)]
17mod fixtures;
18mod layout;
19mod policy;
20
21#[cfg(all(test, not(target_arch = "wasm32")))]
22mod allocation_tests;
23#[cfg(test)]
24mod custody_tests;
25#[cfg(test)]
26mod growth_tests;
27#[cfg(test)]
28#[expect(
29    unsafe_code,
30    reason = "exercise raw reads with valid uninitialized destinations"
31)]
32mod read_tests;
33#[cfg(test)]
34mod request_tests;
35#[cfg(test)]
36mod tests;
37
38pub use admission::{BootstrapAdmission, BootstrapAdmissionError};
39pub use adoption::RuntimeAdoptionError;
40
41pub use allocations::{
42    MemoryAllocation, MemoryAllocationSummary, MemoryAllocations, MemoryBindingSummary,
43};
44pub use backing::RuntimeMemory;
45pub use config::MemoryManagerConfig;
46pub use custody::{BackingIdentity, MemoryBacking};
47pub use error::{
48    MemoryResolutionError, RuntimeBootstrapError, RuntimeConstructionError, RuntimeDiagnosticError,
49    RuntimeGrowError, RuntimeOpenError,
50};
51pub use layout::MemoryManagerLayoutError;
52pub use policy::GenericAllocationPolicy;
53
54use crate::{
55    LedgerCommitStore, PolicyIdentity, RuntimeBootstrapPolicy, STABLE_CELL_VALUE_OFFSET,
56    StableCellLedgerError,
57    requests::SealedDeclarationSnapshot,
58    slot::MEMORY_MANAGER_LEDGER_ID,
59    stable_cell::{EncodedLedgerRecord, decode_stable_cell_commit_store_from_memory},
60};
61use ic_platform::stable_structures::{
62    Cell, Memory,
63    memory_manager::{MemoryId, MemoryManager},
64};
65
66use std::rc::Rc;
67
68enum RuntimeLifecycle {
69    Unbootstrapped,
70    Bootstrapped {
71        allocations: Vec<crate::AllocationDeclaration>,
72        generation: u64,
73        binding: RuntimeBootstrapBinding,
74    },
75}
76
77struct RuntimeBootstrapBinding {
78    source: SealedDeclarationSnapshot,
79    policy_identity: PolicyIdentity,
80    pool: crate::MemoryAllocationPool,
81}
82
83///
84/// MemoryRuntime
85///
86/// Canonical owner of allocation bootstrap state for one backing memory.
87///
88/// The runtime owns its `MemoryManager`, allocation-ledger persistence, bootstrap
89/// lifecycle, committed allocation capability, opens, and diagnostics. Static
90/// linked-program declarations are supplied separately as one immutable
91/// [`SealedDeclarationSnapshot`].
92///
93/// Each bootstrap attempt fallibly decodes its ledger record from persisted
94/// memory. Capacity-checked writes use `Cell`; diagnostics also read persisted
95/// memory directly.
96///
97/// Construction requires [`MemoryBacking`], which identifies physical aliases.
98/// The backing need not be `Send`, `Sync`, `Clone`, or `'static`.
99///
100/// A backing claim rejects another runtime before backing IO while this runtime
101/// or any detached lease lives. Drop them all before constructing a replacement.
102/// Standard VectorMemory/default canister backings identify aliases automatically;
103/// custom hosts must uphold [`MemoryBacking`]'s identity contract. A composition
104/// selects one IC Memory crate identity and shares one runtime. Raw aliased IO or
105/// unmanaged managers remain prohibited. Bounded metadata checks refuse stale
106/// manager state; they are not transactions against arbitrary concurrent raw IO.
107///
108
109pub struct MemoryRuntime<M: Memory> {
110    memory_manager: MemoryManager<Rc<M>>,
111    // Shared backing, immutable geometry and live accounting belong to growth.
112    // Handles reserve physical capacity; the manager owns bucket metadata.
113    growth: Rc<backing::GrowthState<M>>,
114    lifecycle: RuntimeLifecycle,
115}
116
117impl<M: Memory> MemoryRuntime<M> {
118    /// Construct an unbootstrapped runtime without overwriting foreign memory.
119    ///
120    /// Empty backing memory is initialized as an
121    /// [`MemoryManager`]. Nonempty memory must pass bounded
122    /// validation of the current manager header, bucket table, and extents; otherwise
123    /// construction returns a typed error before the manager can
124    /// write its header or allocation table. A pre-grown blank memory is
125    /// nonempty and is therefore rejected rather than assumed disposable.
126    ///
127    /// # Errors
128    ///
129    /// Returns [`RuntimeConstructionError::BackingAlreadyClaimed`] before any
130    /// backing IO if another runtime or handle retains custody.
131    /// Returns [`RuntimeConstructionError::ForeignMemory`] for nonempty memory
132    /// without `MemoryManager` magic, or
133    /// [`RuntimeConstructionError::UnsupportedMemoryManagerVersion`] when the
134    /// magic is recognized but the layout version is not current. Invalid
135    /// metadata returns [`RuntimeConstructionError::Layout`]. Refused fresh
136    /// metadata growth returns [`RuntimeConstructionError::Growth`] without
137    /// writes, allowing the same backing memory to be retried. Reopening honors
138    /// the actual persisted bucket size; only fresh memory uses 128 pages.
139    pub fn new(memory: M) -> Result<Self, RuntimeConstructionError>
140    where
141        M: MemoryBacking,
142    {
143        Self::construct(memory, None)
144    }
145
146    /// Construct with an explicit immutable bucket policy. Existing memory must
147    /// match exactly; mismatches fail before manager initialization or writes.
148    ///
149    /// # Errors
150    ///
151    /// Returns the construction errors described by [`Self::new`], or
152    /// [`RuntimeConstructionError::BucketSizeMismatch`] when existing geometry
153    /// differs from `config`.
154    pub fn new_with_config(
155        memory: M,
156        config: MemoryManagerConfig,
157    ) -> Result<Self, RuntimeConstructionError>
158    where
159        M: MemoryBacking,
160    {
161        Self::construct(memory, Some(config))
162    }
163
164    fn construct(
165        memory: M,
166        requested: Option<MemoryManagerConfig>,
167    ) -> Result<Self, RuntimeConstructionError>
168    where
169        M: MemoryBacking,
170    {
171        let claim = custody::BackingClaim::acquire(&memory)?;
172        if cfg!(target_endian = "big") {
173            return Err(MemoryManagerLayoutError::UnsupportedByteOrder.into());
174        }
175        let bucket_size_pages = if memory.size() == 0 {
176            // Reserve the metadata page before the dependency writes its header.
177            // This fresh allocation is owned here; caller-supplied nonempty
178            // memory still passes the layout checks below before any writes.
179            if memory.grow(1) == -1 {
180                return Err(RuntimeGrowError::BackingRefused {
181                    additional_pages: 1,
182                }
183                .into());
184            }
185            requested.unwrap_or_default().bucket_size_pages()
186        } else {
187            let measured = layout::read(&memory)?;
188            let actual = measured.bucket_pages;
189            if let Some(config) = requested {
190                check_bucket_size(actual, config)?;
191            }
192            actual
193        };
194        let backing = Rc::new(memory);
195        let memory_manager =
196            MemoryManager::init_with_bucket_size(Rc::clone(&backing), bucket_size_pages);
197        let growth = Rc::new(backing::GrowthState {
198            backing: Rc::clone(&backing),
199            bucket_size_pages,
200            authority: std::cell::RefCell::new(layout::ManagerAuthority::capture(backing.as_ref())),
201            _claim: claim,
202            open_slots: [const { std::cell::Cell::new(0) }; 4],
203        });
204        Ok(Self {
205            memory_manager,
206            growth,
207            lifecycle: RuntimeLifecycle::Unbootstrapped,
208        })
209    }
210
211    /// Return the immutable bucket configuration bound to this runtime's manager.
212    #[must_use]
213    pub fn memory_manager_config(&self) -> MemoryManagerConfig {
214        // Construction has already validated the nonzero persisted setting.
215        MemoryManagerConfig::from_validated(self.growth.bucket_size_pages)
216    }
217
218    /// Return whether this runtime has published committed allocation authority.
219    #[must_use]
220    pub const fn is_bootstrapped(&self) -> bool {
221        matches!(self.lifecycle, RuntimeLifecycle::Bootstrapped { .. })
222    }
223
224    /// Bootstrap this backing memory from one immutable declaration snapshot.
225    ///
226    /// Recovery, metadata admission, logical resolution, policy evaluation,
227    /// staging, persistence and binding publication are local to this runtime.
228    /// A repeated call is idempotent only when the sealed declaration snapshot,
229    /// canonical host pool and [`RuntimeBootstrapPolicy::runtime_bootstrap_identity`] match the
230    /// successful bootstrap. A mismatch returns a typed error without
231    /// advancing the durable generation or re-evaluating policy.
232    /// Independently sealed snapshots match when their canonical contents are equal.
233    ///
234    /// # Panics
235    ///
236    /// Panics if a private runtime or encoding invariant is broken, or backing
237    /// memory or a policy callback panics.
238    pub fn bootstrap<P: RuntimeBootstrapPolicy>(
239        &mut self,
240        declarations: &SealedDeclarationSnapshot,
241        pool: &crate::MemoryAllocationPool,
242        policy: &P,
243    ) -> Result<u64, RuntimeBootstrapError<P::Error>> {
244        match &self.lifecycle {
245            RuntimeLifecycle::Unbootstrapped => self.bootstrap_unbootstrapped(
246                declarations,
247                pool,
248                policy,
249                policy.runtime_bootstrap_identity(),
250            ),
251            RuntimeLifecycle::Bootstrapped { .. } => {
252                self.verify_bootstrap(declarations, pool, policy)
253            }
254        }
255    }
256
257    /// Verify the exact host bootstrap binding through a shared runtime reference.
258    ///
259    /// Returns the established generation only when declarations, pool and the
260    /// policy identity match. Evaluates the policy identity once, but never runs
261    /// admission or allocation validation, reads backing memory, writes, grows,
262    /// or opens an allocation. An unbootstrapped runtime is refused rather than
263    /// initialized. Hosts may share one committed runtime through `Rc` without
264    /// reconstructing its binding checks or attempting mutable bootstrap again.
265    pub fn verify_bootstrap<P: RuntimeBootstrapPolicy>(
266        &self,
267        declarations: &SealedDeclarationSnapshot,
268        pool: &crate::MemoryAllocationPool,
269        policy: &P,
270    ) -> Result<u64, RuntimeBootstrapError<P::Error>> {
271        let RuntimeLifecycle::Bootstrapped {
272            binding,
273            generation,
274            ..
275        } = &self.lifecycle
276        else {
277            return Err(RuntimeBootstrapError::NotBootstrapped);
278        };
279        binding.validate(declarations, pool, &policy.runtime_bootstrap_identity())?;
280        Ok(*generation)
281    }
282
283    fn validate_pool_custody(
284        &self,
285        occupied: &[bool; crate::constants::MAX_ALLOCATIONS],
286        pool: &crate::MemoryAllocationPool,
287    ) -> Result<(), super::MemoryResolutionError> {
288        for id in 0..crate::MEMORY_MANAGER_INVALID_ID {
289            if pool.contains(id) && self.memory_size_pages(id) != 0 && !occupied[usize::from(id)] {
290                return Err(super::MemoryResolutionError::UnmanagedAllocation { id });
291            }
292        }
293        Ok(())
294    }
295
296    fn bootstrap_unbootstrapped<P: RuntimeBootstrapPolicy>(
297        &mut self,
298        declarations: &SealedDeclarationSnapshot,
299        pool: &crate::MemoryAllocationPool,
300        policy: &P,
301        policy_identity: PolicyIdentity,
302    ) -> Result<u64, RuntimeBootstrapError<P::Error>> {
303        self.growth.verify()?;
304        let mut memory = self
305            .memory(MEMORY_MANAGER_LEDGER_ID)
306            .expect("governance memory is not publicly leasable");
307        let mut record = decode_stable_cell_commit_store_from_memory(&memory)?;
308        let recovered = record.recover_or_genesis()?;
309        let occupied = recovered.ledger().occupied_slots();
310        self.validate_pool_custody(&occupied, pool)?;
311        if memory.size() == 0 {
312            // Empty memory decodes to an uninitialized record. Preserve fresh
313            // cell acquisition before admission without persisting genesis.
314            memory = write_ledger_cell(memory, &LedgerCommitStore::default())?;
315        }
316        let mut admission = BootstrapAdmission::new(recovered.ledger(), declarations, pool);
317        let preparation = policy.prepare_bootstrap(&mut admission);
318        let historical = admission.complete()?;
319        preparation.map_err(RuntimeBootstrapError::AdmissionPolicy)?;
320        let resolved = declarations.resolve(&recovered, &occupied, historical, pool)?;
321        let (generation, resolved) = {
322            let (staged, resolved) = crate::ledger::stage_generation(recovered, resolved, policy)?;
323            record.commit_generation(&staged)?;
324            (staged.current_generation(), resolved)
325        };
326        drop(write_ledger_cell(memory, &record)?);
327        // Publish application bindings only after the durable Cell write succeeds.
328        let mut allocations = resolved;
329        allocations.retain(|declaration| {
330            !crate::is_ic_memory_stable_key(declaration.stable_key().as_str())
331        });
332        self.lifecycle = RuntimeLifecycle::Bootstrapped {
333            allocations,
334            generation,
335            binding: RuntimeBootstrapBinding {
336                source: declarations.clone(),
337                policy_identity,
338                pool: pool.clone(),
339            },
340        };
341        Ok(generation)
342    }
343
344    /// Borrow this runtime's persisted application bindings without IO.
345    /// These passive views cannot open memory independently of the runtime.
346    pub fn committed_allocations(
347        &self,
348    ) -> Result<&[crate::AllocationDeclaration], RuntimeOpenError> {
349        match &self.lifecycle {
350            RuntimeLifecycle::Unbootstrapped => Err(RuntimeOpenError::NotBootstrapped),
351            RuntimeLifecycle::Bootstrapped { allocations, .. } => Ok(allocations),
352        }
353    }
354
355    /// Return the in-memory persisted generation without decoding the ledger.
356    pub const fn committed_generation(&self) -> Result<u64, RuntimeOpenError> {
357        match &self.lifecycle {
358            RuntimeLifecycle::Unbootstrapped => Err(RuntimeOpenError::NotBootstrapped),
359            RuntimeLifecycle::Bootstrapped { generation, .. } => Ok(*generation),
360        }
361    }
362
363    /// Borrow the host pool bound by successful bootstrap for host projections.
364    /// This is current policy, never evidence of historical ownership or free IDs.
365    pub const fn allocation_pool(&self) -> Result<&crate::MemoryAllocationPool, RuntimeOpenError> {
366        match &self.lifecycle {
367            RuntimeLifecycle::Unbootstrapped => Err(RuntimeOpenError::NotBootstrapped),
368            RuntimeLifecycle::Bootstrapped { binding, .. } => Ok(&binding.pool),
369        }
370    }
371
372    /// Lease by durable key using this runtime's persisted current bindings.
373    ///
374    /// A second live open returns [`RuntimeOpenError::AlreadyOpen`] without IO.
375    /// Dropping the handle, or a collection that owns it, permits reopening.
376    pub fn open_memory(&self, stable_key: &str) -> Result<RuntimeMemory<M>, RuntimeOpenError> {
377        self.memory(self.memory_id(stable_key)?)
378    }
379
380    /// Resolve an application key's committed ID without opening memory,
381    /// reading history, or changing the host's policy or bucket configuration.
382    pub fn memory_id(&self, stable_key: &str) -> Result<u8, RuntimeOpenError> {
383        crate::key::validate(stable_key)?;
384        if crate::is_ic_memory_stable_key(stable_key) {
385            return Err(RuntimeOpenError::ReservedStableKey {
386                stable_key: stable_key.to_string(),
387            });
388        }
389        let declaration =
390            crate::declaration::declaration_for_key(self.committed_allocations()?, stable_key)
391                .ok_or_else(|| RuntimeOpenError::StableKeyNotCommitted(stable_key.to_string()))?;
392        Ok(declaration.slot().id())
393    }
394
395    fn memory(&self, id: u8) -> Result<RuntimeMemory<M>, RuntimeOpenError> {
396        let word = &self.growth.open_slots[usize::from(id / 64)];
397        let mask = 1_u64 << (id % 64);
398        if word.get() & mask != 0 {
399            return Err(RuntimeOpenError::AlreadyOpen { id });
400        }
401        self.growth.verify()?;
402        let memory = self.memory_manager.get(MemoryId::new(id));
403        word.set(word.get() | mask);
404        Ok(RuntimeMemory {
405            memory,
406            growth: Rc::clone(&self.growth),
407            id,
408        })
409    }
410
411    // Size-only observations need no handle carrying runtime growth authority.
412    fn memory_size_pages(&self, id: u8) -> u64 {
413        self.memory_manager.get(MemoryId::new(id)).size()
414    }
415
416    fn ledger_record_from_memory(&self) -> Result<LedgerCommitStore, StableCellLedgerError> {
417        // Decoding only reads; the manager handle retains backing lifetime
418        // without carrying the growth owner needed by writable runtime handles.
419        let memory = self
420            .memory_manager
421            .get(MemoryId::new(MEMORY_MANAGER_LEDGER_ID));
422        decode_stable_cell_commit_store_from_memory(&memory)
423    }
424}
425
426impl RuntimeBootstrapBinding {
427    fn validate<P>(
428        &self,
429        declarations: &SealedDeclarationSnapshot,
430        pool: &crate::MemoryAllocationPool,
431        policy_identity: &PolicyIdentity,
432    ) -> Result<(), RuntimeBootstrapError<P>> {
433        if &self.source != declarations {
434            return Err(RuntimeBootstrapError::DeclarationSnapshotMismatch);
435        }
436        if &self.pool != pool {
437            return Err(RuntimeBootstrapError::AllocationPoolMismatch);
438        }
439        if &self.policy_identity != policy_identity {
440            return Err(RuntimeBootstrapError::PolicyIdentityMismatch {
441                established: self.policy_identity,
442                requested: *policy_identity,
443            });
444        }
445        Ok(())
446    }
447}
448
449fn write_ledger_cell<M: Memory, P>(
450    memory: RuntimeMemory<M>,
451    record: &LedgerCommitStore,
452) -> Result<RuntimeMemory<M>, RuntimeBootstrapError<P>> {
453    let encoded = EncodedLedgerRecord::new(record).map_err(|value_size| {
454        RuntimeBootstrapError::StableCellLedgerWriteTooLarge { value_size }
455    })?;
456    let value_size = encoded.len();
457    // The record limit is below u32::MAX, including the eight-byte cell header.
458    let required_bytes = STABLE_CELL_VALUE_OFFSET + value_size as u64;
459    let available_bytes = memory.size().saturating_mul(crate::WASM_PAGE_SIZE_BYTES);
460    if required_bytes > available_bytes {
461        let grow_by = (required_bytes - available_bytes).div_ceil(crate::WASM_PAGE_SIZE_BYTES);
462        memory.grow(grow_by)?;
463    }
464    Ok(Cell::new(memory, encoded).into_memory())
465}
466
467const fn check_bucket_size(
468    actual: u16,
469    requested: MemoryManagerConfig,
470) -> Result<(), RuntimeConstructionError> {
471    if actual != requested.bucket_size_pages() {
472        return Err(RuntimeConstructionError::BucketSizeMismatch {
473            persisted: actual,
474            requested: requested.bucket_size_pages(),
475        });
476    }
477    Ok(())
478}