Skip to main content

ic_memory/
registry.rs

1use crate::{
2    declaration::{AllocationDeclaration, DeclarationSnapshot},
3    schema::SchemaMetadata,
4    slot::{
5        IC_MEMORY_AUTHORITY_OWNER, IC_MEMORY_LEDGER_LABEL, IC_MEMORY_LEDGER_STABLE_KEY,
6        MEMORY_MANAGER_LEDGER_ID, is_ic_memory_stable_key,
7    },
8    text::validate_diagnostic_text,
9};
10use serde::{Deserialize, Serialize};
11use std::{
12    borrow::Cow,
13    panic::{AssertUnwindSafe, catch_unwind},
14    sync::{Arc, Mutex, MutexGuard},
15    thread::ThreadId,
16};
17
18#[cfg(test)]
19pub static TEST_REGISTRY_LOCK: Mutex<()> = Mutex::new(());
20
21///
22/// MemoryRequest
23///
24/// Key-only request resolved after ledger recovery. All admitted owners share the
25/// host's free application pool; known keys retain their durable slot.
26///
27
28#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
29pub struct MemoryRequest {
30    authority: String,
31    stable_key: crate::StableKey,
32    schema: SchemaMetadata,
33}
34
35impl<'de> Deserialize<'de> for MemoryRequest {
36    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
37        use serde::de::Error as _;
38        #[derive(Deserialize)]
39        #[serde(deny_unknown_fields)]
40        struct Request {
41            authority: String,
42            stable_key: crate::StableKey,
43            schema: SchemaMetadata,
44        }
45        let request = Request::deserialize(deserializer)?;
46        Self::new(
47            request.authority,
48            request.stable_key.as_str(),
49            request.schema,
50        )
51        .map_err(D::Error::custom)
52    }
53}
54
55impl MemoryRequest {
56    /// Build a checked logical request before sealing.
57    pub fn new(
58        authority: impl Into<String>,
59        stable_key: &str,
60        schema: SchemaMetadata,
61    ) -> Result<Self, StaticMemoryDeclarationError> {
62        let authority = authority.into();
63        validate_external_authority(&authority)?;
64        let stable_key =
65            crate::StableKey::parse(stable_key).map_err(crate::DeclarationSnapshotError::Key)?;
66        if is_ic_memory_stable_key(stable_key.as_str()) {
67            return Err(StaticMemoryDeclarationError::ReservedStableKey {
68                stable_key: stable_key.as_str().to_string(),
69            });
70        }
71        Ok(Self {
72            authority,
73            stable_key,
74            schema,
75        })
76    }
77
78    /// Attach schema metadata from the immutable, integrity-checked recovered ledger.
79    pub(crate) const fn with_schema(mut self, schema: SchemaMetadata) -> Self {
80        self.schema = schema;
81        self
82    }
83
84    /// Borrow the requested durable key.
85    #[must_use]
86    pub const fn stable_key(&self) -> &crate::StableKey {
87        &self.stable_key
88    }
89
90    /// Borrow the requested diagnostic schema metadata.
91    #[must_use]
92    pub const fn schema(&self) -> &SchemaMetadata {
93        &self.schema
94    }
95
96    /// Borrow the declaring authority.
97    #[must_use]
98    pub fn authority(&self) -> &str {
99        &self.authority
100    }
101}
102
103/// Register a key-only request before the linked snapshot seals.
104pub fn register_memory_request(request: MemoryRequest) -> Result<(), StaticMemoryDeclarationError> {
105    with_unsealed_registry(|registry| registry.requests.push(request))
106}
107
108///
109/// StaticMemoryDeclarationError
110///
111/// Failure to register or collect static allocation declarations.
112#[non_exhaustive]
113#[derive(Clone, Debug, Eq, thiserror::Error, PartialEq)]
114pub enum StaticMemoryDeclarationError {
115    #[error("at most 254 external requests are supported")]
116    TooManyDeclarations,
117    #[error("duplicate requested stable key {stable_key}")]
118    DuplicateRequest { stable_key: crate::StableKey },
119    /// Static declaration registry lock was poisoned.
120    #[error("static memory declaration registry lock poisoned")]
121    RegistryPoisoned,
122    /// Bootstrap already sealed the declaration snapshot.
123    #[error("static memory declaration registry is already sealed")]
124    RegistrySealed,
125    /// Snapshot sealing was called recursively from an eager hook.
126    #[error("static memory declaration snapshot sealing is already active on this thread")]
127    ReentrantSealing,
128    /// A deferred eager initialization hook panicked while declarations were sealing.
129    #[error("static memory declaration eager-init hook panicked")]
130    EagerInitPanicked,
131    /// Declaration validation failed.
132    #[error(transparent)]
133    Declaration(#[from] crate::DeclarationSnapshotError),
134    /// External registration attempted to use an invalid authority identifier.
135    #[error("authority {reason}")]
136    InvalidAuthority {
137        /// Validation failure.
138        reason: &'static str,
139    },
140    /// External registration attempted to impersonate the internal authority.
141    #[error("authority '{authority}' is reserved for ic-memory runtime internals")]
142    ReservedAuthority {
143        /// Reserved authority identifier.
144        authority: String,
145    },
146    /// External registration attempted to claim the internal stable-key namespace.
147    #[error("stable key '{stable_key}' is reserved for ic-memory runtime internals")]
148    ReservedStableKey {
149        /// Reserved stable key.
150        stable_key: String,
151    },
152}
153
154///
155/// SealedDeclarationSnapshot
156///
157/// Immutable, canonical linked-program allocation requests supplied to each concrete [`crate::MemoryRuntime`].
158///
159/// Sealing runs generated registration hooks and eager declaration hooks
160/// exactly once. Clones share the same immutable snapshot. This value contains
161/// declaration authority only; it contains no memory handles, recovery state,
162/// bootstrap lifecycle, or committed allocation capability.
163///
164
165#[derive(Clone, Debug, Eq, PartialEq)]
166pub struct SealedDeclarationSnapshot {
167    inner: Arc<SealedDeclarationSnapshotInner>,
168}
169
170///
171/// SealedDeclarationFingerprint
172///
173/// Deterministic non-cryptographic fingerprint of one canonical sealed
174/// declaration snapshot.
175///
176/// The fingerprint covers canonical source keys, owner labels and schema metadata. It is diagnostic
177/// metadata for comparing in-memory bootstrap bindings, not persisted
178/// allocation authority or an adversarial integrity proof.
179///
180
181#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
182#[serde(deny_unknown_fields)]
183pub struct SealedDeclarationFingerprint {
184    algorithm_version: u8,
185    value: u64,
186}
187
188impl SealedDeclarationFingerprint {
189    /// Return the diagnostic fingerprint algorithm version.
190    #[must_use]
191    pub const fn algorithm_version(&self) -> u8 {
192        self.algorithm_version
193    }
194
195    /// Return the non-cryptographic fingerprint value.
196    #[must_use]
197    pub const fn value(&self) -> u64 {
198        self.value
199    }
200}
201
202#[derive(Debug, Eq, PartialEq)]
203struct SealedDeclarationSnapshotInner {
204    requests: Vec<MemoryRequest>,
205    fingerprint: SealedDeclarationFingerprint,
206}
207
208impl SealedDeclarationSnapshot {
209    /// Canonicalize explicit requests with the same rules as the linked registry.
210    pub fn new(requests: &[MemoryRequest]) -> Result<Self, StaticMemoryDeclarationError> {
211        build_snapshot(Cow::Borrowed(requests))
212    }
213
214    /// Borrow canonical unresolved key-only requests.
215    #[must_use]
216    pub fn requests(&self) -> &[MemoryRequest] {
217        &self.inner.requests
218    }
219
220    pub(crate) fn resolve(
221        &self,
222        ledger: &crate::AllocationLedger,
223        historical: Vec<MemoryRequest>,
224        pool: &crate::MemoryAllocationPool,
225    ) -> Result<DeclarationSnapshot, crate::MemoryResolutionError> {
226        if self.requests().len() + historical.len() > 254 {
227            return Err(StaticMemoryDeclarationError::TooManyDeclarations.into());
228        }
229        let mut declarations = Vec::with_capacity(self.requests().len() + historical.len() + 1);
230        declarations.push(internal_ledger_declaration());
231        let mut occupied = [false; 255];
232        for record in ledger.records() {
233            occupied[usize::from(record.slot().id())] = true;
234        }
235        // Only the original requests can allocate new slots and they are already
236        // canonical. Admission selections are known-only: all their slots are
237        // occupied above regardless of selection order. Final declarations are
238        // canonicalized and checked together below.
239        for request in self
240            .requests()
241            .iter()
242            .map(Cow::Borrowed)
243            .chain(historical.into_iter().map(Cow::Owned))
244        {
245            let historical = ledger
246                .records()
247                .iter()
248                .find(|record| record.stable_key() == &request.stable_key);
249            pool.validate_authority(&request.stable_key, &request.authority)?;
250            let id = if let Some(record) = historical {
251                // Durable identity selects placement; the host separately grants
252                // current key ownership and retains explicit physical exclusions.
253                pool.validate_id(record.slot().id())?;
254                record.slot().id()
255            } else {
256                (0..crate::MEMORY_MANAGER_INVALID_ID)
257                    .find(|id| pool.contains(*id) && !occupied[usize::from(*id)])
258                    .ok_or_else(|| crate::MemoryResolutionError::Exhausted {
259                        stable_key: request.stable_key.clone(),
260                        authority: request.authority.clone(),
261                    })?
262            };
263            let slot = crate::MemoryManagerSlot::new(id).expect("usable id");
264            occupied[usize::from(id)] = true;
265            // Request construction checked authority/key/schema, and recovery
266            // checked historical schemas. Copy borrowed source requests only;
267            // owned selections move their fields into the final declarations.
268            // The final snapshot still validates all declarations together.
269            let request = request.into_owned();
270            declarations.push(AllocationDeclaration {
271                stable_key: request.stable_key,
272                slot,
273                label: None,
274                schema: request.schema,
275            });
276        }
277        declarations.sort_unstable_by(|a, b| a.stable_key().cmp(b.stable_key()));
278        Ok(DeclarationSnapshot::new(declarations).map_err(StaticMemoryDeclarationError::from)?)
279    }
280
281    /// Return the deterministic fingerprint of this sealed declaration meaning.
282    #[must_use]
283    pub fn fingerprint(&self) -> SealedDeclarationFingerprint {
284        self.inner.fingerprint
285    }
286
287    #[cfg(test)]
288    pub(crate) fn shares_storage_with(&self, other: &Self) -> bool {
289        Arc::ptr_eq(&self.inner, &other.inner)
290    }
291}
292
293type StaticRegistrationHook = fn() -> Result<(), StaticMemoryDeclarationError>;
294
295#[derive(Debug)]
296struct StaticMemoryDeclarationRegistry {
297    requests: Vec<MemoryRequest>,
298    registration_hooks: Vec<StaticRegistrationHook>,
299    eager_init_hooks: Vec<fn()>,
300    lifecycle: StaticRegistryLifecycle,
301}
302
303impl StaticMemoryDeclarationRegistry {
304    fn finish_sealing(
305        &mut self,
306        result: Result<SealedDeclarationSnapshot, StaticMemoryDeclarationError>,
307    ) -> Result<SealedDeclarationSnapshot, StaticMemoryDeclarationError> {
308        // Only the immutable snapshot or terminal error remains useful.
309        self.requests = Vec::new();
310        self.registration_hooks = Vec::new();
311        self.eager_init_hooks = Vec::new();
312        self.lifecycle = match &result {
313            Ok(snapshot) => StaticRegistryLifecycle::Sealed(snapshot.clone()),
314            Err(error) => StaticRegistryLifecycle::Failed(error.clone()),
315        };
316        result
317    }
318}
319
320#[derive(Debug)]
321enum StaticRegistryLifecycle {
322    Open,
323    Sealing {
324        owner: ThreadId,
325        deferred_error: Option<StaticMemoryDeclarationError>,
326    },
327    Sealed(SealedDeclarationSnapshot),
328    Failed(StaticMemoryDeclarationError),
329}
330
331static STATIC_MEMORY_DECLARATIONS: Mutex<StaticMemoryDeclarationRegistry> =
332    Mutex::new(StaticMemoryDeclarationRegistry {
333        requests: Vec::new(),
334        registration_hooks: Vec::new(),
335        eager_init_hooks: Vec::new(),
336        lifecycle: StaticRegistryLifecycle::Open,
337    });
338
339static STATIC_MEMORY_SEAL: Mutex<()> = Mutex::new(());
340
341fn lock_registry()
342-> Result<MutexGuard<'static, StaticMemoryDeclarationRegistry>, StaticMemoryDeclarationError> {
343    STATIC_MEMORY_DECLARATIONS
344        .lock()
345        .map_err(|_| StaticMemoryDeclarationError::RegistryPoisoned)
346}
347
348fn ensure_registration_open(
349    registry: &StaticMemoryDeclarationRegistry,
350) -> Result<(), StaticMemoryDeclarationError> {
351    match &registry.lifecycle {
352        StaticRegistryLifecycle::Open => Ok(()),
353        StaticRegistryLifecycle::Sealing { owner, .. } if *owner == std::thread::current().id() => {
354            Ok(())
355        }
356        StaticRegistryLifecycle::Sealing { .. }
357        | StaticRegistryLifecycle::Sealed(_)
358        | StaticRegistryLifecycle::Failed(_) => Err(StaticMemoryDeclarationError::RegistrySealed),
359    }
360}
361
362fn with_unsealed_registry(
363    op: impl FnOnce(&mut StaticMemoryDeclarationRegistry),
364) -> Result<(), StaticMemoryDeclarationError> {
365    let mut registry = lock_registry()?;
366    ensure_registration_open(&registry)?;
367    op(&mut registry);
368    Ok(())
369}
370
371/// Queue a generated registration hook for the fallible sealing phase.
372///
373/// Static constructors cannot return an error. A late deferral is therefore
374/// retained in registry state and returned by snapshot sealing.
375#[doc(hidden)]
376pub fn defer_static_memory_registration(hook: StaticRegistrationHook) {
377    defer_constructor_registration(|registry| {
378        registry.registration_hooks.push(hook);
379    });
380}
381
382/// Queue a declaration-only hook to run immediately before snapshot sealing.
383///
384/// Static constructors cannot return an error. A late deferral is therefore
385/// retained in registry state and returned by snapshot sealing.
386#[doc(hidden)]
387pub fn defer_eager_init(hook: fn()) {
388    defer_constructor_registration(|registry| {
389        registry.eager_init_hooks.push(hook);
390    });
391}
392
393fn defer_constructor_registration(op: impl FnOnce(&mut StaticMemoryDeclarationRegistry)) {
394    let Ok(mut registry) = STATIC_MEMORY_DECLARATIONS.lock() else {
395        // Mutex poisoning is itself durable evidence of the registration
396        // failure and is reported by the next snapshot request.
397        return;
398    };
399    if matches!(registry.lifecycle, StaticRegistryLifecycle::Open) {
400        op(&mut registry);
401        return;
402    }
403    match &mut registry.lifecycle {
404        StaticRegistryLifecycle::Sealing { deferred_error, .. } => {
405            if deferred_error.is_none() {
406                *deferred_error = Some(StaticMemoryDeclarationError::RegistrySealed);
407            }
408        }
409        StaticRegistryLifecycle::Sealed(_) => {
410            registry.lifecycle =
411                StaticRegistryLifecycle::Failed(StaticMemoryDeclarationError::RegistrySealed);
412        }
413        StaticRegistryLifecycle::Failed(_) | StaticRegistryLifecycle::Open => {}
414    }
415}
416
417fn validate_external_authority(value: &str) -> Result<(), StaticMemoryDeclarationError> {
418    reject_internal_authority(value)?;
419    validate_diagnostic_text(value).map_err(|error| {
420        StaticMemoryDeclarationError::InvalidAuthority {
421            reason: error.reason(),
422        }
423    })
424}
425
426fn reject_internal_authority(value: &str) -> Result<(), StaticMemoryDeclarationError> {
427    if value == IC_MEMORY_AUTHORITY_OWNER {
428        return Err(StaticMemoryDeclarationError::ReservedAuthority {
429            authority: value.to_string(),
430        });
431    }
432    Ok(())
433}
434
435/// Seal and return the canonical linked-program declaration snapshot.
436///
437/// The first caller runs deferred generated registrations and eager hooks,
438/// canonicalizes requests, validates duplicates and requests, and publishes one immutable snapshot. Concurrent and subsequent
439/// callers receive clones backed by that same snapshot.
440///
441/// # Panics
442///
443/// Panics only if a private governance-metadata, sealing or fingerprint-encoding
444/// invariant is broken.
445pub fn sealed_declaration_snapshot()
446-> Result<SealedDeclarationSnapshot, StaticMemoryDeclarationError> {
447    {
448        let registry = lock_registry()?;
449        match &registry.lifecycle {
450            StaticRegistryLifecycle::Sealed(snapshot) => return Ok(snapshot.clone()),
451            StaticRegistryLifecycle::Failed(err) => return Err(err.clone()),
452            StaticRegistryLifecycle::Sealing { owner, .. }
453                if *owner == std::thread::current().id() =>
454            {
455                return Err(StaticMemoryDeclarationError::ReentrantSealing);
456            }
457            StaticRegistryLifecycle::Open | StaticRegistryLifecycle::Sealing { .. } => {}
458        }
459    }
460
461    let _seal = STATIC_MEMORY_SEAL
462        .lock()
463        .map_err(|_| StaticMemoryDeclarationError::RegistryPoisoned)?;
464    let (registration_hooks, eager_init_hooks) = {
465        let mut registry = lock_registry()?;
466        match &registry.lifecycle {
467            StaticRegistryLifecycle::Sealed(snapshot) => return Ok(snapshot.clone()),
468            StaticRegistryLifecycle::Failed(err) => return Err(err.clone()),
469            StaticRegistryLifecycle::Sealing { .. } => {
470                return Err(StaticMemoryDeclarationError::ReentrantSealing);
471            }
472            StaticRegistryLifecycle::Open => {}
473        }
474        registry.lifecycle = StaticRegistryLifecycle::Sealing {
475            owner: std::thread::current().id(),
476            deferred_error: None,
477        };
478        (
479            std::mem::take(&mut registry.registration_hooks),
480            std::mem::take(&mut registry.eager_init_hooks),
481        )
482    };
483
484    for hook in registration_hooks {
485        let result = catch_unwind(AssertUnwindSafe(hook))
486            .map_err(|_| StaticMemoryDeclarationError::EagerInitPanicked)
487            .and_then(std::convert::identity);
488        if let Err(err) = result {
489            return fail_sealing(err);
490        }
491    }
492    for hook in eager_init_hooks {
493        if catch_unwind(AssertUnwindSafe(hook)).is_err() {
494            return fail_sealing(StaticMemoryDeclarationError::EagerInitPanicked);
495        }
496    }
497
498    let mut registry = lock_registry()?;
499    let deferred_error = match &registry.lifecycle {
500        StaticRegistryLifecycle::Sealing { deferred_error, .. } => deferred_error.clone(),
501        StaticRegistryLifecycle::Failed(err) => return Err(err.clone()),
502        StaticRegistryLifecycle::Open | StaticRegistryLifecycle::Sealed(_) => {
503            unreachable!("seal lock preserves the in-progress registry lifecycle");
504        }
505    };
506    let result = match deferred_error {
507        Some(error) => Err(error),
508        None => build_snapshot(Cow::Owned(std::mem::take(&mut registry.requests))),
509    };
510    registry.finish_sealing(result)
511}
512
513fn fail_sealing(
514    err: StaticMemoryDeclarationError,
515) -> Result<SealedDeclarationSnapshot, StaticMemoryDeclarationError> {
516    let mut registry = lock_registry()?;
517    let failure = match &registry.lifecycle {
518        StaticRegistryLifecycle::Sealing {
519            deferred_error: Some(deferred_error),
520            ..
521        } => deferred_error.clone(),
522        StaticRegistryLifecycle::Open
523        | StaticRegistryLifecycle::Sealing {
524            deferred_error: None,
525            ..
526        }
527        | StaticRegistryLifecycle::Sealed(_) => err,
528        StaticRegistryLifecycle::Failed(failure) => failure.clone(),
529    };
530    registry.finish_sealing(Err(failure))
531}
532
533fn build_snapshot(
534    requests: Cow<'_, [MemoryRequest]>,
535) -> Result<SealedDeclarationSnapshot, StaticMemoryDeclarationError> {
536    if requests.len() > 254 {
537        return Err(StaticMemoryDeclarationError::TooManyDeclarations);
538    }
539    let mut requests = requests.into_owned();
540    requests.sort_unstable_by(|a, b| a.stable_key.cmp(&b.stable_key));
541    for pair in requests.windows(2) {
542        if pair[0].stable_key == pair[1].stable_key {
543            return Err(StaticMemoryDeclarationError::DuplicateRequest {
544                stable_key: pair[1].stable_key.clone(),
545            });
546        }
547    }
548    let fingerprint = sealed_declaration_fingerprint(&requests);
549    Ok(SealedDeclarationSnapshot {
550        inner: Arc::new(SealedDeclarationSnapshotInner {
551            requests,
552            fingerprint,
553        }),
554    })
555}
556
557#[derive(Serialize)]
558struct SealedDeclarationFingerprintMaterial<'a> {
559    format: &'static str,
560    requests: &'a [MemoryRequest],
561}
562
563// Fingerprints need the canonical encoded bytes only as input to the hash;
564// keep no payload buffer after serialization.
565struct FingerprintWriter(u64);
566
567impl std::io::Write for FingerprintWriter {
568    fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
569        self.0 = crate::hash::fnv64(self.0, bytes);
570        Ok(bytes.len())
571    }
572
573    fn flush(&mut self) -> std::io::Result<()> {
574        Ok(())
575    }
576}
577
578fn sealed_declaration_fingerprint(requests: &[MemoryRequest]) -> SealedDeclarationFingerprint {
579    let material = SealedDeclarationFingerprintMaterial {
580        format: "ic-memory.sealed-declaration-fingerprint.v1",
581        requests,
582    };
583    let mut writer = FingerprintWriter(crate::hash::FNV_OFFSET);
584    // Concrete derived serializers and this hash writer have no recoverable failures.
585    ciborium::into_writer(&material, &mut writer)
586        .expect("sealed declaration fingerprint encodes into hash");
587
588    SealedDeclarationFingerprint {
589        algorithm_version: SEALED_DECLARATION_FINGERPRINT_VERSION,
590        value: writer.0,
591    }
592}
593
594const SEALED_DECLARATION_FINGERPRINT_VERSION: u8 = 1;
595
596fn internal_ledger_declaration() -> AllocationDeclaration {
597    AllocationDeclaration::memory_manager(
598        IC_MEMORY_LEDGER_STABLE_KEY,
599        MEMORY_MANAGER_LEDGER_ID,
600        IC_MEMORY_LEDGER_LABEL,
601    )
602    .unwrap_or_else(|_| unreachable!("built-in ledger declaration constants are valid"))
603}
604
605#[cfg(test)]
606pub fn reset_static_memory_declarations_for_tests() {
607    let mut registry = STATIC_MEMORY_DECLARATIONS
608        .lock()
609        .expect("static memory declaration registry poisoned");
610    registry.requests.clear();
611    registry.registration_hooks.clear();
612    registry.eager_init_hooks.clear();
613    registry.lifecycle = StaticRegistryLifecycle::Open;
614}
615
616#[cfg(test)]
617mod tests;