Skip to main content

ic_memory/
runtime.rs

1use crate::{
2    AllocationBootstrap, AllocationDeclaration, AllocationHistory, AllocationLedger,
3    AllocationPolicy, AllocationSlotDescriptor, DeclarationSnapshot,
4    DefaultMemoryManagerDoctorReport, DiagnosticCheck, DiagnosticDeclaration, DiagnosticExport,
5    DiagnosticMemorySize, DiagnosticRangeAuthority, DiagnosticStableCell,
6    DiagnosticStableCellStatus, LedgerCommitError, STABLE_CELL_VALUE_OFFSET, StableCellLedgerError,
7    StableCellLedgerRecord, StableKey, ValidatedAllocations,
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 VALIDATED_ALLOCATIONS: Mutex<Option<ValidatedAllocations>> = 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 validated 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 validated 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 validated declaration set.
112    #[error("stable key '{0}' was not validated by ic-memory runtime bootstrap")]
113    StableKeyNotValidated(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 validated slot is not a usable `MemoryManager` ID.
121    #[error(transparent)]
122    MemoryManagerSlot(#[from] MemoryManagerSlotError),
123    /// The requested memory ID does not match the validated stable-key binding.
124    #[error(
125        "stable key '{stable_key}' is validated for MemoryManager ID {validated_id}, not requested ID {requested_id}"
126    )]
127    MemoryIdMismatch {
128        /// Stable key being opened.
129        stable_key: String,
130        /// Validated MemoryManager ID.
131        validated_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 validated allocations for the default runtime substrate.
206pub fn validated_allocations() -> Result<ValidatedAllocations, RuntimeOpenError> {
207    if !is_default_memory_manager_bootstrapped() {
208        return Err(RuntimeOpenError::NotBootstrapped);
209    }
210    VALIDATED_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<ValidatedAllocations, 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 declaring crate.
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<ValidatedAllocations, RuntimeBootstrapError<P::Error>> {
240    if let Ok(validated) = validated_allocations() {
241        return Ok(validated);
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 validated = with_default_ledger_cell(
262        |cell| -> Result<ValidatedAllocations, 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            set_default_ledger_cell(cell, record)?;
269            Ok(external_runtime_allocations(commit.validated))
270        },
271    )?;
272
273    publish_validated_allocations(validated.clone())?;
274    BOOTSTRAPPED.store(true, Ordering::SeqCst);
275    Ok(validated)
276}
277
278/// Open a validated `MemoryManager` memory by stable key and expected ID.
279pub fn open_default_memory_manager_memory(
280    stable_key: &str,
281    id: u8,
282) -> Result<VirtualMemory<DefaultMemoryImpl>, RuntimeOpenError> {
283    let key = StableKey::parse(stable_key)?;
284    if crate::is_ic_memory_stable_key(key.as_str()) {
285        return Err(RuntimeOpenError::ReservedStableKey {
286            stable_key: stable_key.to_string(),
287        });
288    }
289    let validated = validated_allocations()?;
290    let slot = validated
291        .slot_for(&key)
292        .ok_or_else(|| RuntimeOpenError::StableKeyNotValidated(stable_key.to_string()))?;
293    let validated_id = slot.memory_manager_id()?;
294    if validated_id != id {
295        return Err(RuntimeOpenError::MemoryIdMismatch {
296            stable_key: stable_key.to_string(),
297            validated_id,
298            requested_id: id,
299        });
300    }
301    Ok(default_memory_manager_memory(id))
302}
303
304/// Build a diagnostic export for the default `MemoryManager` runtime.
305///
306/// Each allocation record includes the live `VirtualMemory::size()` for its
307/// slot when the committed ledger can be recovered. The reported size is the
308/// virtual memory size in WebAssembly pages and bytes, not logical data bytes
309/// stored by a particular stable-structure collection.
310pub fn default_memory_manager_diagnostic_export() -> Result<DiagnosticExport, RuntimeDiagnosticError>
311{
312    let record = default_ledger_record_for_diagnostics()?;
313    let recovered = record.store().recover()?;
314    let ledger = recovered.ledger();
315    let memory_sizes = default_memory_manager_memory_sizes(ledger)?;
316
317    Ok(
318        DiagnosticExport::from_ledger_with_commit_recovery_and_memory_sizes(
319            ledger,
320            AllocationSlotDescriptor::memory_manager(MEMORY_MANAGER_LEDGER_ID)?,
321            Some(record.store().physical().diagnostic()),
322            memory_sizes,
323        ),
324    )
325}
326
327/// Build a protected commit recovery diagnostic for the default ledger store.
328///
329/// Unlike [`default_memory_manager_diagnostic_export`], this helper does not
330/// require successful bootstrap. It can diagnose empty or partially corrupt
331/// dual-slot commit state as long as the enclosing stable-cell ledger record is
332/// readable.
333pub fn default_memory_manager_commit_recovery_diagnostic()
334-> Result<CommitStoreDiagnostic, RuntimeDiagnosticError> {
335    let record = default_ledger_record_from_memory()?;
336    Ok(record.store().physical().diagnostic())
337}
338
339/// Build a preflight and runtime diagnostic report for the default runtime.
340///
341/// The doctor report can be collected before bootstrap, after bootstrap, or
342/// after a failed bootstrap attempt. Stable-cell, commit-recovery, declaration,
343/// range-authority, validation, ledger, and live memory-size status are
344/// collected into one serializable report. Recoverable problems are reported in
345/// fields rather than returned as errors.
346#[must_use]
347pub fn default_memory_manager_doctor_report() -> DefaultMemoryManagerDoctorReport {
348    let bootstrapped = is_default_memory_manager_bootstrapped();
349    let eager_init_error = if bootstrapped {
350        None
351    } else {
352        run_eager_init_hooks()
353            .err()
354            .map(|_err| format!("eager-init hooks: {}", RuntimeLockPoisoned::MESSAGE))
355    };
356
357    let stable_cell = default_memory_manager_stable_cell_diagnostic();
358    let commit_recovery = stable_cell
359        .record
360        .as_ref()
361        .map(|record| record.store().physical().diagnostic());
362    let recovered = stable_cell
363        .record
364        .as_ref()
365        .map(|record| record.store().recover());
366    let recovered_for_export = recovered.as_ref().and_then(|result| result.as_ref().ok());
367    let ledger_anchor = default_ledger_anchor_descriptor();
368    let ledger = recovered_for_export.map(|recovered| {
369        DiagnosticExport::from_ledger_with_commit_recovery_and_memory_sizes(
370            recovered.ledger(),
371            ledger_anchor.clone(),
372            commit_recovery,
373            default_memory_manager_memory_sizes_lossy(recovered.ledger()),
374        )
375    });
376
377    let registered_declarations = static_memory_declarations();
378    let registered_ranges = static_memory_range_declarations();
379    let diagnostic_declarations = registered_declarations
380        .as_ref()
381        .map(|declarations| {
382            declarations
383                .iter()
384                .map(|registration| {
385                    DiagnosticDeclaration::new(
386                        registration.declaring_crate(),
387                        registration.declaration().clone(),
388                    )
389                })
390                .collect()
391        })
392        .unwrap_or_default();
393    let range_authority = diagnostic_range_authority(&registered_ranges);
394    let validation = eager_init_error.map_or_else(
395        || {
396            diagnostic_validation(
397                &registered_declarations,
398                &registered_ranges,
399                stable_cell.record.as_ref(),
400                recovered.as_ref(),
401            )
402        },
403        DiagnosticCheck::failed,
404    );
405
406    DefaultMemoryManagerDoctorReport {
407        bootstrapped: BOOTSTRAPPED.load(Ordering::SeqCst),
408        ledger_anchor,
409        stable_cell: stable_cell.diagnostic,
410        commit_recovery,
411        ledger,
412        registered_declarations: diagnostic_declarations,
413        range_authority,
414        validation,
415    }
416}
417
418fn run_eager_init_hooks() -> Result<(), RuntimeLockPoisoned> {
419    let hooks = {
420        let mut hooks = EAGER_INIT_HOOKS.lock().map_err(|_| RuntimeLockPoisoned)?;
421        std::mem::take(&mut *hooks)
422    };
423
424    for hook in hooks {
425        hook();
426    }
427    Ok(())
428}
429
430fn with_default_ledger_cell<P, T>(
431    op: impl FnOnce(&mut DefaultLedgerCell) -> Result<T, RuntimeBootstrapError<P>>,
432) -> Result<T, RuntimeBootstrapError<P>> {
433    DEFAULT_LEDGER_CELL.with(|cell| {
434        let mut cell = cell.borrow_mut();
435        if cell.is_none() {
436            let memory = default_memory_manager_memory(MEMORY_MANAGER_LEDGER_ID);
437            crate::validate_stable_cell_ledger_memory(&memory)?;
438            *cell = Some(Cell::init(memory, StableCellLedgerRecord::default()));
439        }
440        let Some(cell) = cell.as_mut() else {
441            return Err(RuntimeBootstrapError::RuntimeLockPoisoned);
442        };
443        op(cell)
444    })
445}
446
447fn set_default_ledger_cell<P>(
448    cell: &mut DefaultLedgerCell,
449    record: StableCellLedgerRecord,
450) -> Result<(), RuntimeBootstrapError<P>> {
451    ensure_default_ledger_cell_capacity(&record)?;
452    let _previous = cell.set(record);
453    Ok(())
454}
455
456fn ensure_default_ledger_cell_capacity<P>(
457    record: &StableCellLedgerRecord,
458) -> Result<(), RuntimeBootstrapError<P>> {
459    let encoded = record.to_bytes();
460    let value_size = encoded.len();
461    if value_size > u32::MAX as usize {
462        return Err(RuntimeBootstrapError::StableCellLedgerWriteTooLarge { value_size });
463    }
464
465    let value_size_u32 = u32::try_from(value_size)
466        .map_err(|_| RuntimeBootstrapError::StableCellLedgerWriteTooLarge { value_size })?;
467    let value_size_u64 = u64::from(value_size_u32);
468    let required_bytes = STABLE_CELL_VALUE_OFFSET
469        .checked_add(value_size_u64)
470        .ok_or(RuntimeBootstrapError::StableCellLedgerWriteTooLarge { value_size })?;
471    let memory = default_memory_manager_memory(MEMORY_MANAGER_LEDGER_ID);
472    let available_bytes = memory.size().saturating_mul(crate::WASM_PAGE_SIZE_BYTES);
473    if required_bytes <= available_bytes {
474        return Ok(());
475    }
476
477    let grow_by = required_bytes
478        .saturating_sub(available_bytes)
479        .div_ceil(crate::WASM_PAGE_SIZE_BYTES);
480    if memory.grow(grow_by) < 0 {
481        return Err(RuntimeBootstrapError::StableCellLedgerWriteTooLarge { value_size });
482    }
483    Ok(())
484}
485
486fn external_runtime_allocations(validated: ValidatedAllocations) -> ValidatedAllocations {
487    validated.without_stable_key(IC_MEMORY_LEDGER_STABLE_KEY)
488}
489
490fn default_memory_manager_memory(id: u8) -> VirtualMemory<DefaultMemoryImpl> {
491    DEFAULT_MEMORY_MANAGER.with(|manager| manager.get(MemoryId::new(id)))
492}
493
494const fn default_ledger_anchor_descriptor() -> AllocationSlotDescriptor {
495    AllocationSlotDescriptor::memory_manager_unchecked(MEMORY_MANAGER_LEDGER_ID)
496}
497
498fn default_ledger_record_for_diagnostics() -> Result<StableCellLedgerRecord, RuntimeDiagnosticError>
499{
500    if !is_default_memory_manager_bootstrapped() {
501        return Err(RuntimeDiagnosticError::NotBootstrapped);
502    }
503
504    default_ledger_record_from_memory().map_err(RuntimeDiagnosticError::StableCellLedger)
505}
506
507fn default_ledger_record_from_memory() -> Result<StableCellLedgerRecord, StableCellLedgerError> {
508    let memory = default_memory_manager_memory(MEMORY_MANAGER_LEDGER_ID);
509    decode_stable_cell_ledger_record_from_memory(&memory)
510}
511
512fn default_memory_manager_memory_sizes(
513    ledger: &AllocationLedger,
514) -> Result<Vec<(AllocationSlotDescriptor, DiagnosticMemorySize)>, RuntimeDiagnosticError> {
515    ledger
516        .allocation_history()
517        .records()
518        .iter()
519        .map(|record| {
520            let id = record.slot().memory_manager_id()?;
521            let memory = default_memory_manager_memory(id);
522            Ok((
523                record.slot().clone(),
524                DiagnosticMemorySize::from_wasm_pages(memory.size()),
525            ))
526        })
527        .collect()
528}
529
530fn default_memory_manager_memory_sizes_lossy(
531    ledger: &AllocationLedger,
532) -> Vec<(AllocationSlotDescriptor, DiagnosticMemorySize)> {
533    default_memory_manager_memory_sizes(ledger).unwrap_or_default()
534}
535
536struct DefaultStableCellDiagnostic {
537    diagnostic: DiagnosticStableCell,
538    record: Option<StableCellLedgerRecord>,
539}
540
541fn default_memory_manager_stable_cell_diagnostic() -> DefaultStableCellDiagnostic {
542    let memory = default_memory_manager_memory(MEMORY_MANAGER_LEDGER_ID);
543    let memory_size = DiagnosticMemorySize::from_wasm_pages(memory.size());
544    if memory.size() == 0 {
545        return DefaultStableCellDiagnostic {
546            diagnostic: DiagnosticStableCell::new(
547                DiagnosticStableCellStatus::Empty,
548                memory_size,
549                None,
550            ),
551            record: Some(StableCellLedgerRecord::default()),
552        };
553    }
554
555    let record = decode_stable_cell_ledger_record_from_memory(&memory);
556    match record {
557        Ok(record) => DefaultStableCellDiagnostic {
558            diagnostic: DiagnosticStableCell::new(
559                DiagnosticStableCellStatus::Readable,
560                memory_size,
561                None,
562            ),
563            record: Some(record),
564        },
565        Err(err) => DefaultStableCellDiagnostic {
566            diagnostic: DiagnosticStableCell::new(
567                DiagnosticStableCellStatus::Corrupt,
568                memory_size,
569                Some(err.to_string()),
570            ),
571            record: None,
572        },
573    }
574}
575
576fn diagnostic_range_authority(
577    registered_ranges: &Result<Vec<StaticMemoryRangeDeclaration>, StaticMemoryDeclarationError>,
578) -> DiagnosticRangeAuthority {
579    match registered_ranges {
580        Ok(ranges) => {
581            let registered_records = ranges
582                .iter()
583                .map(|registration| registration.record().clone())
584                .collect();
585            match range_authority(ranges.clone()) {
586                Ok(authority) => {
587                    DiagnosticRangeAuthority::new(registered_records, Some(authority), None)
588                }
589                Err(err) => {
590                    DiagnosticRangeAuthority::new(registered_records, None, Some(err.to_string()))
591                }
592            }
593        }
594        Err(err) => DiagnosticRangeAuthority::new(Vec::new(), None, Some(err.to_string())),
595    }
596}
597
598fn diagnostic_validation(
599    registered_declarations: &Result<Vec<StaticMemoryDeclaration>, StaticMemoryDeclarationError>,
600    registered_ranges: &Result<Vec<StaticMemoryRangeDeclaration>, StaticMemoryDeclarationError>,
601    stable_cell_record: Option<&StableCellLedgerRecord>,
602    recovered: Option<&Result<crate::RecoveredLedger, LedgerCommitError>>,
603) -> DiagnosticCheck {
604    let registered_declarations = match registered_declarations {
605        Ok(declarations) => declarations.clone(),
606        Err(err) => return DiagnosticCheck::failed(format!("declaration registry: {err}")),
607    };
608    let registered_ranges = match registered_ranges {
609        Ok(ranges) => ranges.clone(),
610        Err(err) => return DiagnosticCheck::failed(format!("range registry: {err}")),
611    };
612    let range_authority = match range_authority(registered_ranges.clone()) {
613        Ok(authority) => authority,
614        Err(err) => return DiagnosticCheck::failed(format!("range authority: {err}")),
615    };
616    let snapshot = match declaration_snapshot(registered_declarations.clone()) {
617        Ok(snapshot) => snapshot,
618        Err(err) => return DiagnosticCheck::failed(format!("declaration snapshot: {err}")),
619    };
620    let recovered = match diagnostic_validation_ledger(stable_cell_record, recovered) {
621        Ok(recovered) => recovered,
622        Err(reason) => return DiagnosticCheck::not_run(reason),
623    };
624    let policy = RuntimeMemoryManagerPolicy {
625        range_authority,
626        user_ranges_registered: !registered_ranges.is_empty(),
627        declaration_metadata: declaration_metadata(&registered_declarations),
628        custom_policy: &NoopPolicy,
629    };
630
631    match crate::validate_allocations(&recovered, snapshot, &policy) {
632        Ok(_) => DiagnosticCheck::passed(),
633        Err(err) => DiagnosticCheck::failed(err.to_string()),
634    }
635}
636
637fn diagnostic_validation_ledger(
638    stable_cell_record: Option<&StableCellLedgerRecord>,
639    recovered: Option<&Result<crate::RecoveredLedger, LedgerCommitError>>,
640) -> Result<crate::RecoveredLedger, String> {
641    if let Some(Ok(recovered)) = recovered {
642        return Ok(recovered.clone());
643    }
644    if let Some(Err(err)) = recovered {
645        if stable_cell_record.is_some_and(|record| record.store().physical().is_uninitialized()) {
646            return diagnostic_genesis_recovered_ledger();
647        }
648        return Err(format!("protected ledger recovery: {err}"));
649    }
650    if stable_cell_record.is_some() {
651        return diagnostic_genesis_recovered_ledger();
652    }
653    Err("stable-cell ledger record is not readable".to_string())
654}
655
656fn diagnostic_genesis_recovered_ledger() -> Result<crate::RecoveredLedger, String> {
657    AllocationLedger::new(0, AllocationHistory::default())
658        .map(|ledger| crate::RecoveredLedger::from_trusted_parts(ledger, 0))
659        .map_err(|err| format!("genesis ledger: {err}"))
660}
661
662fn publish_validated_allocations<P>(
663    validated: ValidatedAllocations,
664) -> Result<(), RuntimeBootstrapError<P>> {
665    *VALIDATED_ALLOCATIONS
666        .lock()
667        .map_err(|_| RuntimeBootstrapError::RuntimeLockPoisoned)? = Some(validated);
668    Ok(())
669}
670
671fn declaration_snapshot(
672    registrations: Vec<StaticMemoryDeclaration>,
673) -> Result<DeclarationSnapshot, StaticMemoryDeclarationError> {
674    let mut declarations = Vec::with_capacity(registrations.len() + 1);
675    declarations.push(internal_ledger_declaration()?);
676    declarations.extend(
677        registrations
678            .into_iter()
679            .map(StaticMemoryDeclaration::into_declaration),
680    );
681    DeclarationSnapshot::new(declarations).map_err(StaticMemoryDeclarationError::Declaration)
682}
683
684fn declaration_metadata(registrations: &[StaticMemoryDeclaration]) -> BTreeMap<String, String> {
685    let mut metadata = BTreeMap::new();
686    metadata.insert(
687        IC_MEMORY_LEDGER_STABLE_KEY.to_string(),
688        IC_MEMORY_AUTHORITY_OWNER.to_string(),
689    );
690    for registration in registrations {
691        metadata.insert(
692            registration.declaration().stable_key().as_str().to_string(),
693            registration.declaring_crate().to_string(),
694        );
695    }
696    metadata
697}
698
699fn range_authority(
700    registrations: Vec<StaticMemoryRangeDeclaration>,
701) -> Result<MemoryManagerRangeAuthority, MemoryManagerRangeAuthorityError> {
702    let mut records = Vec::with_capacity(registrations.len() + 1);
703    records.push(internal_ledger_range()?);
704    records.extend(
705        registrations
706            .into_iter()
707            .map(StaticMemoryRangeDeclaration::into_record),
708    );
709    MemoryManagerRangeAuthority::from_records(records)
710}
711
712fn internal_ledger_declaration() -> Result<AllocationDeclaration, crate::DeclarationSnapshotError> {
713    AllocationDeclaration::memory_manager(
714        IC_MEMORY_LEDGER_STABLE_KEY,
715        MEMORY_MANAGER_LEDGER_ID,
716        IC_MEMORY_LEDGER_LABEL,
717    )
718}
719
720fn internal_ledger_range() -> Result<MemoryManagerAuthorityRecord, MemoryManagerRangeAuthorityError>
721{
722    MemoryManagerAuthorityRecord::new(
723        MemoryManagerIdRange::new(
724            MEMORY_MANAGER_LEDGER_ID,
725            crate::MEMORY_MANAGER_GOVERNANCE_MAX_ID,
726        )?,
727        IC_MEMORY_AUTHORITY_OWNER,
728        MemoryManagerRangeMode::Reserved,
729        Some(IC_MEMORY_AUTHORITY_PURPOSE.to_string()),
730    )
731}
732
733fn runtime_bootstrap_error_from_bootstrap<P>(
734    err: crate::BootstrapError<RuntimePolicyError<P>>,
735) -> RuntimeBootstrapError<P> {
736    match err {
737        crate::BootstrapError::Ledger(err) => RuntimeBootstrapError::LedgerCommit(err),
738        crate::BootstrapError::Validation(err) => RuntimeBootstrapError::Validation(err),
739        crate::BootstrapError::Staging(err) => RuntimeBootstrapError::Staging(err),
740    }
741}
742
743struct RuntimeMemoryManagerPolicy<'a, P> {
744    range_authority: MemoryManagerRangeAuthority,
745    user_ranges_registered: bool,
746    declaration_metadata: BTreeMap<String, String>,
747    custom_policy: &'a P,
748}
749
750impl<P: AllocationPolicy> AllocationPolicy for RuntimeMemoryManagerPolicy<'_, P> {
751    type Error = RuntimePolicyError<P::Error>;
752
753    fn validate_key(&self, key: &StableKey) -> Result<(), Self::Error> {
754        let declaring_crate = self.declaring_crate(key)?;
755        if crate::is_ic_memory_stable_key(key.as_str())
756            && declaring_crate != IC_MEMORY_AUTHORITY_OWNER
757        {
758            return Err(RuntimePolicyError::ReservedStableKeyAuthority {
759                stable_key: key.as_str().to_string(),
760                expected_authority: IC_MEMORY_AUTHORITY_OWNER,
761            });
762        }
763        self.custom_policy
764            .validate_key(key)
765            .map_err(RuntimePolicyError::Custom)
766    }
767
768    fn validate_slot(
769        &self,
770        key: &StableKey,
771        slot: &AllocationSlotDescriptor,
772    ) -> Result<(), Self::Error> {
773        self.validate_runtime_range(key, slot)?;
774        self.custom_policy
775            .validate_slot(key, slot)
776            .map_err(RuntimePolicyError::Custom)
777    }
778
779    fn validate_reserved_slot(
780        &self,
781        key: &StableKey,
782        slot: &AllocationSlotDescriptor,
783    ) -> Result<(), Self::Error> {
784        self.validate_runtime_range(key, slot)?;
785        self.custom_policy
786            .validate_reserved_slot(key, slot)
787            .map_err(RuntimePolicyError::Custom)
788    }
789}
790
791impl<P: AllocationPolicy> RuntimeMemoryManagerPolicy<'_, P> {
792    fn declaring_crate(&self, key: &StableKey) -> Result<&str, RuntimePolicyError<P::Error>> {
793        self.declaration_metadata
794            .get(key.as_str())
795            .map(String::as_str)
796            .ok_or_else(|| RuntimePolicyError::MissingDeclarationMetadata(key.as_str().to_string()))
797    }
798
799    fn validate_runtime_range(
800        &self,
801        key: &StableKey,
802        slot: &AllocationSlotDescriptor,
803    ) -> Result<(), RuntimePolicyError<P::Error>> {
804        let declaring_crate = self.declaring_crate(key)?;
805        // Range claims are authoritative generic policy in the default runtime.
806        // Once any user range is registered, every user declaration must fit
807        // the declaring crate's claimed range. With no user ranges, only the
808        // internal ic-memory governance range is enforced here and custom
809        // policy may decide application-space ownership.
810        if declaring_crate == IC_MEMORY_AUTHORITY_OWNER || self.user_ranges_registered {
811            self.range_authority
812                .validate_slot_authority(slot, declaring_crate)?;
813            return Ok(());
814        }
815
816        let id = slot
817            .memory_manager_id()
818            .map_err(MemoryManagerRangeAuthorityError::Slot)?;
819        if self
820            .range_authority
821            .authority_for_id(id)
822            .map_err(RuntimePolicyError::Range)?
823            .is_some()
824        {
825            self.range_authority
826                .validate_slot_authority(slot, declaring_crate)?;
827        }
828        Ok(())
829    }
830}
831
832struct NoopPolicy;
833
834impl AllocationPolicy for NoopPolicy {
835    type Error = Infallible;
836
837    fn validate_key(&self, _key: &StableKey) -> Result<(), Self::Error> {
838        Ok(())
839    }
840
841    fn validate_slot(
842        &self,
843        _key: &StableKey,
844        _slot: &AllocationSlotDescriptor,
845    ) -> Result<(), Self::Error> {
846        Ok(())
847    }
848
849    fn validate_reserved_slot(
850        &self,
851        _key: &StableKey,
852        _slot: &AllocationSlotDescriptor,
853    ) -> Result<(), Self::Error> {
854        Ok(())
855    }
856}
857
858#[cfg(test)]
859pub fn reset_for_tests() {
860    crate::registry::reset_static_memory_declarations_for_tests();
861    EAGER_INIT_HOOKS
862        .lock()
863        .expect("ic-memory eager-init queue poisoned")
864        .clear();
865    *VALIDATED_ALLOCATIONS
866        .lock()
867        .expect("ic-memory runtime validation state poisoned") = None;
868    BOOTSTRAPPED.store(false, Ordering::SeqCst);
869    DEFAULT_LEDGER_CELL.with_borrow_mut(|cell| {
870        *cell = None;
871    });
872}
873
874#[cfg(test)]
875mod tests {
876    use super::*;
877    use crate::registry::{
878        TEST_REGISTRY_LOCK, register_static_memory_manager_declaration,
879        register_static_memory_manager_range,
880    };
881    use std::sync::atomic::{AtomicBool, Ordering};
882
883    static EAGER_INIT_RAN: AtomicBool = AtomicBool::new(false);
884
885    fn register_crate_a() {
886        register_static_memory_manager_range(
887            100,
888            109,
889            "crate_a",
890            MemoryManagerRangeMode::Reserved,
891            None,
892        )
893        .expect("crate A range");
894        register_static_memory_manager_declaration(100, "crate_a", "users", "crate_a.users.v1")
895            .expect("crate A memory");
896    }
897
898    fn register_crate_b() {
899        register_static_memory_manager_range(
900            110,
901            119,
902            "crate_b",
903            MemoryManagerRangeMode::Reserved,
904            None,
905        )
906        .expect("crate B range");
907        register_static_memory_manager_declaration(110, "crate_b", "orders", "crate_b.orders.v1")
908            .expect("crate B memory");
909    }
910
911    fn mark_eager_init() {
912        EAGER_INIT_RAN.store(true, Ordering::SeqCst);
913        register_static_memory_manager_declaration(101, "crate_a", "audit", "crate_a.audit.v1")
914            .expect("eager-init declaration");
915    }
916
917    #[test]
918    fn multi_crate_declarations_compose_into_one_bootstrap() {
919        let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
920        reset_for_tests();
921        register_crate_a();
922        register_crate_b();
923
924        let validated = bootstrap_default_memory_manager().expect("bootstrap");
925
926        assert_eq!(validated.declarations().len(), 2);
927        assert!(
928            validated
929                .declarations()
930                .iter()
931                .any(|declaration| declaration.stable_key().as_str() == "crate_a.users.v1")
932        );
933        assert!(
934            validated
935                .declarations()
936                .iter()
937                .any(|declaration| declaration.stable_key().as_str() == "crate_b.orders.v1")
938        );
939    }
940
941    #[test]
942    fn default_runtime_keeps_internal_ledger_slot_private() {
943        let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
944        reset_for_tests();
945
946        let validated = bootstrap_default_memory_manager().expect("bootstrap");
947
948        assert!(validated.declarations().is_empty());
949        assert!(
950            validated_allocations()
951                .expect("published allocations")
952                .declarations()
953                .is_empty()
954        );
955        let Err(err) = open_default_memory_manager_memory(
956            IC_MEMORY_LEDGER_STABLE_KEY,
957            MEMORY_MANAGER_LEDGER_ID,
958        ) else {
959            panic!("internal ledger slot must stay private");
960        };
961        assert!(matches!(err, RuntimeOpenError::ReservedStableKey { .. }));
962    }
963
964    #[test]
965    fn conflicting_ranges_fail() {
966        let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
967        reset_for_tests();
968        register_static_memory_manager_range(
969            100,
970            110,
971            "crate_a",
972            MemoryManagerRangeMode::Reserved,
973            None,
974        )
975        .expect("crate A range");
976        register_static_memory_manager_range(
977            105,
978            119,
979            "crate_b",
980            MemoryManagerRangeMode::Reserved,
981            None,
982        )
983        .expect("crate B range");
984
985        let err = bootstrap_default_memory_manager().expect_err("overlap must fail");
986        assert!(matches!(
987            err,
988            RuntimeBootstrapError::Range(
989                MemoryManagerRangeAuthorityError::OverlappingRanges { .. }
990            )
991        ));
992    }
993
994    #[test]
995    fn duplicate_stable_keys_fail() {
996        let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
997        reset_for_tests();
998        register_static_memory_manager_declaration(100, "crate_a", "users", "app.users.v1")
999            .expect("first declaration");
1000        register_static_memory_manager_declaration(101, "crate_b", "users", "app.users.v1")
1001            .expect("second declaration");
1002
1003        let err = bootstrap_default_memory_manager().expect_err("duplicate key must fail");
1004        assert!(matches!(
1005            err,
1006            RuntimeBootstrapError::Registry(StaticMemoryDeclarationError::Declaration(
1007                crate::DeclarationSnapshotError::DuplicateStableKey(_)
1008            ))
1009        ));
1010    }
1011
1012    #[test]
1013    fn duplicate_memory_manager_ids_fail() {
1014        let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
1015        reset_for_tests();
1016        register_static_memory_manager_declaration(100, "crate_a", "users", "crate_a.users.v1")
1017            .expect("first declaration");
1018        register_static_memory_manager_declaration(100, "crate_b", "orders", "crate_b.orders.v1")
1019            .expect("second declaration");
1020
1021        let err = bootstrap_default_memory_manager().expect_err("duplicate slot must fail");
1022        assert!(matches!(
1023            err,
1024            RuntimeBootstrapError::Registry(StaticMemoryDeclarationError::Declaration(
1025                crate::DeclarationSnapshotError::DuplicateSlot(_)
1026            ))
1027        ));
1028    }
1029
1030    #[test]
1031    fn out_of_range_memory_declaration_fails_when_ranges_are_declared() {
1032        let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
1033        reset_for_tests();
1034        register_static_memory_manager_range(
1035            100,
1036            109,
1037            "crate_a",
1038            MemoryManagerRangeMode::Reserved,
1039            None,
1040        )
1041        .expect("crate A range");
1042        register_static_memory_manager_declaration(120, "crate_a", "users", "crate_a.users.v1")
1043            .expect("out-of-range declaration");
1044
1045        let err = bootstrap_default_memory_manager().expect_err("out of range must fail");
1046        assert!(matches!(
1047            err,
1048            RuntimeBootstrapError::Validation(crate::AllocationValidationError::Policy(
1049                RuntimePolicyError::Range(MemoryManagerRangeAuthorityError::UnclaimedId {
1050                    id: 120
1051                })
1052            ))
1053        ));
1054    }
1055
1056    #[test]
1057    fn late_registration_after_bootstrap_fails() {
1058        let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
1059        reset_for_tests();
1060        register_static_memory_manager_declaration(100, "crate_a", "users", "crate_a.users.v1")
1061            .expect("declaration");
1062        bootstrap_default_memory_manager().expect("bootstrap");
1063
1064        let err = register_static_memory_manager_declaration(
1065            101,
1066            "crate_a",
1067            "orders",
1068            "crate_a.orders.v1",
1069        )
1070        .expect_err("late registration must fail");
1071        assert_eq!(err, StaticMemoryDeclarationError::RegistrySealed);
1072    }
1073
1074    #[test]
1075    fn late_eager_init_registration_after_bootstrap_fails() {
1076        let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
1077        reset_for_tests();
1078        register_static_memory_manager_declaration(100, "crate_a", "users", "crate_a.users.v1")
1079            .expect("declaration");
1080        bootstrap_default_memory_manager().expect("bootstrap");
1081
1082        let err = std::panic::catch_unwind(|| defer_eager_init(mark_eager_init))
1083            .expect_err("late eager-init registration must fail");
1084
1085        let message = err
1086            .downcast_ref::<String>()
1087            .map(String::as_str)
1088            .or_else(|| err.downcast_ref::<&str>().copied())
1089            .expect("panic message");
1090        assert!(message.contains("after runtime bootstrap"));
1091    }
1092
1093    #[test]
1094    fn eager_init_runs_before_snapshot_seal() {
1095        let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
1096        reset_for_tests();
1097        EAGER_INIT_RAN.store(false, Ordering::SeqCst);
1098        register_static_memory_manager_range(
1099            100,
1100            109,
1101            "crate_a",
1102            MemoryManagerRangeMode::Reserved,
1103            None,
1104        )
1105        .expect("crate A range");
1106        defer_eager_init(mark_eager_init);
1107
1108        let validated = bootstrap_default_memory_manager().expect("bootstrap");
1109
1110        assert!(EAGER_INIT_RAN.load(Ordering::SeqCst));
1111        assert!(
1112            validated
1113                .declarations()
1114                .iter()
1115                .any(|declaration| declaration.stable_key().as_str() == "crate_a.audit.v1")
1116        );
1117    }
1118
1119    #[test]
1120    fn direct_user_can_bootstrap_and_open_without_canic() {
1121        let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
1122        reset_for_tests();
1123        register_static_memory_manager_range(
1124            120,
1125            129,
1126            "icydb",
1127            MemoryManagerRangeMode::Reserved,
1128            None,
1129        )
1130        .expect("icydb range");
1131        register_static_memory_manager_declaration(120, "icydb", "users", "icydb.users.data.v1")
1132            .expect("icydb declaration");
1133
1134        bootstrap_default_memory_manager().expect("bootstrap");
1135        open_default_memory_manager_memory("icydb.users.data.v1", 120).expect("open memory");
1136    }
1137
1138    #[test]
1139    fn diagnostic_export_reports_default_memory_manager_sizes() {
1140        let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
1141        reset_for_tests();
1142        register_static_memory_manager_range(
1143            130,
1144            139,
1145            "diagnostics",
1146            MemoryManagerRangeMode::Reserved,
1147            None,
1148        )
1149        .expect("diagnostics range");
1150        register_static_memory_manager_declaration(
1151            130,
1152            "diagnostics",
1153            "users",
1154            "diagnostics.users.v1",
1155        )
1156        .expect("diagnostics declaration");
1157
1158        bootstrap_default_memory_manager().expect("bootstrap");
1159        let memory =
1160            open_default_memory_manager_memory("diagnostics.users.v1", 130).expect("open memory");
1161        let old_size = memory.size();
1162        memory.grow(2);
1163
1164        let export = default_memory_manager_diagnostic_export().expect("diagnostic export");
1165        let recovery =
1166            default_memory_manager_commit_recovery_diagnostic().expect("recovery diagnostic");
1167        let record = export
1168            .records
1169            .iter()
1170            .find(|record| record.allocation.stable_key().as_str() == "diagnostics.users.v1")
1171            .expect("diagnostic allocation");
1172
1173        assert_eq!(
1174            recovery.authoritative_generation,
1175            Some(export.current_generation)
1176        );
1177        assert_eq!(
1178            record.memory_size,
1179            Some(DiagnosticMemorySize::from_wasm_pages(old_size + 2))
1180        );
1181    }
1182
1183    #[test]
1184    fn doctor_report_preflights_before_bootstrap() {
1185        let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
1186        reset_for_tests();
1187        register_static_memory_manager_range(
1188            240,
1189            240,
1190            "doctor_preflight",
1191            MemoryManagerRangeMode::Reserved,
1192            None,
1193        )
1194        .expect("doctor range");
1195        register_static_memory_manager_declaration(
1196            240,
1197            "doctor_preflight",
1198            "users",
1199            "doctor_preflight.users.v1",
1200        )
1201        .expect("doctor declaration");
1202
1203        let report = default_memory_manager_doctor_report();
1204
1205        assert!(!report.bootstrapped);
1206        assert_eq!(report.registered_declarations.len(), 1);
1207        assert!(report.range_authority.effective_authority.is_some());
1208        assert_eq!(
1209            report.validation.status,
1210            crate::DiagnosticCheckStatus::Passed
1211        );
1212        assert!(report.commit_recovery.is_some());
1213        assert!(matches!(
1214            report.stable_cell.status,
1215            crate::DiagnosticStableCellStatus::Empty | crate::DiagnosticStableCellStatus::Readable
1216        ));
1217    }
1218
1219    #[test]
1220    fn doctor_report_includes_recovered_ledger_and_memory_sizes_after_bootstrap() {
1221        let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
1222        reset_for_tests();
1223        register_static_memory_manager_range(
1224            241,
1225            241,
1226            "doctor_runtime",
1227            MemoryManagerRangeMode::Reserved,
1228            None,
1229        )
1230        .expect("doctor range");
1231        register_static_memory_manager_declaration(
1232            241,
1233            "doctor_runtime",
1234            "orders",
1235            "doctor_runtime.orders.v1",
1236        )
1237        .expect("doctor declaration");
1238
1239        bootstrap_default_memory_manager().expect("bootstrap");
1240        let memory = open_default_memory_manager_memory("doctor_runtime.orders.v1", 241)
1241            .expect("open memory");
1242        let old_size = memory.size();
1243        memory.grow(1);
1244
1245        let report = default_memory_manager_doctor_report();
1246        let ledger = report.ledger.expect("recovered ledger export");
1247        let record = ledger
1248            .records
1249            .iter()
1250            .find(|record| record.allocation.stable_key().as_str() == "doctor_runtime.orders.v1")
1251            .expect("doctor allocation");
1252
1253        assert!(report.bootstrapped);
1254        assert_eq!(
1255            report.stable_cell.status,
1256            crate::DiagnosticStableCellStatus::Readable
1257        );
1258        assert_eq!(
1259            report.validation.status,
1260            crate::DiagnosticCheckStatus::Passed
1261        );
1262        assert_eq!(
1263            record.memory_size,
1264            Some(DiagnosticMemorySize::from_wasm_pages(old_size + 1))
1265        );
1266    }
1267
1268    #[test]
1269    fn doctor_report_captures_validation_failure() {
1270        let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
1271        reset_for_tests();
1272        register_static_memory_manager_declaration(
1273            242,
1274            "doctor_failure_a",
1275            "users",
1276            "doctor_failure.users.v1",
1277        )
1278        .expect("first declaration");
1279        register_static_memory_manager_declaration(
1280            243,
1281            "doctor_failure_b",
1282            "orders",
1283            "doctor_failure.users.v1",
1284        )
1285        .expect("second declaration");
1286
1287        let report = default_memory_manager_doctor_report();
1288
1289        assert_eq!(
1290            report.validation.status,
1291            crate::DiagnosticCheckStatus::Failed
1292        );
1293        assert!(
1294            report
1295                .validation
1296                .message
1297                .expect("validation failure message")
1298                .contains("declared more than once")
1299        );
1300    }
1301}