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