Skip to main content

ic_memory/
runtime.rs

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