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