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 forever
More 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 101
A bad upgrade can accidentally swap those IDs:
bad v2:
app.users.v1 -> MemoryManager ID 101
app.orders.v1 -> MemoryManager ID 100
That 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 ;
let mut declarations = default;
let declaration = new
.expect;
declarations.push;
let snapshot = declarations.seal.expect;
assert_eq!;
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 ;
use Infallible;
;
let historical_ledger = AllocationLedger ;
let bad_v2 = new
.expect;
let error = validate_allocations
.expect_err;
assert!;
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.