Skip to main content

ic_memory/runtime/
custody.rs

1//! Process-local backing claims, retained by every issued runtime handle.
2use super::RuntimeConstructionError;
3use ic_platform::stable_structures::{Memory, VectorMemory};
4use std::{collections::BTreeSet, rc::Rc, sync::Arc};
5
6///
7/// BackingIdentity
8///
9/// Identity of a physical backing allocation, shared by all of its wrappers.
10/// This is host custody metadata, not a virtual memory ID or durable record.
11///
12
13#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
14pub struct BackingIdentity(usize);
15
16impl BackingIdentity {
17    /// Identify an Rc-owned backing. Its allocation must remain alive through
18    /// every runtime/handle using this identity. Identify the underlying backing,
19    /// not a newly allocated wrapper or an address inside a growable byte buffer.
20    #[must_use]
21    pub fn of_rc<T: ?Sized>(backing: &Rc<T>) -> Self {
22        Self(Rc::as_ptr(backing).cast::<()>() as usize)
23    }
24
25    /// Identify an Arc-owned backing under the same contract as [`Self::of_rc`].
26    #[must_use]
27    pub fn of_arc<T: ?Sized>(backing: &Arc<T>) -> Self {
28        Self(Arc::as_ptr(backing).cast::<()>() as usize)
29    }
30}
31
32///
33/// MemoryBacking
34///
35/// Host boundary for a backing whose aliases can be identified consistently.
36///
37/// All wrappers and clones over the same physical backing must return the same
38/// stable identity; independent backings must have different identities. Keep
39/// its identity allocation alive for the backing's complete runtime/handle
40/// lifetime. A wrapper delegates to its underlying backing's implementation.
41/// Obtaining the identity must not read, grow or write backing memory or create
42/// a manager/runtime; use a retained heap identity, not persisted contents.
43/// Implementing Memory alone does not establish this custody contract. Raw IO
44/// and unmanaged managers over a claimed backing remain prohibited; this is not
45/// an isolation boundary against arbitrary linked code or a dishonest backing.
46///
47pub trait MemoryBacking: Memory {
48    /// Return the physical backing identity under the trait's host contract.
49    fn backing_identity(&self) -> BackingIdentity;
50}
51
52impl MemoryBacking for VectorMemory {
53    fn backing_identity(&self) -> BackingIdentity {
54        BackingIdentity::of_rc(self)
55    }
56}
57
58impl<M: MemoryBacking> MemoryBacking for Rc<M> {
59    fn backing_identity(&self) -> BackingIdentity {
60        self.as_ref().backing_identity()
61    }
62}
63
64#[cfg(target_arch = "wasm32")]
65impl MemoryBacking for ic_platform::stable_structures::DefaultMemoryImpl {
66    fn backing_identity(&self) -> BackingIdentity {
67        // Every default handle addresses the same canister stable memory.
68        BackingIdentity(0)
69    }
70}
71
72#[cfg(not(target_arch = "wasm32"))]
73static CLAIMS: std::sync::Mutex<BTreeSet<BackingIdentity>> = std::sync::Mutex::new(BTreeSet::new());
74
75#[cfg(target_arch = "wasm32")]
76thread_local! {
77    static CLAIMS: std::cell::RefCell<BTreeSet<BackingIdentity>> = const {
78        std::cell::RefCell::new(BTreeSet::new())
79    };
80}
81
82fn with_claims<T>(operation: impl FnOnce(&mut BTreeSet<BackingIdentity>) -> T) -> T {
83    #[cfg(not(target_arch = "wasm32"))]
84    {
85        let mut claims = CLAIMS
86            .lock()
87            .unwrap_or_else(std::sync::PoisonError::into_inner);
88        operation(&mut claims)
89    }
90    #[cfg(target_arch = "wasm32")]
91    CLAIMS.with(|claims| operation(&mut claims.borrow_mut()))
92}
93
94pub(super) struct BackingClaim(BackingIdentity);
95
96impl BackingClaim {
97    pub fn acquire(memory: &impl MemoryBacking) -> Result<Self, RuntimeConstructionError> {
98        let identity = memory.backing_identity();
99        if with_claims(|claims| claims.insert(identity)) {
100            Ok(Self(identity))
101        } else {
102            Err(RuntimeConstructionError::BackingAlreadyClaimed)
103        }
104    }
105}
106
107impl Drop for BackingClaim {
108    fn drop(&mut self) {
109        with_claims(|claims| claims.remove(&self.0));
110    }
111}
112
113#[cfg(all(test, not(target_arch = "wasm32")))]
114impl<M: MemoryBacking> MemoryBacking for ic_testkit_native::memory::MeteredMemory<M> {
115    fn backing_identity(&self) -> BackingIdentity {
116        self.backing().backing_identity()
117    }
118}
119
120#[cfg(all(test, not(target_arch = "wasm32")))]
121mod tests {
122    use super::*;
123    use crate::{MemoryManagerConfig, MemoryRuntime, testing::fixture_runtime};
124    use std::{cell::Cell, panic::AssertUnwindSafe};
125
126    #[derive(Clone, Copy, Debug)]
127    enum Failure {
128        None,
129        Grow,
130        Initialize,
131        Capture,
132    }
133
134    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
135    struct Destruction {
136        strong_refs: usize,
137        claimed_after_free: bool,
138    }
139
140    struct TrackedBacking {
141        memory: Option<VectorMemory>,
142        failure: Failure,
143        destruction: Rc<Cell<Option<Destruction>>>,
144    }
145
146    impl TrackedBacking {
147        fn new(failure: Failure) -> Self {
148            Self {
149                memory: Some(VectorMemory::default()),
150                failure,
151                destruction: Rc::default(),
152            }
153        }
154
155        const fn memory(&self) -> &VectorMemory {
156            self.memory.as_ref().unwrap()
157        }
158    }
159
160    impl MemoryBacking for TrackedBacking {
161        fn backing_identity(&self) -> BackingIdentity {
162            self.memory().backing_identity()
163        }
164    }
165
166    impl Memory for TrackedBacking {
167        fn size(&self) -> u64 {
168            self.memory().size()
169        }
170
171        fn grow(&self, pages: u64) -> i64 {
172            if matches!(self.failure, Failure::Grow) {
173                -1
174            } else {
175                self.memory().grow(pages)
176            }
177        }
178
179        fn read(&self, offset: u64, dst: &mut [u8]) {
180            assert!(
181                !matches!(self.failure, Failure::Capture)
182                    || dst.len() != super::super::layout::METADATA_BYTES,
183                "injected authority capture failure"
184            );
185            self.memory().read(offset, dst);
186        }
187
188        fn write(&self, offset: u64, src: &[u8]) {
189            assert!(
190                !matches!(self.failure, Failure::Initialize),
191                "injected manager initialization failure"
192            );
193            self.memory().write(offset, src);
194        }
195    }
196
197    impl Drop for TrackedBacking {
198        fn drop(&mut self) {
199            let memory = self.memory.take().unwrap();
200            let identity = memory.backing_identity();
201            let strong_refs = Rc::strong_count(&memory);
202            // Hold the registry lock across freeing the final allocation and
203            // observing its former identity. Other tests cannot reuse that
204            // address and install a legitimate new claim before this check.
205            let claimed_after_free = with_claims(|claims| {
206                drop(memory);
207                claims.contains(&identity)
208            });
209            // Record instead of asserting in Drop: construction cases unwind.
210            self.destruction.set(Some(Destruction {
211                strong_refs,
212                claimed_after_free,
213            }));
214        }
215    }
216
217    #[test]
218    fn final_runtime_or_detached_handle_releases_claim_before_identity_allocation() {
219        for detach_handle in [false, true] {
220            let backing = TrackedBacking::new(Failure::None);
221            let destruction = Rc::clone(&backing.destruction);
222            let identity = backing.backing_identity();
223            let runtime = fixture_runtime(
224                backing,
225                "drop",
226                "drop.",
227                &["drop.rows.v1"],
228                MemoryManagerConfig::new(8).unwrap(),
229            )
230            .unwrap();
231            let handle = detach_handle.then(|| runtime.open_memory("drop.rows.v1").unwrap());
232            drop(runtime);
233            if handle.is_some() {
234                assert_eq!(destruction.get(), None);
235                assert!(with_claims(|claims| claims.contains(&identity)));
236            }
237            drop(handle);
238            assert_eq!(
239                destruction.get(),
240                Some(Destruction {
241                    strong_refs: 1,
242                    claimed_after_free: false,
243                })
244            );
245        }
246    }
247
248    #[test]
249    fn construction_refusal_and_unwind_release_claim_before_identity_allocation() {
250        for failure in [Failure::Grow, Failure::Initialize, Failure::Capture] {
251            let backing = TrackedBacking::new(failure);
252            let destruction = Rc::clone(&backing.destruction);
253            let result = std::panic::catch_unwind(AssertUnwindSafe(|| MemoryRuntime::new(backing)));
254            if matches!(failure, Failure::Grow) {
255                assert!(matches!(
256                    result,
257                    Ok(Err(RuntimeConstructionError::Growth(_)))
258                ));
259            } else {
260                assert!(result.is_err(), "expected injected {failure:?} failure");
261            }
262            assert_eq!(
263                destruction.get(),
264                Some(Destruction {
265                    strong_refs: 1,
266                    claimed_after_free: false,
267                }),
268                "failure during {failure:?} retained a dead backing identity"
269            );
270        }
271    }
272
273    #[test]
274    fn independent_backings_construct_and_drop_concurrently() {
275        let start = std::sync::Barrier::new(4);
276        std::thread::scope(|scope| {
277            for _ in 0..4 {
278                let start = &start;
279                scope.spawn(move || {
280                    start.wait();
281                    for _ in 0..2000 {
282                        drop(MemoryRuntime::new(VectorMemory::default()).unwrap());
283                    }
284                });
285            }
286        });
287    }
288}