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}