Skip to main content

ic_memory/
runtime.rs

1use crate::{
2    AllocationBootstrap, AllocationDeclaration, AllocationHistory, AllocationLedger,
3    AllocationPolicy, AllocationSlotDescriptor, DeclarationSnapshot, DiagnosticExport,
4    DiagnosticMemorySize, LedgerCommitError, StableCellLedgerError, StableCellLedgerRecord,
5    StableKey, ValidatedAllocations, decode_stable_cell_ledger_record, decode_stable_cell_payload,
6    physical::CommitStoreDiagnostic,
7    registry::{
8        StaticMemoryDeclaration, StaticMemoryDeclarationError, StaticMemoryRangeDeclaration,
9        seal_static_memory_registry, static_memory_declarations, static_memory_range_declarations,
10    },
11    slot::{
12        IC_MEMORY_AUTHORITY_OWNER, IC_MEMORY_AUTHORITY_PURPOSE, IC_MEMORY_LEDGER_LABEL,
13        IC_MEMORY_LEDGER_STABLE_KEY, MEMORY_MANAGER_LEDGER_ID, MemoryManagerAuthorityRecord,
14        MemoryManagerIdRange, MemoryManagerRangeAuthority, MemoryManagerRangeAuthorityError,
15        MemoryManagerRangeMode, MemoryManagerSlotError,
16    },
17};
18use ic_stable_structures::{
19    Cell, DefaultMemoryImpl, Memory,
20    memory_manager::{MemoryId, MemoryManager, VirtualMemory},
21};
22use std::{
23    cell::RefCell,
24    collections::BTreeMap,
25    convert::Infallible,
26    sync::{
27        Mutex,
28        atomic::{AtomicBool, Ordering},
29    },
30};
31
32type DefaultLedgerCell = Cell<StableCellLedgerRecord, VirtualMemory<DefaultMemoryImpl>>;
33
34thread_local! {
35    static DEFAULT_MEMORY_MANAGER: MemoryManager<DefaultMemoryImpl> =
36        MemoryManager::init(DefaultMemoryImpl::default());
37    static DEFAULT_LEDGER_CELL: RefCell<Option<DefaultLedgerCell>> = const {
38        RefCell::new(None)
39    };
40}
41
42static EAGER_INIT_HOOKS: Mutex<Vec<fn()>> = Mutex::new(Vec::new());
43static VALIDATED_ALLOCATIONS: Mutex<Option<ValidatedAllocations>> = Mutex::new(None);
44static BOOTSTRAPPED: AtomicBool = AtomicBool::new(false);
45
46///
47/// RuntimeBootstrapError
48///
49/// Failure to bootstrap the generic `ic-memory` runtime layer.
50#[derive(Debug, thiserror::Error)]
51pub enum RuntimeBootstrapError<P> {
52    /// Runtime registration or snapshot collection failed.
53    #[error(transparent)]
54    Registry(#[from] StaticMemoryDeclarationError),
55    /// Runtime range authority table is invalid.
56    #[error(transparent)]
57    Range(#[from] MemoryManagerRangeAuthorityError),
58    /// Runtime ledger genesis construction failed.
59    #[error(transparent)]
60    LedgerIntegrity(#[from] crate::LedgerIntegrityError),
61    /// Protected ledger recovery or commit failed.
62    #[error(transparent)]
63    LedgerCommit(#[from] crate::LedgerCommitError),
64    /// Stable-cell ledger storage is corrupt before protected recovery can run.
65    #[error(transparent)]
66    StableCellLedger(#[from] StableCellLedgerError),
67    /// Declaration validation failed.
68    #[error(transparent)]
69    Validation(#[from] crate::AllocationValidationError<RuntimePolicyError<P>>),
70    /// Validated declarations could not be staged.
71    #[error(transparent)]
72    Staging(#[from] crate::AllocationStageError),
73    /// Runtime state lock was poisoned.
74    #[error("ic-memory runtime lock poisoned")]
75    RuntimeLockPoisoned,
76}
77
78///
79/// RuntimeOpenError
80///
81/// Failure to open a validated allocation through the default runtime substrate.
82#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
83pub enum RuntimeOpenError {
84    /// Runtime bootstrap has not published validated allocations.
85    #[error("ic-memory runtime has not completed bootstrap validation")]
86    NotBootstrapped,
87    /// Runtime state lock was poisoned.
88    #[error("ic-memory runtime lock poisoned")]
89    RuntimeLockPoisoned,
90    /// Stable-key grammar failure.
91    #[error(transparent)]
92    StableKey(#[from] crate::StableKeyError),
93    /// The stable key was not present in the validated declaration set.
94    #[error("stable key '{0}' was not validated by ic-memory runtime bootstrap")]
95    StableKeyNotValidated(String),
96    /// The validated slot is not a usable `MemoryManager` ID.
97    #[error(transparent)]
98    MemoryManagerSlot(#[from] MemoryManagerSlotError),
99    /// The requested memory ID does not match the validated stable-key binding.
100    #[error(
101        "stable key '{stable_key}' is validated for MemoryManager ID {validated_id}, not requested ID {requested_id}"
102    )]
103    MemoryIdMismatch {
104        /// Stable key being opened.
105        stable_key: String,
106        /// Validated MemoryManager ID.
107        validated_id: u8,
108        /// Requested MemoryManager ID.
109        requested_id: u8,
110    },
111}
112
113///
114/// RuntimeDiagnosticError
115///
116/// Failure to build diagnostics for the default `MemoryManager` runtime.
117///
118
119#[derive(Debug, thiserror::Error)]
120pub enum RuntimeDiagnosticError {
121    /// Runtime bootstrap has not opened and validated the ledger cell.
122    #[error("ic-memory runtime has not completed bootstrap validation")]
123    NotBootstrapped,
124    /// The recovered allocation ledger failed protected commit validation.
125    #[error(transparent)]
126    LedgerCommit(#[from] LedgerCommitError),
127    /// Stable-cell ledger storage is corrupt before protected recovery can run.
128    #[error(transparent)]
129    StableCellLedger(#[from] StableCellLedgerError),
130    /// A committed allocation slot was not a usable `MemoryManager` ID.
131    #[error(transparent)]
132    MemoryManagerSlot(#[from] MemoryManagerSlotError),
133}
134
135///
136/// RuntimePolicyError
137///
138/// Failure in generic runtime range policy or caller-supplied policy.
139#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
140pub enum RuntimePolicyError<P> {
141    /// Runtime range authority rejected the declaration.
142    #[error(transparent)]
143    Range(#[from] MemoryManagerRangeAuthorityError),
144    /// Runtime metadata is internally inconsistent.
145    #[error("runtime declaration metadata is missing for stable key '{0}'")]
146    MissingDeclarationMetadata(String),
147    /// `ic_memory.*` stable keys are reserved to the `ic-memory` authority.
148    #[error("stable key '{stable_key}' is reserved to authority '{expected_authority}'")]
149    ReservedStableKeyAuthority {
150        /// Stable key being declared.
151        stable_key: String,
152        /// Required declaring authority.
153        expected_authority: &'static str,
154    },
155    /// Caller-supplied policy rejected the declaration.
156    #[error(transparent)]
157    Custom(P),
158}
159
160/// Register a pre-bootstrap declaration hook.
161pub fn defer_eager_init(f: fn()) {
162    assert!(
163        !is_default_memory_manager_bootstrapped(),
164        "ic-memory eager-init registration attempted after runtime bootstrap"
165    );
166    EAGER_INIT_HOOKS
167        .lock()
168        .expect("ic-memory eager-init queue poisoned")
169        .push(f);
170}
171
172/// Return true once default runtime bootstrap has completed.
173#[must_use]
174pub fn is_default_memory_manager_bootstrapped() -> bool {
175    BOOTSTRAPPED.load(Ordering::SeqCst)
176}
177
178/// Return the published validated allocations for the default runtime substrate.
179pub fn validated_allocations() -> Result<ValidatedAllocations, RuntimeOpenError> {
180    if !is_default_memory_manager_bootstrapped() {
181        return Err(RuntimeOpenError::NotBootstrapped);
182    }
183    VALIDATED_ALLOCATIONS
184        .lock()
185        .map_err(|_| RuntimeOpenError::RuntimeLockPoisoned)?
186        .clone()
187        .ok_or(RuntimeOpenError::NotBootstrapped)
188}
189
190/// Bootstrap the default `MemoryManager<DefaultMemoryImpl>` runtime using generic policy.
191pub fn bootstrap_default_memory_manager()
192-> Result<ValidatedAllocations, RuntimeBootstrapError<Infallible>> {
193    bootstrap_default_memory_manager_with_policy(&NoopPolicy)
194}
195
196/// Bootstrap the default runtime and layer caller-supplied policy over generic range checks.
197///
198/// Authority order is explicit:
199///
200/// 1. `ic-memory` always owns its governance range.
201/// 2. If any user range is registered, all `MemoryManager` declarations must
202///    belong to the range claimed by their declaring crate.
203/// 3. The caller-supplied [`AllocationPolicy`] then applies framework-specific
204///    namespace and lifecycle rules.
205///
206/// Framework adapters such as Canic should register only the ranges they want
207/// this generic runtime to enforce. If a framework wants its own policy to be
208/// authoritative for application space, it should omit user range registrations
209/// for that space and enforce the rule in its [`AllocationPolicy`].
210pub fn bootstrap_default_memory_manager_with_policy<P: AllocationPolicy>(
211    policy: &P,
212) -> Result<ValidatedAllocations, RuntimeBootstrapError<P::Error>> {
213    if let Ok(validated) = validated_allocations() {
214        return Ok(validated);
215    }
216
217    run_eager_init_hooks();
218
219    let registered_declarations = static_memory_declarations()?;
220    let registered_ranges = static_memory_range_declarations()?;
221    let user_ranges_registered = !registered_ranges.is_empty();
222    let declaration_metadata = declaration_metadata(&registered_declarations);
223    let range_authority = range_authority(registered_ranges)?;
224    let snapshot = declaration_snapshot(registered_declarations)?;
225    seal_static_memory_registry()?;
226    let policy = RuntimeMemoryManagerPolicy {
227        range_authority,
228        user_ranges_registered,
229        declaration_metadata,
230        custom_policy: policy,
231    };
232    let genesis = AllocationLedger::new(0, AllocationHistory::default())?;
233
234    let validated = with_default_ledger_cell(
235        |cell| -> Result<ValidatedAllocations, RuntimeBootstrapError<P::Error>> {
236            let mut record = cell.get().clone();
237            let mut bootstrap = AllocationBootstrap::new(record.store_mut());
238            let commit = bootstrap
239                .initialize_validate_and_commit(&genesis, snapshot, &policy, None)
240                .map_err(runtime_bootstrap_error_from_bootstrap)?;
241            cell.set(record);
242            Ok(commit.validated)
243        },
244    )?;
245
246    publish_validated_allocations(validated.clone())?;
247    BOOTSTRAPPED.store(true, Ordering::SeqCst);
248    Ok(validated)
249}
250
251/// Open a validated `MemoryManager` memory by stable key and expected ID.
252pub fn open_default_memory_manager_memory(
253    stable_key: &str,
254    id: u8,
255) -> Result<VirtualMemory<DefaultMemoryImpl>, RuntimeOpenError> {
256    let key = StableKey::parse(stable_key)?;
257    let validated = validated_allocations()?;
258    let slot = validated
259        .slot_for(&key)
260        .ok_or_else(|| RuntimeOpenError::StableKeyNotValidated(stable_key.to_string()))?;
261    let validated_id = slot.memory_manager_id()?;
262    if validated_id != id {
263        return Err(RuntimeOpenError::MemoryIdMismatch {
264            stable_key: stable_key.to_string(),
265            validated_id,
266            requested_id: id,
267        });
268    }
269    Ok(default_memory_manager_memory(id))
270}
271
272/// Build a diagnostic export for the default `MemoryManager` runtime.
273///
274/// Each allocation record includes the live `VirtualMemory::size()` for its
275/// slot when the committed ledger can be recovered. The reported size is the
276/// virtual memory size in WebAssembly pages and bytes, not logical data bytes
277/// stored by a particular stable-structure collection.
278pub fn default_memory_manager_diagnostic_export() -> Result<DiagnosticExport, RuntimeDiagnosticError>
279{
280    let record = default_ledger_record_for_diagnostics()?;
281    let recovered = record.store().recover()?;
282    let ledger = recovered.ledger();
283    let memory_sizes = default_memory_manager_memory_sizes(ledger)?;
284
285    Ok(
286        DiagnosticExport::from_ledger_with_commit_recovery_and_memory_sizes(
287            ledger,
288            AllocationSlotDescriptor::memory_manager(MEMORY_MANAGER_LEDGER_ID)?,
289            Some(record.store().physical().diagnostic()),
290            memory_sizes,
291        ),
292    )
293}
294
295/// Build a protected commit recovery diagnostic for the default ledger store.
296///
297/// Unlike [`default_memory_manager_diagnostic_export`], this helper does not
298/// require successful bootstrap. It can diagnose empty or partially corrupt
299/// dual-slot commit state as long as the enclosing stable-cell ledger record is
300/// readable.
301pub fn default_memory_manager_commit_recovery_diagnostic()
302-> Result<CommitStoreDiagnostic, RuntimeDiagnosticError> {
303    let record = default_ledger_record_from_memory()?;
304    Ok(record.store().physical().diagnostic())
305}
306
307fn run_eager_init_hooks() {
308    let hooks = {
309        let mut hooks = EAGER_INIT_HOOKS
310            .lock()
311            .expect("ic-memory eager-init queue poisoned");
312        std::mem::take(&mut *hooks)
313    };
314
315    for hook in hooks {
316        hook();
317    }
318}
319
320fn with_default_ledger_cell<P, T>(
321    op: impl FnOnce(&mut DefaultLedgerCell) -> Result<T, RuntimeBootstrapError<P>>,
322) -> Result<T, RuntimeBootstrapError<P>> {
323    DEFAULT_LEDGER_CELL.with(|cell| {
324        let mut cell = cell.borrow_mut();
325        if cell.is_none() {
326            let memory = default_memory_manager_memory(MEMORY_MANAGER_LEDGER_ID);
327            crate::validate_stable_cell_ledger_memory(&memory)?;
328            *cell = Some(Cell::init(memory, StableCellLedgerRecord::default()));
329        }
330        op(cell.as_mut().expect("default ledger cell initialized"))
331    })
332}
333
334fn default_memory_manager_memory(id: u8) -> VirtualMemory<DefaultMemoryImpl> {
335    DEFAULT_MEMORY_MANAGER.with(|manager| manager.get(MemoryId::new(id)))
336}
337
338fn default_ledger_record_for_diagnostics() -> Result<StableCellLedgerRecord, RuntimeDiagnosticError>
339{
340    if !is_default_memory_manager_bootstrapped() {
341        return Err(RuntimeDiagnosticError::NotBootstrapped);
342    }
343
344    default_ledger_record_from_memory().map_err(RuntimeDiagnosticError::StableCellLedger)
345}
346
347fn default_ledger_record_from_memory() -> Result<StableCellLedgerRecord, StableCellLedgerError> {
348    let memory = default_memory_manager_memory(MEMORY_MANAGER_LEDGER_ID);
349    if memory.size() == 0 {
350        return Ok(StableCellLedgerRecord::default());
351    }
352
353    let payload = decode_stable_cell_payload(&memory)?;
354    decode_stable_cell_ledger_record(&payload).map_err(StableCellLedgerError::Record)
355}
356
357fn default_memory_manager_memory_sizes(
358    ledger: &AllocationLedger,
359) -> Result<Vec<(AllocationSlotDescriptor, DiagnosticMemorySize)>, RuntimeDiagnosticError> {
360    ledger
361        .allocation_history()
362        .records()
363        .iter()
364        .map(|record| {
365            let id = record.slot().memory_manager_id()?;
366            let memory = default_memory_manager_memory(id);
367            Ok((
368                record.slot().clone(),
369                DiagnosticMemorySize::from_wasm_pages(memory.size()),
370            ))
371        })
372        .collect()
373}
374
375fn publish_validated_allocations<P>(
376    validated: ValidatedAllocations,
377) -> Result<(), RuntimeBootstrapError<P>> {
378    *VALIDATED_ALLOCATIONS
379        .lock()
380        .map_err(|_| RuntimeBootstrapError::RuntimeLockPoisoned)? = Some(validated);
381    Ok(())
382}
383
384fn declaration_snapshot(
385    registrations: Vec<StaticMemoryDeclaration>,
386) -> Result<DeclarationSnapshot, StaticMemoryDeclarationError> {
387    let mut declarations = Vec::with_capacity(registrations.len() + 1);
388    declarations.push(internal_ledger_declaration()?);
389    declarations.extend(
390        registrations
391            .into_iter()
392            .map(StaticMemoryDeclaration::into_declaration),
393    );
394    DeclarationSnapshot::new(declarations).map_err(StaticMemoryDeclarationError::Declaration)
395}
396
397fn declaration_metadata(registrations: &[StaticMemoryDeclaration]) -> BTreeMap<String, String> {
398    let mut metadata = BTreeMap::new();
399    metadata.insert(
400        IC_MEMORY_LEDGER_STABLE_KEY.to_string(),
401        IC_MEMORY_AUTHORITY_OWNER.to_string(),
402    );
403    for registration in registrations {
404        metadata.insert(
405            registration.declaration().stable_key().as_str().to_string(),
406            registration.declaring_crate().to_string(),
407        );
408    }
409    metadata
410}
411
412fn range_authority(
413    registrations: Vec<StaticMemoryRangeDeclaration>,
414) -> Result<MemoryManagerRangeAuthority, MemoryManagerRangeAuthorityError> {
415    let mut records = Vec::with_capacity(registrations.len() + 1);
416    records.push(internal_ledger_range()?);
417    records.extend(
418        registrations
419            .into_iter()
420            .map(StaticMemoryRangeDeclaration::into_record),
421    );
422    MemoryManagerRangeAuthority::from_records(records)
423}
424
425fn internal_ledger_declaration() -> Result<AllocationDeclaration, crate::DeclarationSnapshotError> {
426    AllocationDeclaration::memory_manager(
427        IC_MEMORY_LEDGER_STABLE_KEY,
428        MEMORY_MANAGER_LEDGER_ID,
429        IC_MEMORY_LEDGER_LABEL,
430    )
431}
432
433fn internal_ledger_range() -> Result<MemoryManagerAuthorityRecord, MemoryManagerRangeAuthorityError>
434{
435    MemoryManagerAuthorityRecord::new(
436        MemoryManagerIdRange::new(
437            MEMORY_MANAGER_LEDGER_ID,
438            crate::MEMORY_MANAGER_GOVERNANCE_MAX_ID,
439        )?,
440        IC_MEMORY_AUTHORITY_OWNER,
441        MemoryManagerRangeMode::Reserved,
442        Some(IC_MEMORY_AUTHORITY_PURPOSE.to_string()),
443    )
444}
445
446fn runtime_bootstrap_error_from_bootstrap<P>(
447    err: crate::BootstrapError<RuntimePolicyError<P>>,
448) -> RuntimeBootstrapError<P> {
449    match err {
450        crate::BootstrapError::Ledger(err) => RuntimeBootstrapError::LedgerCommit(err),
451        crate::BootstrapError::Validation(err) => RuntimeBootstrapError::Validation(err),
452        crate::BootstrapError::Staging(err) => RuntimeBootstrapError::Staging(err),
453    }
454}
455
456struct RuntimeMemoryManagerPolicy<'a, P> {
457    range_authority: MemoryManagerRangeAuthority,
458    user_ranges_registered: bool,
459    declaration_metadata: BTreeMap<String, String>,
460    custom_policy: &'a P,
461}
462
463impl<P: AllocationPolicy> AllocationPolicy for RuntimeMemoryManagerPolicy<'_, P> {
464    type Error = RuntimePolicyError<P::Error>;
465
466    fn validate_key(&self, key: &StableKey) -> Result<(), Self::Error> {
467        let declaring_crate = self.declaring_crate(key)?;
468        if crate::is_ic_memory_stable_key(key.as_str())
469            && declaring_crate != IC_MEMORY_AUTHORITY_OWNER
470        {
471            return Err(RuntimePolicyError::ReservedStableKeyAuthority {
472                stable_key: key.as_str().to_string(),
473                expected_authority: IC_MEMORY_AUTHORITY_OWNER,
474            });
475        }
476        self.custom_policy
477            .validate_key(key)
478            .map_err(RuntimePolicyError::Custom)
479    }
480
481    fn validate_slot(
482        &self,
483        key: &StableKey,
484        slot: &AllocationSlotDescriptor,
485    ) -> Result<(), Self::Error> {
486        self.validate_runtime_range(key, slot)?;
487        self.custom_policy
488            .validate_slot(key, slot)
489            .map_err(RuntimePolicyError::Custom)
490    }
491
492    fn validate_reserved_slot(
493        &self,
494        key: &StableKey,
495        slot: &AllocationSlotDescriptor,
496    ) -> Result<(), Self::Error> {
497        self.validate_runtime_range(key, slot)?;
498        self.custom_policy
499            .validate_reserved_slot(key, slot)
500            .map_err(RuntimePolicyError::Custom)
501    }
502}
503
504impl<P: AllocationPolicy> RuntimeMemoryManagerPolicy<'_, P> {
505    fn declaring_crate(&self, key: &StableKey) -> Result<&str, RuntimePolicyError<P::Error>> {
506        self.declaration_metadata
507            .get(key.as_str())
508            .map(String::as_str)
509            .ok_or_else(|| RuntimePolicyError::MissingDeclarationMetadata(key.as_str().to_string()))
510    }
511
512    fn validate_runtime_range(
513        &self,
514        key: &StableKey,
515        slot: &AllocationSlotDescriptor,
516    ) -> Result<(), RuntimePolicyError<P::Error>> {
517        let declaring_crate = self.declaring_crate(key)?;
518        // Range claims are authoritative generic policy in the default runtime.
519        // Once any user range is registered, every user declaration must fit
520        // the declaring crate's claimed range. With no user ranges, only the
521        // internal ic-memory governance range is enforced here and custom
522        // policy may decide application-space ownership.
523        if declaring_crate == IC_MEMORY_AUTHORITY_OWNER || self.user_ranges_registered {
524            self.range_authority
525                .validate_slot_authority(slot, declaring_crate)?;
526            return Ok(());
527        }
528
529        let id = slot
530            .memory_manager_id()
531            .map_err(MemoryManagerRangeAuthorityError::Slot)?;
532        if self
533            .range_authority
534            .authority_for_id(id)
535            .map_err(RuntimePolicyError::Range)?
536            .is_some()
537        {
538            self.range_authority
539                .validate_slot_authority(slot, declaring_crate)?;
540        }
541        Ok(())
542    }
543}
544
545struct NoopPolicy;
546
547impl AllocationPolicy for NoopPolicy {
548    type Error = Infallible;
549
550    fn validate_key(&self, _key: &StableKey) -> Result<(), Self::Error> {
551        Ok(())
552    }
553
554    fn validate_slot(
555        &self,
556        _key: &StableKey,
557        _slot: &AllocationSlotDescriptor,
558    ) -> Result<(), Self::Error> {
559        Ok(())
560    }
561
562    fn validate_reserved_slot(
563        &self,
564        _key: &StableKey,
565        _slot: &AllocationSlotDescriptor,
566    ) -> Result<(), Self::Error> {
567        Ok(())
568    }
569}
570
571#[cfg(test)]
572pub(crate) fn reset_for_tests() {
573    crate::registry::reset_static_memory_declarations_for_tests();
574    EAGER_INIT_HOOKS
575        .lock()
576        .expect("ic-memory eager-init queue poisoned")
577        .clear();
578    *VALIDATED_ALLOCATIONS
579        .lock()
580        .expect("ic-memory runtime validation state poisoned") = None;
581    BOOTSTRAPPED.store(false, Ordering::SeqCst);
582    DEFAULT_LEDGER_CELL.with_borrow_mut(|cell| {
583        *cell = None;
584    });
585}
586
587#[cfg(test)]
588mod tests {
589    use super::*;
590    use crate::registry::{
591        TEST_REGISTRY_LOCK, register_static_memory_manager_declaration,
592        register_static_memory_manager_range,
593    };
594    use std::sync::atomic::{AtomicBool, Ordering};
595
596    static EAGER_INIT_RAN: AtomicBool = AtomicBool::new(false);
597
598    fn register_crate_a() {
599        register_static_memory_manager_range(
600            100,
601            109,
602            "crate_a",
603            MemoryManagerRangeMode::Reserved,
604            None,
605        )
606        .expect("crate A range");
607        register_static_memory_manager_declaration(100, "crate_a", "users", "crate_a.users.v1")
608            .expect("crate A memory");
609    }
610
611    fn register_crate_b() {
612        register_static_memory_manager_range(
613            110,
614            119,
615            "crate_b",
616            MemoryManagerRangeMode::Reserved,
617            None,
618        )
619        .expect("crate B range");
620        register_static_memory_manager_declaration(110, "crate_b", "orders", "crate_b.orders.v1")
621            .expect("crate B memory");
622    }
623
624    fn mark_eager_init() {
625        EAGER_INIT_RAN.store(true, Ordering::SeqCst);
626        register_static_memory_manager_declaration(101, "crate_a", "audit", "crate_a.audit.v1")
627            .expect("eager-init declaration");
628    }
629
630    #[test]
631    fn multi_crate_declarations_compose_into_one_bootstrap() {
632        let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
633        reset_for_tests();
634        register_crate_a();
635        register_crate_b();
636
637        let validated = bootstrap_default_memory_manager().expect("bootstrap");
638
639        assert_eq!(validated.declarations().len(), 3);
640        assert!(
641            validated
642                .declarations()
643                .iter()
644                .any(|declaration| declaration.stable_key().as_str() == "crate_a.users.v1")
645        );
646        assert!(
647            validated
648                .declarations()
649                .iter()
650                .any(|declaration| declaration.stable_key().as_str() == "crate_b.orders.v1")
651        );
652    }
653
654    #[test]
655    fn conflicting_ranges_fail() {
656        let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
657        reset_for_tests();
658        register_static_memory_manager_range(
659            100,
660            110,
661            "crate_a",
662            MemoryManagerRangeMode::Reserved,
663            None,
664        )
665        .expect("crate A range");
666        register_static_memory_manager_range(
667            105,
668            119,
669            "crate_b",
670            MemoryManagerRangeMode::Reserved,
671            None,
672        )
673        .expect("crate B range");
674
675        let err = bootstrap_default_memory_manager().expect_err("overlap must fail");
676        assert!(matches!(
677            err,
678            RuntimeBootstrapError::Range(
679                MemoryManagerRangeAuthorityError::OverlappingRanges { .. }
680            )
681        ));
682    }
683
684    #[test]
685    fn duplicate_stable_keys_fail() {
686        let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
687        reset_for_tests();
688        register_static_memory_manager_declaration(100, "crate_a", "users", "app.users.v1")
689            .expect("first declaration");
690        register_static_memory_manager_declaration(101, "crate_b", "users", "app.users.v1")
691            .expect("second declaration");
692
693        let err = bootstrap_default_memory_manager().expect_err("duplicate key must fail");
694        assert!(matches!(
695            err,
696            RuntimeBootstrapError::Registry(StaticMemoryDeclarationError::Declaration(
697                crate::DeclarationSnapshotError::DuplicateStableKey(_)
698            ))
699        ));
700    }
701
702    #[test]
703    fn duplicate_memory_manager_ids_fail() {
704        let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
705        reset_for_tests();
706        register_static_memory_manager_declaration(100, "crate_a", "users", "crate_a.users.v1")
707            .expect("first declaration");
708        register_static_memory_manager_declaration(100, "crate_b", "orders", "crate_b.orders.v1")
709            .expect("second declaration");
710
711        let err = bootstrap_default_memory_manager().expect_err("duplicate slot must fail");
712        assert!(matches!(
713            err,
714            RuntimeBootstrapError::Registry(StaticMemoryDeclarationError::Declaration(
715                crate::DeclarationSnapshotError::DuplicateSlot(_)
716            ))
717        ));
718    }
719
720    #[test]
721    fn out_of_range_memory_declaration_fails_when_ranges_are_declared() {
722        let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
723        reset_for_tests();
724        register_static_memory_manager_range(
725            100,
726            109,
727            "crate_a",
728            MemoryManagerRangeMode::Reserved,
729            None,
730        )
731        .expect("crate A range");
732        register_static_memory_manager_declaration(120, "crate_a", "users", "crate_a.users.v1")
733            .expect("out-of-range declaration");
734
735        let err = bootstrap_default_memory_manager().expect_err("out of range must fail");
736        assert!(matches!(
737            err,
738            RuntimeBootstrapError::Validation(crate::AllocationValidationError::Policy(
739                RuntimePolicyError::Range(MemoryManagerRangeAuthorityError::UnclaimedId {
740                    id: 120
741                })
742            ))
743        ));
744    }
745
746    #[test]
747    fn late_registration_after_bootstrap_fails() {
748        let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
749        reset_for_tests();
750        register_static_memory_manager_declaration(100, "crate_a", "users", "crate_a.users.v1")
751            .expect("declaration");
752        bootstrap_default_memory_manager().expect("bootstrap");
753
754        let err = register_static_memory_manager_declaration(
755            101,
756            "crate_a",
757            "orders",
758            "crate_a.orders.v1",
759        )
760        .expect_err("late registration must fail");
761        assert_eq!(err, StaticMemoryDeclarationError::RegistrySealed);
762    }
763
764    #[test]
765    fn late_eager_init_registration_after_bootstrap_fails() {
766        let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
767        reset_for_tests();
768        register_static_memory_manager_declaration(100, "crate_a", "users", "crate_a.users.v1")
769            .expect("declaration");
770        bootstrap_default_memory_manager().expect("bootstrap");
771
772        let err = std::panic::catch_unwind(|| defer_eager_init(mark_eager_init))
773            .expect_err("late eager-init registration must fail");
774
775        let message = err
776            .downcast_ref::<String>()
777            .map(String::as_str)
778            .or_else(|| err.downcast_ref::<&str>().copied())
779            .expect("panic message");
780        assert!(message.contains("after runtime bootstrap"));
781    }
782
783    #[test]
784    fn eager_init_runs_before_snapshot_seal() {
785        let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
786        reset_for_tests();
787        EAGER_INIT_RAN.store(false, Ordering::SeqCst);
788        register_static_memory_manager_range(
789            100,
790            109,
791            "crate_a",
792            MemoryManagerRangeMode::Reserved,
793            None,
794        )
795        .expect("crate A range");
796        defer_eager_init(mark_eager_init);
797
798        let validated = bootstrap_default_memory_manager().expect("bootstrap");
799
800        assert!(EAGER_INIT_RAN.load(Ordering::SeqCst));
801        assert!(
802            validated
803                .declarations()
804                .iter()
805                .any(|declaration| declaration.stable_key().as_str() == "crate_a.audit.v1")
806        );
807    }
808
809    #[test]
810    fn direct_user_can_bootstrap_and_open_without_canic() {
811        let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
812        reset_for_tests();
813        register_static_memory_manager_range(
814            120,
815            129,
816            "icydb",
817            MemoryManagerRangeMode::Reserved,
818            None,
819        )
820        .expect("icydb range");
821        register_static_memory_manager_declaration(120, "icydb", "users", "icydb.users.data.v1")
822            .expect("icydb declaration");
823
824        bootstrap_default_memory_manager().expect("bootstrap");
825        open_default_memory_manager_memory("icydb.users.data.v1", 120).expect("open memory");
826    }
827
828    #[test]
829    fn diagnostic_export_reports_default_memory_manager_sizes() {
830        let _guard = TEST_REGISTRY_LOCK.lock().expect("test lock poisoned");
831        reset_for_tests();
832        register_static_memory_manager_range(
833            130,
834            139,
835            "diagnostics",
836            MemoryManagerRangeMode::Reserved,
837            None,
838        )
839        .expect("diagnostics range");
840        register_static_memory_manager_declaration(
841            130,
842            "diagnostics",
843            "users",
844            "diagnostics.users.v1",
845        )
846        .expect("diagnostics declaration");
847
848        bootstrap_default_memory_manager().expect("bootstrap");
849        let memory =
850            open_default_memory_manager_memory("diagnostics.users.v1", 130).expect("open memory");
851        let old_size = memory.size();
852        memory.grow(2);
853
854        let export = default_memory_manager_diagnostic_export().expect("diagnostic export");
855        let recovery =
856            default_memory_manager_commit_recovery_diagnostic().expect("recovery diagnostic");
857        let record = export
858            .records
859            .iter()
860            .find(|record| record.allocation.stable_key().as_str() == "diagnostics.users.v1")
861            .expect("diagnostic allocation");
862
863        assert_eq!(
864            recovery.authoritative_generation,
865            Some(export.current_generation)
866        );
867        assert_eq!(
868            record.memory_size,
869            Some(DiagnosticMemorySize::from_wasm_pages(old_size + 2))
870        );
871    }
872}