Expand description
§ic-memory
ic-memory prevents Internet Computer stable-memory slots from being
accidentally reused, moved, or reassigned across canister upgrades.
It does this by keeping a durable allocation ledger that records which canonical stable key owns which physical allocation slot. The invariant is:
stable_key -> allocation_slot foreverMore fully: once a logical stable key has been assigned to a physical allocation slot, that key must never point to a different slot, and that slot must never be reused for a different key, even after retirement.
This crate is not a replacement for ic-stable-structures. It is not a generic
memory abstraction. It is stable-memory allocation-governance infrastructure.
Use it before opening stable-memory handles, so a canister rejects an unsafe
layout before it can open the wrong stable memory for a logical store.
The non-negotiable invariants are recorded in SAFETY.md.
§Status
ic-memory is early infrastructure extracted from Canic. The public API is
intended to stabilize around persistent allocation ownership, but framework
authors should still treat this line as young infrastructure while the
standalone boundary settles.
§The Bug Class
Multi-store canisters and frameworks often map logical stores onto physical
stable-memory slots such as ic-stable-structures::MemoryManager IDs.
For example, a canister may ship with this layout:
v1:
app.users.v1 -> MemoryManager ID 100
app.orders.v1 -> MemoryManager ID 101A bad upgrade can accidentally swap those IDs:
bad v2:
app.users.v1 -> MemoryManager ID 101
app.orders.v1 -> MemoryManager ID 100That upgrade may still compile and install. Rust’s type system and
ic-stable-structures do not automatically know that app.users.v1 used to
own ID 100. The canister can boot while opening the orders memory as users, or
the users memory as orders.
ic-memory exists to reject that mapping before memory handles are opened.
§Why This Exists With ic-stable-structures
ic-stable-structures provides stable data structures and memory abstractions.
It lets you store data in stable memory.
ic-memory records and validates durable ownership of stable-memory slots over
time. It answers a different question: is this logical store still opening the
same physical slot it has always owned?
The two crates are complementary. A framework can use ic-memory to validate
allocation ownership, then use ic-stable-structures to open and operate on the
validated memories.
§How It Fits
ic-stable-structuresstores data in stable memory.ic-memorygoverns which logical store is allowed to open which stable-memory slot.- The framework or application decides namespace policy, range policy, lifecycle timing, controller authorization, and schema migration.
This crate owns allocation invariants, not framework policy. It is generic over
storage substrates: MemoryManager IDs are one supported slot descriptor shape,
not the whole design.
§Who Needs This?
You probably do not need ic-memory if your canister has one stable structure,
a small fixed hand-written layout, and no generated or framework-managed stable
stores.
You may need it if you are building:
- an IC framework
- a multi-store canister
- a generated canister platform
- a plugin/module system
- a canister family where stable-memory declarations evolve over time
- any system where accidental stable-memory ID reuse would be catastrophic
§Lifecycle
The intended flow is:
- Declare expected stable-memory allocations with canonical stable keys.
- Recover the historical allocation ledger.
- Validate current declarations against policy and historical ownership.
- Commit a new allocation generation.
- Open physical memory only through a validated allocation session.
- Export diagnostics when needed.
Opening memory handles is deliberately a later phase. Declaration and validation happen first so slot drift is caught before the application touches stable data.
§Terminology
- Stable key: a canonical logical name for one durable store, such as
app.orders.v1. - Allocation slot: the physical stable-memory location a storage substrate can
open, such as a
MemoryManagerID. - Allocation ledger: durable history of stable-key to allocation-slot ownership.
- Declaration: the current binary’s claim that a stable key should own a slot.
- Generation: one committed version of the allocation ledger.
- Reservation: a slot/key pair held for future use but not yet active.
- Retirement / tombstone: an explicit historical marker that an allocation is no longer active.
- Validated allocation session: the capability produced after declarations pass policy and ledger-history validation.
- Storage substrate: the implementation that interprets slots and opens physical memory handles.
Retirement is a tombstone, not a free-list operation. A retired stable key/slot pair remains historically owned and cannot be reused for a different key. This preserves rollback safety, diagnostics, and historical ABI integrity.
§Non-Goals
ic-memory does not own:
- stable data-structure schemas
- schema migrations
- controller authorization
- IC management-canister calls
- endpoint dispatch
- framework namespace/range policy
- application-level data validation
§What It Provides
- Canonical stable-key parsing.
- Allocation slot descriptors.
- Declaration collection and duplicate rejection.
- Allocation policy and substrate traits.
- Durable allocation history records.
- Generation-scoped staging and commit helpers.
- Tombstone and reservation lifecycle primitives.
- Rollback-safe allocation validation.
- Protected dual-slot commit recovery primitives.
- Read-only diagnostic export shapes.
Decoded durable ledgers and public DTO structs are untrusted until validated. Recovery and commit paths use strict committed-ledger validation before a ledger can become authoritative.
§Example: Declaration Phase
This example demonstrates lifecycle phase 1: collect the current binary’s expected stable-memory allocations. It does not open memory.
use ic_memory::{
AllocationDeclaration, AllocationSlotDescriptor, DeclarationCollector, SchemaMetadata,
};
let mut declarations = DeclarationCollector::default();
let declaration = AllocationDeclaration::new(
"app.orders.v1",
AllocationSlotDescriptor::memory_manager(100).expect("usable slot"),
Some("orders".to_string()),
SchemaMetadata::default(),
)
.expect("valid allocation declaration");
declarations.push(declaration);
let snapshot = declarations.seal().expect("valid declaration snapshot");
assert_eq!(snapshot.len(), 1);§Example: Rejecting Slot Drift
This example demonstrates lifecycle phase 3: validate the current declarations
against historical ownership. The bad upgrade tries to move app.users.v1 from
MemoryManager ID 100 to ID 101, so validation fails before an allocation session
can open any memory handles.
use ic_memory::{
AllocationDeclaration, AllocationHistory, AllocationLedger, AllocationPolicy,
AllocationRecord, AllocationSlotDescriptor, AllocationState, AllocationValidationError,
CURRENT_LEDGER_SCHEMA_VERSION, CURRENT_PHYSICAL_FORMAT_ID, DeclarationSnapshot,
GenerationRecord, SchemaMetadata, StableKey, validate_allocations,
};
use std::convert::Infallible;
#[derive(Debug)]
struct AllowAllPolicy;
impl AllocationPolicy for AllowAllPolicy {
type Error = Infallible;
fn validate_key(&self, _key: &StableKey) -> Result<(), Self::Error> {
Ok(())
}
fn validate_slot(
&self,
_key: &StableKey,
_slot: &AllocationSlotDescriptor,
) -> Result<(), Self::Error> {
Ok(())
}
fn validate_reserved_slot(
&self,
_key: &StableKey,
_slot: &AllocationSlotDescriptor,
) -> Result<(), Self::Error> {
Ok(())
}
}
fn declaration(key: &str, memory_manager_id: u8) -> AllocationDeclaration {
AllocationDeclaration::new(
key,
AllocationSlotDescriptor::memory_manager(memory_manager_id).expect("usable slot"),
None,
SchemaMetadata::default(),
)
.expect("valid declaration")
}
let historical_ledger = AllocationLedger {
ledger_schema_version: CURRENT_LEDGER_SCHEMA_VERSION,
physical_format_id: CURRENT_PHYSICAL_FORMAT_ID,
current_generation: 1,
allocation_history: AllocationHistory {
records: vec![
AllocationRecord::from_declaration(
1,
declaration("app.users.v1", 100),
AllocationState::Active,
),
AllocationRecord::from_declaration(
1,
declaration("app.orders.v1", 101),
AllocationState::Active,
),
],
generations: vec![GenerationRecord {
generation: 1,
parent_generation: Some(0),
runtime_fingerprint: None,
declaration_count: 2,
committed_at: None,
}],
},
};
let bad_v2 = DeclarationSnapshot::new(vec![
declaration("app.users.v1", 101),
declaration("app.orders.v1", 100),
])
.expect("duplicate-free declarations");
let error = validate_allocations(&historical_ledger, bad_v2, &AllowAllPolicy)
.expect_err("slot drift must be rejected");
assert!(matches!(
error,
AllocationValidationError::StableKeySlotConflict { .. }
));The protected physical checksum detects torn writes and accidental corruption. It is not a cryptographic integrity mechanism and must not be treated as adversarial tamper resistance. Stable-memory allocation-governance primitives for Internet Computer canister upgrades.
ic-memory prevents stable-memory slot drift: a logical stable key must keep
owning the same physical allocation slot across upgrades. The crate records
and validates durable ownership as stable_key -> allocation_slot forever.
This crate owns allocation invariants, not framework policy. Namespace rules, range ownership, controller authorization, endpoint lifecycle, schema migrations, and application validation belong to the framework or application.
Use these primitives before opening stable-memory handles. Integrations should recover the historical ledger, validate current declarations, commit a new generation, and only then publish a validated allocation session that can open slots through a storage substrate.
The APIs are generic over storage substrates. ic-stable-structures
MemoryManager IDs are supported as durable slot descriptors, but this crate
is not a replacement for ic-stable-structures and is not Canic-specific.
Re-exports§
pub use bootstrap::AllocationBootstrap;pub use bootstrap::BootstrapCommit;pub use bootstrap::BootstrapError;pub use bootstrap::BootstrapReservationError;pub use bootstrap::BootstrapRetirementError;pub use declaration::AllocationDeclaration;pub use declaration::DeclarationCollector;pub use declaration::DeclarationSnapshot;pub use declaration::DeclarationSnapshotError;pub use diagnostics::DiagnosticExport;pub use diagnostics::DiagnosticGeneration;pub use diagnostics::DiagnosticRecord;pub use generation::GenerationCommit;pub use generation::GenerationMutation;pub use generation::StagedGeneration;pub use key::StableKey;pub use key::StableKeyError;pub use ledger::AllocationHistory;pub use ledger::AllocationLedger;pub use ledger::AllocationRecord;pub use ledger::AllocationReservationError;pub use ledger::AllocationRetirement;pub use ledger::AllocationRetirementError;pub use ledger::AllocationStageError;pub use ledger::AllocationState;pub use ledger::CURRENT_LEDGER_SCHEMA_VERSION;pub use ledger::CURRENT_PHYSICAL_FORMAT_ID;pub use ledger::GenerationRecord;pub use ledger::LedgerCodec;pub use ledger::LedgerCommitError;pub use ledger::LedgerCommitStore;pub use ledger::LedgerCompatibility;pub use ledger::LedgerCompatibilityError;pub use ledger::LedgerIntegrityError;pub use ledger::SchemaMetadataRecord;pub use physical::AuthoritativeSlot;pub use physical::CommitRecoveryError;pub use physical::CommitSlotDiagnostic;pub use physical::CommitSlotIndex;pub use physical::CommitStoreDiagnostic;pub use physical::CommittedGenerationBytes;pub use physical::DualCommitStore;pub use physical::DualProtectedCommitStore;pub use physical::ProtectedGenerationSlot;pub use policy::AllocationPolicy;pub use policy::NamespaceAuthority;pub use policy::RangeAuthority;pub use schema::SchemaMetadata;pub use schema::SchemaMetadataError;pub use session::AllocationSession;pub use session::AllocationSessionError;pub use session::ValidatedAllocations;pub use slot::AllocationSlot;pub use slot::AllocationSlotDescriptor;pub use slot::IC_MEMORY_AUTHORITY_OWNER;pub use slot::IC_MEMORY_AUTHORITY_PURPOSE;pub use slot::IC_MEMORY_LEDGER_LABEL;pub use slot::IC_MEMORY_LEDGER_STABLE_KEY;pub use slot::IC_MEMORY_STABLE_KEY_PREFIX;pub use slot::MEMORY_MANAGER_DESCRIPTOR_VERSION;pub use slot::MEMORY_MANAGER_GOVERNANCE_MAX_ID;pub use slot::MEMORY_MANAGER_INVALID_ID;pub use slot::MEMORY_MANAGER_LEDGER_ID;pub use slot::MEMORY_MANAGER_MAX_ID;pub use slot::MEMORY_MANAGER_MIN_ID;pub use slot::MEMORY_MANAGER_SUBSTRATE;pub use slot::MemoryManagerIdRange;pub use slot::MemoryManagerRangeError;pub use slot::MemoryManagerSlotError;pub use slot::is_ic_memory_stable_key;pub use slot::memory_manager_governance_range;pub use slot::validate_memory_manager_id;pub use substrate::LedgerAnchor;pub use substrate::StorageSubstrate;pub use validation::AllocationValidationError;pub use validation::validate_allocations;