Skip to main content

moirai_core/ipc/
memory.rs

1//! OS shared-memory segments: POSIX `shm_open`/`mmap` and Win32 file mappings.
2//!
3//! [`SharedMemory`] owns one mapping of `size` bytes at `ptr`, valid until
4//! `Drop` unmaps it. It is the raw substrate under
5//! [`SharedQueue`](super::SharedQueue); the two contracts below are what make
6//! its safe API sound.
7//!
8//! # Every mapped byte must be backed
9//!
10//! `as_slice`/`as_mut_slice` build a slice of exactly `size` bytes, so all of
11//! them have to be backed. Two independent things can leave the mapping short,
12//! and either one raises `SIGBUS` on first touch inside those safe accessors.
13//!
14//! ## The object must be at least `size` bytes
15//!
16//! The two platforms differ in who enforces that, and the difference is why
17//! `open` is written the way it is:
18//!
19//! - **Windows** enforces it. `MapViewOfFile` requires the requested view to lie
20//!   within the mapping object, and fails otherwise, so an oversized `open` is
21//!   rejected by the OS.
22//! - **POSIX does not.** `mmap` accepts a length running past the end of the
23//!   object; the pages beyond it simply are not backed. `open` therefore
24//!   `fstat`s the descriptor and rejects a segment smaller than `size` itself —
25//!   without that check a caller could open an existing segment under a
26//!   too-large `size` and get a slice that faults on read. `create` needs no
27//!   such check because its `ftruncate` sets the object to exactly `size`.
28//!
29//! The POSIX check reads the size from the descriptor it goes on to map, so it
30//! cannot be defeated by re-resolving the name. It does not cover a process that
31//! *shrinks* the object with `ftruncate` after the check — an act that already
32//! invalidates every existing mapping of that segment, and which POSIX gives no
33//! way to exclude.
34//!
35//! ## The object's pages must exist
36//!
37//! A correctly sized object is not automatically a backed one. Linux puts POSIX
38//! shared memory on tmpfs, where `ftruncate` sets the length and nothing else:
39//! the pages stay sparse and are allocated on first touch. A segment can pass
40//! every size check above and still fault, when tmpfs — or the RAM and swap
41//! behind it — cannot produce a page at the moment one is written.
42//!
43//! `create` closes that by asking for the store up front with `posix_fallocate`,
44//! so a segment too large to back is refused at creation with the kernel's
45//! `ENOSPC` rather than killing whichever process later writes it. That is a
46//! Linux guarantee rather than a POSIX one; `reserve_backing_store` documents
47//! what the other Unix targets do and do not get. `open` needs no counterpart,
48//! since it maps an object whose creator already reserved it.
49//!
50//! # Names are created exclusively
51//!
52//! `create` fails with [`IpcError::AlreadyExists`] when the name is taken, on
53//! both platforms. A second `create` would otherwise `ftruncate` a live POSIX
54//! object to its own size and shrink every mapping already open on it, and a
55//! Win32 create would silently attach to the existing object at *its* size.
56//! `open` is the only way to reach an existing name.
57//!
58//! # Cross-process aliasing is the caller's contract
59//!
60//! A segment is shared by construction: another process holding the same name
61//! may write it at any time. `&[u8]` and `&mut [u8]` promise Rust that no such
62//! concurrent write happens, and no OS primitive here can enforce that. Two
63//! handles in one process are the same hazard: they map the same pages through
64//! addresses the borrow checker cannot relate. The accessors are therefore
65//! `unsafe fn`s whose caller must uphold the contract — be the only party
66//! touching the bytes for the borrow, or coordinate externally.
67//! [`SharedQueue`](super::SharedQueue) is the coordinated wrapper: it never hands
68//! out a slice, reaching the bytes through raw pointers with the atomic head/tail
69//! protocol in its metadata header instead.
70//!
71//! Ownership is separate from mapping: `owner` records who created the segment,
72//! so only the creator `shm_unlink`s the name on drop. Every handle unmaps its
73//! own view and closes its own descriptor regardless.
74
75use super::error::{IpcError, last_os_error};
76use core::slice;
77
78#[cfg(unix)]
79use super::backing_store::reserve_backing_store;
80#[cfg(unix)]
81use std::os::unix::io::RawFd;
82
83/// Raw Win32 file-mapping bindings
84#[cfg(windows)]
85mod win {
86    pub const PAGE_READWRITE: u32 = 0x04;
87    pub const FILE_MAP_ALL_ACCESS: u32 = 0x000F_001F;
88    pub const INVALID_HANDLE_VALUE: usize = usize::MAX;
89    pub const ERROR_ALREADY_EXISTS: i32 = 183;
90
91    unsafe extern "system" {
92        pub fn CreateFileMappingW(
93            file: usize,
94            attributes: *mut core::ffi::c_void,
95            protect: u32,
96            size_high: u32,
97            size_low: u32,
98            name: *const u16,
99        ) -> usize;
100        pub fn OpenFileMappingW(desired_access: u32, inherit: i32, name: *const u16) -> usize;
101        pub fn MapViewOfFile(
102            mapping: usize,
103            desired_access: u32,
104            offset_high: u32,
105            offset_low: u32,
106            size: usize,
107        ) -> *mut core::ffi::c_void;
108        pub fn UnmapViewOfFile(address: *const core::ffi::c_void) -> i32;
109        pub fn CloseHandle(handle: usize) -> i32;
110        pub fn SetLastError(code: u32);
111    }
112
113    pub fn wide_name(name: &str) -> Vec<u16> {
114        name.trim_start_matches('/')
115            .encode_utf16()
116            .chain(core::iter::once(0))
117            .collect()
118    }
119}
120
121/// Shared memory segment for zero-copy IPC
122pub struct SharedMemory {
123    /// Memory-mapped region
124    pub(crate) ptr: *mut u8,
125    /// Size of the shared memory
126    pub(crate) size: usize,
127    /// File descriptor (Unix) or handle (Windows)
128    #[cfg(unix)]
129    fd: RawFd,
130    #[cfg(windows)]
131    handle: usize,
132    /// Whether this instance owns the memory
133    #[cfg_attr(windows, allow(dead_code))]
134    owner: bool,
135    /// Name of the segment (Unix only, to allow `shm_unlink` on drop)
136    #[cfg(unix)]
137    name: Option<std::ffi::CString>,
138}
139
140#[cfg(unix)]
141fn unix_mapping_length(size: usize) -> Result<libc::off_t, IpcError> {
142    if size == 0 {
143        return Err(IpcError::InvalidArgument);
144    }
145
146    libc::off_t::try_from(size).map_err(|_| IpcError::InvalidArgument)
147}
148
149// SAFETY: the mapping is process-wide, not thread-owned — `ptr` stays valid on
150// any thread for the lifetime of this handle, and neither the descriptor nor the
151// handle is thread-affine. `Send` therefore moves a still-valid mapping.
152unsafe impl Send for SharedMemory {}
153
154// SAFETY: `&SharedMemory` reaches the bytes only through the `unsafe fn`
155// `as_slice`, whose caller vouches that no other handle writes the segment, so
156// sharing the handle across threads adds no access the caller has not already
157// taken responsibility for. `SharedQueue` reads and writes through raw pointers
158// under its own atomic protocol.
159unsafe impl Sync for SharedMemory {}
160
161impl SharedMemory {
162    /// Create a new shared memory segment.
163    ///
164    /// Sizes the object with `ftruncate` and then reserves its backing store, so
165    /// a segment larger than the store can hold fails here — as the kernel's
166    /// `ENOSPC` — instead of raising `SIGBUS` in whichever process first writes
167    /// an unbacked page. `reserve_backing_store` covers the Unix targets that
168    /// cannot make that reservation.
169    #[cfg(unix)]
170    pub fn create(name: &str, size: usize) -> Result<Self, IpcError> {
171        use std::ffi::CString;
172
173        let mapping_length = unix_mapping_length(size)?;
174        let c_name = CString::new(name).map_err(|_| IpcError::InvalidArgument)?;
175
176        // SAFETY: `c_name` is NUL-terminated, `mapping_length` is positive and
177        // representable as `off_t`, and every failed descriptor/mapping path is
178        // closed before returning. The successful mapping owns `size` bytes
179        // until `Drop` unmaps it.
180        unsafe {
181            use std::ptr::null_mut;
182            let fd = libc::shm_open(
183                c_name.as_ptr(),
184                libc::O_CREAT | libc::O_EXCL | libc::O_RDWR,
185                0o666,
186            );
187
188            if fd < 0 {
189                return Err(match last_os_error() {
190                    IpcError::SystemError(libc::EEXIST) => IpcError::AlreadyExists,
191                    other => other,
192                });
193            }
194
195            if libc::ftruncate(fd, mapping_length) < 0 {
196                libc::close(fd);
197                return Err(last_os_error());
198            }
199
200            if let Err(error) = reserve_backing_store(fd, mapping_length) {
201                // Unlike the paths around it, this one has to take the name down
202                // with the descriptor. What it would otherwise leave behind is
203                // precisely the trap the reservation exists to remove: an object
204                // of exactly `size` bytes, which `open`'s `fstat` check waves
205                // through, over pages the store was just proven unable to hold.
206                libc::close(fd);
207                libc::shm_unlink(c_name.as_ptr());
208                return Err(error);
209            }
210
211            let ptr = libc::mmap(
212                null_mut(),
213                size,
214                libc::PROT_READ | libc::PROT_WRITE,
215                libc::MAP_SHARED,
216                fd,
217                0,
218            );
219
220            if ptr == libc::MAP_FAILED {
221                libc::close(fd);
222                return Err(last_os_error());
223            }
224
225            Ok(Self {
226                ptr: ptr as *mut u8,
227                size,
228                fd,
229                owner: true,
230                name: Some(c_name),
231            })
232        }
233    }
234
235    /// Open an existing shared memory segment.
236    ///
237    /// Fails with [`IpcError::InvalidArgument`] if the segment is smaller than
238    /// `size`. `mmap` accepts a length past the end of the object, but the pages
239    /// beyond it are not backed: reading them raises `SIGBUS`, so an unchecked
240    /// mapping would hand out an `as_slice` that faults instead of reading.
241    #[cfg(unix)]
242    pub fn open(name: &str, size: usize) -> Result<Self, IpcError> {
243        use std::ffi::CString;
244
245        let mapping_length = unix_mapping_length(size)?;
246        let c_name = CString::new(name).map_err(|_| IpcError::InvalidArgument)?;
247
248        // SAFETY: `c_name` is NUL-terminated and `mapping_length` is positive and
249        // representable as `off_t`. The descriptor is closed on every failure
250        // path, and the mapping is only kept once `fstat` proves the object
251        // covers all `size` bytes.
252        unsafe {
253            use std::ptr::null_mut;
254            let fd = libc::shm_open(c_name.as_ptr(), libc::O_RDWR, 0);
255
256            if fd < 0 {
257                return Err(last_os_error());
258            }
259
260            let mut segment = core::mem::MaybeUninit::<libc::stat>::uninit();
261            if libc::fstat(fd, segment.as_mut_ptr()) < 0 {
262                let error = last_os_error();
263                libc::close(fd);
264                return Err(error);
265            }
266
267            // `fstat` succeeded, so the OS initialized the struct.
268            if segment.assume_init().st_size < mapping_length {
269                libc::close(fd);
270                return Err(IpcError::InvalidArgument);
271            }
272
273            let ptr = libc::mmap(
274                null_mut(),
275                size,
276                libc::PROT_READ | libc::PROT_WRITE,
277                libc::MAP_SHARED,
278                fd,
279                0,
280            );
281
282            if ptr == libc::MAP_FAILED {
283                libc::close(fd);
284                return Err(last_os_error());
285            }
286
287            Ok(Self {
288                ptr: ptr as *mut u8,
289                size,
290                fd,
291                owner: false,
292                name: None,
293            })
294        }
295    }
296
297    /// Create a new shared memory segment
298    #[cfg(windows)]
299    pub fn create(name: &str, size: usize) -> Result<Self, IpcError> {
300        if size == 0 {
301            return Err(IpcError::InvalidArgument);
302        }
303        let wide = win::wide_name(name);
304
305        // justification: Win32 `CreateFileMappingW` takes the mapping size as a
306        // (high DWORD, low DWORD) pair. `size_high` carries the top 32 bits and
307        // `size_low` the bottom 32; the `as u32` truncation on `size_low` is the
308        // API contract, not a lossy conversion.
309        #[allow(clippy::cast_possible_truncation)]
310        let size_low = size as u32;
311        let size_high = (size as u64 >> 32) as u32;
312        // SAFETY: `wide` is a NUL-terminated UTF-16 name that outlives the call,
313        // and the size pair describes `size` bytes. The handle is closed if the
314        // view fails to map, so no failure path leaks it.
315        unsafe {
316            // Clear the last-error slot so a stale `ERROR_ALREADY_EXISTS` from an
317            // earlier call cannot be mistaken for this one's answer.
318            win::SetLastError(0);
319            let handle = win::CreateFileMappingW(
320                win::INVALID_HANDLE_VALUE,
321                core::ptr::null_mut(),
322                win::PAGE_READWRITE,
323                size_high,
324                size_low,
325                wide.as_ptr(),
326            );
327            if handle == 0 {
328                return Err(last_os_error());
329            }
330
331            // `CreateFileMappingW` succeeds on an existing name and hands back
332            // the live object at its own size, reporting the fact only through
333            // the last-error code. Read it before any other call resets it.
334            if matches!(
335                last_os_error(),
336                IpcError::SystemError(win::ERROR_ALREADY_EXISTS)
337            ) {
338                win::CloseHandle(handle);
339                return Err(IpcError::AlreadyExists);
340            }
341
342            let ptr = win::MapViewOfFile(handle, win::FILE_MAP_ALL_ACCESS, 0, 0, size);
343            if ptr.is_null() {
344                let error = last_os_error();
345                win::CloseHandle(handle);
346                return Err(error);
347            }
348
349            Ok(Self {
350                ptr: ptr as *mut u8,
351                size,
352                handle,
353                owner: true,
354            })
355        }
356    }
357
358    /// Open an existing shared memory segment
359    #[cfg(windows)]
360    pub fn open(name: &str, size: usize) -> Result<Self, IpcError> {
361        if size == 0 {
362            return Err(IpcError::InvalidArgument);
363        }
364        let wide = win::wide_name(name);
365
366        // SAFETY: `wide` is a NUL-terminated UTF-16 name that outlives the call.
367        // `MapViewOfFile` rejects a view larger than the mapping object, so a
368        // successful return proves all `size` bytes are backed; the handle is
369        // closed on the failure path.
370        unsafe {
371            let handle = win::OpenFileMappingW(win::FILE_MAP_ALL_ACCESS, 0, wide.as_ptr());
372            if handle == 0 {
373                return Err(last_os_error());
374            }
375
376            let ptr = win::MapViewOfFile(handle, win::FILE_MAP_ALL_ACCESS, 0, 0, size);
377            if ptr.is_null() {
378                let error = last_os_error();
379                win::CloseHandle(handle);
380                return Err(error);
381            }
382
383            Ok(Self {
384                ptr: ptr as *mut u8,
385                size,
386                handle,
387                owner: false,
388            })
389        }
390    }
391
392    /// Get a slice of the shared memory.
393    ///
394    /// # Safety
395    ///
396    /// For as long as the returned slice lives, no other handle to this segment
397    /// -- in this process or another -- may write it. A second handle maps the
398    /// same pages through an address the borrow checker cannot connect to this
399    /// one, so the exclusion is the caller's to arrange (see the module docs).
400    /// Use [`SharedQueue`](super::SharedQueue) when another party is an active
401    /// writer.
402    pub unsafe fn as_slice(&self) -> &[u8] {
403        // SAFETY: `ptr` is a live mapping of `size` bytes — `create` sizes the
404        // object with `ftruncate` and reserves its backing store, `open` rejects
405        // a segment smaller than `size` — so every byte is readable, and `u8`
406        // needs no alignment beyond the page-aligned base. The residual backing
407        // hazard on Unix targets that cannot preallocate, and the absence of a
408        // concurrent cross-process writer, are the module docs' two contracts.
409        unsafe { slice::from_raw_parts(self.ptr, self.size) }
410    }
411
412    /// Get a mutable slice of the shared memory.
413    ///
414    /// # Safety
415    ///
416    /// For as long as the returned slice lives, no other handle to this segment
417    /// -- in this process or another -- may read or write it (see
418    /// [`as_slice`](Self::as_slice) and the module docs).
419    pub unsafe fn as_mut_slice(&mut self) -> &mut [u8] {
420        // SAFETY: as `as_slice`, and `&mut self` excludes any other in-process
421        // borrow of the same mapping for the lifetime of the returned slice.
422        unsafe { slice::from_raw_parts_mut(self.ptr, self.size) }
423    }
424}
425
426impl Drop for SharedMemory {
427    fn drop(&mut self) {
428        // SAFETY: `&mut self` in `drop` is exclusive, and `ptr`/`size` are the
429        // exact base and length this handle mapped, so `munmap` releases its own
430        // view and nothing else. The name is unlinked only by the creator, so a
431        // handle from `open` never removes a segment others still use.
432        #[cfg(unix)]
433        unsafe {
434            libc::munmap(self.ptr as *mut libc::c_void, self.size);
435            libc::close(self.fd);
436            if self.owner
437                && let Some(ref name) = self.name
438            {
439                libc::shm_unlink(name.as_ptr());
440            }
441        }
442        // SAFETY: `&mut self` in `drop` is exclusive; `ptr` is the base this
443        // handle received from `MapViewOfFile` and `handle` the mapping it came
444        // from, so each is released exactly once. The mapping object outlives
445        // this close while any other process still holds it open.
446        #[cfg(windows)]
447        unsafe {
448            win::UnmapViewOfFile(self.ptr as *const core::ffi::c_void);
449            win::CloseHandle(self.handle);
450        }
451    }
452}