mod admission;
#[cfg(test)]
mod admission_tests;
mod adoption;
#[cfg(test)]
mod adoption_tests;
mod allocations;
mod backing;
mod config;
mod default;
mod diagnostics;
mod error;
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, RecoveredAllocationMetadata};
pub use adoption::RuntimeAdoptionError;
pub use allocations::{
AllocationBinding, AllocationRangeClaim, MemoryAllocation, MemoryAllocationSummary,
MemoryAllocations, MemoryBindingSummary,
};
pub use backing::RuntimeMemory;
pub use config::MemoryManagerConfig;
pub use default::{
bootstrap_default_memory_manager, bootstrap_default_memory_manager_with_config,
bootstrap_default_memory_manager_with_policy, committed_allocations,
default_memory_manager_commit_recovery_diagnostic, default_memory_manager_diagnostic_export,
default_memory_manager_doctor_report, default_memory_manager_doctor_report_with_policy,
default_memory_manager_memory_allocation_summary, default_memory_manager_memory_allocations,
default_memory_manager_memory_id, is_default_memory_manager_bootstrapped,
open_default_memory_manager_memory, open_default_memory_manager_memory_by_key,
verify_default_memory_manager_authority,
};
pub use error::{
MemoryResolutionError, RuntimeBootstrapError, RuntimeConstructionError, RuntimeDiagnosticError,
RuntimeGrowError, RuntimeOpenError, RuntimePolicyError, RuntimeStateError,
};
pub use layout::MemoryManagerLayoutError;
pub use policy::GenericRangePolicy;
use self::policy::{RuntimeMemoryManagerPolicy, runtime_bootstrap_error_from_bootstrap};
use crate::{
AllocationBootstrap, AllocationLedger, CommittedAllocations, PolicyIdentity,
RuntimeBootstrapPolicy, STABLE_CELL_VALUE_OFFSET, StableCellLedgerError,
StableCellLedgerRecord, registry::SealedDeclarationSnapshot, slot::MEMORY_MANAGER_LEDGER_ID,
stable_cell::decode_stable_cell_ledger_record_from_memory,
};
use ic_stable_structures::{
Cell, Memory,
memory_manager::{MemoryId, MemoryManager},
};
use std::rc::Rc;
enum RuntimeLifecycle {
Unbootstrapped,
Bootstrapped {
committed_allocations: CommittedAllocations,
binding: RuntimeBootstrapBinding,
},
}
struct RuntimeBootstrapBinding {
source: SealedDeclarationSnapshot,
declarations: SealedDeclarationSnapshot,
policy_identity: PolicyIdentity,
}
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),
});
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,
policy: &P,
) -> Result<&CommittedAllocations, RuntimeBootstrapError<P::Error>> {
let policy_identity = policy.runtime_bootstrap_identity()?;
match &self.lifecycle {
RuntimeLifecycle::Unbootstrapped => {
self.bootstrap_unbootstrapped(declarations, policy, policy_identity)?;
}
RuntimeLifecycle::Bootstrapped { binding, .. } => {
binding.validate(declarations, &policy_identity)?;
}
}
match &self.lifecycle {
RuntimeLifecycle::Bootstrapped {
committed_allocations,
..
} => Ok(committed_allocations),
RuntimeLifecycle::Unbootstrapped => {
unreachable!("successful bootstrap publishes committed allocations")
}
}
}
fn bootstrap_unbootstrapped<P: RuntimeBootstrapPolicy>(
&mut self,
declarations: &SealedDeclarationSnapshot,
policy: &P,
policy_identity: PolicyIdentity,
) -> Result<(), RuntimeBootstrapError<P::Error>> {
let memory = self.memory(MEMORY_MANAGER_LEDGER_ID);
let mut record = decode_stable_cell_ledger_record_from_memory(&memory)?;
if memory.size() == 0 {
ensure_ledger_cell_capacity(&memory, &record)?;
drop(Cell::new(memory.clone(), StableCellLedgerRecord::default()));
}
let genesis = AllocationLedger::empty_genesis();
let recovered = record.store_mut().recover_or_initialize(&genesis)?;
let mut admission = BootstrapAdmission::new(recovered.ledger(), declarations);
let preparation = policy.prepare_bootstrap(&mut admission);
let historical = admission.complete()?;
preparation.map_err(RuntimeBootstrapError::AdmissionPolicy)?;
let resolved = declarations.resolve(recovered.ledger(), historical)?;
let runtime_policy = RuntimeMemoryManagerPolicy {
declarations: &resolved,
custom_policy: policy,
};
let commit = AllocationBootstrap::new(record.store_mut())
.validate_against(
recovered,
resolved.allocation_snapshot().clone(),
&runtime_policy,
None,
)
.map_err(runtime_bootstrap_error_from_bootstrap)?;
ensure_ledger_cell_capacity(&memory, &record)?;
drop(Cell::new(memory, record));
let committed = commit.confirm_persisted().into_application_allocations();
self.lifecycle = RuntimeLifecycle::Bootstrapped {
committed_allocations: committed,
binding: RuntimeBootstrapBinding {
source: declarations.clone(),
declarations: resolved,
policy_identity,
},
};
Ok(())
}
pub const fn committed_allocations(&self) -> Result<&CommittedAllocations, RuntimeOpenError> {
match &self.lifecycle {
RuntimeLifecycle::Unbootstrapped => Err(RuntimeOpenError::NotBootstrapped),
RuntimeLifecycle::Bootstrapped {
committed_allocations,
..
} => Ok(committed_allocations),
}
}
pub fn open_memory_by_key(
&self,
stable_key: &str,
) -> Result<RuntimeMemory<M>, RuntimeOpenError> {
Ok(self.memory(self.memory_id(stable_key)?))
}
pub fn open_memory(
&self,
stable_key: &str,
expected_id: u8,
) -> Result<RuntimeMemory<M>, RuntimeOpenError> {
let committed_id = self.memory_id(stable_key)?;
if committed_id != expected_id {
return Err(RuntimeOpenError::MemoryIdMismatch {
stable_key: stable_key.to_string(),
committed_id,
requested_id: expected_id,
});
}
Ok(self.memory(committed_id))
}
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 slot = crate::capability::slot_for_key(
self.committed_allocations()?.declarations(),
stable_key,
)
.ok_or_else(|| RuntimeOpenError::StableKeyNotCommitted(stable_key.to_string()))?;
Ok(slot.id())
}
fn memory(&self, id: u8) -> RuntimeMemory<M> {
RuntimeMemory {
memory: self.memory_manager.get(MemoryId::new(id)),
growth: Rc::clone(&self.growth),
}
}
fn memory_size_pages(&self, id: u8) -> u64 {
self.memory_manager.get(MemoryId::new(id)).size()
}
fn ledger_record_from_memory(&self) -> Result<StableCellLedgerRecord, StableCellLedgerError> {
decode_stable_cell_ledger_record_from_memory(&self.memory(MEMORY_MANAGER_LEDGER_ID))
}
}
impl RuntimeBootstrapBinding {
fn validate<P>(
&self,
declarations: &SealedDeclarationSnapshot,
policy_identity: &PolicyIdentity,
) -> Result<(), RuntimeBootstrapError<P>> {
if &self.source != declarations {
return Err(RuntimeBootstrapError::DeclarationSnapshotMismatch);
}
if &self.policy_identity != policy_identity {
return Err(RuntimeBootstrapError::PolicyIdentityMismatch {
established: self.policy_identity.clone(),
requested: policy_identity.clone(),
});
}
Ok(())
}
}
fn ensure_ledger_cell_capacity<M: Memory, P>(
memory: &RuntimeMemory<M>,
record: &StableCellLedgerRecord,
) -> Result<(), RuntimeBootstrapError<P>> {
let value_size = record.encoded_size();
if value_size > crate::constants::MAX_LEDGER_RECORD_BYTES {
return Err(RuntimeBootstrapError::StableCellLedgerWriteTooLarge { value_size });
}
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 {
return Ok(());
}
let grow_by = (required_bytes - available_bytes).div_ceil(crate::WASM_PAGE_SIZE_BYTES);
memory.grow(grow_by)?;
Ok(())
}
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(())
}