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 to external declarations only.
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(DiagnosticStableCellStatus::Empty, memory_size),
550            record: Some(StableCellLedgerRecord::default()),
551        };
552    }
553
554    let record = decode_stable_cell_ledger_record_from_memory(&memory);
555    match record {
556        Ok(record) => DefaultStableCellDiagnostic {
557            diagnostic: DiagnosticStableCell::new(
558                DiagnosticStableCellStatus::Readable,
559                memory_size,
560            ),
561            record: Some(record),
562        },
563        Err(err) => DefaultStableCellDiagnostic {
564            diagnostic: DiagnosticStableCell::new(
565                DiagnosticStableCellStatus::Corrupt {
566                    error: err.to_string(),
567                },
568                memory_size,
569            ),
570            record: None,
571        },
572    }
573}
574
575fn diagnostic_range_authority(
576    registered_ranges: &Result<Vec<StaticMemoryRangeDeclaration>, StaticMemoryDeclarationError>,
577) -> DiagnosticRangeAuthority {
578    match registered_ranges {
579        Ok(ranges) => {
580            let registered_records = ranges
581                .iter()
582                .map(|registration| registration.record().clone())
583                .collect();
584            match range_authority(ranges.clone()) {
585                Ok(authority) => DiagnosticRangeAuthority::new(registered_records, Ok(authority)),
586                Err(err) => DiagnosticRangeAuthority::new(registered_records, Err(err.to_string())),
587            }
588        }
589        Err(err) => DiagnosticRangeAuthority::new(Vec::new(), Err(err.to_string())),
590    }
591}
592
593fn diagnostic_validation(
594    registered_declarations: &Result<Vec<StaticMemoryDeclaration>, StaticMemoryDeclarationError>,
595    registered_ranges: &Result<Vec<StaticMemoryRangeDeclaration>, StaticMemoryDeclarationError>,
596    stable_cell_record: Option<&StableCellLedgerRecord>,
597    recovered: Option<&Result<crate::RecoveredLedger, LedgerCommitError>>,
598) -> DiagnosticCheck {
599    let registered_declarations = match registered_declarations {
600        Ok(declarations) => declarations.clone(),
601        Err(err) => return DiagnosticCheck::failed(format!("declaration registry: {err}")),
602    };
603    let registered_ranges = match registered_ranges {
604        Ok(ranges) => ranges.clone(),
605        Err(err) => return DiagnosticCheck::failed(format!("range registry: {err}")),
606    };
607    let range_authority = match range_authority(registered_ranges.clone()) {
608        Ok(authority) => authority,
609        Err(err) => return DiagnosticCheck::failed(format!("range authority: {err}")),
610    };
611    let snapshot = match declaration_snapshot(registered_declarations.clone()) {
612        Ok(snapshot) => snapshot,
613        Err(err) => return DiagnosticCheck::failed(format!("declaration snapshot: {err}")),
614    };
615    let recovered = match diagnostic_validation_ledger(stable_cell_record, recovered) {
616        Ok(recovered) => recovered,
617        Err(reason) => return DiagnosticCheck::not_run(reason),
618    };
619    let policy = RuntimeMemoryManagerPolicy {
620        range_authority,
621        user_ranges_registered: !registered_ranges.is_empty(),
622        declaration_metadata: declaration_metadata(&registered_declarations),
623        custom_policy: &NoopPolicy,
624    };
625
626    match crate::validate_allocations(&recovered, snapshot, &policy) {
627        Ok(_) => DiagnosticCheck::passed(),
628        Err(err) => DiagnosticCheck::failed(err.to_string()),
629    }
630}
631
632fn diagnostic_validation_ledger(
633    stable_cell_record: Option<&StableCellLedgerRecord>,
634    recovered: Option<&Result<crate::RecoveredLedger, LedgerCommitError>>,
635) -> Result<crate::RecoveredLedger, String> {
636    if let Some(Ok(recovered)) = recovered {
637        return Ok(recovered.clone());
638    }
639    if let Some(Err(err)) = recovered {
640        if stable_cell_record.is_some_and(|record| record.store().physical().is_uninitialized()) {
641            return diagnostic_genesis_recovered_ledger();
642        }
643        return Err(format!("protected ledger recovery: {err}"));
644    }
645    if stable_cell_record.is_some() {
646        return diagnostic_genesis_recovered_ledger();
647    }
648    Err("stable-cell ledger record is not readable".to_string())
649}
650
651fn diagnostic_genesis_recovered_ledger() -> Result<crate::RecoveredLedger, String> {
652    AllocationLedger::new(0, AllocationHistory::default())
653        .map(|ledger| crate::RecoveredLedger::from_trusted_parts(ledger, 0))
654        .map_err(|err| format!("genesis ledger: {err}"))
655}
656
657fn publish_committed_allocations<P>(
658    committed: CommittedAllocations,
659) -> Result<(), RuntimeBootstrapError<P>> {
660    *COMMITTED_ALLOCATIONS
661        .lock()
662        .map_err(|_| RuntimeBootstrapError::RuntimeLockPoisoned)? = Some(committed);
663    Ok(())
664}
665
666fn declaration_snapshot(
667    registrations: Vec<StaticMemoryDeclaration>,
668) -> Result<DeclarationSnapshot, StaticMemoryDeclarationError> {
669    let mut declarations = Vec::with_capacity(registrations.len() + 1);
670    declarations.push(internal_ledger_declaration()?);
671    declarations.extend(
672        registrations
673            .into_iter()
674            .map(StaticMemoryDeclaration::into_declaration),
675    );
676    DeclarationSnapshot::new(declarations).map_err(StaticMemoryDeclarationError::Declaration)
677}
678
679fn declaration_metadata(
680    registrations: &[StaticMemoryDeclaration],
681) -> BTreeMap<String, RuntimeDeclarationAuthority> {
682    let mut metadata = BTreeMap::new();
683    metadata.insert(
684        IC_MEMORY_LEDGER_STABLE_KEY.to_string(),
685        RuntimeDeclarationAuthority::Internal,
686    );
687    for registration in registrations {
688        metadata.insert(
689            registration.declaration().stable_key().as_str().to_string(),
690            RuntimeDeclarationAuthority::External(registration.authority().to_string()),
691        );
692    }
693    metadata
694}
695
696fn range_authority(
697    registrations: Vec<StaticMemoryRangeDeclaration>,
698) -> Result<MemoryManagerRangeAuthority, MemoryManagerRangeAuthorityError> {
699    let mut records = Vec::with_capacity(registrations.len() + 1);
700    records.push(internal_ledger_range()?);
701    records.extend(
702        registrations
703            .into_iter()
704            .map(StaticMemoryRangeDeclaration::into_record),
705    );
706    MemoryManagerRangeAuthority::from_records(records)
707}
708
709fn internal_ledger_declaration() -> Result<AllocationDeclaration, crate::DeclarationSnapshotError> {
710    AllocationDeclaration::memory_manager(
711        IC_MEMORY_LEDGER_STABLE_KEY,
712        MEMORY_MANAGER_LEDGER_ID,
713        IC_MEMORY_LEDGER_LABEL,
714    )
715}
716
717fn internal_ledger_range() -> Result<MemoryManagerAuthorityRecord, MemoryManagerRangeAuthorityError>
718{
719    MemoryManagerAuthorityRecord::new(
720        MemoryManagerIdRange::new(
721            MEMORY_MANAGER_LEDGER_ID,
722            crate::MEMORY_MANAGER_GOVERNANCE_MAX_ID,
723        )?,
724        IC_MEMORY_AUTHORITY_OWNER,
725        MemoryManagerRangeMode::Reserved,
726        Some(IC_MEMORY_AUTHORITY_PURPOSE.to_string()),
727    )
728}
729
730fn runtime_bootstrap_error_from_bootstrap<P>(
731    err: crate::BootstrapError<RuntimePolicyError<P>>,
732) -> RuntimeBootstrapError<P> {
733    match err {
734        crate::BootstrapError::Ledger(err) => RuntimeBootstrapError::LedgerCommit(err),
735        crate::BootstrapError::Validation(err) => RuntimeBootstrapError::Validation(err),
736        crate::BootstrapError::Staging(err) => RuntimeBootstrapError::Staging(err),
737    }
738}
739
740struct RuntimeMemoryManagerPolicy<'a, P> {
741    range_authority: MemoryManagerRangeAuthority,
742    user_ranges_registered: bool,
743    declaration_metadata: BTreeMap<String, RuntimeDeclarationAuthority>,
744    custom_policy: &'a P,
745}
746
747enum RuntimeDeclarationAuthority {
748    Internal,
749    External(String),
750}
751
752impl<P: AllocationPolicy> AllocationPolicy for RuntimeMemoryManagerPolicy<'_, P> {
753    type Error = RuntimePolicyError<P::Error>;
754
755    fn validate_key(&self, key: &StableKey) -> Result<(), Self::Error> {
756        let authority = self.declaration_authority(key)?;
757        if matches!(authority, RuntimeDeclarationAuthority::Internal) {
758            return Ok(());
759        }
760        if crate::is_ic_memory_stable_key(key.as_str()) {
761            return Err(RuntimePolicyError::ReservedStableKeyAuthority {
762                stable_key: key.as_str().to_string(),
763                expected_authority: IC_MEMORY_AUTHORITY_OWNER,
764            });
765        }
766        self.custom_policy
767            .validate_key(key)
768            .map_err(RuntimePolicyError::Custom)
769    }
770
771    fn validate_slot(
772        &self,
773        key: &StableKey,
774        slot: &AllocationSlotDescriptor,
775    ) -> Result<(), Self::Error> {
776        self.validate_runtime_range(key, slot)?;
777        if matches!(
778            self.declaration_authority(key)?,
779            RuntimeDeclarationAuthority::Internal
780        ) {
781            return Ok(());
782        }
783        self.custom_policy
784            .validate_slot(key, slot)
785            .map_err(RuntimePolicyError::Custom)
786    }
787
788    fn validate_reserved_slot(
789        &self,
790        key: &StableKey,
791        slot: &AllocationSlotDescriptor,
792    ) -> Result<(), Self::Error> {
793        self.validate_runtime_range(key, slot)?;
794        if matches!(
795            self.declaration_authority(key)?,
796            RuntimeDeclarationAuthority::Internal
797        ) {
798            return Ok(());
799        }
800        self.custom_policy
801            .validate_reserved_slot(key, slot)
802            .map_err(RuntimePolicyError::Custom)
803    }
804}
805
806impl<P: AllocationPolicy> RuntimeMemoryManagerPolicy<'_, P> {
807    fn declaration_authority(
808        &self,
809        key: &StableKey,
810    ) -> Result<&RuntimeDeclarationAuthority, RuntimePolicyError<P::Error>> {
811        self.declaration_metadata
812            .get(key.as_str())
813            .ok_or_else(|| RuntimePolicyError::MissingDeclarationMetadata(key.as_str().to_string()))
814    }
815
816    fn validate_runtime_range(
817        &self,
818        key: &StableKey,
819        slot: &AllocationSlotDescriptor,
820    ) -> Result<(), RuntimePolicyError<P::Error>> {
821        let authority = self.declaration_authority(key)?;
822        // Range claims are authoritative generic policy in the default runtime.
823        // Once any user range is registered, every user declaration must fit
824        // the authority's claimed range. With no user ranges, only the
825        // internal ic-memory governance range is enforced here and custom
826        // policy may decide application-space ownership.
827        if matches!(authority, RuntimeDeclarationAuthority::Internal) {
828            self.range_authority
829                .validate_slot_authority(slot, IC_MEMORY_AUTHORITY_OWNER)?;
830            return Ok(());
831        }
832
833        let RuntimeDeclarationAuthority::External(authority) = authority else {
834            return Err(RuntimePolicyError::MissingDeclarationMetadata(
835                key.as_str().to_string(),
836            ));
837        };
838        if self.user_ranges_registered {
839            self.range_authority
840                .validate_slot_authority(slot, authority)?;
841            return Ok(());
842        }
843
844        let id = slot
845            .memory_manager_id()
846            .map_err(MemoryManagerRangeAuthorityError::Slot)?;
847        if self
848            .range_authority
849            .authority_for_id(id)
850            .map_err(RuntimePolicyError::Range)?
851            .is_some()
852        {
853            self.range_authority
854                .validate_slot_authority(slot, authority)?;
855        }
856        Ok(())
857    }
858}
859
860struct NoopPolicy;
861
862impl AllocationPolicy for NoopPolicy {
863    type Error = Infallible;
864
865    fn validate_key(&self, _key: &StableKey) -> Result<(), Self::Error> {
866        Ok(())
867    }
868
869    fn validate_slot(
870        &self,
871        _key: &StableKey,
872        _slot: &AllocationSlotDescriptor,
873    ) -> Result<(), Self::Error> {
874        Ok(())
875    }
876
877    fn validate_reserved_slot(
878        &self,
879        _key: &StableKey,
880        _slot: &AllocationSlotDescriptor,
881    ) -> Result<(), Self::Error> {
882        Ok(())
883    }
884}
885
886#[cfg(test)]
887pub fn reset_for_tests() {
888    crate::registry::reset_static_memory_declarations_for_tests();
889    EAGER_INIT_HOOKS
890        .lock()
891        .expect("ic-memory eager-init queue poisoned")
892        .clear();
893    *COMMITTED_ALLOCATIONS
894        .lock()
895        .expect("ic-memory runtime validation state poisoned") = None;
896    BOOTSTRAPPED.store(false, Ordering::SeqCst);
897    DEFAULT_LEDGER_CELL.with_borrow_mut(|cell| {
898        *cell = None;
899    });
900}
901
902#[cfg(test)]
903mod tests {
904    use super::*;
905    use crate::registry::{
906        TEST_REGISTRY_LOCK, register_static_memory_manager_declaration,
907        register_static_memory_manager_range,
908    };
909    use std::sync::atomic::{AtomicBool, Ordering};
910
911    static EAGER_INIT_RAN: AtomicBool = AtomicBool::new(false);
912
913    struct ExternalOnlyPolicy;
914
915    impl AllocationPolicy for ExternalOnlyPolicy {
916        type Error = &'static str;
917
918        fn validate_key(&self, key: &StableKey) -> Result<(), Self::Error> {
919            if crate::is_ic_memory_stable_key(key.as_str()) {
920                return Err("internal key reached external policy");
921            }
922            Ok(())
923        }
924
925        fn validate_slot(
926            &self,
927            _key: &StableKey,
928            slot: &AllocationSlotDescriptor,
929        ) -> Result<(), Self::Error> {
930            if slot
931                .memory_manager_id()
932                .is_ok_and(|id| id <= crate::MEMORY_MANAGER_GOVERNANCE_MAX_ID)
933            {
934                return Err("internal slot reached external policy");
935            }
936            Ok(())
937        }
938
939        fn validate_reserved_slot(
940            &self,
941            key: &StableKey,
942            slot: &AllocationSlotDescriptor,
943        ) -> Result<(), Self::Error> {
944            self.validate_slot(key, slot)
945        }
946    }
947
948    fn register_crate_a() {
949        register_static_memory_manager_range(
950            100,
951            109,
952            "crate_a",
953            MemoryManagerRangeMode::Reserved,
954            None,
955        )
956        .expect("crate A range");
957        register_static_memory_manager_declaration(100, "crate_a", "users", "crate_a.users.v1")
958            .expect("crate A memory");
959    }
960
961    fn register_crate_b() {
962        register_static_memory_manager_range(
963            110,
964            119,
965            "crate_b",
966            MemoryManagerRangeMode::Reserved,
967            None,
968        )
969        .expect("crate B range");
970        register_static_memory_manager_declaration(110, "crate_b", "orders", "crate_b.orders.v1")
971            .expect("crate B memory");
972    }
973
974    fn mark_eager_init() {
975        EAGER_INIT_RAN.store(true, Ordering::SeqCst);
976        register_static_memory_manager_declaration(101, "crate_a", "audit", "crate_a.audit.v1")
977            .expect("eager-init declaration");
978    }
979
980    #[test]
981    fn multi_crate_declarations_compose_into_one_bootstrap() {
982        let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
983        reset_for_tests();
984        register_crate_a();
985        register_crate_b();
986
987        let validated = bootstrap_default_memory_manager().expect("bootstrap");
988
989        assert_eq!(validated.declarations().len(), 2);
990        assert!(
991            validated
992                .declarations()
993                .iter()
994                .any(|declaration| declaration.stable_key().as_str() == "crate_a.users.v1")
995        );
996        assert!(
997            validated
998                .declarations()
999                .iter()
1000                .any(|declaration| declaration.stable_key().as_str() == "crate_b.orders.v1")
1001        );
1002    }
1003
1004    #[test]
1005    fn default_runtime_keeps_internal_ledger_slot_private() {
1006        let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
1007        reset_for_tests();
1008
1009        let validated = bootstrap_default_memory_manager().expect("bootstrap");
1010
1011        assert!(validated.declarations().is_empty());
1012        assert!(
1013            committed_allocations()
1014                .expect("published allocations")
1015                .declarations()
1016                .is_empty()
1017        );
1018        let Err(err) = open_default_memory_manager_memory(
1019            IC_MEMORY_LEDGER_STABLE_KEY,
1020            MEMORY_MANAGER_LEDGER_ID,
1021        ) else {
1022            panic!("internal ledger slot must stay private");
1023        };
1024        assert!(matches!(err, RuntimeOpenError::ReservedStableKey { .. }));
1025    }
1026
1027    #[test]
1028    fn custom_policy_validates_external_declarations_only() {
1029        let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
1030        reset_for_tests();
1031        register_static_memory_manager_range(
1032            244,
1033            244,
1034            "external_policy",
1035            MemoryManagerRangeMode::Reserved,
1036            None,
1037        )
1038        .expect("external range");
1039        register_static_memory_manager_declaration(
1040            244,
1041            "external_policy",
1042            "users",
1043            "external_policy.users.v1",
1044        )
1045        .expect("external declaration");
1046
1047        let committed = bootstrap_default_memory_manager_with_policy(&ExternalOnlyPolicy)
1048            .expect("external-only policy bootstrap");
1049
1050        assert_eq!(committed.declarations().len(), 1);
1051        assert_eq!(
1052            committed.declarations()[0].stable_key().as_str(),
1053            "external_policy.users.v1"
1054        );
1055    }
1056
1057    #[test]
1058    fn conflicting_ranges_fail() {
1059        let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
1060        reset_for_tests();
1061        register_static_memory_manager_range(
1062            100,
1063            110,
1064            "crate_a",
1065            MemoryManagerRangeMode::Reserved,
1066            None,
1067        )
1068        .expect("crate A range");
1069        register_static_memory_manager_range(
1070            105,
1071            119,
1072            "crate_b",
1073            MemoryManagerRangeMode::Reserved,
1074            None,
1075        )
1076        .expect("crate B range");
1077
1078        let err = bootstrap_default_memory_manager().expect_err("overlap must fail");
1079        assert!(matches!(
1080            err,
1081            RuntimeBootstrapError::Range(
1082                MemoryManagerRangeAuthorityError::OverlappingRanges { .. }
1083            )
1084        ));
1085    }
1086
1087    #[test]
1088    fn duplicate_stable_keys_fail() {
1089        let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
1090        reset_for_tests();
1091        register_static_memory_manager_declaration(100, "crate_a", "users", "app.users.v1")
1092            .expect("first declaration");
1093        register_static_memory_manager_declaration(101, "crate_b", "users", "app.users.v1")
1094            .expect("second declaration");
1095
1096        let err = bootstrap_default_memory_manager().expect_err("duplicate key must fail");
1097        assert!(matches!(
1098            err,
1099            RuntimeBootstrapError::Registry(StaticMemoryDeclarationError::Declaration(
1100                crate::DeclarationSnapshotError::DuplicateStableKey(_)
1101            ))
1102        ));
1103    }
1104
1105    #[test]
1106    fn duplicate_memory_manager_ids_fail() {
1107        let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
1108        reset_for_tests();
1109        register_static_memory_manager_declaration(100, "crate_a", "users", "crate_a.users.v1")
1110            .expect("first declaration");
1111        register_static_memory_manager_declaration(100, "crate_b", "orders", "crate_b.orders.v1")
1112            .expect("second declaration");
1113
1114        let err = bootstrap_default_memory_manager().expect_err("duplicate slot must fail");
1115        assert!(matches!(
1116            err,
1117            RuntimeBootstrapError::Registry(StaticMemoryDeclarationError::Declaration(
1118                crate::DeclarationSnapshotError::DuplicateSlot(_)
1119            ))
1120        ));
1121    }
1122
1123    #[test]
1124    fn out_of_range_memory_declaration_fails_when_ranges_are_declared() {
1125        let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
1126        reset_for_tests();
1127        register_static_memory_manager_range(
1128            100,
1129            109,
1130            "crate_a",
1131            MemoryManagerRangeMode::Reserved,
1132            None,
1133        )
1134        .expect("crate A range");
1135        register_static_memory_manager_declaration(120, "crate_a", "users", "crate_a.users.v1")
1136            .expect("out-of-range declaration");
1137
1138        let err = bootstrap_default_memory_manager().expect_err("out of range must fail");
1139        assert!(matches!(
1140            err,
1141            RuntimeBootstrapError::Validation(crate::AllocationValidationError::Policy(
1142                RuntimePolicyError::Range(MemoryManagerRangeAuthorityError::UnclaimedId {
1143                    id: 120
1144                })
1145            ))
1146        ));
1147    }
1148
1149    #[test]
1150    fn late_registration_after_bootstrap_fails() {
1151        let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
1152        reset_for_tests();
1153        register_static_memory_manager_declaration(100, "crate_a", "users", "crate_a.users.v1")
1154            .expect("declaration");
1155        bootstrap_default_memory_manager().expect("bootstrap");
1156
1157        let err = register_static_memory_manager_declaration(
1158            101,
1159            "crate_a",
1160            "orders",
1161            "crate_a.orders.v1",
1162        )
1163        .expect_err("late registration must fail");
1164        assert_eq!(err, StaticMemoryDeclarationError::RegistrySealed);
1165    }
1166
1167    #[test]
1168    fn late_eager_init_registration_after_bootstrap_fails() {
1169        let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
1170        reset_for_tests();
1171        register_static_memory_manager_declaration(100, "crate_a", "users", "crate_a.users.v1")
1172            .expect("declaration");
1173        bootstrap_default_memory_manager().expect("bootstrap");
1174
1175        let err = std::panic::catch_unwind(|| defer_eager_init(mark_eager_init))
1176            .expect_err("late eager-init registration must fail");
1177
1178        let message = err
1179            .downcast_ref::<String>()
1180            .map(String::as_str)
1181            .or_else(|| err.downcast_ref::<&str>().copied())
1182            .expect("panic message");
1183        assert!(message.contains("after runtime bootstrap"));
1184    }
1185
1186    #[test]
1187    fn eager_init_runs_before_snapshot_seal() {
1188        let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
1189        reset_for_tests();
1190        EAGER_INIT_RAN.store(false, Ordering::SeqCst);
1191        register_static_memory_manager_range(
1192            100,
1193            109,
1194            "crate_a",
1195            MemoryManagerRangeMode::Reserved,
1196            None,
1197        )
1198        .expect("crate A range");
1199        defer_eager_init(mark_eager_init);
1200
1201        let validated = bootstrap_default_memory_manager().expect("bootstrap");
1202
1203        assert!(EAGER_INIT_RAN.load(Ordering::SeqCst));
1204        assert!(
1205            validated
1206                .declarations()
1207                .iter()
1208                .any(|declaration| declaration.stable_key().as_str() == "crate_a.audit.v1")
1209        );
1210    }
1211
1212    #[test]
1213    fn direct_user_can_bootstrap_and_open_without_canic() {
1214        let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
1215        reset_for_tests();
1216        register_static_memory_manager_range(
1217            120,
1218            129,
1219            "icydb",
1220            MemoryManagerRangeMode::Reserved,
1221            None,
1222        )
1223        .expect("icydb range");
1224        register_static_memory_manager_declaration(120, "icydb", "users", "icydb.users.data.v1")
1225            .expect("icydb declaration");
1226
1227        bootstrap_default_memory_manager().expect("bootstrap");
1228        open_default_memory_manager_memory("icydb.users.data.v1", 120).expect("open memory");
1229    }
1230
1231    #[test]
1232    fn diagnostic_export_reports_default_memory_manager_sizes() {
1233        let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
1234        reset_for_tests();
1235        register_static_memory_manager_range(
1236            130,
1237            139,
1238            "diagnostics",
1239            MemoryManagerRangeMode::Reserved,
1240            None,
1241        )
1242        .expect("diagnostics range");
1243        register_static_memory_manager_declaration(
1244            130,
1245            "diagnostics",
1246            "users",
1247            "diagnostics.users.v1",
1248        )
1249        .expect("diagnostics declaration");
1250
1251        bootstrap_default_memory_manager().expect("bootstrap");
1252        let memory =
1253            open_default_memory_manager_memory("diagnostics.users.v1", 130).expect("open memory");
1254        let old_size = memory.size();
1255        memory.grow(2);
1256
1257        let export = default_memory_manager_diagnostic_export().expect("diagnostic export");
1258        let recovery =
1259            default_memory_manager_commit_recovery_diagnostic().expect("recovery diagnostic");
1260        let record = export
1261            .records
1262            .iter()
1263            .find(|record| record.allocation.stable_key().as_str() == "diagnostics.users.v1")
1264            .expect("diagnostic allocation");
1265
1266        assert_eq!(recovery.recovery, Ok(export.current_generation));
1267        assert_eq!(
1268            record.memory_size,
1269            Some(DiagnosticMemorySize::from_wasm_pages(old_size + 2))
1270        );
1271    }
1272
1273    #[test]
1274    fn doctor_report_preflights_before_bootstrap() {
1275        let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
1276        reset_for_tests();
1277        register_static_memory_manager_range(
1278            240,
1279            240,
1280            "doctor_preflight",
1281            MemoryManagerRangeMode::Reserved,
1282            None,
1283        )
1284        .expect("doctor range");
1285        register_static_memory_manager_declaration(
1286            240,
1287            "doctor_preflight",
1288            "users",
1289            "doctor_preflight.users.v1",
1290        )
1291        .expect("doctor declaration");
1292
1293        let report = default_memory_manager_doctor_report();
1294
1295        assert!(!report.bootstrapped);
1296        assert_eq!(report.registered_declarations.len(), 1);
1297        assert!(report.range_authority.effective_authority.is_ok());
1298        assert_eq!(report.validation, crate::DiagnosticCheck::Passed);
1299        assert!(report.commit_recovery.is_some());
1300        assert!(matches!(
1301            report.stable_cell.status,
1302            crate::DiagnosticStableCellStatus::Empty | crate::DiagnosticStableCellStatus::Readable
1303        ));
1304    }
1305
1306    #[test]
1307    fn doctor_report_includes_recovered_ledger_and_memory_sizes_after_bootstrap() {
1308        let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
1309        reset_for_tests();
1310        register_static_memory_manager_range(
1311            241,
1312            241,
1313            "doctor_runtime",
1314            MemoryManagerRangeMode::Reserved,
1315            None,
1316        )
1317        .expect("doctor range");
1318        register_static_memory_manager_declaration(
1319            241,
1320            "doctor_runtime",
1321            "orders",
1322            "doctor_runtime.orders.v1",
1323        )
1324        .expect("doctor declaration");
1325
1326        bootstrap_default_memory_manager().expect("bootstrap");
1327        let memory = open_default_memory_manager_memory("doctor_runtime.orders.v1", 241)
1328            .expect("open memory");
1329        let old_size = memory.size();
1330        memory.grow(1);
1331
1332        let report = default_memory_manager_doctor_report();
1333        let ledger = report.ledger.expect("recovered ledger export");
1334        let record = ledger
1335            .records
1336            .iter()
1337            .find(|record| record.allocation.stable_key().as_str() == "doctor_runtime.orders.v1")
1338            .expect("doctor allocation");
1339
1340        assert!(report.bootstrapped);
1341        assert_eq!(
1342            report.stable_cell.status,
1343            crate::DiagnosticStableCellStatus::Readable
1344        );
1345        assert_eq!(report.validation, crate::DiagnosticCheck::Passed);
1346        assert_eq!(
1347            record.memory_size,
1348            Some(DiagnosticMemorySize::from_wasm_pages(old_size + 1))
1349        );
1350    }
1351
1352    #[test]
1353    fn doctor_report_captures_validation_failure() {
1354        let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
1355        reset_for_tests();
1356        register_static_memory_manager_declaration(
1357            242,
1358            "doctor_failure_a",
1359            "users",
1360            "doctor_failure.users.v1",
1361        )
1362        .expect("first declaration");
1363        register_static_memory_manager_declaration(
1364            243,
1365            "doctor_failure_b",
1366            "orders",
1367            "doctor_failure.users.v1",
1368        )
1369        .expect("second declaration");
1370
1371        let report = default_memory_manager_doctor_report();
1372
1373        let crate::DiagnosticCheck::Failed { message } = report.validation else {
1374            panic!("validation must fail");
1375        };
1376        assert!(message.contains("declared more than once"));
1377    }
1378}