Skip to main content

shm_primitives/unix/
mmap.rs

1//! File-backed memory-mapped regions for cross-process shared memory.
2//!
3//! This module provides `MmapRegion`, a file-backed memory region that can be
4//! shared across processes using mmap with `MAP_SHARED`.
5
6use std::fs::{File, OpenOptions};
7use std::io;
8use std::os::unix::fs::PermissionsExt;
9use std::os::unix::io::{AsRawFd, FromRawFd, IntoRawFd, OwnedFd, RawFd};
10use std::path::{Path, PathBuf};
11
12use crate::Region;
13
14/// Cleanup behavior for memory-mapped files.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum FileCleanup {
17    /// Keep the file after all processes exit (manual cleanup required).
18    Manual,
19    /// Automatically delete the file when all processes exit.
20    /// On Unix: file is unlinked immediately (stays alive while mapped).
21    /// On Windows: file is opened with FILE_FLAG_DELETE_ON_CLOSE.
22    Auto,
23}
24
25/// File-backed memory-mapped region for cross-process shared memory.
26///
27/// r[impl shm.file]
28pub struct MmapRegion {
29    /// Pointer to the mapped memory
30    ptr: *mut u8,
31    /// Length of the mapping in bytes
32    len: usize,
33    /// The underlying file (kept open to maintain the mapping)
34    #[allow(dead_code)]
35    file: File,
36    /// Path to the file (for cleanup)
37    path: PathBuf,
38    /// Whether this region owns the file (should delete on drop)
39    owns_file: bool,
40}
41
42impl MmapRegion {
43    /// Create a new file-backed region.
44    ///
45    /// This creates the file, truncates it to the given size, and maps it
46    /// into memory with `MAP_SHARED`. The file is created with permissions 0666.
47    ///
48    /// r[impl shm.file.create]
49    /// r[impl shm.file.permissions]
50    pub fn create(path: &Path, size: usize, cleanup: FileCleanup) -> io::Result<Self> {
51        if size == 0 {
52            return Err(io::Error::new(
53                io::ErrorKind::InvalidInput,
54                "size must be > 0",
55            ));
56        }
57
58        // 1. Open or create file with read/write, truncate
59        let file = OpenOptions::new()
60            .read(true)
61            .write(true)
62            .create(true)
63            .truncate(true)
64            .open(path)
65            .map_err(|e| {
66                let msg = std::format!("Failed to create SHM file at {}: {}", path.display(), e);
67                io::Error::new(e.kind(), msg)
68            })?;
69
70        // 2. Set permissions to 0666.
71        // On macOS FS extensions, host and extension may run under different
72        // effective identities; owner-only mode can cause EPERM at attach time.
73        file.set_permissions(std::fs::Permissions::from_mode(0o666))?;
74
75        // 3. Truncate to desired size
76        file.set_len(size as u64)?;
77
78        // 4. mmap with MAP_SHARED
79        let ptr = unsafe {
80            libc::mmap(
81                std::ptr::null_mut(),
82                size,
83                libc::PROT_READ | libc::PROT_WRITE,
84                libc::MAP_SHARED,
85                file.as_raw_fd(),
86                0,
87            )
88        };
89
90        if ptr == libc::MAP_FAILED {
91            return Err(io::Error::last_os_error());
92        }
93
94        let path_buf = path.to_path_buf();
95
96        // Immediately unlink the file if auto cleanup is requested.
97        // The file stays alive while mapped and is cleaned up by the OS when all
98        // processes die (even from SIGKILL/crash/power loss).
99        if cleanup == FileCleanup::Auto {
100            std::fs::remove_file(&path_buf)?;
101        }
102
103        Ok(Self {
104            ptr: ptr as *mut u8,
105            len: size,
106            file,
107            path: path_buf,
108            owns_file: cleanup == FileCleanup::Manual,
109        })
110    }
111
112    /// Attach to an existing file-backed region.
113    ///
114    /// This opens the file and maps it into memory with `MAP_SHARED`.
115    /// The file size determines the mapping size.
116    ///
117    /// r[impl shm.file.attach]
118    pub fn attach(path: &Path) -> io::Result<Self> {
119        // Open existing file for read/write
120        let file = OpenOptions::new()
121            .read(true)
122            .write(true)
123            .open(path)
124            .map_err(|e| {
125                let msg = std::format!("Failed to open SHM file at {}: {}", path.display(), e);
126                io::Error::new(e.kind(), msg)
127            })?;
128
129        // Get file size
130        let metadata = file.metadata()?;
131        let size = metadata.len() as usize;
132
133        if size == 0 {
134            return Err(io::Error::new(
135                io::ErrorKind::InvalidData,
136                "segment file is empty",
137            ));
138        }
139
140        // mmap with MAP_SHARED
141        let ptr = unsafe {
142            libc::mmap(
143                std::ptr::null_mut(),
144                size,
145                libc::PROT_READ | libc::PROT_WRITE,
146                libc::MAP_SHARED,
147                file.as_raw_fd(),
148                0,
149            )
150        };
151
152        if ptr == libc::MAP_FAILED {
153            return Err(io::Error::last_os_error());
154        }
155
156        Ok(Self {
157            ptr: ptr as *mut u8,
158            len: size,
159            file,
160            path: path.to_path_buf(),
161            owns_file: false, // Attached regions don't own the file
162        })
163    }
164
165    /// Attach to a memory-mapped region from a file descriptor.
166    ///
167    /// This is used on the receiver side after receiving an fd via SCM_RIGHTS.
168    /// The fd is mmap'd with MAP_SHARED at the given size.
169    pub fn attach_fd(fd: OwnedFd, size: usize) -> io::Result<Self> {
170        if size == 0 {
171            return Err(io::Error::new(
172                io::ErrorKind::InvalidInput,
173                "size must be > 0",
174            ));
175        }
176
177        let raw_fd = fd.as_raw_fd();
178        let ptr = unsafe {
179            libc::mmap(
180                std::ptr::null_mut(),
181                size,
182                libc::PROT_READ | libc::PROT_WRITE,
183                libc::MAP_SHARED,
184                raw_fd,
185                0,
186            )
187        };
188
189        if ptr == libc::MAP_FAILED {
190            return Err(io::Error::last_os_error());
191        }
192
193        // Convert OwnedFd to File so we keep it alive for the mapping's lifetime
194        let file = unsafe { File::from_raw_fd(fd.into_raw_fd()) };
195
196        Ok(Self {
197            ptr: ptr as *mut u8,
198            len: size,
199            file,
200            path: PathBuf::new(),
201            owns_file: false,
202        })
203    }
204
205    /// Get the raw file descriptor of the backing file.
206    pub fn as_raw_fd(&self) -> RawFd {
207        self.file.as_raw_fd()
208    }
209
210    /// Get a `Region` view of this mmap.
211    #[inline]
212    pub fn region(&self) -> Region {
213        // SAFETY: The mmap is valid for the lifetime of MmapRegion
214        unsafe { Region::from_raw(self.ptr, self.len) }
215    }
216
217    /// Get the size of the region in bytes.
218    #[inline]
219    pub fn len(&self) -> usize {
220        self.len
221    }
222
223    /// Returns true if the region is empty (zero bytes).
224    #[inline]
225    pub fn is_empty(&self) -> bool {
226        self.len == 0
227    }
228
229    /// Get the path to the backing file.
230    #[inline]
231    pub fn path(&self) -> &Path {
232        &self.path
233    }
234
235    /// Take ownership of the file for cleanup purposes.
236    ///
237    /// After calling this, the file will be deleted when this region is dropped.
238    pub fn take_ownership(&mut self) {
239        self.owns_file = true;
240    }
241
242    /// Release ownership of the file.
243    ///
244    /// After calling this, the file will NOT be deleted when this region is dropped.
245    pub fn release_ownership(&mut self) {
246        self.owns_file = false;
247    }
248
249    /// Resize the region by growing the backing file and remapping.
250    ///
251    /// This is typically a host-only operation. The base pointer may change,
252    /// so callers must update any cached `Region` references after calling this.
253    ///
254    /// # Errors
255    ///
256    /// Returns an error if the new size is smaller than current size (shrinking
257    /// is not supported), or if the underlying file/mmap operations fail.
258    ///
259    /// r[impl shm.varslot.extents]
260    pub fn resize(&mut self, new_size: usize) -> io::Result<()> {
261        if new_size < self.len {
262            return Err(io::Error::new(
263                io::ErrorKind::InvalidInput,
264                "shrinking is not supported",
265            ));
266        }
267        if new_size == self.len {
268            return Ok(()); // No change needed
269        }
270
271        // 1. Grow the backing file
272        self.file.set_len(new_size as u64)?;
273
274        // 2. Map new region before tearing down the old one, so that a
275        //    failure here leaves self in a valid state.
276        let new_ptr = unsafe {
277            libc::mmap(
278                std::ptr::null_mut(),
279                new_size,
280                libc::PROT_READ | libc::PROT_WRITE,
281                libc::MAP_SHARED,
282                self.file.as_raw_fd(),
283                0,
284            )
285        };
286
287        if new_ptr == libc::MAP_FAILED {
288            return Err(io::Error::last_os_error());
289        }
290
291        // 3. New mapping is live — now it is safe to release the old one.
292        unsafe { libc::munmap(self.ptr as *mut libc::c_void, self.len) };
293
294        self.ptr = new_ptr as *mut u8;
295        self.len = new_size;
296        Ok(())
297    }
298
299    /// Check if the backing file has grown and remap if needed.
300    ///
301    /// This is useful for guests to detect when the host has grown the segment.
302    /// Returns `true` if the region was remapped, `false` if no change.
303    ///
304    /// # Errors
305    ///
306    /// Returns an error if file metadata cannot be read or remapping fails.
307    pub fn check_and_remap(&mut self) -> io::Result<bool> {
308        let file_size = self.file.metadata()?.len() as usize;
309        if file_size > self.len {
310            self.resize(file_size)?;
311            Ok(true)
312        } else {
313            Ok(false)
314        }
315    }
316}
317
318impl Drop for MmapRegion {
319    fn drop(&mut self) {
320        // Unmap the memory
321        unsafe {
322            libc::munmap(self.ptr as *mut libc::c_void, self.len);
323        }
324
325        // Delete the file if we own it
326        // r[impl shm.file.cleanup]
327        if self.owns_file {
328            let _ = std::fs::remove_file(&self.path);
329        }
330    }
331}
332
333// SAFETY: The mmap region is valid for the lifetime of MmapRegion and can be
334// safely accessed from multiple threads (the underlying memory is shared).
335unsafe impl Send for MmapRegion {}
336unsafe impl Sync for MmapRegion {}
337
338#[cfg(test)]
339mod tests {
340    use super::*;
341
342    #[test]
343    fn test_create_and_attach() {
344        let dir = tempfile::tempdir().unwrap();
345        let path = dir.path().join("test.shm");
346
347        // Create region
348        let region1 = MmapRegion::create(&path, 4096, FileCleanup::Manual).unwrap();
349        assert_eq!(region1.len(), 4096);
350        assert!(path.exists());
351
352        // Write some data
353        let data = region1.region();
354        unsafe {
355            std::ptr::write(data.as_ptr(), 0x42);
356            std::ptr::write(data.as_ptr().add(1), 0x43);
357        }
358
359        // Attach from another "process" (same process, different mapping)
360        let region2 = MmapRegion::attach(&path).unwrap();
361        assert_eq!(region2.len(), 4096);
362
363        // Verify data is visible
364        let data2 = region2.region();
365        unsafe {
366            assert_eq!(std::ptr::read(data2.as_ptr()), 0x42);
367            assert_eq!(std::ptr::read(data2.as_ptr().add(1)), 0x43);
368        }
369    }
370
371    #[test]
372    fn test_cleanup_on_drop() {
373        let dir = tempfile::tempdir().unwrap();
374        let path = dir.path().join("cleanup.shm");
375
376        {
377            let _region = MmapRegion::create(&path, 1024, FileCleanup::Manual).unwrap();
378            assert!(path.exists());
379        }
380
381        // File should be deleted after owner drops
382        assert!(!path.exists());
383    }
384
385    #[test]
386    fn test_attached_does_not_cleanup() {
387        let dir = tempfile::tempdir().unwrap();
388        let path = dir.path().join("attached.shm");
389
390        let owner = MmapRegion::create(&path, 1024, FileCleanup::Manual).unwrap();
391
392        {
393            let _attached = MmapRegion::attach(&path).unwrap();
394            assert!(path.exists());
395        }
396
397        // File should still exist after attached drops
398        assert!(path.exists());
399
400        // File should be deleted after owner drops
401        drop(owner);
402        assert!(!path.exists());
403    }
404
405    #[test]
406    fn test_shared_writes() {
407        let dir = tempfile::tempdir().unwrap();
408        let path = dir.path().join("shared.shm");
409
410        let region1 = MmapRegion::create(&path, 4096, FileCleanup::Manual).unwrap();
411        let region2 = MmapRegion::attach(&path).unwrap();
412
413        // Write from region2
414        let data2 = region2.region();
415        unsafe {
416            std::ptr::write(data2.as_ptr().add(100), 0xAB);
417        }
418
419        // Read from region1
420        let data1 = region1.region();
421        unsafe {
422            assert_eq!(std::ptr::read(data1.as_ptr().add(100)), 0xAB);
423        }
424    }
425
426    #[test]
427    fn test_permissions() {
428        let dir = tempfile::tempdir().unwrap();
429        let path = dir.path().join("perms.shm");
430
431        let _region = MmapRegion::create(&path, 1024, FileCleanup::Manual).unwrap();
432
433        let metadata = std::fs::metadata(&path).unwrap();
434        let mode = metadata.permissions().mode() & 0o777;
435        assert_eq!(mode, 0o666);
436    }
437
438    #[test]
439    fn test_zero_size_rejected() {
440        let dir = tempfile::tempdir().unwrap();
441        let path = dir.path().join("zero.shm");
442
443        let result = MmapRegion::create(&path, 0, FileCleanup::Manual);
444        assert!(result.is_err());
445    }
446
447    #[test]
448    fn test_resize_grows_region() {
449        let dir = tempfile::tempdir().unwrap();
450        let path = dir.path().join("resize.shm");
451
452        let mut region = MmapRegion::create(&path, 4096, FileCleanup::Manual).unwrap();
453        assert_eq!(region.len(), 4096);
454
455        // Write data at the start
456        unsafe {
457            std::ptr::write(region.region().as_ptr(), 0xAB);
458        }
459
460        // Resize to 8192
461        region.resize(8192).unwrap();
462        assert_eq!(region.len(), 8192);
463
464        // Original data should still be accessible
465        unsafe {
466            assert_eq!(std::ptr::read(region.region().as_ptr()), 0xAB);
467        }
468
469        // Can write to new area
470        unsafe {
471            std::ptr::write(region.region().as_ptr().add(5000), 0xCD);
472            assert_eq!(std::ptr::read(region.region().as_ptr().add(5000)), 0xCD);
473        }
474    }
475
476    #[test]
477    fn test_resize_shrink_rejected() {
478        let dir = tempfile::tempdir().unwrap();
479        let path = dir.path().join("shrink.shm");
480
481        let mut region = MmapRegion::create(&path, 8192, FileCleanup::Manual).unwrap();
482        let result = region.resize(4096);
483        assert!(result.is_err());
484    }
485
486    #[test]
487    fn test_check_and_remap() {
488        let dir = tempfile::tempdir().unwrap();
489        let path = dir.path().join("remap.shm");
490
491        // Create owner region
492        let mut owner = MmapRegion::create(&path, 4096, FileCleanup::Manual).unwrap();
493
494        // Attach guest
495        let mut guest = MmapRegion::attach(&path).unwrap();
496        assert_eq!(guest.len(), 4096);
497
498        // Owner grows the file
499        owner.resize(8192).unwrap();
500
501        // Guest detects and remaps
502        let remapped = guest.check_and_remap().unwrap();
503        assert!(remapped);
504        assert_eq!(guest.len(), 8192);
505
506        // Second check should return false (no change)
507        let remapped2 = guest.check_and_remap().unwrap();
508        assert!(!remapped2);
509    }
510
511    #[test]
512    fn test_resize_preserves_shared_data() {
513        let dir = tempfile::tempdir().unwrap();
514        let path = dir.path().join("shared_resize.shm");
515
516        let mut owner = MmapRegion::create(&path, 4096, FileCleanup::Manual).unwrap();
517        let mut guest = MmapRegion::attach(&path).unwrap();
518
519        // Write from owner
520        unsafe {
521            std::ptr::write(owner.region().as_ptr().add(100), 0x42);
522        }
523
524        // Verify guest sees it
525        unsafe {
526            assert_eq!(std::ptr::read(guest.region().as_ptr().add(100)), 0x42);
527        }
528
529        // Owner resizes
530        owner.resize(8192).unwrap();
531
532        // Guest remaps
533        guest.check_and_remap().unwrap();
534
535        // Data should still be visible
536        unsafe {
537            assert_eq!(std::ptr::read(guest.region().as_ptr().add(100)), 0x42);
538        }
539
540        // Owner writes to new area
541        unsafe {
542            std::ptr::write(owner.region().as_ptr().add(5000), 0x99);
543        }
544
545        // Guest should see it
546        unsafe {
547            assert_eq!(std::ptr::read(guest.region().as_ptr().add(5000)), 0x99);
548        }
549    }
550}