Expand description
§ic-memory
EARLY INFRASTRUCTURE: validate before opening stable memory.
ic-memory helps Internet Computer canisters avoid opening the wrong stable
memory after an upgrade.
The invariant:
Once a stable key is committed to a physical allocation slot, future binaries must either reopen that same stable key on that same slot or declare a new stable key.
It remembers this mapping forever:
logical store -> physical stable-memory slotIf a future version tries to move that store to a different slot, or reuse that
slot for a different store, ic-memory rejects the layout before stable-memory
handles are opened.
The meme is funny. The bug is not.
§Why Use It?
Use ic-memory when a canister has more than one stable store and the layout
can change over time.
It is most useful for:
- IC frameworks
- generated canisters
- multi-store canisters
- plugin or module systems
- canister families that evolve across releases
- any project where stable-memory ID reuse would be a serious bug
You probably do not need it for a tiny canister with one hand-written stable structure and a fixed layout.
§What It Protects Against
The dangerous bug is slot drift.
Version 1 ships with:
app.users.v1 -> MemoryManager ID 100
app.orders.v1 -> MemoryManager ID 101A later upgrade accidentally ships with:
app.users.v1 -> MemoryManager ID 101
app.orders.v1 -> MemoryManager ID 100That can still compile. It can even install.
But now the canister may open orders data as users data, and users data as orders data (or more likely just fail to deserialize anything.)
ic-memory catches that mismatch first.
It enforces both directions for active allocations:
- The same active stable key cannot move to a different physical slot.
- The same active physical slot cannot be reused by a different stable key.
§How It Fits
ic-stable-structures stores the data.
ic-memory checks that each logical store is still opening the same physical
slot it owned before.
The native IC ledger anchor is:
MemoryManager ID 0
-> ic-stable-structures::Cell<StableCellLedgerRecord, _>
-> LedgerCommitStore
-> dual protected committed AllocationLedger payloadsCborLedgerCodec is the built-in codec for those committed ledger payloads.
A typical framework flow is:
- Recover the saved allocation ledger.
- Declare the stores this binary expects.
- Validate those declarations against history and policy.
- Commit the new generation.
- Open stable-memory handles only after validation passes.
The important rule: validate layout before touching stable data.
Do not rawdog stable-memory IDs.
§Golden Path
The native ledger anchor is an ic-stable-structures::Cell at MemoryManager
ID 0. AllocationBootstrap is the golden path for whichever layer owns that
ledger store.
Supported ownership modes:
- Framework-owned bootstrap: Canic owns the allocation ledger bootstrap, and IcyDB/application declarations flow through Canic.
- Library-owned bootstrap: IcyDB may use
ic-memorydirectly without Canic, and applications using IcyDB rely on IcyDB’s bootstrap. - Application-owned bootstrap: a standalone canister may use
ic-memorydirectly without Canic or IcyDB.
Exactly one owner should bootstrap a given ic-memory ledger store. If
multiple layers use ic-memory in the same canister, they must either compose
declarations into one bootstrap owner or use distinct ledger stores and
allocation domains.
The safe order is fixed for every owner:
recover persisted allocation ledger
declare this binary's expected stable stores
validate declarations against ledger/history/policy
commit the new generation
only then open stable-memory handlesMinimal sketch:
let ledger = recover_allocation_ledger()?;
let declarations = DeclarationCollector::new()
.with_memory_manager("app.orders.v1", 100, "orders")?
.seal()?;
let mut bootstrap = AllocationBootstrap::new(record.store_mut());
let commit = bootstrap.initialize_validate_and_commit(
&CborLedgerCodec,
&genesis_ledger,
declarations,
&policy,
runtime_fingerprint,
)?;
let session = AllocationSession::new(storage, commit.validated);
let orders = session.open(&StableKey::parse("app.orders.v1")?)?;The helper names for record, genesis_ledger, policy, and storage are
placeholders. Frameworks and libraries wire those to their own stable-memory
persistence and collection construction. The ordering is the contract.
AllocationLedger::new(...) builds a structurally valid ledger DTO. Use
AllocationLedger::new_committed(...) only when you are manually constructing
committed ledger state and want the stricter committed-generation checks.
Normal integrations should usually recover through the commit/recovery flow
instead of hand-assembling committed state.
§Basic Declaration
Declare every stable store with a stable name and a physical slot:
use ic_memory::DeclarationCollector;
let snapshot = DeclarationCollector::new()
.with_memory_manager("app.orders.v1", 100, "orders")
.expect("valid allocation declaration")
.seal()
.expect("valid declaration snapshot");
assert_eq!(snapshot.len(), 1);That snapshot is what you validate against the recovered ledger before opening the store.
§Stable Keys
Stable keys are permanent logical store names. They should describe ownership and purpose, not the current memory ID.
Format:
namespace.component.store_or_role.vNRules:
- ASCII only.
- Lowercase only.
- Dot-separated segments.
- Each segment starts with a lowercase letter.
- Segments may contain lowercase letters, digits, and underscores.
- No whitespace, slashes, or hyphens.
- Must end with a nonzero version suffix such as
.v1or.v12. - Maximum length is 128 bytes.
Examples:
use ic_memory::StableKey;
StableKey::parse("app.orders.v1").expect("app key");
StableKey::parse("myapp.audit_log.v1").expect("app key");
StableKey::parse("framework.cache.index.v1").expect("framework key");
StableKey::parse("database.users.data.v1").expect("database key");Suggested namespace conventions:
ic_memory.*is reserved foric-memorygovernance records.- Application-owned stores can use an application namespace, such as
app.orders.v1ormyapp.audit_log.v1. - Frameworks and generated stores should use namespaces they own, such as
framework.cache.index.v1ordatabase.users.data.v1.
Canic and IcyDB examples:
canic.core.*is appropriate for Canic framework-owned stores.icydb.<memory_namespace>.<store_name>.<role>.vNworks for generated IcyDB stores, such asicydb.test_db.users.data.v1.
Changing a key creates a new logical allocation identity. If the durable store is the same, keep the stable key and update schema metadata instead.
§Range Authority
Range authority is policy metadata. It does not allocate stable-memory IDs and does not write to the allocation ledger.
Packages should publish only the ranges they own:
use ic_memory::{
IC_MEMORY_AUTHORITY_OWNER, MemoryManagerRangeAuthority, MemoryManagerRangeMode,
memory_manager_governance_range,
};
let authority = MemoryManagerRangeAuthority::new()
.reserve(memory_manager_governance_range(), IC_MEMORY_AUTHORITY_OWNER)
.expect("ic-memory governance range")
.reserve_ids(10, 99, "framework.example")
.expect("framework range");
authority
.validate_id_authority_mode(42, "framework.example", MemoryManagerRangeMode::Reserved)
.expect("framework-owned ID");An open stack composes records from multiple packages and rejects overlaps:
use ic_memory::MemoryManagerRangeAuthority;
let framework_records = MemoryManagerRangeAuthority::new()
.reserve_ids(10, 99, "framework.example")
.expect("framework range")
.authorities()
.to_vec();
let database_records = MemoryManagerRangeAuthority::new()
.reserve_ids(120, 149, "database.framework")
.expect("database range")
.authorities()
.to_vec();
let authority = MemoryManagerRangeAuthority::from_records(
framework_records
.into_iter()
.chain(database_records)
.collect(),
)
.expect("non-overlapping package ranges");
assert_eq!(authority.authorities().len(), 2);A final closed policy may claim the remaining application space and require full coverage:
use ic_memory::{
IC_MEMORY_AUTHORITY_OWNER, MEMORY_MANAGER_MAX_ID, MemoryManagerIdRange,
MemoryManagerRangeAuthority, memory_manager_governance_range,
};
let authority = MemoryManagerRangeAuthority::new()
.reserve(memory_manager_governance_range(), IC_MEMORY_AUTHORITY_OWNER)
.expect("ic-memory governance range")
.reserve_ids(10, 99, "framework.example")
.expect("framework range")
.allow_ids(100, MEMORY_MANAGER_MAX_ID, "applications")
.expect("application range");
authority
.validate_complete_coverage(MemoryManagerIdRange::all_usable())
.expect("closed policy covers every usable ID");§Current MemoryManager Rules
For the built-in ic-stable-structures::MemoryManager slot descriptor:
- IDs
0..=254are usable stable-memory slots. - ID
255is rejected because it is the unallocated sentinel. - IDs
0..=9are reserved foric-memorygovernance. - ID
0is assigned to the allocation ledger.
The crate also exposes range-authority helpers for frameworks that want to split ID ranges between infrastructure and application stores.
Canic currently uses 10..=99 as a framework-reserved example range. That is
Canic policy, not an ic-memory rule.
§What It Does Not Do
ic-memory does not replace ic-stable-structures.
It owns allocation governance. It does not re-export or wrap every
ic-stable-structures collection type.
It also does not handle:
- schema migrations
- schema compatibility or data semantics
- controller authorization
- application data validation
- endpoint routing
- IC management-canister calls
- malicious-controller protection
- disaster recovery
It only protects stable-memory allocation ownership.
§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.
Earlier drafts exposed some durable DTO fields directly. Current versions use checked constructors and accessors so invalid allocation state is harder to construct accidentally.
The non-negotiable invariants are recorded in SAFETY.md.
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.
Once a stable key is committed to a physical allocation slot, future binaries must either reopen that same stable key on that same slot or declare a new stable key.
The crate records and validates durable ownership in both directions: an active stable key cannot move to a different physical slot, and an active physical slot cannot be reused by a different stable key.
The intended integration flow is:
- Recover the persisted allocation ledger.
- Declare the stable stores expected by the current binary.
- Validate those declarations against ledger history and any framework policy.
- Commit the next generation.
- Only then open stable-memory handles through a validated allocation session.
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, declare the stores expected by the current binary, validate declarations against history and policy, commit a new generation, and only then publish a validated allocation session that can open slots through a storage substrate.
AllocationBootstrap is the golden path for whichever layer owns a given
ledger store. Canic may own bootstrap for a framework canister and compose
IcyDB/application declarations through its registry; IcyDB may own bootstrap
directly for generated database stores; or a standalone application canister
may own bootstrap itself. Exactly one owner should bootstrap one ledger
store. Multiple layers in the same canister must either compose declarations
into that owner or use distinct ledger stores and allocation domains.
ic-stable-structures MemoryManager IDs are the first-class supported
physical slot substrate. The crate still keeps narrow internal abstractions
for storage adapters and diagnostics, but the native IC path is
MemoryManager ID 0 -> ic-stable-structures::Cell<StableCellLedgerRecord, _> -> LedgerCommitStore -> committed AllocationLedger payloads.
ic-memory is not a replacement for ic-stable-structures collections and
does not wrap typed stores such as StableBTreeMap.
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 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::CborLedgerCodec;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 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::MemoryManagerAuthorityRecord;pub use slot::MemoryManagerIdRange;pub use slot::MemoryManagerRangeAuthority;pub use slot::MemoryManagerRangeAuthorityError;pub use slot::MemoryManagerRangeError;pub use slot::MemoryManagerRangeMode;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 stable_cell::STABLE_CELL_HEADER_SIZE;pub use stable_cell::STABLE_CELL_LAYOUT_VERSION;pub use stable_cell::STABLE_CELL_MAGIC;pub use stable_cell::STABLE_CELL_VALUE_OFFSET;pub use stable_cell::StableCellLedgerRecord;pub use stable_cell::StableCellPayloadError;pub use stable_cell::decode_stable_cell_ledger_record;pub use stable_cell::decode_stable_cell_payload;pub use substrate::LedgerAnchor;pub use substrate::StorageSubstrate;pub use validation::AllocationValidationError;pub use validation::validate_allocations;pub use ic_stable_structures as stable_structures;