ic_memory/runtime/
custody.rs1use super::RuntimeConstructionError;
3use ic_platform::stable_structures::{Memory, VectorMemory};
4use std::{collections::BTreeSet, rc::Rc, sync::Arc};
5
6#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
14pub struct BackingIdentity(usize);
15
16impl BackingIdentity {
17 #[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 #[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
32pub trait MemoryBacking: Memory {
48 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 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 let claimed_after_free = with_claims(|claims| {
206 drop(memory);
207 claims.contains(&identity)
208 });
209 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}