Skip to main content

ic_memory/
runtime.rs

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