Skip to main content

ic_memory/
runtime.rs

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