1mod admission;
2#[cfg(test)]
3mod admission_tests;
4mod adoption;
5#[cfg(test)]
6mod adoption_tests;
7mod allocations;
8mod backing;
9mod config;
10mod custody;
11mod diagnostics;
12mod error;
13#[cfg(any(test, feature = "test-support"))]
14#[path = "../testing/diagnostics.rs"]
15pub mod fixture_diagnostics;
16#[cfg(test)]
17mod fixtures;
18mod layout;
19mod policy;
20
21#[cfg(all(test, not(target_arch = "wasm32")))]
22mod allocation_tests;
23#[cfg(test)]
24mod custody_tests;
25#[cfg(test)]
26mod growth_tests;
27#[cfg(test)]
28#[expect(
29 unsafe_code,
30 reason = "exercise raw reads with valid uninitialized destinations"
31)]
32mod read_tests;
33#[cfg(test)]
34mod request_tests;
35#[cfg(test)]
36mod tests;
37
38pub use admission::{BootstrapAdmission, BootstrapAdmissionError};
39pub use adoption::RuntimeAdoptionError;
40
41pub use allocations::{
42 MemoryAllocation, MemoryAllocationSummary, MemoryAllocations, MemoryBindingSummary,
43};
44pub use backing::RuntimeMemory;
45pub use config::MemoryManagerConfig;
46pub use custody::{BackingIdentity, MemoryBacking};
47pub use error::{
48 MemoryResolutionError, RuntimeBootstrapError, RuntimeConstructionError, RuntimeDiagnosticError,
49 RuntimeGrowError, RuntimeOpenError,
50};
51pub use layout::MemoryManagerLayoutError;
52pub use policy::GenericAllocationPolicy;
53
54use crate::{
55 LedgerCommitStore, PolicyIdentity, RuntimeBootstrapPolicy, STABLE_CELL_VALUE_OFFSET,
56 StableCellLedgerError,
57 requests::SealedDeclarationSnapshot,
58 slot::MEMORY_MANAGER_LEDGER_ID,
59 stable_cell::{EncodedLedgerRecord, decode_stable_cell_commit_store_from_memory},
60};
61use ic_platform::stable_structures::{
62 Cell, Memory,
63 memory_manager::{MemoryId, MemoryManager},
64};
65
66use std::rc::Rc;
67
68enum RuntimeLifecycle {
69 Unbootstrapped,
70 Bootstrapped {
71 allocations: Vec<crate::AllocationDeclaration>,
72 generation: u64,
73 binding: RuntimeBootstrapBinding,
74 },
75}
76
77struct RuntimeBootstrapBinding {
78 source: SealedDeclarationSnapshot,
79 policy_identity: PolicyIdentity,
80 pool: crate::MemoryAllocationPool,
81}
82
83pub struct MemoryRuntime<M: Memory> {
110 memory_manager: MemoryManager<Rc<M>>,
111 growth: Rc<backing::GrowthState<M>>,
114 lifecycle: RuntimeLifecycle,
115}
116
117impl<M: Memory> MemoryRuntime<M> {
118 pub fn new(memory: M) -> Result<Self, RuntimeConstructionError>
140 where
141 M: MemoryBacking,
142 {
143 Self::construct(memory, None)
144 }
145
146 pub fn new_with_config(
155 memory: M,
156 config: MemoryManagerConfig,
157 ) -> Result<Self, RuntimeConstructionError>
158 where
159 M: MemoryBacking,
160 {
161 Self::construct(memory, Some(config))
162 }
163
164 fn construct(
165 memory: M,
166 requested: Option<MemoryManagerConfig>,
167 ) -> Result<Self, RuntimeConstructionError>
168 where
169 M: MemoryBacking,
170 {
171 let backing = Rc::new(memory);
174 let claim = custody::BackingClaim::acquire(backing.as_ref())?;
175 if cfg!(target_endian = "big") {
176 return Err(MemoryManagerLayoutError::UnsupportedByteOrder.into());
177 }
178 let bucket_size_pages = if backing.size() == 0 {
179 if backing.grow(1) == -1 {
183 return Err(RuntimeGrowError::BackingRefused {
184 additional_pages: 1,
185 }
186 .into());
187 }
188 requested.unwrap_or_default().bucket_size_pages()
189 } else {
190 let measured = layout::read(backing.as_ref())?;
191 let actual = measured.bucket_pages;
192 if let Some(config) = requested {
193 check_bucket_size(actual, config)?;
194 }
195 actual
196 };
197 let memory_manager =
198 MemoryManager::init_with_bucket_size(Rc::clone(&backing), bucket_size_pages);
199 let authority =
200 std::cell::RefCell::new(layout::ManagerAuthority::capture(backing.as_ref()));
201 let growth = Rc::new(backing::GrowthState {
202 _claim: claim,
203 backing: Rc::clone(&backing),
204 bucket_size_pages,
205 authority,
206 open_slots: [const { std::cell::Cell::new(0) }; 4],
207 });
208 Ok(Self {
209 memory_manager,
210 growth,
211 lifecycle: RuntimeLifecycle::Unbootstrapped,
212 })
213 }
214
215 #[must_use]
217 pub fn memory_manager_config(&self) -> MemoryManagerConfig {
218 MemoryManagerConfig::from_validated(self.growth.bucket_size_pages)
220 }
221
222 #[must_use]
224 pub const fn is_bootstrapped(&self) -> bool {
225 matches!(self.lifecycle, RuntimeLifecycle::Bootstrapped { .. })
226 }
227
228 pub fn bootstrap<P: RuntimeBootstrapPolicy>(
243 &mut self,
244 declarations: &SealedDeclarationSnapshot,
245 pool: &crate::MemoryAllocationPool,
246 policy: &P,
247 ) -> Result<u64, RuntimeBootstrapError<P::Error>> {
248 match &self.lifecycle {
249 RuntimeLifecycle::Unbootstrapped => self.bootstrap_unbootstrapped(
250 declarations,
251 pool,
252 policy,
253 policy.runtime_bootstrap_identity(),
254 ),
255 RuntimeLifecycle::Bootstrapped { .. } => {
256 self.verify_bootstrap(declarations, pool, policy)
257 }
258 }
259 }
260
261 pub fn verify_bootstrap<P: RuntimeBootstrapPolicy>(
270 &self,
271 declarations: &SealedDeclarationSnapshot,
272 pool: &crate::MemoryAllocationPool,
273 policy: &P,
274 ) -> Result<u64, RuntimeBootstrapError<P::Error>> {
275 let RuntimeLifecycle::Bootstrapped {
276 binding,
277 generation,
278 ..
279 } = &self.lifecycle
280 else {
281 return Err(RuntimeBootstrapError::NotBootstrapped);
282 };
283 binding.validate(declarations, pool, &policy.runtime_bootstrap_identity())?;
284 Ok(*generation)
285 }
286
287 fn validate_pool_custody(
288 &self,
289 occupied: &[bool; crate::constants::MAX_ALLOCATIONS],
290 pool: &crate::MemoryAllocationPool,
291 ) -> Result<(), super::MemoryResolutionError> {
292 for id in 0..crate::MEMORY_MANAGER_INVALID_ID {
293 if pool.contains(id) && self.memory_size_pages(id) != 0 && !occupied[usize::from(id)] {
294 return Err(super::MemoryResolutionError::UnmanagedAllocation { id });
295 }
296 }
297 Ok(())
298 }
299
300 fn bootstrap_unbootstrapped<P: RuntimeBootstrapPolicy>(
301 &mut self,
302 declarations: &SealedDeclarationSnapshot,
303 pool: &crate::MemoryAllocationPool,
304 policy: &P,
305 policy_identity: PolicyIdentity,
306 ) -> Result<u64, RuntimeBootstrapError<P::Error>> {
307 self.growth.verify()?;
308 let mut memory = self
309 .memory(MEMORY_MANAGER_LEDGER_ID)
310 .expect("governance memory is not publicly leasable");
311 let mut record = decode_stable_cell_commit_store_from_memory(&memory)?;
312 let recovered = record.recover_or_genesis()?;
313 let occupied = recovered.ledger().occupied_slots();
314 self.validate_pool_custody(&occupied, pool)?;
315 if memory.size() == 0 {
316 memory = write_ledger_cell(memory, &LedgerCommitStore::default())?;
319 }
320 let mut admission = BootstrapAdmission::new(recovered.ledger(), declarations, pool);
321 let preparation = policy.prepare_bootstrap(&mut admission);
322 let historical = admission.complete()?;
323 preparation.map_err(RuntimeBootstrapError::AdmissionPolicy)?;
324 let resolved = declarations.resolve(&recovered, &occupied, historical, pool)?;
325 let (generation, resolved) = {
326 let (staged, resolved) = crate::ledger::stage_generation(recovered, resolved, policy)?;
327 record.commit_generation(&staged)?;
328 (staged.current_generation(), resolved)
329 };
330 drop(write_ledger_cell(memory, &record)?);
331 let mut allocations = resolved;
333 allocations.retain(|declaration| {
334 !crate::is_ic_memory_stable_key(declaration.stable_key().as_str())
335 });
336 self.lifecycle = RuntimeLifecycle::Bootstrapped {
337 allocations,
338 generation,
339 binding: RuntimeBootstrapBinding {
340 source: declarations.clone(),
341 policy_identity,
342 pool: pool.clone(),
343 },
344 };
345 Ok(generation)
346 }
347
348 pub fn committed_allocations(
351 &self,
352 ) -> Result<&[crate::AllocationDeclaration], RuntimeOpenError> {
353 match &self.lifecycle {
354 RuntimeLifecycle::Unbootstrapped => Err(RuntimeOpenError::NotBootstrapped),
355 RuntimeLifecycle::Bootstrapped { allocations, .. } => Ok(allocations),
356 }
357 }
358
359 pub const fn committed_generation(&self) -> Result<u64, RuntimeOpenError> {
361 match &self.lifecycle {
362 RuntimeLifecycle::Unbootstrapped => Err(RuntimeOpenError::NotBootstrapped),
363 RuntimeLifecycle::Bootstrapped { generation, .. } => Ok(*generation),
364 }
365 }
366
367 pub const fn allocation_pool(&self) -> Result<&crate::MemoryAllocationPool, RuntimeOpenError> {
370 match &self.lifecycle {
371 RuntimeLifecycle::Unbootstrapped => Err(RuntimeOpenError::NotBootstrapped),
372 RuntimeLifecycle::Bootstrapped { binding, .. } => Ok(&binding.pool),
373 }
374 }
375
376 pub fn open_memory(&self, stable_key: &str) -> Result<RuntimeMemory<M>, RuntimeOpenError> {
383 self.memory(self.memory_id(stable_key)?)
384 }
385
386 pub fn memory_id(&self, stable_key: &str) -> Result<u8, RuntimeOpenError> {
389 crate::key::validate(stable_key)?;
390 if crate::is_ic_memory_stable_key(stable_key) {
391 return Err(RuntimeOpenError::ReservedStableKey {
392 stable_key: stable_key.to_string(),
393 });
394 }
395 let declaration =
396 crate::declaration::declaration_for_key(self.committed_allocations()?, stable_key)
397 .ok_or_else(|| RuntimeOpenError::StableKeyNotCommitted(stable_key.to_string()))?;
398 Ok(declaration.slot().id())
399 }
400
401 fn memory(&self, id: u8) -> Result<RuntimeMemory<M>, RuntimeOpenError> {
402 let word = &self.growth.open_slots[usize::from(id / 64)];
403 let mask = 1_u64 << (id % 64);
404 if word.get() & mask != 0 {
405 return Err(RuntimeOpenError::AlreadyOpen { id });
406 }
407 self.growth.verify()?;
408 let memory = self.memory_manager.get(MemoryId::new(id));
409 word.set(word.get() | mask);
410 Ok(RuntimeMemory {
411 memory,
412 growth: Rc::clone(&self.growth),
413 id,
414 })
415 }
416
417 fn memory_size_pages(&self, id: u8) -> u64 {
419 self.memory_manager.get(MemoryId::new(id)).size()
420 }
421
422 fn ledger_record_from_memory(&self) -> Result<LedgerCommitStore, StableCellLedgerError> {
423 let memory = self
426 .memory_manager
427 .get(MemoryId::new(MEMORY_MANAGER_LEDGER_ID));
428 decode_stable_cell_commit_store_from_memory(&memory)
429 }
430}
431
432impl RuntimeBootstrapBinding {
433 fn validate<P>(
434 &self,
435 declarations: &SealedDeclarationSnapshot,
436 pool: &crate::MemoryAllocationPool,
437 policy_identity: &PolicyIdentity,
438 ) -> Result<(), RuntimeBootstrapError<P>> {
439 if &self.source != declarations {
440 return Err(RuntimeBootstrapError::DeclarationSnapshotMismatch);
441 }
442 if &self.pool != pool {
443 return Err(RuntimeBootstrapError::AllocationPoolMismatch);
444 }
445 if &self.policy_identity != policy_identity {
446 return Err(RuntimeBootstrapError::PolicyIdentityMismatch {
447 established: self.policy_identity,
448 requested: *policy_identity,
449 });
450 }
451 Ok(())
452 }
453}
454
455fn write_ledger_cell<M: Memory, P>(
456 memory: RuntimeMemory<M>,
457 record: &LedgerCommitStore,
458) -> Result<RuntimeMemory<M>, RuntimeBootstrapError<P>> {
459 let encoded = EncodedLedgerRecord::new(record).map_err(|value_size| {
460 RuntimeBootstrapError::StableCellLedgerWriteTooLarge { value_size }
461 })?;
462 let value_size = encoded.len();
463 let required_bytes = STABLE_CELL_VALUE_OFFSET + value_size as u64;
465 let available_bytes = memory.size().saturating_mul(crate::WASM_PAGE_SIZE_BYTES);
466 if required_bytes > available_bytes {
467 let grow_by = (required_bytes - available_bytes).div_ceil(crate::WASM_PAGE_SIZE_BYTES);
468 memory.grow(grow_by)?;
469 }
470 Ok(Cell::new(memory, encoded).into_memory())
471}
472
473const fn check_bucket_size(
474 actual: u16,
475 requested: MemoryManagerConfig,
476) -> Result<(), RuntimeConstructionError> {
477 if actual != requested.bucket_size_pages() {
478 return Err(RuntimeConstructionError::BucketSizeMismatch {
479 persisted: actual,
480 requested: requested.bucket_size_pages(),
481 });
482 }
483 Ok(())
484}