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(CommittedRuntime),
71}
72
73struct CommittedRuntime {
74 allocations: Vec<crate::AllocationDeclaration>,
75 generation: u64,
76 binding: RuntimeBootstrapBinding,
77}
78
79struct RuntimeBootstrapBinding {
80 source: SealedDeclarationSnapshot,
81 policy_identity: PolicyIdentity,
82 pool: crate::MemoryAllocationPool,
83}
84
85pub struct MemoryRuntime<M: Memory> {
112 memory_manager: MemoryManager<Rc<M>>,
113 growth: Rc<backing::GrowthState<M>>,
116 lifecycle: RuntimeLifecycle,
117}
118
119impl<M: Memory> MemoryRuntime<M> {
120 pub fn new(memory: M) -> Result<Self, RuntimeConstructionError>
142 where
143 M: MemoryBacking,
144 {
145 Self::construct(memory, None)
146 }
147
148 pub fn new_with_config(
157 memory: M,
158 config: MemoryManagerConfig,
159 ) -> Result<Self, RuntimeConstructionError>
160 where
161 M: MemoryBacking,
162 {
163 Self::construct(memory, Some(config))
164 }
165
166 fn construct(
167 memory: M,
168 requested: Option<MemoryManagerConfig>,
169 ) -> Result<Self, RuntimeConstructionError>
170 where
171 M: MemoryBacking,
172 {
173 let backing = Rc::new(memory);
176 let claim = custody::BackingClaim::acquire(backing.as_ref())?;
177 if cfg!(target_endian = "big") {
178 return Err(MemoryManagerLayoutError::UnsupportedByteOrder.into());
179 }
180 let bucket_size_pages = if backing.size() == 0 {
181 if backing.grow(1) == -1 {
185 return Err(RuntimeGrowError::BackingRefused {
186 additional_pages: 1,
187 }
188 .into());
189 }
190 requested.unwrap_or_default().bucket_size_pages()
191 } else {
192 let measured = layout::read(backing.as_ref())?;
193 let actual = measured.bucket_pages;
194 if let Some(config) = requested {
195 check_bucket_size(actual, config)?;
196 }
197 actual
198 };
199 let memory_manager =
200 MemoryManager::init_with_bucket_size(Rc::clone(&backing), bucket_size_pages);
201 let authority =
202 std::cell::RefCell::new(layout::ManagerAuthority::capture(backing.as_ref()));
203 let growth = Rc::new(backing::GrowthState {
204 _claim: claim,
205 backing: Rc::clone(&backing),
206 bucket_size_pages,
207 authority,
208 open_slots: [const { std::cell::Cell::new(0) }; 4],
209 });
210 Ok(Self {
211 memory_manager,
212 growth,
213 lifecycle: RuntimeLifecycle::Unbootstrapped,
214 })
215 }
216
217 #[must_use]
219 pub fn memory_manager_config(&self) -> MemoryManagerConfig {
220 MemoryManagerConfig::from_validated(self.growth.bucket_size_pages)
222 }
223
224 #[must_use]
226 pub const fn is_bootstrapped(&self) -> bool {
227 matches!(self.lifecycle, RuntimeLifecycle::Bootstrapped(_))
228 }
229
230 pub fn bootstrap<P: RuntimeBootstrapPolicy>(
245 &mut self,
246 declarations: &SealedDeclarationSnapshot,
247 pool: &crate::MemoryAllocationPool,
248 policy: &P,
249 ) -> Result<u64, RuntimeBootstrapError<P::Error>> {
250 match &self.lifecycle {
251 RuntimeLifecycle::Unbootstrapped => self.bootstrap_unbootstrapped(
252 declarations,
253 pool,
254 policy,
255 policy.runtime_bootstrap_identity(),
256 ),
257 RuntimeLifecycle::Bootstrapped(_) => self.verify_bootstrap(declarations, pool, policy),
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 committed = self
276 .committed()
277 .map_err(|_| RuntimeBootstrapError::NotBootstrapped)?;
278 committed
279 .binding
280 .validate(declarations, pool, &policy.runtime_bootstrap_identity())?;
281 Ok(committed.generation)
282 }
283
284 const fn committed_state(&self) -> Option<&CommittedRuntime> {
285 match &self.lifecycle {
286 RuntimeLifecycle::Unbootstrapped => None,
287 RuntimeLifecycle::Bootstrapped(committed) => Some(committed),
288 }
289 }
290
291 fn committed(&self) -> Result<&CommittedRuntime, RuntimeOpenError> {
292 self.committed_state()
293 .ok_or(RuntimeOpenError::NotBootstrapped)
294 }
295
296 fn validate_pool_custody(
297 &self,
298 occupied: &[bool; crate::constants::MAX_ALLOCATIONS],
299 pool: &crate::MemoryAllocationPool,
300 ) -> Result<(), super::MemoryResolutionError> {
301 for id in 0..crate::MEMORY_MANAGER_INVALID_ID {
302 if pool.contains(id) && self.memory_size_pages(id) != 0 && !occupied[usize::from(id)] {
303 return Err(super::MemoryResolutionError::UnmanagedAllocation { id });
304 }
305 }
306 Ok(())
307 }
308
309 fn bootstrap_unbootstrapped<P: RuntimeBootstrapPolicy>(
310 &mut self,
311 declarations: &SealedDeclarationSnapshot,
312 pool: &crate::MemoryAllocationPool,
313 policy: &P,
314 policy_identity: PolicyIdentity,
315 ) -> Result<u64, RuntimeBootstrapError<P::Error>> {
316 self.growth.verify()?;
317 let mut memory = self
318 .memory(MEMORY_MANAGER_LEDGER_ID)
319 .expect("governance memory is not publicly leasable");
320 let mut record = decode_stable_cell_commit_store_from_memory(&memory)?;
321 let recovered = record.recover_or_genesis()?;
322 let occupied = recovered.ledger().occupied_slots();
323 self.validate_pool_custody(&occupied, pool)?;
324 if memory.size() == 0 {
325 memory = write_ledger_cell(memory, &LedgerCommitStore::default())?;
328 }
329 let mut admission = BootstrapAdmission::new(recovered.ledger(), declarations, pool);
330 let preparation = policy.prepare_bootstrap(&mut admission);
331 let historical = admission.complete()?;
332 preparation.map_err(RuntimeBootstrapError::AdmissionPolicy)?;
333 let resolved = declarations.resolve(&recovered, &occupied, historical, pool)?;
334 let (generation, resolved) = {
335 let (staged, resolved) = crate::ledger::stage_generation(recovered, resolved, policy)?;
336 record.commit_generation(&staged)?;
337 (staged.current_generation(), resolved)
338 };
339 drop(write_ledger_cell(memory, &record)?);
340 let mut allocations = resolved;
342 allocations.retain(|declaration| {
343 !crate::is_ic_memory_stable_key(declaration.stable_key().as_str())
344 });
345 self.lifecycle = RuntimeLifecycle::Bootstrapped(CommittedRuntime {
346 allocations,
347 generation,
348 binding: RuntimeBootstrapBinding {
349 source: declarations.clone(),
350 policy_identity,
351 pool: pool.clone(),
352 },
353 });
354 Ok(generation)
355 }
356
357 pub fn committed_allocations(
360 &self,
361 ) -> Result<&[crate::AllocationDeclaration], RuntimeOpenError> {
362 Ok(&self.committed()?.allocations)
363 }
364
365 pub const fn committed_generation(&self) -> Result<u64, RuntimeOpenError> {
367 match self.committed_state() {
368 Some(committed) => Ok(committed.generation),
369 None => Err(RuntimeOpenError::NotBootstrapped),
370 }
371 }
372
373 pub const fn allocation_pool(&self) -> Result<&crate::MemoryAllocationPool, RuntimeOpenError> {
376 match self.committed_state() {
377 Some(committed) => Ok(&committed.binding.pool),
378 None => Err(RuntimeOpenError::NotBootstrapped),
379 }
380 }
381
382 pub fn open_memory(&self, stable_key: &str) -> Result<RuntimeMemory<M>, RuntimeOpenError> {
389 self.memory(self.memory_id(stable_key)?)
390 }
391
392 pub fn memory_id(&self, stable_key: &str) -> Result<u8, RuntimeOpenError> {
395 crate::key::validate(stable_key)?;
396 if crate::is_ic_memory_stable_key(stable_key) {
397 return Err(RuntimeOpenError::ReservedStableKey {
398 stable_key: stable_key.to_string(),
399 });
400 }
401 let declaration =
402 crate::declaration::declaration_for_key(self.committed_allocations()?, stable_key)
403 .ok_or_else(|| RuntimeOpenError::StableKeyNotCommitted(stable_key.to_string()))?;
404 Ok(declaration.slot().id())
405 }
406
407 fn memory(&self, id: u8) -> Result<RuntimeMemory<M>, RuntimeOpenError> {
408 let word = &self.growth.open_slots[usize::from(id / 64)];
409 let mask = 1_u64 << (id % 64);
410 if word.get() & mask != 0 {
411 return Err(RuntimeOpenError::AlreadyOpen { id });
412 }
413 self.growth.verify()?;
414 let memory = self.memory_manager.get(MemoryId::new(id));
415 word.set(word.get() | mask);
416 Ok(RuntimeMemory {
417 memory,
418 growth: Rc::clone(&self.growth),
419 id,
420 })
421 }
422
423 fn memory_size_pages(&self, id: u8) -> u64 {
425 self.memory_manager.get(MemoryId::new(id)).size()
426 }
427
428 fn ledger_record_from_memory(&self) -> Result<LedgerCommitStore, StableCellLedgerError> {
429 let memory = self
432 .memory_manager
433 .get(MemoryId::new(MEMORY_MANAGER_LEDGER_ID));
434 decode_stable_cell_commit_store_from_memory(&memory)
435 }
436}
437
438impl RuntimeBootstrapBinding {
439 fn validate<P>(
440 &self,
441 declarations: &SealedDeclarationSnapshot,
442 pool: &crate::MemoryAllocationPool,
443 policy_identity: &PolicyIdentity,
444 ) -> Result<(), RuntimeBootstrapError<P>> {
445 if &self.source != declarations {
446 return Err(RuntimeBootstrapError::DeclarationSnapshotMismatch);
447 }
448 if &self.pool != pool {
449 return Err(RuntimeBootstrapError::AllocationPoolMismatch);
450 }
451 if &self.policy_identity != policy_identity {
452 return Err(RuntimeBootstrapError::PolicyIdentityMismatch {
453 established: self.policy_identity,
454 requested: *policy_identity,
455 });
456 }
457 Ok(())
458 }
459}
460
461fn write_ledger_cell<M: Memory, P>(
462 memory: RuntimeMemory<M>,
463 record: &LedgerCommitStore,
464) -> Result<RuntimeMemory<M>, RuntimeBootstrapError<P>> {
465 let encoded =
466 EncodedLedgerRecord::new(record).map_err(RuntimeBootstrapError::StableCellLedgerEncode)?;
467 let value_size = encoded.len();
468 let required_bytes = STABLE_CELL_VALUE_OFFSET + value_size as u64;
470 let available_bytes = memory.size().saturating_mul(crate::WASM_PAGE_SIZE_BYTES);
471 if required_bytes > available_bytes {
472 let grow_by = (required_bytes - available_bytes).div_ceil(crate::WASM_PAGE_SIZE_BYTES);
473 memory.grow(grow_by)?;
474 }
475 Ok(Cell::new(memory, encoded).into_memory())
476}
477
478const fn check_bucket_size(
479 actual: u16,
480 requested: MemoryManagerConfig,
481) -> Result<(), RuntimeConstructionError> {
482 if actual != requested.bucket_size_pages() {
483 return Err(RuntimeConstructionError::BucketSizeMismatch {
484 persisted: actual,
485 requested: requested.bucket_size_pages(),
486 });
487 }
488 Ok(())
489}