Skip to main content

ic_storage/runtime/
mod.rs

1//! Named allocation admission and exclusive leases over the owned directory.
2mod admission;
3mod custody;
4mod directory_model;
5
6use crate::{Memory, StableKey};
7pub use admission::{
8    AdmissionError, AllocationGrants, BootstrapAdmission, GenericAllocationPolicy, MemoryAuthority,
9    MemoryRequest, PolicyIdentity, RuntimeBootstrapPolicy, SealedDeclarationSnapshot,
10};
11pub use custody::{BackingIdentity, MemoryBacking};
12pub use directory_model::Error as DirectoryError;
13use directory_model::leases::{Lease, Owner};
14pub use directory_model::{AllocationId, StorageConfig};
15use std::collections::BTreeSet;
16
17/// Construction refuses aliased or malformed backing without resetting it.
18#[derive(Debug, thiserror::Error)]
19pub enum RuntimeConstructionError {
20    /// Another runtime or detached lease retains custody.
21    #[error("backing already claimed")]
22    BackingAlreadyClaimed,
23    /// The current owned directory could not be loaded or initialized.
24    #[error(transparent)]
25    Directory(#[from] DirectoryError),
26}
27
28/// Allocation growth failures before authority publication.
29#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
30pub enum RuntimeGrowError {
31    /// Requested page arithmetic overflowed.
32    #[error("allocation page count overflow")]
33    ArithmeticOverflow,
34    /// The configured allocation or directory resource budget was exceeded.
35    #[error("allocation capacity exceeded")]
36    CapacityExceeded,
37    /// Physical backing capacity was refused.
38    #[error("backing refused growth by {additional_pages} pages")]
39    BackingRefused { additional_pages: u64 },
40    /// A previous backing failure poisoned this owner.
41    #[error("allocation owner is poisoned")]
42    Poisoned,
43    /// Directory validation failed at a checked boundary.
44    #[error("allocation directory validation failed")]
45    InvalidDirectory,
46}
47
48/// A key cannot be opened until current host admission succeeds.
49#[derive(Debug, thiserror::Error)]
50pub enum RuntimeOpenError {
51    /// Bootstrap has not committed current authority.
52    #[error("runtime is not bootstrapped")]
53    NotBootstrapped,
54    /// The key is absent from the current admitted declarations.
55    #[error("key is not admitted: {0}")]
56    NotAdmitted(StableKey),
57    /// An exclusive lease for this allocation is already alive.
58    #[error("allocation {id} already open")]
59    AlreadyOpen { id: u64 },
60    /// The directory refused opening.
61    #[error(transparent)]
62    Directory(#[from] DirectoryError),
63}
64
65/// Bootstrap failed before current authority could be published.
66#[derive(Debug, thiserror::Error)]
67pub enum RuntimeBootstrapError<E> {
68    /// Host declarations or namespace grants were invalid.
69    #[error(transparent)]
70    Admission(#[from] AdmissionError),
71    /// The host's policy rejected the preparation or an allocation.
72    #[error("host policy rejected allocation")]
73    Policy(E),
74    /// Current bootstrap inputs differ from the established binding.
75    #[error("bootstrap binding mismatch")]
76    BindingMismatch,
77    /// A shared verification cannot bootstrap an uninitialized owner.
78    #[error("runtime is not bootstrapped")]
79    NotBootstrapped,
80    /// Durable directory commitment failed.
81    #[error(transparent)]
82    Directory(#[from] DirectoryError),
83}
84
85struct Binding {
86    source: SealedDeclarationSnapshot,
87    grants: AllocationGrants,
88    policy: PolicyIdentity,
89    admitted: BTreeSet<StableKey>,
90}
91
92///
93/// `MemoryRuntime`
94///
95/// One custodian of named allocations on a backing. Bootstrap validates namespace
96/// grants and host policy, commits bindings, then permits exclusive leases.
97/// Omitted retained names keep their allocation, but require current admission
98/// before opening. IDs are opaque u64 values, never selected by applications.
99///
100pub struct MemoryRuntime<M: Memory> {
101    owner: Owner<M>,
102    config: StorageConfig,
103    binding: Option<Binding>,
104}
105
106impl<M: Memory> MemoryRuntime<M> {
107    /// Claim backing and create or read its owned directory. Never reset nonempty backing.
108    ///
109    /// # Errors
110    /// Rejects duplicate backing custody or invalid/non-admissible retained directory data.
111    pub fn new(memory: M) -> Result<Self, RuntimeConstructionError>
112    where
113        M: MemoryBacking,
114    {
115        Self::new_with_config(memory, StorageConfig::default())
116    }
117    /// Use explicit directory resource limits; reopening revalidates under those limits.
118    ///
119    /// # Errors
120    /// Rejects duplicate backing custody, invalid configuration, or invalid/non-admissible retained directory data.
121    pub fn new_with_config(
122        memory: M,
123        config: StorageConfig,
124    ) -> Result<Self, RuntimeConstructionError>
125    where
126        M: MemoryBacking,
127    {
128        let owner = Owner::init(memory, config).map_err(|error| match error {
129            DirectoryError::AlreadyClaimed => RuntimeConstructionError::BackingAlreadyClaimed,
130            error => RuntimeConstructionError::Directory(error),
131        })?;
132        Ok(Self {
133            owner,
134            config,
135            binding: None,
136        })
137    }
138    /// Whether current host authority has been committed.
139    #[must_use]
140    pub const fn is_bootstrapped(&self) -> bool {
141        self.binding.is_some()
142    }
143    /// Current durable directory generation, after bootstrap admission.
144    ///
145    /// # Errors
146    /// Returns `NotBootstrapped` until bootstrap establishes a binding.
147    pub fn committed_generation(&self) -> Result<u64, RuntimeOpenError> {
148        if self.binding.is_none() {
149            return Err(RuntimeOpenError::NotBootstrapped);
150        }
151        Ok(self.owner.generation())
152    }
153    /// Effective resource admission limits.
154    #[must_use]
155    pub const fn config(&self) -> StorageConfig {
156        self.config
157    }
158    /// Admit a complete declaration set, retaining omitted historical bindings.
159    ///
160    /// # Errors
161    /// Rejects invalid grants or declarations, host-policy refusal, conflicting bindings, and directory validation or commitment failures.
162    pub fn bootstrap<P: RuntimeBootstrapPolicy>(
163        &mut self,
164        source: &SealedDeclarationSnapshot,
165        grants: &AllocationGrants,
166        policy: &P,
167    ) -> Result<u64, RuntimeBootstrapError<P::Error>> {
168        if self.binding.is_some() {
169            return self.verify_bootstrap(source, grants, policy);
170        }
171        self.owner.verify()?;
172        let policy_identity = policy.runtime_bootstrap_identity();
173        let bindings = self.owner.bindings();
174        let mut admission =
175            BootstrapAdmission::new(source, grants, &bindings, self.config.allocations)?;
176        let result = policy.prepare_bootstrap(&mut admission);
177        let (requests, admitted) = admission.complete()?;
178        result.map_err(RuntimeBootstrapError::Policy)?;
179        for request in requests {
180            policy
181                .validate_allocation(request.stable_key())
182                .map_err(RuntimeBootstrapError::Policy)?;
183        }
184        self.owner.bind(&admitted)?;
185        self.binding = Some(Binding {
186            source: source.clone(),
187            grants: grants.clone(),
188            policy: policy_identity,
189            admitted,
190        });
191        Ok(self.owner.generation())
192    }
193    /// Compare the complete established binding without IO or policy callbacks.
194    ///
195    /// # Errors
196    /// Returns `NotBootstrapped` or `BindingMismatch` when the supplied composition differs from the established binding.
197    pub fn verify_bootstrap<P: RuntimeBootstrapPolicy>(
198        &self,
199        source: &SealedDeclarationSnapshot,
200        grants: &AllocationGrants,
201        policy: &P,
202    ) -> Result<u64, RuntimeBootstrapError<P::Error>> {
203        let binding = self
204            .binding
205            .as_ref()
206            .ok_or(RuntimeBootstrapError::NotBootstrapped)?;
207        if binding.source != *source
208            || binding.grants != *grants
209            || binding.policy != policy.runtime_bootstrap_identity()
210        {
211            return Err(RuntimeBootstrapError::BindingMismatch);
212        }
213        Ok(self.owner.generation())
214    }
215    /// Open one currently admitted name, preserving custody through detached handles.
216    ///
217    /// # Panics
218    /// Propagates backing failures or a broken internal binding invariant.
219    ///
220    /// # Errors
221    /// Rejects unbootstrapped or unadmitted access, an existing lease, and invalid directory state.
222    pub fn open_memory(&self, key: &StableKey) -> Result<RuntimeMemory<M>, RuntimeOpenError> {
223        let binding = self
224            .binding
225            .as_ref()
226            .ok_or(RuntimeOpenError::NotBootstrapped)?;
227        if !binding.admitted.contains(key) {
228            return Err(RuntimeOpenError::NotAdmitted(key.clone()));
229        }
230        let lease = self.owner.lease(key).map_err(|error| match error {
231            DirectoryError::AlreadyOpen => RuntimeOpenError::AlreadyOpen {
232                id: self.allocation_id(key).unwrap().get(),
233            },
234            error => RuntimeOpenError::Directory(error),
235        })?;
236        Ok(RuntimeMemory { lease })
237    }
238    /// Verify component requirements against current host admission without IO.
239    ///
240    /// # Errors
241    /// Rejects unbootstrapped access or requirements not admitted under the requested authority.
242    pub fn verify_authority(
243        &self,
244        requirements: &SealedDeclarationSnapshot,
245        authority: &str,
246    ) -> Result<(), RuntimeOpenError> {
247        let binding = self
248            .binding
249            .as_ref()
250            .ok_or(RuntimeOpenError::NotBootstrapped)?;
251        for request in requirements.requests() {
252            if request.authority() != authority
253                || !binding.admitted.contains(request.stable_key())
254                || binding.grants.check(request).is_err()
255            {
256                return Err(RuntimeOpenError::NotAdmitted(request.stable_key().clone()));
257            }
258        }
259        Ok(())
260    }
261    /// Resolve a retained identity for diagnostics; this grants no access.
262    #[must_use]
263    pub fn allocation_id(&self, key: &StableKey) -> Option<AllocationId> {
264        self.owner.allocation_id(key)
265    }
266    /// Observe checked directory entries and actual logical extents.
267    ///
268    /// # Errors
269    /// Returns a directory error when ownership is poisoned or the retained directory fails validation.
270    pub fn memory_allocations(&self) -> Result<Vec<MemoryAllocation>, DirectoryError> {
271        self.owner.verify()?;
272        Ok(self
273            .owner
274            .bindings()
275            .into_iter()
276            .map(|(stable_key, id, pages)| MemoryAllocation {
277                stable_key,
278                id,
279                pages,
280            })
281            .collect())
282    }
283
284    /// Validate the current directory and report physical capacity and custody.
285    /// Available before bootstrap; collecting diagnostics never admits or opens a key.
286    /// Page totals describe reserved capacity, not application payload occupancy.
287    ///
288    /// # Errors
289    /// Rejects a poisoned owner or backing that no longer matches its directory.
290    pub fn diagnostics(&self) -> Result<StorageDiagnostics, DirectoryError> {
291        let mut report = self.owner.diagnostics()?;
292        report.admitted_allocations = self.binding.as_ref().map_or(0, |b| b.admitted.len());
293        Ok(report)
294    }
295}
296
297///
298/// `StorageDiagnostics`
299///
300/// Read-only totals from the validated current directory and physical backing.
301/// The protected root occupies one additional page: data, metadata, unassigned
302/// and root pages sum to physical pages. Unassigned pages are not reclaimable
303/// through this API. Diagnostics grant no allocation authority.
304///
305
306#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Serialize)]
307pub struct StorageDiagnostics {
308    /// Committed directory generation.
309    pub generation: u64,
310    /// Retained named allocations, including currently omitted names.
311    pub allocations: usize,
312    /// Names admitted by this runtime's bootstrap, or zero before bootstrap.
313    pub admitted_allocations: usize,
314    /// Currently live exclusive allocation leases.
315    pub live_leases: usize,
316    /// Total application data extents.
317    pub extents: usize,
318    /// Logical data capacity across retained allocations, in 64 KiB pages.
319    pub data_pages: u64,
320    /// Reserved active and inactive directory arenas, in 64 KiB pages.
321    pub metadata_pages: u64,
322    /// Encoded byte length of the active directory snapshot.
323    pub directory_bytes: u64,
324    /// Actual physical backing extent, in 64 KiB pages.
325    pub physical_pages: u64,
326    /// Physical pages outside data, metadata arenas and the protected root.
327    pub unassigned_pages: u64,
328}
329
330///
331/// `MemoryAllocation`
332///
333/// Passive diagnostic entry; no lease or allocation authority is carried.
334///
335#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
336pub struct MemoryAllocation {
337    /// Durable name.
338    pub stable_key: StableKey,
339    /// Opaque identity.
340    pub id: AllocationId,
341    /// Logical extent in Wasm pages.
342    pub pages: u64,
343}
344
345///
346/// `RuntimeMemory`
347///
348/// Exclusive non-cloneable allocation lease. IO trusts custody between checked
349/// boundaries; growth commits the directory before publishing its new extent.
350/// A detached lease retains the backing claim until it is dropped.
351///
352pub struct RuntimeMemory<M: Memory> {
353    lease: Lease<M>,
354}
355impl<M: Memory> RuntimeMemory<M> {
356    #[cfg(not(target_arch = "wasm32"))]
357    pub(crate) fn poison_on_unwind(&self) -> directory_model::leases::PanicGuard<M> {
358        self.lease.poison_on_unwind()
359    }
360    /// Logical page extent.
361    #[must_use]
362    pub fn size(&self) -> u64 {
363        self.lease.size()
364    }
365    /// Reserve additional zero-filled logical pages without exposing backing.
366    ///
367    /// # Errors
368    /// Rejects arithmetic overflow, resource limits, backing refusal, poisoned ownership, or invalid directory state.
369    pub fn grow(&self, pages: u64) -> Result<u64, RuntimeGrowError> {
370        self.lease.try_grow(pages).map_err(|error| match error {
371            DirectoryError::ArithmeticOverflow => RuntimeGrowError::ArithmeticOverflow,
372            DirectoryError::Growth => RuntimeGrowError::BackingRefused {
373                additional_pages: pages,
374            },
375            DirectoryError::Budget | DirectoryError::Wire(_) => RuntimeGrowError::CapacityExceeded,
376            DirectoryError::Poisoned => RuntimeGrowError::Poisoned,
377            _ => RuntimeGrowError::InvalidDirectory,
378        })
379    }
380    /// Read a completely bounds-checked logical range.
381    pub fn read(&self, offset: u64, bytes: &mut [u8]) {
382        self.lease.read(offset, bytes);
383    }
384    /// Write a completely bounds-checked logical range.
385    pub fn write(&self, offset: u64, bytes: &[u8]) {
386        self.lease.write(offset, bytes);
387    }
388}
389impl<M: Memory> Memory for RuntimeMemory<M> {
390    fn size(&self) -> u64 {
391        self.size()
392    }
393    fn grow(&self, pages: u64) -> i64 {
394        self.grow(pages)
395            .map_or(-1, |previous| i64::try_from(previous).expect("page count"))
396    }
397    fn read(&self, offset: u64, bytes: &mut [u8]) {
398        self.read(offset, bytes);
399    }
400    #[expect(
401        unsafe_code,
402        reason = "Forward the raw-read contract through the governed lease"
403    )]
404    unsafe fn read_unsafe(&self, offset: u64, dst: *mut u8, count: usize) {
405        // SAFETY: forwarding preserves the caller's exclusive destination.
406        unsafe { self.lease.read_unsafe(offset, dst, count) }
407    }
408    fn write(&self, offset: u64, bytes: &[u8]) {
409        self.write(offset, bytes);
410    }
411}