shadow_shmem/
allocator.rs

1//! In this module is a shared memory allocator that can be used in Shadow to share data between
2//! the main simulator process and managed processes. There are three main global functions that
3//! are provided:
4//!
5//! (1) `shmalloc()`, which places the input argument into shared memory and returns a `Block`
6//! smart pointer.
7//! (2) `shfree()`, which is used to deallocate allocated blocks.
8//! (3) `shdeserialize()`, which is used to take a serialized block and convert it back into a
9//! `Block` smart pointer that can be dereferenced.
10//!
11//! Blocks can be serialized with the `.serialize()` member function, which converts the block to a
12//! process-memory-layout agnostic representation of the block. The serialized block can be
13//! one-to-one converted to and from a string for passing in between different processes.
14//!
15//! The intended workflow is:
16//!
17//! (a) The main Shadow simulator process allocates a shared memory block containing an object.
18//! (b) The block is serialized.
19//! (c) The serialized block is turned into a string.
20//! (d) The string is passed to one of Shadow's child, managed processes.
21//! (e) The managed process converts the string back to a serialized block.
22//! (f) The serialized block is deserialized into a shared memory block alias.
23//! (g) The alias is dereferenced and the shared object is retrieved.
24
25use lazy_static::lazy_static;
26
27use shadow_pod::Pod;
28use vasi::VirtualAddressSpaceIndependent;
29use vasi_sync::scmutex::SelfContainedMutex;
30
31/// This function moves the input parameter into a newly-allocated shared memory block. Analogous to
32/// `malloc()`.
33pub fn shmalloc<T>(val: T) -> ShMemBlock<'static, T>
34where
35    T: Sync + VirtualAddressSpaceIndependent,
36{
37    register_teardown();
38    SHMALLOC.lock().alloc(val)
39}
40
41/// This function frees a previously allocated block.
42pub fn shfree<T>(block: ShMemBlock<'static, T>)
43where
44    T: Sync + VirtualAddressSpaceIndependent,
45{
46    SHMALLOC.lock().free(block);
47}
48
49/// This function takes a serialized block and converts it back into a BlockAlias that can be
50/// dereferenced.
51///
52/// # Safety
53///
54/// This function can violate type safety if a template type is provided that does not match
55/// original block that was serialized.
56pub unsafe fn shdeserialize<T>(serialized: &ShMemBlockSerialized) -> ShMemBlockAlias<'static, T>
57where
58    T: Sync + VirtualAddressSpaceIndependent,
59{
60    unsafe { SHDESERIALIZER.lock().deserialize(serialized) }
61}
62
63#[cfg(test)]
64extern "C" fn shmalloc_teardown() {
65    SHMALLOC.lock().destruct();
66}
67
68// Just needed because we can't put the drop guard in a global place. We don't want to drop
69// after every function, and there's no global test main routine we can take advantage of. No
70// big deal.
71#[cfg(test)]
72#[cfg_attr(miri, ignore)]
73fn register_teardown() {
74    extern crate std;
75    use std::sync::Once;
76
77    static START: Once = Once::new();
78    START.call_once(|| unsafe {
79        libc::atexit(shmalloc_teardown);
80    });
81}
82
83#[cfg(not(test))]
84fn register_teardown() {}
85
86// The global, singleton shared memory allocator and deserializer objects.
87// TODO(rwails): Adjust to use lazy lock instead.
88lazy_static! {
89    static ref SHMALLOC: SelfContainedMutex<SharedMemAllocator<'static>> = {
90        let alloc = SharedMemAllocator::new();
91        SelfContainedMutex::new(alloc)
92    };
93    static ref SHDESERIALIZER: SelfContainedMutex<SharedMemDeserializer<'static>> = {
94        let deserial = SharedMemDeserializer::new();
95        SelfContainedMutex::new(deserial)
96    };
97}
98
99/// The intended singleton destructor for the global singleton shared memory allocator.
100///
101/// Because the global allocator has static lifetime, drop() will never be called on it. Therefore,
102/// necessary cleanup routines are not called. Instead, this object can be instantiated once, eg at
103/// the start of main(), and then when it is dropped at program exit the cleanup routine is called.
104pub struct SharedMemAllocatorDropGuard(());
105
106impl SharedMemAllocatorDropGuard {
107    /// # Safety
108    ///
109    /// Must outlive all `ShMemBlock` objects allocated by the current process.
110    pub unsafe fn new() -> Self {
111        Self(())
112    }
113}
114
115impl Drop for SharedMemAllocatorDropGuard {
116    fn drop(&mut self) {
117        SHMALLOC.lock().destruct();
118    }
119}
120
121/// A smart pointer class that holds a `Sync` and `VirtualAddressSpaceIndependent` object.
122///
123/// The pointer is obtained by a call to a shared memory allocator's `alloc()` function (or the
124/// global `shalloc()` function. The memory is freed when the block is dropped.
125///
126/// This smart pointer is unique in that it may be serialized to a string, passed across process
127/// boundaries, and deserialized in a (potentially) separate process to obtain a view of the
128/// contained data.
129#[derive(Debug)]
130pub struct ShMemBlock<'allocator, T>
131where
132    T: Sync + VirtualAddressSpaceIndependent,
133{
134    block: *mut crate::shmalloc_impl::Block,
135    phantom: core::marker::PhantomData<&'allocator T>,
136}
137
138impl<T> ShMemBlock<'_, T>
139where
140    T: Sync + VirtualAddressSpaceIndependent,
141{
142    pub fn serialize(&self) -> ShMemBlockSerialized {
143        let serialized = SHMALLOC.lock().internal.serialize(self.block);
144        ShMemBlockSerialized {
145            internal: serialized,
146        }
147    }
148}
149
150// SAFETY: T is already required to be Sync, and ShMemBlock only exposes
151// immutable references to the underlying data.
152unsafe impl<T> Sync for ShMemBlock<'_, T> where T: Sync + VirtualAddressSpaceIndependent {}
153unsafe impl<T> Send for ShMemBlock<'_, T> where T: Send + Sync + VirtualAddressSpaceIndependent {}
154
155impl<T> core::ops::Deref for ShMemBlock<'_, T>
156where
157    T: Sync + VirtualAddressSpaceIndependent,
158{
159    type Target = T;
160
161    fn deref(&self) -> &Self::Target {
162        let block = unsafe { &*self.block };
163        &block.get_ref::<T>()[0]
164    }
165}
166
167impl<T> core::ops::Drop for ShMemBlock<'_, T>
168where
169    T: Sync + VirtualAddressSpaceIndependent,
170{
171    fn drop(&mut self) {
172        if !self.block.is_null() {
173            // Guard here to prevent deadlock on free.
174            SHMALLOC.lock().internal.dealloc(self.block);
175            self.block = core::ptr::null_mut();
176        }
177    }
178}
179
180/// This struct is analogous to the `ShMemBlock` smart pointer, except it does not assume ownership
181/// of the underlying memory and thus does not free the memory when dropped.
182///
183/// An alias of a block is obtained with a call to `deserialize()` on a `SharedMemDeserializer`
184/// object (or likely by using the `shdeserialize()` to make this call on the global shared memory
185/// deserializer.
186#[derive(Debug)]
187pub struct ShMemBlockAlias<'deserializer, T>
188where
189    T: Sync + VirtualAddressSpaceIndependent,
190{
191    block: *mut crate::shmalloc_impl::Block,
192    phantom: core::marker::PhantomData<&'deserializer T>,
193}
194
195// SAFETY: T is already required to be Sync, and ShMemBlock only exposes
196// immutable references to the underlying data.
197unsafe impl<T> Sync for ShMemBlockAlias<'_, T> where T: Sync + VirtualAddressSpaceIndependent {}
198unsafe impl<T> Send for ShMemBlockAlias<'_, T> where T: Send + Sync + VirtualAddressSpaceIndependent {}
199
200impl<T> core::ops::Deref for ShMemBlockAlias<'_, T>
201where
202    T: Sync + VirtualAddressSpaceIndependent,
203{
204    type Target = T;
205
206    fn deref(&self) -> &Self::Target {
207        let block = unsafe { &*self.block };
208        &block.get_ref::<T>()[0]
209    }
210}
211
212#[derive(Copy, Clone, Debug, VirtualAddressSpaceIndependent)]
213#[repr(transparent)]
214pub struct ShMemBlockSerialized {
215    internal: crate::shmalloc_impl::BlockSerialized,
216}
217
218unsafe impl Pod for ShMemBlockSerialized {}
219
220impl core::fmt::Display for ShMemBlockSerialized {
221    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
222        let s =
223            core::str::from_utf8(crate::util::trim_null_bytes(&self.internal.chunk_name).unwrap())
224                .unwrap();
225        write!(f, "{};{}", self.internal.offset, s)
226    }
227}
228
229impl core::str::FromStr for ShMemBlockSerialized {
230    type Err = anyhow::Error;
231
232    // Required method
233    fn from_str(s: &str) -> anyhow::Result<Self> {
234        use core::fmt::Write;
235        use formatting_nostd::FormatBuffer;
236
237        if let Some((offset_str, path_str)) = s.split_once(';') {
238            // let offset = offset_str.parse::<isize>().map_err(Err).unwrap();
239            let offset = offset_str
240                .parse::<isize>()
241                .map_err(Err::<(), core::num::ParseIntError>)
242                .unwrap();
243
244            let mut chunk_format = FormatBuffer::<{ crate::util::PATH_MAX_NBYTES }>::new();
245
246            write!(&mut chunk_format, "{}", &path_str).unwrap();
247
248            let mut chunk_name = crate::util::NULL_PATH_BUF;
249            chunk_name
250                .iter_mut()
251                .zip(chunk_format.as_str().as_bytes().iter())
252                .for_each(|(x, y)| *x = *y);
253
254            Ok(ShMemBlockSerialized {
255                internal: crate::shmalloc_impl::BlockSerialized { chunk_name, offset },
256            })
257        } else {
258            Err(anyhow::anyhow!("missing ;"))
259        }
260    }
261}
262
263/// Safe wrapper around our low-level, unsafe, nostd shared memory allocator.
264///
265/// This allocator type is not meant to be used directly, but can be accessed indirectly via calls
266/// made to `shmalloc()` and `shfree()`.
267pub struct SharedMemAllocator<'alloc> {
268    internal: crate::shmalloc_impl::FreelistAllocator,
269    nallocs: isize,
270    phantom: core::marker::PhantomData<&'alloc ()>,
271}
272
273impl<'alloc> SharedMemAllocator<'alloc> {
274    fn new() -> Self {
275        let mut internal = crate::shmalloc_impl::FreelistAllocator::new();
276        internal.init().unwrap();
277
278        Self {
279            internal,
280            nallocs: 0,
281            phantom: Default::default(),
282        }
283    }
284
285    // TODO(rwails): Fix the lifetime of the allocated block to match the allocator's lifetime.
286    fn alloc<T: Sync + VirtualAddressSpaceIndependent>(&mut self, val: T) -> ShMemBlock<'alloc, T> {
287        let t_nbytes: usize = core::mem::size_of::<T>();
288        let t_alignment: usize = core::mem::align_of::<T>();
289
290        let block = self.internal.alloc(t_nbytes, t_alignment);
291        unsafe {
292            (*block).get_mut_ref::<T>()[0] = val;
293        }
294
295        self.nallocs += 1;
296        ShMemBlock::<'alloc, T> {
297            block,
298            phantom: Default::default(),
299        }
300    }
301
302    fn free<T: Sync + VirtualAddressSpaceIndependent>(&mut self, mut block: ShMemBlock<'alloc, T>) {
303        self.nallocs -= 1;
304        block.block = core::ptr::null_mut();
305        self.internal.dealloc(block.block);
306    }
307
308    fn destruct(&mut self) {
309        // if self.nallocs != 0 {
310        //crate::shmalloc_impl::log_err(crate::shmalloc_impl::AllocError::Leak, None);
311
312        // TODO(rwails): This condition currently occurs when running Shadow. It's not actually
313        // a leak to worry about because the shared memory file backing store does get cleaned
314        // up. It's possible that all blocks are not dropped before this allocator is dropped.
315        // }
316
317        self.internal.destruct();
318    }
319}
320
321unsafe impl Send for SharedMemAllocator<'_> {}
322unsafe impl Sync for SharedMemAllocator<'_> {}
323
324// Experimental... implements the global allocator using the shared memory allocator.
325/*
326unsafe impl Sync for GlobalAllocator {}
327unsafe impl Send for GlobalAllocator {}
328
329struct GlobalAllocator {}
330
331unsafe impl core::alloc::GlobalAlloc for GlobalAllocator {
332    unsafe fn alloc(&self, layout: core::alloc::Layout) -> *mut u8 {
333        let block_p = SHMALLOC
334            .lock()
335            .internal
336            .alloc(layout.size(), layout.align());
337        let (p, _) = unsafe { (*block_p).get_mut_block_data_range() };
338        p
339    }
340
341    unsafe fn dealloc(&self, ptr: *mut u8, _layout: core::alloc::Layout) {
342        let block_p = crate::shmalloc_impl::rewind(ptr);
343        SHMALLOC.lock().internal.dealloc(block_p);
344    }
345}
346*/
347
348pub struct SharedMemDeserializer<'alloc> {
349    internal: crate::shmalloc_impl::FreelistDeserializer,
350    phantom: core::marker::PhantomData<&'alloc ()>,
351}
352
353impl<'alloc> SharedMemDeserializer<'alloc> {
354    fn new() -> Self {
355        let internal = crate::shmalloc_impl::FreelistDeserializer::new();
356
357        Self {
358            internal,
359            phantom: Default::default(),
360        }
361    }
362
363    /// # Safety
364    ///
365    /// This function can violate type safety if a template type is provided that does not match
366    /// original block that was serialized.
367    // TODO(rwails): Fix the lifetime of the allocated block to match the deserializer's lifetime.
368    pub unsafe fn deserialize<T>(
369        &mut self,
370        serialized: &ShMemBlockSerialized,
371    ) -> ShMemBlockAlias<'alloc, T>
372    where
373        T: Sync + VirtualAddressSpaceIndependent,
374    {
375        let block = self.internal.deserialize(&serialized.internal);
376
377        ShMemBlockAlias {
378            block,
379            phantom: Default::default(),
380        }
381    }
382}
383
384unsafe impl Send for SharedMemDeserializer<'_> {}
385unsafe impl Sync for SharedMemDeserializer<'_> {}
386
387#[cfg(test)]
388mod tests {
389    use super::*;
390    use rand::Rng;
391    use std::str::FromStr;
392    use std::string::ToString;
393    use std::sync::atomic::{AtomicI32, Ordering};
394
395    extern crate std;
396
397    #[test]
398    #[cfg_attr(miri, ignore)]
399    fn allocator_random_allocations() {
400        const NROUNDS: usize = 100;
401        let mut marked_blocks: std::vec::Vec<(u32, ShMemBlock<u32>)> = Default::default();
402        let mut rng = rand::rng();
403
404        let mut execute_round = || {
405            // Some allocations
406            for i in 0..255 {
407                let b = shmalloc(i);
408                marked_blocks.push((i, b));
409            }
410
411            // Generate some number of items to pop
412            let n1: u8 = rng.random();
413
414            for _ in 0..n1 {
415                let last_marked_block = marked_blocks.pop().unwrap();
416                assert_eq!(last_marked_block.0, *last_marked_block.1);
417                shfree(last_marked_block.1);
418            }
419
420            // Then check all blocks
421            for block in &marked_blocks {
422                assert_eq!(block.0, *block.1);
423            }
424        };
425
426        for _ in 0..NROUNDS {
427            execute_round();
428        }
429
430        while let Some(b) = marked_blocks.pop() {
431            shfree(b.1);
432        }
433    }
434
435    #[test]
436    #[cfg_attr(miri, ignore)]
437    fn round_trip_through_serializer() {
438        type T = i32;
439        let x: T = 42;
440
441        let original_block: ShMemBlock<T> = shmalloc(x);
442        {
443            let serialized_block = original_block.serialize();
444            let serialized_str = serialized_block.to_string();
445            let serialized_block = ShMemBlockSerialized::from_str(&serialized_str).unwrap();
446            let block = unsafe { shdeserialize::<i32>(&serialized_block) };
447            assert_eq!(*block, 42);
448        }
449
450        shfree(original_block);
451    }
452
453    #[test]
454    // Uses FFI
455    #[cfg_attr(miri, ignore)]
456    fn mutations() {
457        type T = AtomicI32;
458        let original_block = shmalloc(AtomicI32::new(0));
459
460        let serialized_block = original_block.serialize();
461
462        let deserialized_block = unsafe { shdeserialize::<T>(&serialized_block) };
463
464        assert_eq!(original_block.load(Ordering::SeqCst), 0);
465        assert_eq!(deserialized_block.load(Ordering::SeqCst), 0);
466
467        // Mutate through original
468        original_block.store(10, Ordering::SeqCst);
469        assert_eq!(original_block.load(Ordering::SeqCst), 10);
470        assert_eq!(deserialized_block.load(Ordering::SeqCst), 10);
471
472        // Mutate through deserialized
473        deserialized_block.store(20, Ordering::SeqCst);
474        assert_eq!(original_block.load(Ordering::SeqCst), 20);
475        assert_eq!(deserialized_block.load(Ordering::SeqCst), 20);
476
477        shfree(original_block);
478    }
479
480    // Validate our guarantee that the data pointer doesn't move, even if the block does.
481    // Host relies on this for soundness.
482    #[test]
483    // Uses FFI
484    #[cfg_attr(miri, ignore)]
485    fn shmemblock_stable_pointer() {
486        type T = u32;
487        let original_block: ShMemBlock<T> = shmalloc(0);
488
489        let block_addr = &original_block as *const ShMemBlock<T>;
490        let data_addr = *original_block as *const T;
491
492        // Use an `Option` to move the `ShMemBlock`. We have no guarantee here that it actually
493        // moves and that the compiler doesn't optimize the move away, so the before/after addresses
494        // are compared below.
495        let block = Some(original_block);
496
497        // Validate that the block itself actually moved.
498        let new_block_addr = block.as_ref().unwrap() as *const ShMemBlock<T>;
499        assert_ne!(block_addr, new_block_addr);
500
501        // Validate that the data referenced by the block *hasn't* moved.
502        let new_data_addr = **(block.as_ref().unwrap()) as *const T;
503        assert_eq!(data_addr, new_data_addr);
504
505        #[allow(clippy::unnecessary_literal_unwrap)]
506        shfree(block.unwrap());
507    }
508
509    // Validate our guarantee that the data pointer doesn't move, even if the block does.
510    #[test]
511    // Uses FFI
512    #[cfg_attr(miri, ignore)]
513    fn shmemblockremote_stable_pointer() {
514        type T = u32;
515        let alloced_block: ShMemBlock<T> = shmalloc(0);
516
517        let block = unsafe { shdeserialize::<T>(&alloced_block.serialize()) };
518
519        let block_addr = &block as *const ShMemBlockAlias<T>;
520        let data_addr = *block as *const T;
521
522        let block = Some(block);
523
524        // Validate that the block itself actually moved.
525        let new_block_addr = block.as_ref().unwrap() as *const ShMemBlockAlias<T>;
526        assert_ne!(block_addr, new_block_addr);
527
528        // Validate that the data referenced by the block *hasn't* moved.
529        let new_data_addr = **(block.as_ref().unwrap()) as *const T;
530        assert_eq!(data_addr, new_data_addr);
531
532        shfree(alloced_block);
533    }
534}