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