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