Skip to main content

ic_memory/
runtime.rs

1use crate::{
2    AllocationBootstrap, AllocationDeclaration, AllocationHistory, AllocationLedger,
3    AllocationPolicy, AllocationSlotDescriptor, DeclarationSnapshot,
4    DefaultMemoryManagerDoctorReport, DiagnosticCheck, DiagnosticDeclaration, DiagnosticExport,
5    DiagnosticMemorySize, DiagnosticRangeAuthority, DiagnosticStableCell,
6    DiagnosticStableCellStatus, LedgerCommitError, StableCellLedgerError, StableCellLedgerRecord,
7    StableKey, ValidatedAllocations, decode_stable_cell_ledger_record, decode_stable_cell_payload,
8    physical::CommitStoreDiagnostic,
9    registry::{
10        StaticMemoryDeclaration, StaticMemoryDeclarationError, StaticMemoryRangeDeclaration,
11        seal_static_memory_registry, static_memory_declarations, static_memory_range_declarations,
12    },
13    slot::{
14        IC_MEMORY_AUTHORITY_OWNER, IC_MEMORY_AUTHORITY_PURPOSE, IC_MEMORY_LEDGER_LABEL,
15        IC_MEMORY_LEDGER_STABLE_KEY, MEMORY_MANAGER_LEDGER_ID, MemoryManagerAuthorityRecord,
16        MemoryManagerIdRange, MemoryManagerRangeAuthority, MemoryManagerRangeAuthorityError,
17        MemoryManagerRangeMode, MemoryManagerSlotError,
18    },
19};
20use ic_stable_structures::{
21    Cell, DefaultMemoryImpl, Memory,
22    memory_manager::{MemoryId, MemoryManager, VirtualMemory},
23};
24use std::{
25    cell::RefCell,
26    collections::BTreeMap,
27    convert::Infallible,
28    sync::{
29        Mutex,
30        atomic::{AtomicBool, Ordering},
31    },
32};
33
34type DefaultLedgerCell = Cell<StableCellLedgerRecord, VirtualMemory<DefaultMemoryImpl>>;
35
36thread_local! {
37    static DEFAULT_MEMORY_MANAGER: MemoryManager<DefaultMemoryImpl> =
38        MemoryManager::init(DefaultMemoryImpl::default());
39    static DEFAULT_LEDGER_CELL: RefCell<Option<DefaultLedgerCell>> = const {
40        RefCell::new(None)
41    };
42}
43
44static EAGER_INIT_HOOKS: Mutex<Vec<fn()>> = Mutex::new(Vec::new());
45static VALIDATED_ALLOCATIONS: Mutex<Option<ValidatedAllocations>> = Mutex::new(None);
46static BOOTSTRAPPED: AtomicBool = AtomicBool::new(false);
47
48///
49/// RuntimeBootstrapError
50///
51/// Failure to bootstrap the generic `ic-memory` runtime layer.
52#[derive(Debug, thiserror::Error)]
53pub enum RuntimeBootstrapError<P> {
54    /// Runtime registration or snapshot collection failed.
55    #[error(transparent)]
56    Registry(#[from] StaticMemoryDeclarationError),
57    /// Runtime range authority table is invalid.
58    #[error(transparent)]
59    Range(#[from] MemoryManagerRangeAuthorityError),
60    /// Runtime ledger genesis construction failed.
61    #[error(transparent)]
62    LedgerIntegrity(#[from] crate::LedgerIntegrityError),
63    /// Protected ledger recovery or commit failed.
64    #[error(transparent)]
65    LedgerCommit(#[from] crate::LedgerCommitError),
66    /// Stable-cell ledger storage is corrupt before protected recovery can run.
67    #[error(transparent)]
68    StableCellLedger(#[from] StableCellLedgerError),
69    /// Declaration validation failed.
70    #[error(transparent)]
71    Validation(#[from] crate::AllocationValidationError<RuntimePolicyError<P>>),
72    /// Validated declarations could not be staged.
73    #[error(transparent)]
74    Staging(#[from] crate::AllocationStageError),
75    /// Runtime state lock was poisoned.
76    #[error("ic-memory runtime lock poisoned")]
77    RuntimeLockPoisoned,
78}
79
80///
81/// RuntimeOpenError
82///
83/// Failure to open a validated allocation through the default runtime substrate.
84#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
85pub enum RuntimeOpenError {
86    /// Runtime bootstrap has not published validated allocations.
87    #[error("ic-memory runtime has not completed bootstrap validation")]
88    NotBootstrapped,
89    /// Runtime state lock was poisoned.
90    #[error("ic-memory runtime lock poisoned")]
91    RuntimeLockPoisoned,
92    /// Stable-key grammar failure.
93    #[error(transparent)]
94    StableKey(#[from] crate::StableKeyError),
95    /// The stable key was not present in the validated declaration set.
96    #[error("stable key '{0}' was not validated by ic-memory runtime bootstrap")]
97    StableKeyNotValidated(String),
98    /// The validated slot is not a usable `MemoryManager` ID.
99    #[error(transparent)]
100    MemoryManagerSlot(#[from] MemoryManagerSlotError),
101    /// The requested memory ID does not match the validated stable-key binding.
102    #[error(
103        "stable key '{stable_key}' is validated for MemoryManager ID {validated_id}, not requested ID {requested_id}"
104    )]
105    MemoryIdMismatch {
106        /// Stable key being opened.
107        stable_key: String,
108        /// Validated MemoryManager ID.
109        validated_id: u8,
110        /// Requested MemoryManager ID.
111        requested_id: u8,
112    },
113}
114
115///
116/// RuntimeDiagnosticError
117///
118/// Failure to build diagnostics for the default `MemoryManager` runtime.
119///
120
121#[derive(Debug, thiserror::Error)]
122pub enum RuntimeDiagnosticError {
123    /// Runtime bootstrap has not opened and validated the ledger cell.
124    #[error("ic-memory runtime has not completed bootstrap validation")]
125    NotBootstrapped,
126    /// The recovered allocation ledger failed protected commit validation.
127    #[error(transparent)]
128    LedgerCommit(#[from] LedgerCommitError),
129    /// Stable-cell ledger storage is corrupt before protected recovery can run.
130    #[error(transparent)]
131    StableCellLedger(#[from] StableCellLedgerError),
132    /// A committed allocation slot was not a usable `MemoryManager` ID.
133    #[error(transparent)]
134    MemoryManagerSlot(#[from] MemoryManagerSlotError),
135}
136
137///
138/// RuntimePolicyError
139///
140/// Failure in generic runtime range policy or caller-supplied policy.
141#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
142pub enum RuntimePolicyError<P> {
143    /// Runtime range authority rejected the declaration.
144    #[error(transparent)]
145    Range(#[from] MemoryManagerRangeAuthorityError),
146    /// Runtime metadata is internally inconsistent.
147    #[error("runtime declaration metadata is missing for stable key '{0}'")]
148    MissingDeclarationMetadata(String),
149    /// `ic_memory.*` stable keys are reserved to the `ic-memory` authority.
150    #[error("stable key '{stable_key}' is reserved to authority '{expected_authority}'")]
151    ReservedStableKeyAuthority {
152        /// Stable key being declared.
153        stable_key: String,
154        /// Required declaring authority.
155        expected_authority: &'static str,
156    },
157    /// Caller-supplied policy rejected the declaration.
158    #[error(transparent)]
159    Custom(P),
160}
161
162/// Register a pre-bootstrap declaration hook.
163pub fn defer_eager_init(f: fn()) {
164    assert!(
165        !is_default_memory_manager_bootstrapped(),
166        "ic-memory eager-init registration attempted after runtime bootstrap"
167    );
168    EAGER_INIT_HOOKS
169        .lock()
170        .expect("ic-memory eager-init queue poisoned")
171        .push(f);
172}
173
174/// Return true once default runtime bootstrap has completed.
175#[must_use]
176pub fn is_default_memory_manager_bootstrapped() -> bool {
177    BOOTSTRAPPED.load(Ordering::SeqCst)
178}
179
180/// Return the published validated allocations for the default runtime substrate.
181pub fn validated_allocations() -> Result<ValidatedAllocations, RuntimeOpenError> {
182    if !is_default_memory_manager_bootstrapped() {
183        return Err(RuntimeOpenError::NotBootstrapped);
184    }
185    VALIDATED_ALLOCATIONS
186        .lock()
187        .map_err(|_| RuntimeOpenError::RuntimeLockPoisoned)?
188        .clone()
189        .ok_or(RuntimeOpenError::NotBootstrapped)
190}
191
192/// Bootstrap the default `MemoryManager<DefaultMemoryImpl>` runtime using generic policy.
193pub fn bootstrap_default_memory_manager()
194-> Result<ValidatedAllocations, RuntimeBootstrapError<Infallible>> {
195    bootstrap_default_memory_manager_with_policy(&NoopPolicy)
196}
197
198/// Bootstrap the default runtime and layer caller-supplied policy over generic range checks.
199///
200/// Authority order is explicit:
201///
202/// 1. `ic-memory` always owns its governance range.
203/// 2. If any user range is registered, all `MemoryManager` declarations must
204///    belong to the range claimed by their declaring crate.
205/// 3. The caller-supplied [`AllocationPolicy`] then applies framework-specific
206///    namespace and lifecycle rules.
207///
208/// Framework adapters such as Canic should register only the ranges they want
209/// this generic runtime to enforce. If a framework wants its own policy to be
210/// authoritative for application space, it should omit user range registrations
211/// for that space and enforce the rule in its [`AllocationPolicy`].
212pub fn bootstrap_default_memory_manager_with_policy<P: AllocationPolicy>(
213    policy: &P,
214) -> Result<ValidatedAllocations, RuntimeBootstrapError<P::Error>> {
215    if let Ok(validated) = validated_allocations() {
216        return Ok(validated);
217    }
218
219    run_eager_init_hooks();
220
221    let registered_declarations = static_memory_declarations()?;
222    let registered_ranges = static_memory_range_declarations()?;
223    let user_ranges_registered = !registered_ranges.is_empty();
224    let declaration_metadata = declaration_metadata(&registered_declarations);
225    let range_authority = range_authority(registered_ranges)?;
226    let snapshot = declaration_snapshot(registered_declarations)?;
227    seal_static_memory_registry()?;
228    let policy = RuntimeMemoryManagerPolicy {
229        range_authority,
230        user_ranges_registered,
231        declaration_metadata,
232        custom_policy: policy,
233    };
234    let genesis = AllocationLedger::new(0, AllocationHistory::default())?;
235
236    let validated = with_default_ledger_cell(
237        |cell| -> Result<ValidatedAllocations, RuntimeBootstrapError<P::Error>> {
238            let mut record = cell.get().clone();
239            let mut bootstrap = AllocationBootstrap::new(record.store_mut());
240            let commit = bootstrap
241                .initialize_validate_and_commit(&genesis, snapshot, &policy, None)
242                .map_err(runtime_bootstrap_error_from_bootstrap)?;
243            cell.set(record);
244            Ok(commit.validated)
245        },
246    )?;
247
248    publish_validated_allocations(validated.clone())?;
249    BOOTSTRAPPED.store(true, Ordering::SeqCst);
250    Ok(validated)
251}
252
253/// Open a validated `MemoryManager` memory by stable key and expected ID.
254pub fn open_default_memory_manager_memory(
255    stable_key: &str,
256    id: u8,
257) -> Result<VirtualMemory<DefaultMemoryImpl>, RuntimeOpenError> {
258    let key = StableKey::parse(stable_key)?;
259    let validated = validated_allocations()?;
260    let slot = validated
261        .slot_for(&key)
262        .ok_or_else(|| RuntimeOpenError::StableKeyNotValidated(stable_key.to_string()))?;
263    let validated_id = slot.memory_manager_id()?;
264    if validated_id != id {
265        return Err(RuntimeOpenError::MemoryIdMismatch {
266            stable_key: stable_key.to_string(),
267            validated_id,
268            requested_id: id,
269        });
270    }
271    Ok(default_memory_manager_memory(id))
272}
273
274/// Build a diagnostic export for the default `MemoryManager` runtime.
275///
276/// Each allocation record includes the live `VirtualMemory::size()` for its
277/// slot when the committed ledger can be recovered. The reported size is the
278/// virtual memory size in WebAssembly pages and bytes, not logical data bytes
279/// stored by a particular stable-structure collection.
280pub fn default_memory_manager_diagnostic_export() -> Result<DiagnosticExport, RuntimeDiagnosticError>
281{
282    let record = default_ledger_record_for_diagnostics()?;
283    let recovered = record.store().recover()?;
284    let ledger = recovered.ledger();
285    let memory_sizes = default_memory_manager_memory_sizes(ledger)?;
286
287    Ok(
288        DiagnosticExport::from_ledger_with_commit_recovery_and_memory_sizes(
289            ledger,
290            AllocationSlotDescriptor::memory_manager(MEMORY_MANAGER_LEDGER_ID)?,
291            Some(record.store().physical().diagnostic()),
292            memory_sizes,
293        ),
294    )
295}
296
297/// Build a protected commit recovery diagnostic for the default ledger store.
298///
299/// Unlike [`default_memory_manager_diagnostic_export`], this helper does not
300/// require successful bootstrap. It can diagnose empty or partially corrupt
301/// dual-slot commit state as long as the enclosing stable-cell ledger record is
302/// readable.
303pub fn default_memory_manager_commit_recovery_diagnostic()
304-> Result<CommitStoreDiagnostic, RuntimeDiagnosticError> {
305    let record = default_ledger_record_from_memory()?;
306    Ok(record.store().physical().diagnostic())
307}
308
309/// Build a preflight and runtime diagnostic report for the default runtime.
310///
311/// The doctor report can be collected before bootstrap, after bootstrap, or
312/// after a failed bootstrap attempt. Stable-cell, commit-recovery, declaration,
313/// range-authority, validation, ledger, and live memory-size status are
314/// collected into one serializable report. Recoverable problems are reported in
315/// fields rather than returned as errors.
316#[must_use]
317pub fn default_memory_manager_doctor_report() -> DefaultMemoryManagerDoctorReport {
318    if !is_default_memory_manager_bootstrapped() {
319        run_eager_init_hooks();
320    }
321
322    let stable_cell = default_memory_manager_stable_cell_diagnostic();
323    let commit_recovery = stable_cell
324        .record
325        .as_ref()
326        .map(|record| record.store().physical().diagnostic());
327    let recovered = stable_cell
328        .record
329        .as_ref()
330        .map(|record| record.store().recover());
331    let recovered_for_export = recovered.as_ref().and_then(|result| result.as_ref().ok());
332    let ledger_anchor = default_ledger_anchor_descriptor();
333    let ledger = recovered_for_export.map(|recovered| {
334        DiagnosticExport::from_ledger_with_commit_recovery_and_memory_sizes(
335            recovered.ledger(),
336            ledger_anchor.clone(),
337            commit_recovery,
338            default_memory_manager_memory_sizes_lossy(recovered.ledger()),
339        )
340    });
341
342    let registered_declarations = static_memory_declarations();
343    let registered_ranges = static_memory_range_declarations();
344    let diagnostic_declarations = registered_declarations
345        .as_ref()
346        .map(|declarations| {
347            declarations
348                .iter()
349                .map(|registration| {
350                    DiagnosticDeclaration::new(
351                        registration.declaring_crate(),
352                        registration.declaration().clone(),
353                    )
354                })
355                .collect()
356        })
357        .unwrap_or_default();
358    let range_authority = diagnostic_range_authority(&registered_ranges);
359    let validation = diagnostic_validation(
360        &registered_declarations,
361        &registered_ranges,
362        stable_cell.record.as_ref(),
363        recovered.as_ref(),
364    );
365
366    DefaultMemoryManagerDoctorReport {
367        bootstrapped: is_default_memory_manager_bootstrapped(),
368        ledger_anchor,
369        stable_cell: stable_cell.diagnostic,
370        commit_recovery,
371        ledger,
372        registered_declarations: diagnostic_declarations,
373        range_authority,
374        validation,
375    }
376}
377
378fn run_eager_init_hooks() {
379    let hooks = {
380        let mut hooks = EAGER_INIT_HOOKS
381            .lock()
382            .expect("ic-memory eager-init queue poisoned");
383        std::mem::take(&mut *hooks)
384    };
385
386    for hook in hooks {
387        hook();
388    }
389}
390
391fn with_default_ledger_cell<P, T>(
392    op: impl FnOnce(&mut DefaultLedgerCell) -> Result<T, RuntimeBootstrapError<P>>,
393) -> Result<T, RuntimeBootstrapError<P>> {
394    DEFAULT_LEDGER_CELL.with(|cell| {
395        let mut cell = cell.borrow_mut();
396        if cell.is_none() {
397            let memory = default_memory_manager_memory(MEMORY_MANAGER_LEDGER_ID);
398            crate::validate_stable_cell_ledger_memory(&memory)?;
399            *cell = Some(Cell::init(memory, StableCellLedgerRecord::default()));
400        }
401        op(cell.as_mut().expect("default ledger cell initialized"))
402    })
403}
404
405fn default_memory_manager_memory(id: u8) -> VirtualMemory<DefaultMemoryImpl> {
406    DEFAULT_MEMORY_MANAGER.with(|manager| manager.get(MemoryId::new(id)))
407}
408
409const fn default_ledger_anchor_descriptor() -> AllocationSlotDescriptor {
410    AllocationSlotDescriptor::memory_manager_unchecked(MEMORY_MANAGER_LEDGER_ID)
411}
412
413fn default_ledger_record_for_diagnostics() -> Result<StableCellLedgerRecord, RuntimeDiagnosticError>
414{
415    if !is_default_memory_manager_bootstrapped() {
416        return Err(RuntimeDiagnosticError::NotBootstrapped);
417    }
418
419    default_ledger_record_from_memory().map_err(RuntimeDiagnosticError::StableCellLedger)
420}
421
422fn default_ledger_record_from_memory() -> Result<StableCellLedgerRecord, StableCellLedgerError> {
423    let memory = default_memory_manager_memory(MEMORY_MANAGER_LEDGER_ID);
424    if memory.size() == 0 {
425        return Ok(StableCellLedgerRecord::default());
426    }
427
428    let payload = decode_stable_cell_payload(&memory)?;
429    decode_stable_cell_ledger_record(&payload).map_err(StableCellLedgerError::Record)
430}
431
432fn default_memory_manager_memory_sizes(
433    ledger: &AllocationLedger,
434) -> Result<Vec<(AllocationSlotDescriptor, DiagnosticMemorySize)>, RuntimeDiagnosticError> {
435    ledger
436        .allocation_history()
437        .records()
438        .iter()
439        .map(|record| {
440            let id = record.slot().memory_manager_id()?;
441            let memory = default_memory_manager_memory(id);
442            Ok((
443                record.slot().clone(),
444                DiagnosticMemorySize::from_wasm_pages(memory.size()),
445            ))
446        })
447        .collect()
448}
449
450fn default_memory_manager_memory_sizes_lossy(
451    ledger: &AllocationLedger,
452) -> Vec<(AllocationSlotDescriptor, DiagnosticMemorySize)> {
453    default_memory_manager_memory_sizes(ledger).unwrap_or_default()
454}
455
456struct DefaultStableCellDiagnostic {
457    diagnostic: DiagnosticStableCell,
458    record: Option<StableCellLedgerRecord>,
459}
460
461fn default_memory_manager_stable_cell_diagnostic() -> DefaultStableCellDiagnostic {
462    let memory = default_memory_manager_memory(MEMORY_MANAGER_LEDGER_ID);
463    let memory_size = DiagnosticMemorySize::from_wasm_pages(memory.size());
464    if memory.size() == 0 {
465        return DefaultStableCellDiagnostic {
466            diagnostic: DiagnosticStableCell::new(
467                DiagnosticStableCellStatus::Empty,
468                memory_size,
469                None,
470            ),
471            record: Some(StableCellLedgerRecord::default()),
472        };
473    }
474
475    let record = decode_stable_cell_payload(&memory)
476        .map_err(StableCellLedgerError::Payload)
477        .and_then(|payload| {
478            decode_stable_cell_ledger_record(&payload).map_err(StableCellLedgerError::Record)
479        });
480    match record {
481        Ok(record) => DefaultStableCellDiagnostic {
482            diagnostic: DiagnosticStableCell::new(
483                DiagnosticStableCellStatus::Readable,
484                memory_size,
485                None,
486            ),
487            record: Some(record),
488        },
489        Err(err) => DefaultStableCellDiagnostic {
490            diagnostic: DiagnosticStableCell::new(
491                DiagnosticStableCellStatus::Corrupt,
492                memory_size,
493                Some(err.to_string()),
494            ),
495            record: None,
496        },
497    }
498}
499
500fn diagnostic_range_authority(
501    registered_ranges: &Result<Vec<StaticMemoryRangeDeclaration>, StaticMemoryDeclarationError>,
502) -> DiagnosticRangeAuthority {
503    match registered_ranges {
504        Ok(ranges) => {
505            let registered_records = ranges
506                .iter()
507                .map(|registration| registration.record().clone())
508                .collect();
509            match range_authority(ranges.clone()) {
510                Ok(authority) => {
511                    DiagnosticRangeAuthority::new(registered_records, Some(authority), None)
512                }
513                Err(err) => {
514                    DiagnosticRangeAuthority::new(registered_records, None, Some(err.to_string()))
515                }
516            }
517        }
518        Err(err) => DiagnosticRangeAuthority::new(Vec::new(), None, Some(err.to_string())),
519    }
520}
521
522fn diagnostic_validation(
523    registered_declarations: &Result<Vec<StaticMemoryDeclaration>, StaticMemoryDeclarationError>,
524    registered_ranges: &Result<Vec<StaticMemoryRangeDeclaration>, StaticMemoryDeclarationError>,
525    stable_cell_record: Option<&StableCellLedgerRecord>,
526    recovered: Option<&Result<crate::RecoveredLedger, LedgerCommitError>>,
527) -> DiagnosticCheck {
528    let registered_declarations = match registered_declarations {
529        Ok(declarations) => declarations.clone(),
530        Err(err) => return DiagnosticCheck::failed(format!("declaration registry: {err}")),
531    };
532    let registered_ranges = match registered_ranges {
533        Ok(ranges) => ranges.clone(),
534        Err(err) => return DiagnosticCheck::failed(format!("range registry: {err}")),
535    };
536    let range_authority = match range_authority(registered_ranges.clone()) {
537        Ok(authority) => authority,
538        Err(err) => return DiagnosticCheck::failed(format!("range authority: {err}")),
539    };
540    let snapshot = match declaration_snapshot(registered_declarations.clone()) {
541        Ok(snapshot) => snapshot,
542        Err(err) => return DiagnosticCheck::failed(format!("declaration snapshot: {err}")),
543    };
544    let recovered = match diagnostic_validation_ledger(stable_cell_record, recovered) {
545        Ok(recovered) => recovered,
546        Err(reason) => return DiagnosticCheck::not_run(reason),
547    };
548    let policy = RuntimeMemoryManagerPolicy {
549        range_authority,
550        user_ranges_registered: !registered_ranges.is_empty(),
551        declaration_metadata: declaration_metadata(&registered_declarations),
552        custom_policy: &NoopPolicy,
553    };
554
555    match crate::validate_allocations(&recovered, snapshot, &policy) {
556        Ok(_) => DiagnosticCheck::passed(),
557        Err(err) => DiagnosticCheck::failed(err.to_string()),
558    }
559}
560
561fn diagnostic_validation_ledger(
562    stable_cell_record: Option<&StableCellLedgerRecord>,
563    recovered: Option<&Result<crate::RecoveredLedger, LedgerCommitError>>,
564) -> Result<crate::RecoveredLedger, String> {
565    if let Some(Ok(recovered)) = recovered {
566        return Ok(recovered.clone());
567    }
568    if let Some(Err(err)) = recovered {
569        if stable_cell_record.is_some_and(|record| record.store().physical().is_uninitialized()) {
570            return diagnostic_genesis_recovered_ledger();
571        }
572        return Err(format!("protected ledger recovery: {err}"));
573    }
574    if stable_cell_record.is_some() {
575        return diagnostic_genesis_recovered_ledger();
576    }
577    Err("stable-cell ledger record is not readable".to_string())
578}
579
580fn diagnostic_genesis_recovered_ledger() -> Result<crate::RecoveredLedger, String> {
581    AllocationLedger::new(0, AllocationHistory::default())
582        .map(|ledger| crate::RecoveredLedger::from_trusted_parts(ledger, 0))
583        .map_err(|err| format!("genesis ledger: {err}"))
584}
585
586fn publish_validated_allocations<P>(
587    validated: ValidatedAllocations,
588) -> Result<(), RuntimeBootstrapError<P>> {
589    *VALIDATED_ALLOCATIONS
590        .lock()
591        .map_err(|_| RuntimeBootstrapError::RuntimeLockPoisoned)? = Some(validated);
592    Ok(())
593}
594
595fn declaration_snapshot(
596    registrations: Vec<StaticMemoryDeclaration>,
597) -> Result<DeclarationSnapshot, StaticMemoryDeclarationError> {
598    let mut declarations = Vec::with_capacity(registrations.len() + 1);
599    declarations.push(internal_ledger_declaration()?);
600    declarations.extend(
601        registrations
602            .into_iter()
603            .map(StaticMemoryDeclaration::into_declaration),
604    );
605    DeclarationSnapshot::new(declarations).map_err(StaticMemoryDeclarationError::Declaration)
606}
607
608fn declaration_metadata(registrations: &[StaticMemoryDeclaration]) -> BTreeMap<String, String> {
609    let mut metadata = BTreeMap::new();
610    metadata.insert(
611        IC_MEMORY_LEDGER_STABLE_KEY.to_string(),
612        IC_MEMORY_AUTHORITY_OWNER.to_string(),
613    );
614    for registration in registrations {
615        metadata.insert(
616            registration.declaration().stable_key().as_str().to_string(),
617            registration.declaring_crate().to_string(),
618        );
619    }
620    metadata
621}
622
623fn range_authority(
624    registrations: Vec<StaticMemoryRangeDeclaration>,
625) -> Result<MemoryManagerRangeAuthority, MemoryManagerRangeAuthorityError> {
626    let mut records = Vec::with_capacity(registrations.len() + 1);
627    records.push(internal_ledger_range()?);
628    records.extend(
629        registrations
630            .into_iter()
631            .map(StaticMemoryRangeDeclaration::into_record),
632    );
633    MemoryManagerRangeAuthority::from_records(records)
634}
635
636fn internal_ledger_declaration() -> Result<AllocationDeclaration, crate::DeclarationSnapshotError> {
637    AllocationDeclaration::memory_manager(
638        IC_MEMORY_LEDGER_STABLE_KEY,
639        MEMORY_MANAGER_LEDGER_ID,
640        IC_MEMORY_LEDGER_LABEL,
641    )
642}
643
644fn internal_ledger_range() -> Result<MemoryManagerAuthorityRecord, MemoryManagerRangeAuthorityError>
645{
646    MemoryManagerAuthorityRecord::new(
647        MemoryManagerIdRange::new(
648            MEMORY_MANAGER_LEDGER_ID,
649            crate::MEMORY_MANAGER_GOVERNANCE_MAX_ID,
650        )?,
651        IC_MEMORY_AUTHORITY_OWNER,
652        MemoryManagerRangeMode::Reserved,
653        Some(IC_MEMORY_AUTHORITY_PURPOSE.to_string()),
654    )
655}
656
657fn runtime_bootstrap_error_from_bootstrap<P>(
658    err: crate::BootstrapError<RuntimePolicyError<P>>,
659) -> RuntimeBootstrapError<P> {
660    match err {
661        crate::BootstrapError::Ledger(err) => RuntimeBootstrapError::LedgerCommit(err),
662        crate::BootstrapError::Validation(err) => RuntimeBootstrapError::Validation(err),
663        crate::BootstrapError::Staging(err) => RuntimeBootstrapError::Staging(err),
664    }
665}
666
667struct RuntimeMemoryManagerPolicy<'a, P> {
668    range_authority: MemoryManagerRangeAuthority,
669    user_ranges_registered: bool,
670    declaration_metadata: BTreeMap<String, String>,
671    custom_policy: &'a P,
672}
673
674impl<P: AllocationPolicy> AllocationPolicy for RuntimeMemoryManagerPolicy<'_, P> {
675    type Error = RuntimePolicyError<P::Error>;
676
677    fn validate_key(&self, key: &StableKey) -> Result<(), Self::Error> {
678        let declaring_crate = self.declaring_crate(key)?;
679        if crate::is_ic_memory_stable_key(key.as_str())
680            && declaring_crate != IC_MEMORY_AUTHORITY_OWNER
681        {
682            return Err(RuntimePolicyError::ReservedStableKeyAuthority {
683                stable_key: key.as_str().to_string(),
684                expected_authority: IC_MEMORY_AUTHORITY_OWNER,
685            });
686        }
687        self.custom_policy
688            .validate_key(key)
689            .map_err(RuntimePolicyError::Custom)
690    }
691
692    fn validate_slot(
693        &self,
694        key: &StableKey,
695        slot: &AllocationSlotDescriptor,
696    ) -> Result<(), Self::Error> {
697        self.validate_runtime_range(key, slot)?;
698        self.custom_policy
699            .validate_slot(key, slot)
700            .map_err(RuntimePolicyError::Custom)
701    }
702
703    fn validate_reserved_slot(
704        &self,
705        key: &StableKey,
706        slot: &AllocationSlotDescriptor,
707    ) -> Result<(), Self::Error> {
708        self.validate_runtime_range(key, slot)?;
709        self.custom_policy
710            .validate_reserved_slot(key, slot)
711            .map_err(RuntimePolicyError::Custom)
712    }
713}
714
715impl<P: AllocationPolicy> RuntimeMemoryManagerPolicy<'_, P> {
716    fn declaring_crate(&self, key: &StableKey) -> Result<&str, RuntimePolicyError<P::Error>> {
717        self.declaration_metadata
718            .get(key.as_str())
719            .map(String::as_str)
720            .ok_or_else(|| RuntimePolicyError::MissingDeclarationMetadata(key.as_str().to_string()))
721    }
722
723    fn validate_runtime_range(
724        &self,
725        key: &StableKey,
726        slot: &AllocationSlotDescriptor,
727    ) -> Result<(), RuntimePolicyError<P::Error>> {
728        let declaring_crate = self.declaring_crate(key)?;
729        // Range claims are authoritative generic policy in the default runtime.
730        // Once any user range is registered, every user declaration must fit
731        // the declaring crate's claimed range. With no user ranges, only the
732        // internal ic-memory governance range is enforced here and custom
733        // policy may decide application-space ownership.
734        if declaring_crate == IC_MEMORY_AUTHORITY_OWNER || self.user_ranges_registered {
735            self.range_authority
736                .validate_slot_authority(slot, declaring_crate)?;
737            return Ok(());
738        }
739
740        let id = slot
741            .memory_manager_id()
742            .map_err(MemoryManagerRangeAuthorityError::Slot)?;
743        if self
744            .range_authority
745            .authority_for_id(id)
746            .map_err(RuntimePolicyError::Range)?
747            .is_some()
748        {
749            self.range_authority
750                .validate_slot_authority(slot, declaring_crate)?;
751        }
752        Ok(())
753    }
754}
755
756struct NoopPolicy;
757
758impl AllocationPolicy for NoopPolicy {
759    type Error = Infallible;
760
761    fn validate_key(&self, _key: &StableKey) -> Result<(), Self::Error> {
762        Ok(())
763    }
764
765    fn validate_slot(
766        &self,
767        _key: &StableKey,
768        _slot: &AllocationSlotDescriptor,
769    ) -> Result<(), Self::Error> {
770        Ok(())
771    }
772
773    fn validate_reserved_slot(
774        &self,
775        _key: &StableKey,
776        _slot: &AllocationSlotDescriptor,
777    ) -> Result<(), Self::Error> {
778        Ok(())
779    }
780}
781
782#[cfg(test)]
783pub(crate) fn reset_for_tests() {
784    crate::registry::reset_static_memory_declarations_for_tests();
785    EAGER_INIT_HOOKS
786        .lock()
787        .expect("ic-memory eager-init queue poisoned")
788        .clear();
789    *VALIDATED_ALLOCATIONS
790        .lock()
791        .expect("ic-memory runtime validation state poisoned") = None;
792    BOOTSTRAPPED.store(false, Ordering::SeqCst);
793    DEFAULT_LEDGER_CELL.with_borrow_mut(|cell| {
794        *cell = None;
795    });
796}
797
798#[cfg(test)]
799mod tests {
800    use super::*;
801    use crate::registry::{
802        TEST_REGISTRY_LOCK, register_static_memory_manager_declaration,
803        register_static_memory_manager_range,
804    };
805    use std::sync::atomic::{AtomicBool, Ordering};
806
807    static EAGER_INIT_RAN: AtomicBool = AtomicBool::new(false);
808
809    fn register_crate_a() {
810        register_static_memory_manager_range(
811            100,
812            109,
813            "crate_a",
814            MemoryManagerRangeMode::Reserved,
815            None,
816        )
817        .expect("crate A range");
818        register_static_memory_manager_declaration(100, "crate_a", "users", "crate_a.users.v1")
819            .expect("crate A memory");
820    }
821
822    fn register_crate_b() {
823        register_static_memory_manager_range(
824            110,
825            119,
826            "crate_b",
827            MemoryManagerRangeMode::Reserved,
828            None,
829        )
830        .expect("crate B range");
831        register_static_memory_manager_declaration(110, "crate_b", "orders", "crate_b.orders.v1")
832            .expect("crate B memory");
833    }
834
835    fn mark_eager_init() {
836        EAGER_INIT_RAN.store(true, Ordering::SeqCst);
837        register_static_memory_manager_declaration(101, "crate_a", "audit", "crate_a.audit.v1")
838            .expect("eager-init declaration");
839    }
840
841    #[test]
842    fn multi_crate_declarations_compose_into_one_bootstrap() {
843        let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
844        reset_for_tests();
845        register_crate_a();
846        register_crate_b();
847
848        let validated = bootstrap_default_memory_manager().expect("bootstrap");
849
850        assert_eq!(validated.declarations().len(), 3);
851        assert!(
852            validated
853                .declarations()
854                .iter()
855                .any(|declaration| declaration.stable_key().as_str() == "crate_a.users.v1")
856        );
857        assert!(
858            validated
859                .declarations()
860                .iter()
861                .any(|declaration| declaration.stable_key().as_str() == "crate_b.orders.v1")
862        );
863    }
864
865    #[test]
866    fn conflicting_ranges_fail() {
867        let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
868        reset_for_tests();
869        register_static_memory_manager_range(
870            100,
871            110,
872            "crate_a",
873            MemoryManagerRangeMode::Reserved,
874            None,
875        )
876        .expect("crate A range");
877        register_static_memory_manager_range(
878            105,
879            119,
880            "crate_b",
881            MemoryManagerRangeMode::Reserved,
882            None,
883        )
884        .expect("crate B range");
885
886        let err = bootstrap_default_memory_manager().expect_err("overlap must fail");
887        assert!(matches!(
888            err,
889            RuntimeBootstrapError::Range(
890                MemoryManagerRangeAuthorityError::OverlappingRanges { .. }
891            )
892        ));
893    }
894
895    #[test]
896    fn duplicate_stable_keys_fail() {
897        let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
898        reset_for_tests();
899        register_static_memory_manager_declaration(100, "crate_a", "users", "app.users.v1")
900            .expect("first declaration");
901        register_static_memory_manager_declaration(101, "crate_b", "users", "app.users.v1")
902            .expect("second declaration");
903
904        let err = bootstrap_default_memory_manager().expect_err("duplicate key must fail");
905        assert!(matches!(
906            err,
907            RuntimeBootstrapError::Registry(StaticMemoryDeclarationError::Declaration(
908                crate::DeclarationSnapshotError::DuplicateStableKey(_)
909            ))
910        ));
911    }
912
913    #[test]
914    fn duplicate_memory_manager_ids_fail() {
915        let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
916        reset_for_tests();
917        register_static_memory_manager_declaration(100, "crate_a", "users", "crate_a.users.v1")
918            .expect("first declaration");
919        register_static_memory_manager_declaration(100, "crate_b", "orders", "crate_b.orders.v1")
920            .expect("second declaration");
921
922        let err = bootstrap_default_memory_manager().expect_err("duplicate slot must fail");
923        assert!(matches!(
924            err,
925            RuntimeBootstrapError::Registry(StaticMemoryDeclarationError::Declaration(
926                crate::DeclarationSnapshotError::DuplicateSlot(_)
927            ))
928        ));
929    }
930
931    #[test]
932    fn out_of_range_memory_declaration_fails_when_ranges_are_declared() {
933        let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
934        reset_for_tests();
935        register_static_memory_manager_range(
936            100,
937            109,
938            "crate_a",
939            MemoryManagerRangeMode::Reserved,
940            None,
941        )
942        .expect("crate A range");
943        register_static_memory_manager_declaration(120, "crate_a", "users", "crate_a.users.v1")
944            .expect("out-of-range declaration");
945
946        let err = bootstrap_default_memory_manager().expect_err("out of range must fail");
947        assert!(matches!(
948            err,
949            RuntimeBootstrapError::Validation(crate::AllocationValidationError::Policy(
950                RuntimePolicyError::Range(MemoryManagerRangeAuthorityError::UnclaimedId {
951                    id: 120
952                })
953            ))
954        ));
955    }
956
957    #[test]
958    fn late_registration_after_bootstrap_fails() {
959        let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
960        reset_for_tests();
961        register_static_memory_manager_declaration(100, "crate_a", "users", "crate_a.users.v1")
962            .expect("declaration");
963        bootstrap_default_memory_manager().expect("bootstrap");
964
965        let err = register_static_memory_manager_declaration(
966            101,
967            "crate_a",
968            "orders",
969            "crate_a.orders.v1",
970        )
971        .expect_err("late registration must fail");
972        assert_eq!(err, StaticMemoryDeclarationError::RegistrySealed);
973    }
974
975    #[test]
976    fn late_eager_init_registration_after_bootstrap_fails() {
977        let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
978        reset_for_tests();
979        register_static_memory_manager_declaration(100, "crate_a", "users", "crate_a.users.v1")
980            .expect("declaration");
981        bootstrap_default_memory_manager().expect("bootstrap");
982
983        let err = std::panic::catch_unwind(|| defer_eager_init(mark_eager_init))
984            .expect_err("late eager-init registration must fail");
985
986        let message = err
987            .downcast_ref::<String>()
988            .map(String::as_str)
989            .or_else(|| err.downcast_ref::<&str>().copied())
990            .expect("panic message");
991        assert!(message.contains("after runtime bootstrap"));
992    }
993
994    #[test]
995    fn eager_init_runs_before_snapshot_seal() {
996        let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
997        reset_for_tests();
998        EAGER_INIT_RAN.store(false, Ordering::SeqCst);
999        register_static_memory_manager_range(
1000            100,
1001            109,
1002            "crate_a",
1003            MemoryManagerRangeMode::Reserved,
1004            None,
1005        )
1006        .expect("crate A range");
1007        defer_eager_init(mark_eager_init);
1008
1009        let validated = bootstrap_default_memory_manager().expect("bootstrap");
1010
1011        assert!(EAGER_INIT_RAN.load(Ordering::SeqCst));
1012        assert!(
1013            validated
1014                .declarations()
1015                .iter()
1016                .any(|declaration| declaration.stable_key().as_str() == "crate_a.audit.v1")
1017        );
1018    }
1019
1020    #[test]
1021    fn direct_user_can_bootstrap_and_open_without_canic() {
1022        let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
1023        reset_for_tests();
1024        register_static_memory_manager_range(
1025            120,
1026            129,
1027            "icydb",
1028            MemoryManagerRangeMode::Reserved,
1029            None,
1030        )
1031        .expect("icydb range");
1032        register_static_memory_manager_declaration(120, "icydb", "users", "icydb.users.data.v1")
1033            .expect("icydb declaration");
1034
1035        bootstrap_default_memory_manager().expect("bootstrap");
1036        open_default_memory_manager_memory("icydb.users.data.v1", 120).expect("open memory");
1037    }
1038
1039    #[test]
1040    fn diagnostic_export_reports_default_memory_manager_sizes() {
1041        let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
1042        reset_for_tests();
1043        register_static_memory_manager_range(
1044            130,
1045            139,
1046            "diagnostics",
1047            MemoryManagerRangeMode::Reserved,
1048            None,
1049        )
1050        .expect("diagnostics range");
1051        register_static_memory_manager_declaration(
1052            130,
1053            "diagnostics",
1054            "users",
1055            "diagnostics.users.v1",
1056        )
1057        .expect("diagnostics declaration");
1058
1059        bootstrap_default_memory_manager().expect("bootstrap");
1060        let memory =
1061            open_default_memory_manager_memory("diagnostics.users.v1", 130).expect("open memory");
1062        let old_size = memory.size();
1063        memory.grow(2);
1064
1065        let export = default_memory_manager_diagnostic_export().expect("diagnostic export");
1066        let recovery =
1067            default_memory_manager_commit_recovery_diagnostic().expect("recovery diagnostic");
1068        let record = export
1069            .records
1070            .iter()
1071            .find(|record| record.allocation.stable_key().as_str() == "diagnostics.users.v1")
1072            .expect("diagnostic allocation");
1073
1074        assert_eq!(
1075            recovery.authoritative_generation,
1076            Some(export.current_generation)
1077        );
1078        assert_eq!(
1079            record.memory_size,
1080            Some(DiagnosticMemorySize::from_wasm_pages(old_size + 2))
1081        );
1082    }
1083
1084    #[test]
1085    fn doctor_report_preflights_before_bootstrap() {
1086        let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
1087        reset_for_tests();
1088        register_static_memory_manager_range(
1089            240,
1090            240,
1091            "doctor_preflight",
1092            MemoryManagerRangeMode::Reserved,
1093            None,
1094        )
1095        .expect("doctor range");
1096        register_static_memory_manager_declaration(
1097            240,
1098            "doctor_preflight",
1099            "users",
1100            "doctor_preflight.users.v1",
1101        )
1102        .expect("doctor declaration");
1103
1104        let report = default_memory_manager_doctor_report();
1105
1106        assert!(!report.bootstrapped);
1107        assert_eq!(report.registered_declarations.len(), 1);
1108        assert!(report.range_authority.effective_authority.is_some());
1109        assert_eq!(
1110            report.validation.status,
1111            crate::DiagnosticCheckStatus::Passed
1112        );
1113        assert!(report.commit_recovery.is_some());
1114        assert!(matches!(
1115            report.stable_cell.status,
1116            crate::DiagnosticStableCellStatus::Empty | crate::DiagnosticStableCellStatus::Readable
1117        ));
1118    }
1119
1120    #[test]
1121    fn doctor_report_includes_recovered_ledger_and_memory_sizes_after_bootstrap() {
1122        let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
1123        reset_for_tests();
1124        register_static_memory_manager_range(
1125            241,
1126            241,
1127            "doctor_runtime",
1128            MemoryManagerRangeMode::Reserved,
1129            None,
1130        )
1131        .expect("doctor range");
1132        register_static_memory_manager_declaration(
1133            241,
1134            "doctor_runtime",
1135            "orders",
1136            "doctor_runtime.orders.v1",
1137        )
1138        .expect("doctor declaration");
1139
1140        bootstrap_default_memory_manager().expect("bootstrap");
1141        let memory = open_default_memory_manager_memory("doctor_runtime.orders.v1", 241)
1142            .expect("open memory");
1143        let old_size = memory.size();
1144        memory.grow(1);
1145
1146        let report = default_memory_manager_doctor_report();
1147        let ledger = report.ledger.expect("recovered ledger export");
1148        let record = ledger
1149            .records
1150            .iter()
1151            .find(|record| record.allocation.stable_key().as_str() == "doctor_runtime.orders.v1")
1152            .expect("doctor allocation");
1153
1154        assert!(report.bootstrapped);
1155        assert_eq!(
1156            report.stable_cell.status,
1157            crate::DiagnosticStableCellStatus::Readable
1158        );
1159        assert_eq!(
1160            report.validation.status,
1161            crate::DiagnosticCheckStatus::Passed
1162        );
1163        assert_eq!(
1164            record.memory_size,
1165            Some(DiagnosticMemorySize::from_wasm_pages(old_size + 1))
1166        );
1167    }
1168
1169    #[test]
1170    fn doctor_report_captures_validation_failure() {
1171        let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
1172        reset_for_tests();
1173        register_static_memory_manager_declaration(
1174            242,
1175            "doctor_failure_a",
1176            "users",
1177            "doctor_failure.users.v1",
1178        )
1179        .expect("first declaration");
1180        register_static_memory_manager_declaration(
1181            243,
1182            "doctor_failure_b",
1183            "orders",
1184            "doctor_failure.users.v1",
1185        )
1186        .expect("second declaration");
1187
1188        let report = default_memory_manager_doctor_report();
1189
1190        assert_eq!(
1191            report.validation.status,
1192            crate::DiagnosticCheckStatus::Failed
1193        );
1194        assert!(
1195            report
1196                .validation
1197                .message
1198                .expect("validation failure message")
1199                .contains("declared more than once")
1200        );
1201    }
1202}