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;
12mod layout;
13mod policy;
14
15#[cfg(test)]
16mod allocation_tests;
17#[cfg(test)]
18mod growth_tests;
19#[cfg(test)]
20#[expect(
21 unsafe_code,
22 reason = "exercise raw reads with valid uninitialized destinations"
23)]
24mod read_tests;
25#[cfg(test)]
26mod request_tests;
27#[cfg(test)]
28mod tests;
29
30pub use admission::{BootstrapAdmission, BootstrapAdmissionError};
31pub use adoption::RuntimeAdoptionError;
32
33pub use allocations::{
34 AllocationBinding, MemoryAllocation, MemoryAllocationSummary, MemoryAllocations,
35 MemoryBindingSummary,
36};
37pub use backing::RuntimeMemory;
38pub use config::MemoryManagerConfig;
39pub use error::{
40 MemoryResolutionError, RuntimeBootstrapError, RuntimeConstructionError, RuntimeDiagnosticError,
41 RuntimeGrowError, RuntimeOpenError,
42};
43pub use layout::MemoryManagerLayoutError;
44pub use policy::GenericAllocationPolicy;
45
46use crate::{
47 CommittedAllocations, LedgerCommitStore, PolicyIdentity, RuntimeBootstrapPolicy,
48 STABLE_CELL_VALUE_OFFSET, StableCellLedgerError,
49 requests::SealedDeclarationSnapshot,
50 slot::MEMORY_MANAGER_LEDGER_ID,
51 stable_cell::{EncodedLedgerRecord, decode_stable_cell_commit_store_from_memory},
52};
53use ic_stable_structures::{
54 Cell, Memory,
55 memory_manager::{MemoryId, MemoryManager},
56};
57
58use std::rc::Rc;
59
60enum RuntimeLifecycle {
61 Unbootstrapped,
62 Bootstrapped {
63 committed_allocations: CommittedAllocations,
64 binding: RuntimeBootstrapBinding,
65 },
66}
67
68struct RuntimeBootstrapBinding {
69 source: SealedDeclarationSnapshot,
70 policy_identity: PolicyIdentity,
71 pool: crate::MemoryAllocationPool,
72}
73
74pub struct MemoryRuntime<M: Memory> {
93 memory_manager: MemoryManager<Rc<M>>,
94 growth: Rc<backing::GrowthState<M>>,
97 lifecycle: RuntimeLifecycle,
98}
99
100impl<M: Memory> MemoryRuntime<M> {
101 pub fn new(memory: M) -> Result<Self, RuntimeConstructionError> {
121 Self::construct(memory, None)
122 }
123
124 pub fn new_with_config(
133 memory: M,
134 config: MemoryManagerConfig,
135 ) -> Result<Self, RuntimeConstructionError> {
136 Self::construct(memory, Some(config))
137 }
138
139 fn construct(
140 memory: M,
141 requested: Option<MemoryManagerConfig>,
142 ) -> Result<Self, RuntimeConstructionError> {
143 if cfg!(target_endian = "big") {
144 return Err(MemoryManagerLayoutError::UnsupportedByteOrder.into());
145 }
146 let (bucket_size_pages, allocated_buckets) = if memory.size() == 0 {
147 if memory.grow(1) == -1 {
151 return Err(RuntimeGrowError::BackingRefused {
152 additional_pages: 1,
153 }
154 .into());
155 }
156 (requested.unwrap_or_default().bucket_size_pages(), 0)
157 } else {
158 let measured = layout::read(&memory)?;
159 let actual = measured.bucket_pages;
160 if let Some(config) = requested {
161 check_bucket_size(actual, config)?;
162 }
163 (actual, measured.allocated_buckets)
164 };
165 let backing = Rc::new(memory);
166 let growth = Rc::new(backing::GrowthState {
167 backing: Rc::clone(&backing),
168 bucket_size_pages,
169 allocated_buckets: std::cell::RefCell::new(allocated_buckets),
170 });
171 Ok(Self {
172 memory_manager: MemoryManager::init_with_bucket_size(
173 Rc::clone(&backing),
174 bucket_size_pages,
175 ),
176 growth,
177 lifecycle: RuntimeLifecycle::Unbootstrapped,
178 })
179 }
180
181 #[must_use]
183 pub fn memory_manager_config(&self) -> MemoryManagerConfig {
184 MemoryManagerConfig::from_validated(self.growth.bucket_size_pages)
186 }
187
188 #[must_use]
190 pub const fn is_bootstrapped(&self) -> bool {
191 matches!(self.lifecycle, RuntimeLifecycle::Bootstrapped { .. })
192 }
193
194 pub fn bootstrap<P: RuntimeBootstrapPolicy>(
209 &mut self,
210 declarations: &SealedDeclarationSnapshot,
211 pool: &crate::MemoryAllocationPool,
212 policy: &P,
213 ) -> Result<&CommittedAllocations, RuntimeBootstrapError<P::Error>> {
214 let policy_identity = policy.runtime_bootstrap_identity()?;
215 match &self.lifecycle {
216 RuntimeLifecycle::Unbootstrapped => {
217 self.bootstrap_unbootstrapped(declarations, pool, policy, policy_identity)?;
218 }
219 RuntimeLifecycle::Bootstrapped { binding, .. } => {
220 binding.validate(declarations, pool, &policy_identity)?;
221 }
222 }
223 match &self.lifecycle {
224 RuntimeLifecycle::Bootstrapped {
225 committed_allocations,
226 ..
227 } => Ok(committed_allocations),
228 RuntimeLifecycle::Unbootstrapped => {
229 unreachable!("successful bootstrap publishes committed allocations")
230 }
231 }
232 }
233
234 fn validate_pool_custody(
235 &self,
236 occupied: &[bool; crate::constants::MAX_ALLOCATIONS],
237 pool: &crate::MemoryAllocationPool,
238 ) -> Result<(), super::MemoryResolutionError> {
239 for id in 0..crate::MEMORY_MANAGER_INVALID_ID {
240 if pool.contains(id) && self.memory_size_pages(id) != 0 && !occupied[usize::from(id)] {
241 return Err(super::MemoryResolutionError::UnmanagedAllocation { id });
242 }
243 }
244 Ok(())
245 }
246
247 fn bootstrap_unbootstrapped<P: RuntimeBootstrapPolicy>(
248 &mut self,
249 declarations: &SealedDeclarationSnapshot,
250 pool: &crate::MemoryAllocationPool,
251 policy: &P,
252 policy_identity: PolicyIdentity,
253 ) -> Result<(), RuntimeBootstrapError<P::Error>> {
254 let memory = self.memory(MEMORY_MANAGER_LEDGER_ID);
255 let mut record = decode_stable_cell_commit_store_from_memory(&memory)?;
256 let recovered = record.recover_or_genesis()?;
257 let occupied = recovered.ledger().occupied_slots();
258 self.validate_pool_custody(&occupied, pool)?;
259 if memory.size() == 0 {
260 write_ledger_cell(memory.clone(), &LedgerCommitStore::default())?;
263 }
264 let mut admission = BootstrapAdmission::new(recovered.ledger(), declarations, pool);
265 let preparation = policy.prepare_bootstrap(&mut admission);
266 let historical = admission.complete()?;
267 preparation.map_err(RuntimeBootstrapError::AdmissionPolicy)?;
268 let resolved = declarations.resolve(&recovered, &occupied, historical, pool)?;
269 let (generation, resolved) = {
270 let (staged, resolved) = crate::ledger::stage_generation(recovered, resolved, policy)?;
271 record.commit_generation(&staged)?;
272 (staged.current_generation(), resolved)
273 };
274 write_ledger_cell(memory, &record)?;
275 let committed = CommittedAllocations::from_persisted(resolved, generation);
276 self.lifecycle = RuntimeLifecycle::Bootstrapped {
277 committed_allocations: committed,
278 binding: RuntimeBootstrapBinding {
279 source: declarations.clone(),
280 policy_identity,
281 pool: pool.clone(),
282 },
283 };
284 Ok(())
285 }
286
287 pub const fn committed_allocations(&self) -> Result<&CommittedAllocations, RuntimeOpenError> {
289 match &self.lifecycle {
290 RuntimeLifecycle::Unbootstrapped => Err(RuntimeOpenError::NotBootstrapped),
291 RuntimeLifecycle::Bootstrapped {
292 committed_allocations,
293 ..
294 } => Ok(committed_allocations),
295 }
296 }
297
298 pub fn open_memory(&self, stable_key: &str) -> Result<RuntimeMemory<M>, RuntimeOpenError> {
300 Ok(self.memory(self.memory_id(stable_key)?))
301 }
302
303 pub fn memory_id(&self, stable_key: &str) -> Result<u8, RuntimeOpenError> {
306 crate::key::validate(stable_key)?;
307 if crate::is_ic_memory_stable_key(stable_key) {
308 return Err(RuntimeOpenError::ReservedStableKey {
309 stable_key: stable_key.to_string(),
310 });
311 }
312 let declaration = crate::capability::declaration_for_key(
313 self.committed_allocations()?.declarations(),
314 stable_key,
315 )
316 .ok_or_else(|| RuntimeOpenError::StableKeyNotCommitted(stable_key.to_string()))?;
317 Ok(declaration.slot().id())
318 }
319
320 fn memory(&self, id: u8) -> RuntimeMemory<M> {
321 RuntimeMemory {
322 memory: self.memory_manager.get(MemoryId::new(id)),
323 growth: Rc::clone(&self.growth),
324 }
325 }
326
327 fn memory_size_pages(&self, id: u8) -> u64 {
329 self.memory_manager.get(MemoryId::new(id)).size()
330 }
331
332 fn ledger_record_from_memory(&self) -> Result<LedgerCommitStore, StableCellLedgerError> {
333 let memory = self
336 .memory_manager
337 .get(MemoryId::new(MEMORY_MANAGER_LEDGER_ID));
338 decode_stable_cell_commit_store_from_memory(&memory)
339 }
340}
341
342impl RuntimeBootstrapBinding {
343 fn validate<P>(
344 &self,
345 declarations: &SealedDeclarationSnapshot,
346 pool: &crate::MemoryAllocationPool,
347 policy_identity: &PolicyIdentity,
348 ) -> Result<(), RuntimeBootstrapError<P>> {
349 if &self.source != declarations {
350 return Err(RuntimeBootstrapError::DeclarationSnapshotMismatch);
351 }
352 if &self.pool != pool {
353 return Err(RuntimeBootstrapError::AllocationPoolMismatch);
354 }
355 if &self.policy_identity != policy_identity {
356 return Err(RuntimeBootstrapError::PolicyIdentityMismatch {
357 established: self.policy_identity.clone(),
358 requested: policy_identity.clone(),
359 });
360 }
361 Ok(())
362 }
363}
364
365fn write_ledger_cell<M: Memory, P>(
366 memory: RuntimeMemory<M>,
367 record: &LedgerCommitStore,
368) -> Result<(), RuntimeBootstrapError<P>> {
369 let encoded = EncodedLedgerRecord::new(record).map_err(|value_size| {
370 RuntimeBootstrapError::StableCellLedgerWriteTooLarge { value_size }
371 })?;
372 let value_size = encoded.len();
373 let required_bytes = STABLE_CELL_VALUE_OFFSET + value_size as u64;
375 let available_bytes = memory.size().saturating_mul(crate::WASM_PAGE_SIZE_BYTES);
376 if required_bytes > available_bytes {
377 let grow_by = (required_bytes - available_bytes).div_ceil(crate::WASM_PAGE_SIZE_BYTES);
378 memory.grow(grow_by)?;
379 }
380 drop(Cell::new(memory, encoded));
381 Ok(())
382}
383
384const fn check_bucket_size(
385 actual: u16,
386 requested: MemoryManagerConfig,
387) -> Result<(), RuntimeConstructionError> {
388 if actual != requested.bucket_size_pages() {
389 return Err(RuntimeConstructionError::BucketSizeMismatch {
390 persisted: actual,
391 requested: requested.bucket_size_pages(),
392 });
393 }
394 Ok(())
395}