mod admission;
#[cfg(test)]
mod admission_tests;
mod adoption;
#[cfg(test)]
mod adoption_tests;
mod allocations;
mod backing;
mod config;
mod diagnostics;
mod error;
#[cfg(any(test, feature = "test-support"))]
#[path = "../testing/diagnostics.rs"]
pub mod fixture_diagnostics;
mod layout;
mod policy;
#[cfg(test)]
mod allocation_tests;
#[cfg(test)]
mod growth_tests;
#[cfg(test)]
#[expect(
unsafe_code,
reason = "exercise raw reads with valid uninitialized destinations"
)]
mod read_tests;
#[cfg(test)]
mod request_tests;
#[cfg(test)]
mod tests;
pub use admission::{BootstrapAdmission, BootstrapAdmissionError};
pub use adoption::RuntimeAdoptionError;
pub use allocations::{
MemoryAllocation, MemoryAllocationSummary, MemoryAllocations, MemoryBindingSummary,
};
pub use backing::RuntimeMemory;
pub use config::MemoryManagerConfig;
pub use error::{
MemoryResolutionError, RuntimeBootstrapError, RuntimeConstructionError, RuntimeDiagnosticError,
RuntimeGrowError, RuntimeOpenError,
};
pub use layout::MemoryManagerLayoutError;
pub use policy::GenericAllocationPolicy;
use crate::{
LedgerCommitStore, PolicyIdentity, RuntimeBootstrapPolicy, STABLE_CELL_VALUE_OFFSET,
StableCellLedgerError,
requests::SealedDeclarationSnapshot,
slot::MEMORY_MANAGER_LEDGER_ID,
stable_cell::{EncodedLedgerRecord, decode_stable_cell_commit_store_from_memory},
};
use ic_stable_structures::{
Cell, Memory,
memory_manager::{MemoryId, MemoryManager},
};
use std::rc::Rc;
enum RuntimeLifecycle {
Unbootstrapped,
Bootstrapped {
allocations: Vec<crate::AllocationDeclaration>,
generation: u64,
binding: RuntimeBootstrapBinding,
},
}
struct RuntimeBootstrapBinding {
source: SealedDeclarationSnapshot,
policy_identity: PolicyIdentity,
pool: crate::MemoryAllocationPool,
}
pub struct MemoryRuntime<M: Memory> {
memory_manager: MemoryManager<Rc<M>>,
growth: Rc<backing::GrowthState<M>>,
lifecycle: RuntimeLifecycle,
}
impl<M: Memory> MemoryRuntime<M> {
pub fn new(memory: M) -> Result<Self, RuntimeConstructionError> {
Self::construct(memory, None)
}
pub fn new_with_config(
memory: M,
config: MemoryManagerConfig,
) -> Result<Self, RuntimeConstructionError> {
Self::construct(memory, Some(config))
}
fn construct(
memory: M,
requested: Option<MemoryManagerConfig>,
) -> Result<Self, RuntimeConstructionError> {
if cfg!(target_endian = "big") {
return Err(MemoryManagerLayoutError::UnsupportedByteOrder.into());
}
let (bucket_size_pages, allocated_buckets) = if memory.size() == 0 {
if memory.grow(1) == -1 {
return Err(RuntimeGrowError::BackingRefused {
additional_pages: 1,
}
.into());
}
(requested.unwrap_or_default().bucket_size_pages(), 0)
} else {
let measured = layout::read(&memory)?;
let actual = measured.bucket_pages;
if let Some(config) = requested {
check_bucket_size(actual, config)?;
}
(actual, measured.allocated_buckets)
};
let backing = Rc::new(memory);
let growth = Rc::new(backing::GrowthState {
backing: Rc::clone(&backing),
bucket_size_pages,
allocated_buckets: std::cell::RefCell::new(allocated_buckets),
open_slots: [const { std::cell::Cell::new(0) }; 4],
});
Ok(Self {
memory_manager: MemoryManager::init_with_bucket_size(
Rc::clone(&backing),
bucket_size_pages,
),
growth,
lifecycle: RuntimeLifecycle::Unbootstrapped,
})
}
#[must_use]
pub fn memory_manager_config(&self) -> MemoryManagerConfig {
MemoryManagerConfig::from_validated(self.growth.bucket_size_pages)
}
#[must_use]
pub const fn is_bootstrapped(&self) -> bool {
matches!(self.lifecycle, RuntimeLifecycle::Bootstrapped { .. })
}
pub fn bootstrap<P: RuntimeBootstrapPolicy>(
&mut self,
declarations: &SealedDeclarationSnapshot,
pool: &crate::MemoryAllocationPool,
policy: &P,
) -> Result<u64, RuntimeBootstrapError<P::Error>> {
match &self.lifecycle {
RuntimeLifecycle::Unbootstrapped => {
self.bootstrap_unbootstrapped(
declarations,
pool,
policy,
policy.runtime_bootstrap_identity(),
)?;
}
RuntimeLifecycle::Bootstrapped { .. } => {
return self.verify_bootstrap(declarations, pool, policy);
}
}
match &self.lifecycle {
RuntimeLifecycle::Bootstrapped { generation, .. } => Ok(*generation),
RuntimeLifecycle::Unbootstrapped => {
unreachable!("successful bootstrap publishes committed allocations")
}
}
}
pub fn verify_bootstrap<P: RuntimeBootstrapPolicy>(
&self,
declarations: &SealedDeclarationSnapshot,
pool: &crate::MemoryAllocationPool,
policy: &P,
) -> Result<u64, RuntimeBootstrapError<P::Error>> {
let RuntimeLifecycle::Bootstrapped {
binding,
generation,
..
} = &self.lifecycle
else {
return Err(RuntimeBootstrapError::NotBootstrapped);
};
binding.validate(declarations, pool, &policy.runtime_bootstrap_identity())?;
Ok(*generation)
}
fn validate_pool_custody(
&self,
occupied: &[bool; crate::constants::MAX_ALLOCATIONS],
pool: &crate::MemoryAllocationPool,
) -> Result<(), super::MemoryResolutionError> {
for id in 0..crate::MEMORY_MANAGER_INVALID_ID {
if pool.contains(id) && self.memory_size_pages(id) != 0 && !occupied[usize::from(id)] {
return Err(super::MemoryResolutionError::UnmanagedAllocation { id });
}
}
Ok(())
}
fn bootstrap_unbootstrapped<P: RuntimeBootstrapPolicy>(
&mut self,
declarations: &SealedDeclarationSnapshot,
pool: &crate::MemoryAllocationPool,
policy: &P,
policy_identity: PolicyIdentity,
) -> Result<(), RuntimeBootstrapError<P::Error>> {
let mut memory = self
.memory(MEMORY_MANAGER_LEDGER_ID)
.expect("governance memory is not publicly leasable");
let mut record = decode_stable_cell_commit_store_from_memory(&memory)?;
let recovered = record.recover_or_genesis()?;
let occupied = recovered.ledger().occupied_slots();
self.validate_pool_custody(&occupied, pool)?;
if memory.size() == 0 {
memory = write_ledger_cell(memory, &LedgerCommitStore::default())?;
}
let mut admission = BootstrapAdmission::new(recovered.ledger(), declarations, pool);
let preparation = policy.prepare_bootstrap(&mut admission);
let historical = admission.complete()?;
preparation.map_err(RuntimeBootstrapError::AdmissionPolicy)?;
let resolved = declarations.resolve(&recovered, &occupied, historical, pool)?;
let (generation, resolved) = {
let (staged, resolved) = crate::ledger::stage_generation(recovered, resolved, policy)?;
record.commit_generation(&staged)?;
(staged.current_generation(), resolved)
};
drop(write_ledger_cell(memory, &record)?);
let mut allocations = resolved;
allocations.retain(|declaration| {
!crate::is_ic_memory_stable_key(declaration.stable_key().as_str())
});
self.lifecycle = RuntimeLifecycle::Bootstrapped {
allocations,
generation,
binding: RuntimeBootstrapBinding {
source: declarations.clone(),
policy_identity,
pool: pool.clone(),
},
};
Ok(())
}
pub fn committed_allocations(
&self,
) -> Result<&[crate::AllocationDeclaration], RuntimeOpenError> {
match &self.lifecycle {
RuntimeLifecycle::Unbootstrapped => Err(RuntimeOpenError::NotBootstrapped),
RuntimeLifecycle::Bootstrapped { allocations, .. } => Ok(allocations),
}
}
pub const fn committed_generation(&self) -> Result<u64, RuntimeOpenError> {
match &self.lifecycle {
RuntimeLifecycle::Unbootstrapped => Err(RuntimeOpenError::NotBootstrapped),
RuntimeLifecycle::Bootstrapped { generation, .. } => Ok(*generation),
}
}
pub const fn allocation_pool(&self) -> Result<&crate::MemoryAllocationPool, RuntimeOpenError> {
match &self.lifecycle {
RuntimeLifecycle::Unbootstrapped => Err(RuntimeOpenError::NotBootstrapped),
RuntimeLifecycle::Bootstrapped { binding, .. } => Ok(&binding.pool),
}
}
pub fn open_memory(&self, stable_key: &str) -> Result<RuntimeMemory<M>, RuntimeOpenError> {
self.memory(self.memory_id(stable_key)?)
}
pub fn memory_id(&self, stable_key: &str) -> Result<u8, RuntimeOpenError> {
crate::key::validate(stable_key)?;
if crate::is_ic_memory_stable_key(stable_key) {
return Err(RuntimeOpenError::ReservedStableKey {
stable_key: stable_key.to_string(),
});
}
let declaration =
crate::declaration::declaration_for_key(self.committed_allocations()?, stable_key)
.ok_or_else(|| RuntimeOpenError::StableKeyNotCommitted(stable_key.to_string()))?;
Ok(declaration.slot().id())
}
fn memory(&self, id: u8) -> Result<RuntimeMemory<M>, RuntimeOpenError> {
let word = &self.growth.open_slots[usize::from(id / 64)];
let mask = 1_u64 << (id % 64);
if word.get() & mask != 0 {
return Err(RuntimeOpenError::AlreadyOpen { id });
}
let memory = self.memory_manager.get(MemoryId::new(id));
word.set(word.get() | mask);
Ok(RuntimeMemory {
memory,
growth: Rc::clone(&self.growth),
id,
})
}
fn memory_size_pages(&self, id: u8) -> u64 {
self.memory_manager.get(MemoryId::new(id)).size()
}
fn ledger_record_from_memory(&self) -> Result<LedgerCommitStore, StableCellLedgerError> {
let memory = self
.memory_manager
.get(MemoryId::new(MEMORY_MANAGER_LEDGER_ID));
decode_stable_cell_commit_store_from_memory(&memory)
}
}
impl RuntimeBootstrapBinding {
fn validate<P>(
&self,
declarations: &SealedDeclarationSnapshot,
pool: &crate::MemoryAllocationPool,
policy_identity: &PolicyIdentity,
) -> Result<(), RuntimeBootstrapError<P>> {
if &self.source != declarations {
return Err(RuntimeBootstrapError::DeclarationSnapshotMismatch);
}
if &self.pool != pool {
return Err(RuntimeBootstrapError::AllocationPoolMismatch);
}
if &self.policy_identity != policy_identity {
return Err(RuntimeBootstrapError::PolicyIdentityMismatch {
established: self.policy_identity,
requested: *policy_identity,
});
}
Ok(())
}
}
fn write_ledger_cell<M: Memory, P>(
memory: RuntimeMemory<M>,
record: &LedgerCommitStore,
) -> Result<RuntimeMemory<M>, RuntimeBootstrapError<P>> {
let encoded = EncodedLedgerRecord::new(record).map_err(|value_size| {
RuntimeBootstrapError::StableCellLedgerWriteTooLarge { value_size }
})?;
let value_size = encoded.len();
let required_bytes = STABLE_CELL_VALUE_OFFSET + value_size as u64;
let available_bytes = memory.size().saturating_mul(crate::WASM_PAGE_SIZE_BYTES);
if required_bytes > available_bytes {
let grow_by = (required_bytes - available_bytes).div_ceil(crate::WASM_PAGE_SIZE_BYTES);
memory.grow(grow_by)?;
}
Ok(Cell::new(memory, encoded).into_memory())
}
const fn check_bucket_size(
actual: u16,
requested: MemoryManagerConfig,
) -> Result<(), RuntimeConstructionError> {
if actual != requested.bucket_size_pages() {
return Err(RuntimeConstructionError::BucketSizeMismatch {
persisted: actual,
requested: requested.bucket_size_pages(),
});
}
Ok(())
}