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