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;
12#[cfg(any(test, feature = "test-support"))]
13#[path = "../testing/diagnostics.rs"]
14pub mod fixture_diagnostics;
15mod layout;
16mod policy;
17
18#[cfg(test)]
19mod allocation_tests;
20#[cfg(test)]
21mod growth_tests;
22#[cfg(test)]
23#[expect(
24 unsafe_code,
25 reason = "exercise raw reads with valid uninitialized destinations"
26)]
27mod read_tests;
28#[cfg(test)]
29mod request_tests;
30#[cfg(test)]
31mod tests;
32
33pub use admission::{BootstrapAdmission, BootstrapAdmissionError};
34pub use adoption::RuntimeAdoptionError;
35
36pub use allocations::{
37 MemoryAllocation, MemoryAllocationSummary, MemoryAllocations, MemoryBindingSummary,
38};
39pub use backing::RuntimeMemory;
40pub use config::MemoryManagerConfig;
41pub use error::{
42 MemoryResolutionError, RuntimeBootstrapError, RuntimeConstructionError, RuntimeDiagnosticError,
43 RuntimeGrowError, RuntimeOpenError,
44};
45pub use layout::MemoryManagerLayoutError;
46pub use policy::GenericAllocationPolicy;
47
48use crate::{
49 LedgerCommitStore, PolicyIdentity, RuntimeBootstrapPolicy, STABLE_CELL_VALUE_OFFSET,
50 StableCellLedgerError,
51 requests::SealedDeclarationSnapshot,
52 slot::MEMORY_MANAGER_LEDGER_ID,
53 stable_cell::{EncodedLedgerRecord, decode_stable_cell_commit_store_from_memory},
54};
55use ic_stable_structures::{
56 Cell, Memory,
57 memory_manager::{MemoryId, MemoryManager},
58};
59
60use std::rc::Rc;
61
62enum RuntimeLifecycle {
63 Unbootstrapped,
64 Bootstrapped {
65 allocations: Vec<crate::AllocationDeclaration>,
66 generation: u64,
67 binding: RuntimeBootstrapBinding,
68 },
69}
70
71struct RuntimeBootstrapBinding {
72 source: SealedDeclarationSnapshot,
73 policy_identity: PolicyIdentity,
74 pool: crate::MemoryAllocationPool,
75}
76
77pub struct MemoryRuntime<M: Memory> {
103 memory_manager: MemoryManager<Rc<M>>,
104 growth: Rc<backing::GrowthState<M>>,
107 lifecycle: RuntimeLifecycle,
108}
109
110impl<M: Memory> MemoryRuntime<M> {
111 pub fn new(memory: M) -> Result<Self, RuntimeConstructionError> {
131 Self::construct(memory, None)
132 }
133
134 pub fn new_with_config(
143 memory: M,
144 config: MemoryManagerConfig,
145 ) -> Result<Self, RuntimeConstructionError> {
146 Self::construct(memory, Some(config))
147 }
148
149 fn construct(
150 memory: M,
151 requested: Option<MemoryManagerConfig>,
152 ) -> Result<Self, RuntimeConstructionError> {
153 if cfg!(target_endian = "big") {
154 return Err(MemoryManagerLayoutError::UnsupportedByteOrder.into());
155 }
156 let (bucket_size_pages, allocated_buckets) = if memory.size() == 0 {
157 if memory.grow(1) == -1 {
161 return Err(RuntimeGrowError::BackingRefused {
162 additional_pages: 1,
163 }
164 .into());
165 }
166 (requested.unwrap_or_default().bucket_size_pages(), 0)
167 } else {
168 let measured = layout::read(&memory)?;
169 let actual = measured.bucket_pages;
170 if let Some(config) = requested {
171 check_bucket_size(actual, config)?;
172 }
173 (actual, measured.allocated_buckets)
174 };
175 let backing = Rc::new(memory);
176 let growth = Rc::new(backing::GrowthState {
177 backing: Rc::clone(&backing),
178 bucket_size_pages,
179 allocated_buckets: std::cell::RefCell::new(allocated_buckets),
180 open_slots: [const { std::cell::Cell::new(0) }; 4],
181 });
182 Ok(Self {
183 memory_manager: MemoryManager::init_with_bucket_size(
184 Rc::clone(&backing),
185 bucket_size_pages,
186 ),
187 growth,
188 lifecycle: RuntimeLifecycle::Unbootstrapped,
189 })
190 }
191
192 #[must_use]
194 pub fn memory_manager_config(&self) -> MemoryManagerConfig {
195 MemoryManagerConfig::from_validated(self.growth.bucket_size_pages)
197 }
198
199 #[must_use]
201 pub const fn is_bootstrapped(&self) -> bool {
202 matches!(self.lifecycle, RuntimeLifecycle::Bootstrapped { .. })
203 }
204
205 pub fn bootstrap<P: RuntimeBootstrapPolicy>(
220 &mut self,
221 declarations: &SealedDeclarationSnapshot,
222 pool: &crate::MemoryAllocationPool,
223 policy: &P,
224 ) -> Result<u64, RuntimeBootstrapError<P::Error>> {
225 match &self.lifecycle {
226 RuntimeLifecycle::Unbootstrapped => {
227 self.bootstrap_unbootstrapped(
228 declarations,
229 pool,
230 policy,
231 policy.runtime_bootstrap_identity(),
232 )?;
233 }
234 RuntimeLifecycle::Bootstrapped { .. } => {
235 return self.verify_bootstrap(declarations, pool, policy);
236 }
237 }
238 match &self.lifecycle {
239 RuntimeLifecycle::Bootstrapped { generation, .. } => Ok(*generation),
240 RuntimeLifecycle::Unbootstrapped => {
241 unreachable!("successful bootstrap publishes committed allocations")
242 }
243 }
244 }
245
246 pub fn verify_bootstrap<P: RuntimeBootstrapPolicy>(
255 &self,
256 declarations: &SealedDeclarationSnapshot,
257 pool: &crate::MemoryAllocationPool,
258 policy: &P,
259 ) -> Result<u64, RuntimeBootstrapError<P::Error>> {
260 let RuntimeLifecycle::Bootstrapped {
261 binding,
262 generation,
263 ..
264 } = &self.lifecycle
265 else {
266 return Err(RuntimeBootstrapError::NotBootstrapped);
267 };
268 binding.validate(declarations, pool, &policy.runtime_bootstrap_identity())?;
269 Ok(*generation)
270 }
271
272 fn validate_pool_custody(
273 &self,
274 occupied: &[bool; crate::constants::MAX_ALLOCATIONS],
275 pool: &crate::MemoryAllocationPool,
276 ) -> Result<(), super::MemoryResolutionError> {
277 for id in 0..crate::MEMORY_MANAGER_INVALID_ID {
278 if pool.contains(id) && self.memory_size_pages(id) != 0 && !occupied[usize::from(id)] {
279 return Err(super::MemoryResolutionError::UnmanagedAllocation { id });
280 }
281 }
282 Ok(())
283 }
284
285 fn bootstrap_unbootstrapped<P: RuntimeBootstrapPolicy>(
286 &mut self,
287 declarations: &SealedDeclarationSnapshot,
288 pool: &crate::MemoryAllocationPool,
289 policy: &P,
290 policy_identity: PolicyIdentity,
291 ) -> Result<(), RuntimeBootstrapError<P::Error>> {
292 let mut memory = self
293 .memory(MEMORY_MANAGER_LEDGER_ID)
294 .expect("governance memory is not publicly leasable");
295 let mut record = decode_stable_cell_commit_store_from_memory(&memory)?;
296 let recovered = record.recover_or_genesis()?;
297 let occupied = recovered.ledger().occupied_slots();
298 self.validate_pool_custody(&occupied, pool)?;
299 if memory.size() == 0 {
300 memory = write_ledger_cell(memory, &LedgerCommitStore::default())?;
303 }
304 let mut admission = BootstrapAdmission::new(recovered.ledger(), declarations, pool);
305 let preparation = policy.prepare_bootstrap(&mut admission);
306 let historical = admission.complete()?;
307 preparation.map_err(RuntimeBootstrapError::AdmissionPolicy)?;
308 let resolved = declarations.resolve(&recovered, &occupied, historical, pool)?;
309 let (generation, resolved) = {
310 let (staged, resolved) = crate::ledger::stage_generation(recovered, resolved, policy)?;
311 record.commit_generation(&staged)?;
312 (staged.current_generation(), resolved)
313 };
314 drop(write_ledger_cell(memory, &record)?);
315 let mut allocations = resolved;
317 allocations.retain(|declaration| {
318 !crate::is_ic_memory_stable_key(declaration.stable_key().as_str())
319 });
320 self.lifecycle = RuntimeLifecycle::Bootstrapped {
321 allocations,
322 generation,
323 binding: RuntimeBootstrapBinding {
324 source: declarations.clone(),
325 policy_identity,
326 pool: pool.clone(),
327 },
328 };
329 Ok(())
330 }
331
332 pub fn committed_allocations(
335 &self,
336 ) -> Result<&[crate::AllocationDeclaration], RuntimeOpenError> {
337 match &self.lifecycle {
338 RuntimeLifecycle::Unbootstrapped => Err(RuntimeOpenError::NotBootstrapped),
339 RuntimeLifecycle::Bootstrapped { allocations, .. } => Ok(allocations),
340 }
341 }
342
343 pub const fn committed_generation(&self) -> Result<u64, RuntimeOpenError> {
345 match &self.lifecycle {
346 RuntimeLifecycle::Unbootstrapped => Err(RuntimeOpenError::NotBootstrapped),
347 RuntimeLifecycle::Bootstrapped { generation, .. } => Ok(*generation),
348 }
349 }
350
351 pub const fn allocation_pool(&self) -> Result<&crate::MemoryAllocationPool, RuntimeOpenError> {
354 match &self.lifecycle {
355 RuntimeLifecycle::Unbootstrapped => Err(RuntimeOpenError::NotBootstrapped),
356 RuntimeLifecycle::Bootstrapped { binding, .. } => Ok(&binding.pool),
357 }
358 }
359
360 pub fn open_memory(&self, stable_key: &str) -> Result<RuntimeMemory<M>, RuntimeOpenError> {
365 self.memory(self.memory_id(stable_key)?)
366 }
367
368 pub fn memory_id(&self, stable_key: &str) -> Result<u8, RuntimeOpenError> {
371 crate::key::validate(stable_key)?;
372 if crate::is_ic_memory_stable_key(stable_key) {
373 return Err(RuntimeOpenError::ReservedStableKey {
374 stable_key: stable_key.to_string(),
375 });
376 }
377 let declaration =
378 crate::declaration::declaration_for_key(self.committed_allocations()?, stable_key)
379 .ok_or_else(|| RuntimeOpenError::StableKeyNotCommitted(stable_key.to_string()))?;
380 Ok(declaration.slot().id())
381 }
382
383 fn memory(&self, id: u8) -> Result<RuntimeMemory<M>, RuntimeOpenError> {
384 let word = &self.growth.open_slots[usize::from(id / 64)];
385 let mask = 1_u64 << (id % 64);
386 if word.get() & mask != 0 {
387 return Err(RuntimeOpenError::AlreadyOpen { id });
388 }
389 let memory = self.memory_manager.get(MemoryId::new(id));
390 word.set(word.get() | mask);
391 Ok(RuntimeMemory {
392 memory,
393 growth: Rc::clone(&self.growth),
394 id,
395 })
396 }
397
398 fn memory_size_pages(&self, id: u8) -> u64 {
400 self.memory_manager.get(MemoryId::new(id)).size()
401 }
402
403 fn ledger_record_from_memory(&self) -> Result<LedgerCommitStore, StableCellLedgerError> {
404 let memory = self
407 .memory_manager
408 .get(MemoryId::new(MEMORY_MANAGER_LEDGER_ID));
409 decode_stable_cell_commit_store_from_memory(&memory)
410 }
411}
412
413impl RuntimeBootstrapBinding {
414 fn validate<P>(
415 &self,
416 declarations: &SealedDeclarationSnapshot,
417 pool: &crate::MemoryAllocationPool,
418 policy_identity: &PolicyIdentity,
419 ) -> Result<(), RuntimeBootstrapError<P>> {
420 if &self.source != declarations {
421 return Err(RuntimeBootstrapError::DeclarationSnapshotMismatch);
422 }
423 if &self.pool != pool {
424 return Err(RuntimeBootstrapError::AllocationPoolMismatch);
425 }
426 if &self.policy_identity != policy_identity {
427 return Err(RuntimeBootstrapError::PolicyIdentityMismatch {
428 established: self.policy_identity,
429 requested: *policy_identity,
430 });
431 }
432 Ok(())
433 }
434}
435
436fn write_ledger_cell<M: Memory, P>(
437 memory: RuntimeMemory<M>,
438 record: &LedgerCommitStore,
439) -> Result<RuntimeMemory<M>, RuntimeBootstrapError<P>> {
440 let encoded = EncodedLedgerRecord::new(record).map_err(|value_size| {
441 RuntimeBootstrapError::StableCellLedgerWriteTooLarge { value_size }
442 })?;
443 let value_size = encoded.len();
444 let required_bytes = STABLE_CELL_VALUE_OFFSET + value_size as u64;
446 let available_bytes = memory.size().saturating_mul(crate::WASM_PAGE_SIZE_BYTES);
447 if required_bytes > available_bytes {
448 let grow_by = (required_bytes - available_bytes).div_ceil(crate::WASM_PAGE_SIZE_BYTES);
449 memory.grow(grow_by)?;
450 }
451 Ok(Cell::new(memory, encoded).into_memory())
452}
453
454const fn check_bucket_size(
455 actual: u16,
456 requested: MemoryManagerConfig,
457) -> Result<(), RuntimeConstructionError> {
458 if actual != requested.bucket_size_pages() {
459 return Err(RuntimeConstructionError::BucketSizeMismatch {
460 persisted: actual,
461 requested: requested.bucket_size_pages(),
462 });
463 }
464 Ok(())
465}