Skip to main content

canic_core/memory/
policy.rs

1//! Module: memory::policy
2//!
3//! Responsibility: enforce Canic memory-manager namespace and ID-range ownership.
4//! Does not own: memory-manager storage, stable schemas, or diagnostics rendering.
5//! Boundary: memory bootstrap passes this policy into `ic-memory` validation.
6
7use crate::{
8    memory::{
9        CANIC_CONTROL_PLANE_MEMORY_AUTHORITY, CANIC_CORE_MEMORY_AUTHORITY,
10        admission::MemoryBootstrapAdmission, registry::MemoryRegistryError,
11    },
12    role_contract::allocation::{
13        CANIC_CONTROL_PLANE_MAX_ID, CANIC_CONTROL_PLANE_MIN_ID, CANIC_CORE_AUTH_MAX_ID,
14        CANIC_CORE_AUTH_MIN_ID, CANIC_CORE_LOWER_MAX_ID, CANIC_CORE_MAX_ID, CANIC_CORE_MIN_ID,
15        CANIC_CORE_UPPER_MIN_ID,
16        memory::control_plane::{
17            FIXTURE_STORE_ID, FLEET_COORDINATOR_ADMISSION_ID, FLEET_COORDINATOR_FUNDING_ID,
18            ROOT_ADMISSION_ID, ROOT_FUNDING_ID,
19        },
20    },
21};
22use ic_memory::{
23    AllocationPolicy, AllocationSlotDescriptor, MemoryManagerAuthorityRecord, MemoryManagerIdRange,
24    MemoryManagerRangeMode, MemoryManagerSlotError, PolicyIdentity, PolicyIdentityError,
25    RuntimeBootstrapPolicy, StableKey,
26};
27
28use sha2::{Digest as _, Sha256};
29
30pub const CANIC_CORE_AUTHORITY_PURPOSE: &str = "Canic core allocation authority";
31pub const CANIC_CONTROL_PLANE_AUTHORITY_PURPOSE: &str = "Canic control-plane allocation authority";
32const CANIC_MEMORY_BOOTSTRAP_POLICY_NAME: &str = "canic.memory-bootstrap-policy";
33const CANIC_MEMORY_BOOTSTRAP_POLICY_VERSION: u32 = 1;
34
35///
36/// CanicMemoryManagerPolicy
37///
38/// Canic policy adapter for the `ic-memory` MemoryManager substrate
39/// allocation slots.
40/// Owned by memory policy and supplied to memory-manager bootstrap.
41///
42
43#[derive(Clone, Debug, Default)]
44pub struct CanicMemoryManagerPolicy {
45    admission: Option<MemoryBootstrapAdmission>,
46}
47
48impl CanicMemoryManagerPolicy {
49    /// Use Canic's fixed namespace and range policy without a consumer callback.
50    #[must_use]
51    pub const fn new() -> Self {
52        Self { admission: None }
53    }
54
55    /// Compose consumer admission while retaining all Canic namespace/range checks.
56    #[must_use]
57    pub fn with_admission(mut self, admission: MemoryBootstrapAdmission) -> Self {
58        self.admission = Some(admission);
59        self
60    }
61}
62
63impl AllocationPolicy for CanicMemoryManagerPolicy {
64    type Error = MemoryRegistryError;
65
66    fn validate_key(&self, _key: &StableKey) -> Result<(), Self::Error> {
67        Ok(())
68    }
69
70    fn validate_slot(
71        &self,
72        key: &StableKey,
73        slot: &AllocationSlotDescriptor,
74    ) -> Result<(), Self::Error> {
75        let id = slot
76            .memory_manager_id()
77            .map_err(memory_slot_error_to_registry_error)?;
78        validate_key_id_claim(id, key.as_str())
79    }
80
81    fn validate_reserved_slot(
82        &self,
83        key: &StableKey,
84        slot: &AllocationSlotDescriptor,
85    ) -> Result<(), Self::Error> {
86        let id = slot
87            .memory_manager_id()
88            .map_err(memory_slot_error_to_registry_error)?;
89        if !ic_memory::is_ic_memory_stable_key(key.as_str()) && !key.as_str().starts_with("canic.")
90        {
91            return Err(MemoryRegistryError::RangeAuthorityViolation {
92                stable_key: key.as_str().to_string(),
93                id,
94                reason: "application stable keys may not be pre-reserved by Canic",
95            });
96        }
97        validate_key_id_claim(id, key.as_str())
98    }
99}
100
101impl RuntimeBootstrapPolicy for CanicMemoryManagerPolicy {
102    fn prepare_bootstrap(
103        &self,
104        admission: &mut ic_memory::BootstrapAdmission<'_>,
105    ) -> Result<(), Self::Error> {
106        match &self.admission {
107            Some(participant) => (participant.prepare)(admission),
108            None => Ok(()),
109        }
110    }
111
112    fn runtime_bootstrap_identity(&self) -> Result<PolicyIdentity, PolicyIdentityError> {
113        let identity = PolicyIdentity::new(
114            CANIC_MEMORY_BOOTSTRAP_POLICY_NAME,
115            CANIC_MEMORY_BOOTSTRAP_POLICY_VERSION,
116        )?;
117        let Some(participant) = &self.admission else {
118            return Ok(identity);
119        };
120        let mut hash = Sha256::new();
121        hash.update(b"canic:memory-admission:v1");
122        hash.update((participant.identity.name().len() as u64).to_be_bytes());
123        hash.update(participant.identity.name().as_bytes());
124        hash.update(participant.identity.version().to_be_bytes());
125        if let Some(configuration) = participant.identity.configuration_digest() {
126            hash.update([1]);
127            hash.update(configuration);
128        } else {
129            hash.update([0]);
130        }
131        Ok(identity.with_configuration_digest(hash.finalize().into()))
132    }
133}
134
135/// Return the canonical memory-manager authority records for diagnostics.
136#[must_use]
137pub fn canonical_authority_records() -> Vec<MemoryManagerAuthorityRecord> {
138    vec![
139        MemoryManagerAuthorityRecord::new(
140            ic_memory::memory_manager_governance_range(),
141            ic_memory::IC_MEMORY_AUTHORITY_OWNER,
142            MemoryManagerRangeMode::Reserved,
143            Some(ic_memory::IC_MEMORY_AUTHORITY_PURPOSE.to_string()),
144        )
145        .expect("valid ic-memory authority record"),
146        MemoryManagerAuthorityRecord::new(
147            canic_control_plane_range(),
148            CANIC_CONTROL_PLANE_MEMORY_AUTHORITY,
149            MemoryManagerRangeMode::Reserved,
150            Some(CANIC_CONTROL_PLANE_AUTHORITY_PURPOSE.to_string()),
151        )
152        .expect("valid Canic control-plane authority record"),
153        MemoryManagerAuthorityRecord::new(
154            canic_core_lower_range(),
155            CANIC_CORE_MEMORY_AUTHORITY,
156            MemoryManagerRangeMode::Reserved,
157            Some(CANIC_CORE_AUTHORITY_PURPOSE.to_string()),
158        )
159        .expect("valid Canic core authority record"),
160        MemoryManagerAuthorityRecord::new(
161            control_plane_infrastructure_range(),
162            CANIC_CONTROL_PLANE_MEMORY_AUTHORITY,
163            MemoryManagerRangeMode::Reserved,
164            Some(CANIC_CONTROL_PLANE_AUTHORITY_PURPOSE.to_string()),
165        )
166        .expect("valid infrastructure control-plane authority record"),
167        MemoryManagerAuthorityRecord::new(
168            canic_core_auth_range(),
169            CANIC_CORE_MEMORY_AUTHORITY,
170            MemoryManagerRangeMode::Reserved,
171            Some(CANIC_CORE_AUTHORITY_PURPOSE.to_string()),
172        )
173        .expect("valid Canic auth authority record"),
174        MemoryManagerAuthorityRecord::new(
175            fixture_store_range(),
176            CANIC_CONTROL_PLANE_MEMORY_AUTHORITY,
177            MemoryManagerRangeMode::Reserved,
178            Some(CANIC_CONTROL_PLANE_AUTHORITY_PURPOSE.to_string()),
179        )
180        .expect("valid fixture Store authority record"),
181        MemoryManagerAuthorityRecord::new(
182            canic_core_upper_range(),
183            CANIC_CORE_MEMORY_AUTHORITY,
184            MemoryManagerRangeMode::Reserved,
185            Some(CANIC_CORE_AUTHORITY_PURPOSE.to_string()),
186        )
187        .expect("valid Canic core authority record"),
188    ]
189}
190
191fn validate_key_id_claim(id: u8, stable_key: &str) -> Result<(), MemoryRegistryError> {
192    if ic_memory::is_ic_memory_stable_key(stable_key) {
193        return Ok(());
194    }
195
196    if stable_key.starts_with("canic.core.") {
197        return require_core_range(id, stable_key);
198    }
199
200    if stable_key.starts_with("canic.control_plane.") {
201        if stable_key == "canic.control_plane.fleet_coordinator.funding.v1" {
202            return require_range(
203                id,
204                stable_key,
205                MemoryManagerIdRange::new(
206                    FLEET_COORDINATOR_FUNDING_ID,
207                    FLEET_COORDINATOR_FUNDING_ID,
208                )
209                .expect("valid Coordinator funding range"),
210                "the Fleet Coordinator funding key must use reserved id 62",
211            );
212        }
213        if stable_key == "canic.control_plane.root.funding.v1" {
214            return require_range(
215                id,
216                stable_key,
217                MemoryManagerIdRange::new(ROOT_FUNDING_ID, ROOT_FUNDING_ID)
218                    .expect("valid Root funding range"),
219                "the Root funding key must use reserved id 63",
220            );
221        }
222        if stable_key == "canic.control_plane.fleet_admission.v1" {
223            return require_range(
224                id,
225                stable_key,
226                MemoryManagerIdRange::new(
227                    FLEET_COORDINATOR_ADMISSION_ID,
228                    FLEET_COORDINATOR_ADMISSION_ID,
229                )
230                .expect("valid Coordinator admission range"),
231                "the Fleet Coordinator admission key must use reserved id 64",
232            );
233        }
234        if stable_key == "canic.control_plane.root.admission.v1" {
235            return require_range(
236                id,
237                stable_key,
238                MemoryManagerIdRange::new(ROOT_ADMISSION_ID, ROOT_ADMISSION_ID)
239                    .expect("valid Root admission range"),
240                "the Root admission key must use reserved id 65",
241            );
242        }
243        if stable_key == "canic.control_plane.fixture_store.v1" {
244            return require_range(
245                id,
246                stable_key,
247                fixture_store_range(),
248                "fixture Store must use its reserved id",
249            );
250        }
251        return require_range(
252            id,
253            stable_key,
254            canic_control_plane_range(),
255            "canic.control_plane.* keys must use Canic control-plane ids 10-29",
256        );
257    }
258
259    if stable_key.starts_with("canic.") {
260        return Err(MemoryRegistryError::RangeAuthorityViolation {
261            stable_key: stable_key.to_string(),
262            id,
263            reason: "unrecognized canic.* stable key namespace",
264        });
265    }
266
267    validate_application_claim(id, stable_key)
268}
269
270fn validate_application_claim(id: u8, stable_key: &str) -> Result<(), MemoryRegistryError> {
271    if ic_memory::memory_manager_governance_range().contains(id)
272        || canic_core_lower_range().contains(id)
273        || control_plane_infrastructure_range().contains(id)
274        || canic_core_upper_range().contains(id)
275        || canic_core_auth_range().contains(id)
276        || fixture_store_range().contains(id)
277        || canic_control_plane_range().contains(id)
278    {
279        return Err(MemoryRegistryError::RangeAuthorityViolation {
280            stable_key: stable_key.to_string(),
281            id,
282            reason: "application keys may not use reserved MemoryManager IDs",
283        });
284    }
285    Ok(())
286}
287
288fn require_range(
289    id: u8,
290    stable_key: &str,
291    range: MemoryManagerIdRange,
292    reason: &'static str,
293) -> Result<(), MemoryRegistryError> {
294    if range.contains(id) {
295        Ok(())
296    } else {
297        Err(MemoryRegistryError::RangeAuthorityViolation {
298            stable_key: stable_key.to_string(),
299            id,
300            reason,
301        })
302    }
303}
304
305fn require_core_range(id: u8, stable_key: &str) -> Result<(), MemoryRegistryError> {
306    if canic_core_lower_range().contains(id)
307        || canic_core_auth_range().contains(id)
308        || canic_core_upper_range().contains(id)
309    {
310        Ok(())
311    } else {
312        Err(MemoryRegistryError::RangeAuthorityViolation {
313            stable_key: stable_key.to_string(),
314            id,
315            reason: "canic.core.* keys must use Canic core ids 30-61, 66-67 or 69-99",
316        })
317    }
318}
319
320fn canic_core_lower_range() -> MemoryManagerIdRange {
321    MemoryManagerIdRange::new(CANIC_CORE_MIN_ID, CANIC_CORE_LOWER_MAX_ID)
322        .expect("valid lower Canic core range")
323}
324
325fn control_plane_infrastructure_range() -> MemoryManagerIdRange {
326    MemoryManagerIdRange::new(FLEET_COORDINATOR_FUNDING_ID, ROOT_ADMISSION_ID)
327        .expect("valid infrastructure control-plane range")
328}
329
330fn canic_core_upper_range() -> MemoryManagerIdRange {
331    MemoryManagerIdRange::new(CANIC_CORE_UPPER_MIN_ID, CANIC_CORE_MAX_ID)
332        .expect("valid upper Canic core range")
333}
334
335fn canic_core_auth_range() -> MemoryManagerIdRange {
336    MemoryManagerIdRange::new(CANIC_CORE_AUTH_MIN_ID, CANIC_CORE_AUTH_MAX_ID)
337        .expect("valid Canic auth range")
338}
339
340fn fixture_store_range() -> MemoryManagerIdRange {
341    MemoryManagerIdRange::new(FIXTURE_STORE_ID, FIXTURE_STORE_ID)
342        .expect("valid fixture Store range")
343}
344
345fn canic_control_plane_range() -> MemoryManagerIdRange {
346    MemoryManagerIdRange::new(CANIC_CONTROL_PLANE_MIN_ID, CANIC_CONTROL_PLANE_MAX_ID)
347        .expect("valid Canic control-plane range")
348}
349
350fn memory_slot_error_to_registry_error(err: MemoryManagerSlotError) -> MemoryRegistryError {
351    match err {
352        MemoryManagerSlotError::InvalidMemoryManagerId { id } => {
353            MemoryRegistryError::InvalidDeclaration {
354                stable_key: "<slot>".to_string(),
355                reason: if id == ic_memory::MEMORY_MANAGER_INVALID_ID {
356                    "MemoryManager ID 255 is not usable"
357                } else {
358                    "MemoryManager ID is not usable"
359                },
360            }
361        }
362        _ => MemoryRegistryError::InvalidDeclaration {
363            stable_key: "<slot>".to_string(),
364            reason: "unsupported MemoryManager slot error",
365        },
366    }
367}
368
369// -----------------------------------------------------------------------------
370// Tests
371// -----------------------------------------------------------------------------
372
373#[cfg(test)]
374mod tests {
375    use super::*;
376
377    fn policy() -> CanicMemoryManagerPolicy {
378        CanicMemoryManagerPolicy::new()
379    }
380
381    #[test]
382    fn runtime_bootstrap_policy_has_explicit_v1_identity() {
383        assert_eq!(
384            policy()
385                .runtime_bootstrap_identity()
386                .expect("valid Canic policy identity"),
387            PolicyIdentity::new("canic.memory-bootstrap-policy", 1)
388                .expect("valid expected policy identity")
389        );
390    }
391
392    fn key(value: &str) -> StableKey {
393        StableKey::parse(value).expect("stable key")
394    }
395
396    fn slot(id: u8) -> AllocationSlotDescriptor {
397        AllocationSlotDescriptor::memory_manager(id).expect("usable MemoryManager id")
398    }
399
400    #[test]
401    fn rejects_memory_manager_sentinel_id_through_ic_memory() {
402        let err = AllocationSlotDescriptor::memory_manager(ic_memory::MEMORY_MANAGER_INVALID_ID)
403            .expect_err("ID 255 is the unallocated-bucket sentinel");
404        std::assert_matches!(
405            err,
406            MemoryManagerSlotError::InvalidMemoryManagerId { id }
407                if id == ic_memory::MEMORY_MANAGER_INVALID_ID
408        );
409    }
410
411    fn validate(stable_key: &str, id: u8) -> Result<(), MemoryRegistryError> {
412        policy().validate_slot(&key(stable_key), &slot(id))
413    }
414
415    fn validate_reserved(stable_key: &str, id: u8) -> Result<(), MemoryRegistryError> {
416        policy().validate_reserved_slot(&key(stable_key), &slot(id))
417    }
418
419    #[test]
420    fn accepts_canic_framework_namespaces_in_owned_ranges() {
421        validate("canic.control_plane.fixture_store.v1", FIXTURE_STORE_ID)
422            .expect("dedicated fixture Store slot");
423        validate("canic.core.runtime.canister_children.v1", CANIC_CORE_MIN_ID)
424            .expect("first core slot");
425        validate("canic.core.future.v1", CANIC_CORE_MAX_ID).expect("last core slot");
426        validate(
427            "canic.control_plane.template.manifests.v1",
428            CANIC_CONTROL_PLANE_MIN_ID,
429        )
430        .expect("first control-plane slot");
431        validate("canic.control_plane.future.v1", CANIC_CONTROL_PLANE_MAX_ID)
432            .expect("last control-plane slot");
433        validate(
434            "canic.control_plane.fleet_coordinator.funding.v1",
435            FLEET_COORDINATOR_FUNDING_ID,
436        )
437        .expect("dedicated Fleet Coordinator funding slot");
438        validate("canic.control_plane.root.funding.v1", ROOT_FUNDING_ID)
439            .expect("dedicated Root funding slot");
440        validate(
441            "canic.control_plane.fleet_admission.v1",
442            FLEET_COORDINATOR_ADMISSION_ID,
443        )
444        .expect("dedicated Fleet Coordinator admission slot");
445        validate("canic.control_plane.root.admission.v1", ROOT_ADMISSION_ID)
446            .expect("dedicated Root admission slot");
447    }
448
449    #[test]
450    fn rejects_canic_framework_namespaces_outside_owned_ranges() {
451        for invalid_key in [
452            "canic.core.future.v1",
453            "app.fixture.v1",
454            "canic.control_plane.future.v1",
455        ] {
456            std::assert_matches!(
457                validate(invalid_key, FIXTURE_STORE_ID),
458                Err(MemoryRegistryError::RangeAuthorityViolation { .. })
459            );
460        }
461        std::assert_matches!(
462            validate(
463                "canic.control_plane.fixture_store.v1",
464                CANIC_CONTROL_PLANE_MIN_ID
465            ),
466            Err(MemoryRegistryError::RangeAuthorityViolation { .. })
467        );
468        let err = validate("canic.core.fleet.state.v1", CANIC_CONTROL_PLANE_MIN_ID)
469            .expect_err("core key cannot claim control-plane range");
470        std::assert_matches!(err, MemoryRegistryError::RangeAuthorityViolation { .. });
471
472        let err = validate(
473            "canic.control_plane.template.manifests.v1",
474            CANIC_CORE_MIN_ID,
475        )
476        .expect_err("control-plane key cannot claim core range");
477        std::assert_matches!(err, MemoryRegistryError::RangeAuthorityViolation { .. });
478
479        let err = validate("canic.core.future.v1", FLEET_COORDINATOR_FUNDING_ID)
480            .expect_err("core key cannot claim the dedicated funding slot");
481        std::assert_matches!(err, MemoryRegistryError::RangeAuthorityViolation { .. });
482        let err = validate("canic.core.future.v1", ROOT_FUNDING_ID)
483            .expect_err("core key cannot claim the dedicated Root funding slot");
484        std::assert_matches!(err, MemoryRegistryError::RangeAuthorityViolation { .. });
485        let err = validate("canic.core.future.v1", FLEET_COORDINATOR_ADMISSION_ID)
486            .expect_err("core key cannot claim the dedicated Coordinator admission slot");
487        std::assert_matches!(err, MemoryRegistryError::RangeAuthorityViolation { .. });
488        let err = validate("canic.core.future.v1", ROOT_ADMISSION_ID)
489            .expect_err("core key cannot claim the dedicated Root admission slot");
490        std::assert_matches!(err, MemoryRegistryError::RangeAuthorityViolation { .. });
491
492        let err = validate("canic.unknown.state.v1", CANIC_CORE_MAX_ID + 1)
493            .expect_err("unknown canic namespace is reserved");
494        std::assert_matches!(err, MemoryRegistryError::RangeAuthorityViolation { .. });
495    }
496
497    #[test]
498    fn accepts_application_keys_only_outside_reserved_ranges() {
499        validate("app.users.v1", CANIC_CORE_MAX_ID + 1).expect("application slot");
500        validate("app.archive.v1", ic_memory::MEMORY_MANAGER_MAX_ID).expect("last app slot");
501
502        let err = validate("app.users.v1", CANIC_CORE_MIN_ID)
503            .expect_err("application key cannot claim Canic core range");
504        std::assert_matches!(err, MemoryRegistryError::RangeAuthorityViolation { .. });
505
506        let err = validate("app.users.v1", CANIC_CONTROL_PLANE_MAX_ID)
507            .expect_err("application key cannot claim Canic control-plane reserve");
508        std::assert_matches!(err, MemoryRegistryError::RangeAuthorityViolation { .. });
509
510        let err = validate("app.users.v1", ic_memory::MEMORY_MANAGER_LEDGER_ID)
511            .expect_err("application key cannot claim ic-memory governance range");
512        std::assert_matches!(err, MemoryRegistryError::RangeAuthorityViolation { .. });
513    }
514
515    #[test]
516    fn rejects_application_reservations() {
517        let err = validate_reserved("app.users.v1", CANIC_CORE_MAX_ID + 1)
518            .expect_err("Canic does not pre-reserve application keys");
519        std::assert_matches!(err, MemoryRegistryError::RangeAuthorityViolation { .. });
520    }
521}