Skip to main content

orbit_core/
shm.rs

1//! POSIX shared-memory helpers — V1 substrate for cross-process rings.
2//!
3//! Wraps `shm_open` / `ftruncate` / `mmap` / `munmap` / `shm_unlink`
4//! into a small, RAII-friendly API. Unix-only; Windows support is
5//! a separate concern (Win32 named file mapping) that can land later.
6//!
7//! ## Naming
8//!
9//! Segments are named `/orbit-{fleet}-{kind}-{uid}` — fleet name from
10//! the embedder, KIND from `OrbitTyped::KIND`, UID from `geteuid()`.
11//! UID-scoping avoids the `/dev/shm` sticky-bit cross-user collision
12//! problem (a stale segment owned by one user blocks another from
13//! `shm_unlink`-ing it on next boot).
14//!
15//! Rings that require a process-recoverable writer lock also open a
16//! companion `orbit-{fleet}-{kind}-{uid}.lock` file. It carries no
17//! ring data or state; it only supplies a regular-file inode for `flock`,
18//! because advisory locking on a POSIX SHM descriptor is not uniformly
19//! supported across the Unix targets Orbit serves. An unlocked stale
20//! companion file is safe to reuse.
21//!
22//! Those files live in a per-uid directory — `$XDG_RUNTIME_DIR/orbit-{uid}`
23//! where the session provides one, `/tmp/orbit-{uid}` otherwise — created
24//! `0700` and checked on every lock. They were once in `/tmp` directly, which
25//! made them squattable: see [`lock_dir`].
26//!
27//! ## Lifetime
28//!
29//! [`ShmRegion`] owns the mapped pointer and unmaps on drop. It does
30//! NOT `shm_unlink` on drop — the segment lives until an explicit
31//! [`ShmRegion::unlink`] call. This matches POSIX convention: a
32//! segment with mapped users is not removed; `shm_unlink` only
33//! prevents *new* opens, the current mapping stays valid until the
34//! last process unmaps.
35
36#![cfg(unix)]
37
38use std::ffi::CString;
39use std::fs::OpenOptions;
40use std::io;
41use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
42use std::os::unix::fs::OpenOptionsExt;
43use std::path::{Path, PathBuf};
44use std::ptr::NonNull;
45use std::sync::atomic::{AtomicBool, Ordering};
46
47/// Namespace used by Orbit POSIX shared-memory objects.
48pub const SHM_NAMESPACE: &str = "orbit";
49
50/// Maximum POSIX access allowed for one SHM object, independent of its layout.
51///
52/// New objects are owned by the effective uid. On macOS, the selected group
53/// must be the creator's effective gid and the requested mode is passed directly
54/// to `shm_open` (with the platform's native creation-mask semantics).
55/// Other Unix targets create privately, then set the requested group and exact
56/// group-sharing mode. `OwnerOnly` uses the existing `shm_open(..., 0600)` path.
57/// Orbit never changes `umask` or process credentials, or chmods/chowns an
58/// existing object. This does not
59/// isolate processes running under the same uid.
60#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
61pub enum ShmAccessPolicy {
62    /// Owner read/write, no group or other access (`0600`).
63    #[default]
64    OwnerOnly,
65    /// Owner read/write and selected group read (`0640`).
66    GroupRead { gid: u32 },
67    /// Owner and selected group read/write (`0660`). Trust every group writer.
68    GroupReadWrite { gid: u32 },
69}
70
71impl ShmAccessPolicy {
72    pub const fn mode(self) -> u32 {
73        match self {
74            Self::OwnerOnly => 0o600,
75            Self::GroupRead { .. } => 0o640,
76            Self::GroupReadWrite { .. } => 0o660,
77        }
78    }
79
80    pub const fn gid(self) -> Option<u32> {
81        match self {
82            Self::OwnerOnly => None,
83            Self::GroupRead { gid } | Self::GroupReadWrite { gid } => Some(gid),
84        }
85    }
86}
87
88/// Result of physically validating an existing POSIX SHM object.
89///
90/// This check is deliberately below ring semantics: it verifies that the
91/// named object satisfies the access policy and is large enough for the requested mapping,
92/// but it does not inspect an owning data structure's magic, version, or
93/// geometry header.
94#[derive(Clone, Copy, Debug, Eq, PartialEq)]
95pub enum ShmValidation {
96    /// No object currently exists under the requested name.
97    Missing,
98    /// The object exists and can safely back at least the requested mapping.
99    Valid { actual_size: usize },
100}
101
102/// A mapped POSIX SHM region. Drop unmaps; `unlink` removes the
103/// underlying name and any companion lock file (only the *creator*
104/// should call it on shutdown).
105pub struct ShmRegion {
106    name: CString,
107    lock_path: PathBuf,
108    /// Whether this region uses the companion file for ordered writes.
109    ///
110    /// The descriptor itself is deliberately not retained: every critical
111    /// section opens its own file description so a later fork does not inherit
112    /// an idle descriptor that can keep a future `flock` alive.
113    process_lock: bool,
114    ptr: NonNull<u8>,
115    len: usize,
116    /// True when this handle was the one that *created* the segment
117    /// (so it knows to `shm_unlink` if asked). Other attachers see
118    /// `false`.
119    created: bool,
120}
121
122impl ShmRegion {
123    /// Validate an existing shared-memory object without creating, mapping,
124    /// resetting, or unlinking it.
125    ///
126    /// Returns [`ShmValidation::Missing`] when the name does not exist. A
127    /// present object must be at least `minimum_size` bytes; larger objects
128    /// are accepted because some platforms report page-rounded SHM sizes.
129    /// The owning ring or table remains responsible for validating its own
130    /// persisted ABI header after mapping.
131    /// The expected owner is the effective uid and the policy is `OwnerOnly`.
132    pub fn validate_existing(
133        name: &str,
134        minimum_size: usize,
135    ) -> io::Result<ShmValidation> {
136        Self::validate_existing_with_policy(
137            name,
138            minimum_size,
139            unsafe { libc::geteuid() },
140            ShmAccessPolicy::default(),
141        )
142    }
143
144    /// Validate size, expected owner and maximum permissions without mutation.
145    /// `owner_uid` is trusted configuration, not metadata read from the object.
146    pub fn validate_existing_with_policy(
147        name: &str,
148        minimum_size: usize,
149        owner_uid: u32,
150        policy: ShmAccessPolicy,
151    ) -> io::Result<ShmValidation> {
152        let cname = CString::new(name).map_err(|_| {
153            io::Error::new(io::ErrorKind::InvalidInput, "shm name has nul byte")
154        })?;
155        let raw_fd = loop {
156            // SAFETY: passing a valid C string and well-known POSIX flags.
157            // macOS `shm_open` rejects `O_CLOEXEC` with EINVAL. The descriptor
158            // is scoped to this validation call and closes before return.
159            let fd = unsafe {
160                libc::shm_open(cname.as_ptr(), libc::O_RDONLY, 0o600)
161            };
162            if fd >= 0 {
163                break fd;
164            }
165            let error = io::Error::last_os_error();
166            if error.kind() == io::ErrorKind::Interrupted {
167                continue;
168            }
169            if error.raw_os_error() == Some(libc::ENOENT) {
170                return Ok(ShmValidation::Missing);
171            }
172            return Err(error);
173        };
174        // SAFETY: `raw_fd` was returned by `shm_open` and is now uniquely
175        // owned by this scope.
176        let fd = unsafe { OwnedFd::from_raw_fd(raw_fd) };
177        let actual_size = shm_object_size(&fd, name, owner_uid, policy)?;
178        validate_minimum_size(name, actual_size, minimum_size)?;
179        Ok(ShmValidation::Valid { actual_size })
180    }
181
182    /// Map an existing shared-memory object read-only.
183    ///
184    /// This path never creates, sizes, locks, resets, or unlinks the object.
185    /// It is kept crate-private so callers receive a capability such as a
186    /// read-only ring view rather than a [`ShmRegion`] that also exposes
187    /// lifecycle and writable-pointer operations.
188    pub(crate) fn open_existing_read_only(
189        name: &str,
190        minimum_size: usize,
191        owner_uid: u32,
192        policy: ShmAccessPolicy,
193    ) -> io::Result<Self> {
194        Self::open_existing(name, minimum_size, owner_uid, policy, false)
195    }
196
197    /// Attach writable to an existing object, without creating or sizing it.
198    /// Semantic readers use this only for existing atomic reader-pin fields.
199    pub fn open_existing_read_write(
200        name: &str,
201        minimum_size: usize,
202        owner_uid: u32,
203        policy: ShmAccessPolicy,
204    ) -> io::Result<Self> {
205        Self::open_existing(name, minimum_size, owner_uid, policy, true)
206    }
207
208    fn open_existing(
209        name: &str,
210        minimum_size: usize,
211        owner_uid: u32,
212        policy: ShmAccessPolicy,
213        writable: bool,
214    ) -> io::Result<Self> {
215        let cname = CString::new(name).map_err(|_| {
216            io::Error::new(io::ErrorKind::InvalidInput, "shm name has nul byte")
217        })?;
218        let raw_fd = loop {
219            // SAFETY: passing a valid C string and existing-only POSIX flags.
220            // macOS `shm_open` rejects `O_CLOEXEC` with EINVAL. This
221            // descriptor closes immediately after `mmap`, before the view is
222            // returned, so it cannot leak across a later exec.
223            let fd = unsafe {
224                libc::shm_open(
225                    cname.as_ptr(),
226                    if writable { libc::O_RDWR } else { libc::O_RDONLY },
227                    0o600,
228                )
229            };
230            if fd >= 0 {
231                break fd;
232            }
233            let error = io::Error::last_os_error();
234            if error.kind() == io::ErrorKind::Interrupted {
235                continue;
236            }
237            return Err(error);
238        };
239        // SAFETY: `raw_fd` was returned by `shm_open` and is now uniquely
240        // owned by this scope.
241        let fd = unsafe { OwnedFd::from_raw_fd(raw_fd) };
242        let actual_size = shm_object_size(&fd, name, owner_uid, policy)?;
243        validate_minimum_size(name, actual_size, minimum_size)?;
244
245        // Map the complete object so its persisted header can describe the
246        // geometry without the observer reproducing the producer's layout.
247        // SAFETY: fd is valid, actual_size is positive after minimum
248        // validation. Protection follows the requested attachment capability.
249        let ptr = unsafe {
250            libc::mmap(
251                std::ptr::null_mut(),
252                actual_size,
253                libc::PROT_READ | if writable { libc::PROT_WRITE } else { 0 },
254                libc::MAP_SHARED,
255                fd.as_raw_fd(),
256                0,
257            )
258        };
259        if ptr == libc::MAP_FAILED {
260            return Err(io::Error::last_os_error());
261        }
262        // SAFETY: mmap returned a non-null pointer (checked above).
263        let ptr = NonNull::new(ptr.cast::<u8>())
264            .expect("mmap returned non-null on success");
265
266        Ok(Self {
267            lock_path: lock_dir_for_uid(owner_uid)
268                .join(format!("{}.lock", name.trim_start_matches('/'))),
269            name: cname,
270            process_lock: false,
271            ptr,
272            len: actual_size,
273            created: false,
274        })
275    }
276
277    /// Open or create a shared-memory segment of `size` bytes,
278    /// memory-mapped read/write. Idempotent: if the segment already
279    /// exists with the same name and enough mapped bytes, it is reused
280    /// (`created = false`). First creation does `ftruncate(size)`;
281    /// later opens verify the existing object before mapping it. Some
282    /// platforms report a page-rounded SHM size, so a larger `st_size`
283    /// is valid; the owning data structure must verify its own header.
284    /// Existing objects must belong to the effective uid and satisfy `OwnerOnly`.
285    pub fn open_or_create(
286        name: &str,
287        size: usize,
288    ) -> io::Result<Self> {
289        Self::open_or_create_with_policy(name, size, ShmAccessPolicy::default())
290    }
291
292    /// Open or create using an explicit access policy. Existing objects must
293    /// belong to the effective uid and pass the policy before being mapped.
294    /// Rejection never resizes, chmods, chowns or unlinks an existing object.
295    pub fn open_or_create_with_policy(
296        name: &str,
297        size: usize,
298        policy: ShmAccessPolicy,
299    ) -> io::Result<Self> {
300        let (region, initialization_lock) =
301            Self::open_or_create_inner(name, size, false, policy)?;
302        debug_assert!(initialization_lock.is_none());
303        Ok(region)
304    }
305
306    /// Open or create a region while holding its process lock through caller
307    /// initialization. This prevents a peer from observing the interval
308    /// between `shm_open` and the owning data structure's initialized header.
309    pub fn open_or_create_locked(
310        name: &str,
311        size: usize,
312    ) -> io::Result<(Self, ShmRegionLock)> {
313        Self::open_or_create_locked_with_policy(
314            name,
315            size,
316            ShmAccessPolicy::default(),
317        )
318    }
319
320    /// Policy-aware creation with the existing owner-local initialization lock.
321    /// Group permissions do not make that lock or fleet membership cross-user.
322    pub fn open_or_create_locked_with_policy(
323        name: &str,
324        size: usize,
325        policy: ShmAccessPolicy,
326    ) -> io::Result<(Self, ShmRegionLock)> {
327        let (region, initialization_lock) =
328            Self::open_or_create_inner(name, size, true, policy)?;
329        Ok((
330            region,
331            initialization_lock
332                .expect("locked SHM open must return its initialization lock"),
333        ))
334    }
335
336    fn open_or_create_inner(
337        name: &str,
338        size: usize,
339        process_lock: bool,
340        policy: ShmAccessPolicy,
341    ) -> io::Result<(Self, Option<ShmRegionLock>)> {
342        let cname = CString::new(name).map_err(|_| {
343            io::Error::new(io::ErrorKind::InvalidInput, "shm name has nul byte")
344        })?;
345        let lock_path = lock_file_path(name);
346        let initialization_lock = if process_lock {
347            Some(lock_path_exclusive(&lock_path)?)
348        } else {
349            None
350        };
351
352        // Darwin cannot fchmod/fchown a POSIX SHM descriptor. Grant only the
353        // intended group at creation; attaching a matching existing object is OK.
354        #[cfg(target_os = "macos")]
355        let can_create =
356            policy.gid().is_none_or(|gid| gid == unsafe { libc::getegid() });
357        #[cfg(target_os = "macos")]
358        let creation_mode = policy.mode();
359        // Elsewhere, don't expose the object to its initial group before chown.
360        #[cfg(not(target_os = "macos"))]
361        let (can_create, creation_mode) = (true, 0o600u32);
362        // Apple declares shm_open as variadic, so its mode stays promoted to u32.
363        #[cfg(not(target_vendor = "apple"))]
364        let creation_mode = creation_mode as libc::mode_t;
365        // Try create-exclusive first; if it already exists, open.
366        let (raw_fd, created) = unsafe {
367            // SAFETY: passing a valid C string and well-known POSIX flags.
368            let fd = if can_create {
369                libc::shm_open(
370                    cname.as_ptr(),
371                    libc::O_RDWR | libc::O_CREAT | libc::O_EXCL,
372                    creation_mode,
373                )
374            } else {
375                libc::shm_open(cname.as_ptr(), libc::O_RDWR, 0)
376            };
377            if fd >= 0 {
378                (fd, can_create)
379            } else {
380                // Could be EEXIST (already created by a peer) or another error.
381                let err = io::Error::last_os_error();
382                if !can_create && err.raw_os_error() == Some(libc::ENOENT) {
383                    return Err(io::Error::new(
384                        io::ErrorKind::InvalidInput,
385                        "creating group-shared SHM requires the selected effective gid",
386                    ));
387                }
388                if err.raw_os_error() != Some(libc::EEXIST) {
389                    return Err(err);
390                }
391                let fd = libc::shm_open(cname.as_ptr(), libc::O_RDWR, 0o600);
392                if fd < 0 {
393                    return Err(io::Error::last_os_error());
394                }
395                (fd, false)
396            }
397        };
398        // SAFETY: `raw_fd` was returned by `shm_open` and is now uniquely
399        // owned by this scope.
400        let fd = unsafe { OwnedFd::from_raw_fd(raw_fd) };
401
402        #[cfg(not(target_os = "macos"))]
403        if created && let Err(error) = configure_new_shm(&fd, policy) {
404            let _ = unsafe { libc::shm_unlink(cname.as_ptr()) };
405            return Err(error);
406        }
407
408        // Verify ownership and permissions before ftruncate or mmap. Only a
409        // newly created object is ours to unlink on initialization failure.
410        let owner_uid = unsafe { libc::geteuid() };
411        if let Err(error) = shm_object_size(&fd, name, owner_uid, policy) {
412            if created {
413                let _ = unsafe { libc::shm_unlink(cname.as_ptr()) };
414            }
415            return Err(error);
416        }
417
418        // Size the segment on first creation.
419        if created {
420            // SAFETY: fd is a valid POSIX fd we just received.
421            let rc =
422                unsafe { libc::ftruncate(fd.as_raw_fd(), size as libc::off_t) };
423            if rc != 0 {
424                let err = io::Error::last_os_error();
425                let _ = unsafe { libc::shm_unlink(cname.as_ptr()) };
426                return Err(err);
427            }
428        }
429
430        // Never mmap beyond the real SHM object: access past it can raise
431        // SIGBUS. A larger reported size is valid on platforms (notably
432        // macOS) that page-round POSIX SHM objects; callers verify their
433        // own ABI metadata after mapping.
434        let actual_size = match shm_object_size(&fd, name, owner_uid, policy) {
435            Ok(actual_size) => actual_size,
436            Err(error) => {
437                if created {
438                    let _ = unsafe { libc::shm_unlink(cname.as_ptr()) };
439                }
440                return Err(error);
441            }
442        };
443        if let Err(error) = validate_minimum_size(name, actual_size, size) {
444            if created {
445                let _ = unsafe { libc::shm_unlink(cname.as_ptr()) };
446            }
447            return Err(error);
448        }
449
450        // Memory-map the segment.
451        // SAFETY: fd valid, size positive, flags well-known.
452        let ptr = unsafe {
453            libc::mmap(
454                std::ptr::null_mut(),
455                size,
456                libc::PROT_READ | libc::PROT_WRITE,
457                libc::MAP_SHARED,
458                fd.as_raw_fd(),
459                0,
460            )
461        };
462
463        if ptr == libc::MAP_FAILED {
464            let err = io::Error::last_os_error();
465            if created {
466                let _ = unsafe { libc::shm_unlink(cname.as_ptr()) };
467            }
468            return Err(err);
469        }
470
471        // SAFETY: mmap returned a non-null pointer (we just checked).
472        let ptr = NonNull::new(ptr.cast::<u8>())
473            .expect("mmap returned non-null on success");
474
475        Ok((
476            Self {
477                name: cname,
478                lock_path,
479                process_lock,
480                ptr,
481                len: size,
482                created,
483            },
484            initialization_lock,
485        ))
486    }
487
488    /// Raw mapped pointer to the start of the region.
489    pub fn as_ptr(&self) -> *mut u8 {
490        self.ptr.as_ptr()
491    }
492
493    /// Length of the mapped region (the `size` passed to `open_or_create`).
494    pub fn len(&self) -> usize {
495        self.len
496    }
497
498    pub fn is_empty(&self) -> bool {
499        self.len == 0
500    }
501
502    /// True when this handle was the one that created the segment.
503    /// Useful for picking which process performs first-time
504    /// initialization of the header.
505    pub fn created(&self) -> bool {
506        self.created
507    }
508
509    /// Acquire an exclusive cross-process lock tied to this SHM name.
510    ///
511    /// `flock` ownership is held by the kernel and is released when a process
512    /// exits or the descriptor closes, including abnormal termination.
513    pub fn lock_exclusive(&self) -> io::Result<ShmRegionLock> {
514        if !self.process_lock {
515            return Err(io::Error::new(
516                io::ErrorKind::InvalidInput,
517                "SHM region was opened without a process lock",
518            ));
519        }
520        lock_path_exclusive(&self.lock_path)
521    }
522
523    #[cfg(test)]
524    pub(crate) fn try_lock_exclusive(&self) -> io::Result<ShmRegionLock> {
525        if !self.process_lock {
526            return Err(io::Error::new(
527                io::ErrorKind::InvalidInput,
528                "SHM region was opened without a process lock",
529            ));
530        }
531        let lock_fd = open_lock_file(&self.lock_path)?;
532        let rc = unsafe {
533            libc::flock(lock_fd.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB)
534        };
535        if rc == 0 {
536            Ok(ShmRegionLock { lock_fd })
537        } else {
538            Err(io::Error::last_os_error())
539        }
540    }
541
542    /// Remove the underlying segment name. Existing mappings stay
543    /// valid until each process drops its `ShmRegion`. Use only on
544    /// shutdown / fleet teardown by the process that owns lifecycle.
545    pub fn unlink(&self) -> io::Result<()> {
546        // SAFETY: name is a valid C string.
547        let rc = unsafe { libc::shm_unlink(self.name.as_ptr()) };
548        let shm_error = if rc != 0 {
549            let err = io::Error::last_os_error();
550            // ENOENT is fine — segment was already unlinked.
551            if err.raw_os_error() == Some(libc::ENOENT) {
552                None
553            } else {
554                Some(err)
555            }
556        } else {
557            None
558        };
559        let mut lock_error = match std::fs::remove_file(&self.lock_path) {
560            Ok(()) => None,
561            Err(error) if error.kind() == io::ErrorKind::NotFound => None,
562            Err(error) => Some(error),
563        };
564        let name = self.name.to_string_lossy();
565        // SAFETY: a plain syscall with no arguments.
566        let uid = unsafe { libc::geteuid() };
567        match companion_lock_files(&name, uid) {
568            Ok(files) => {
569                for file in files {
570                    if let Err(error) = std::fs::remove_file(&file)
571                        && error.kind() != io::ErrorKind::NotFound
572                    {
573                        lock_error.get_or_insert(error);
574                    }
575                }
576            }
577            Err(error) => {
578                lock_error.get_or_insert(error);
579            }
580        }
581        if let Some(error) = shm_error.or(lock_error) {
582            return Err(error);
583        }
584        Ok(())
585    }
586}
587
588#[cfg(not(target_os = "macos"))]
589fn configure_new_shm(
590    fd: &OwnedFd,
591    policy: ShmAccessPolicy,
592) -> io::Result<()> {
593    let Some(gid) = policy.gid() else {
594        return Ok(());
595    };
596    if gid == !0 {
597        return Err(io::Error::new(
598            io::ErrorKind::InvalidInput,
599            "invalid SHM group id",
600        ));
601    }
602    // SAFETY: this is our new, still owner-only object. Preserve its uid.
603    if unsafe { libc::fchown(fd.as_raw_fd(), !0, gid) } != 0 {
604        return Err(io::Error::last_os_error());
605    }
606    // Explicit group-sharing policy, applied only after group ownership is set.
607    if unsafe { libc::fchmod(fd.as_raw_fd(), policy.mode() as _) } != 0 {
608        return Err(io::Error::last_os_error());
609    }
610    Ok(())
611}
612
613fn shm_object_size(
614    fd: &OwnedFd,
615    name: &str,
616    owner_uid: u32,
617    policy: ShmAccessPolicy,
618) -> io::Result<usize> {
619    let mut stat = std::mem::MaybeUninit::<libc::stat>::uninit();
620    // SAFETY: `fd` is valid and `stat` points to writable storage.
621    let stat_rc = unsafe { libc::fstat(fd.as_raw_fd(), stat.as_mut_ptr()) };
622    if stat_rc != 0 {
623        return Err(io::Error::last_os_error());
624    }
625    // SAFETY: `fstat` succeeded and initialized the structure.
626    let stat = unsafe { stat.assume_init() };
627    let mode = u64::from(stat.st_mode) & 0o7777;
628    if stat.st_uid != owner_uid
629        || policy.gid().is_some_and(|gid| gid != stat.st_gid)
630        || mode & !u64::from(policy.mode()) != 0
631    {
632        return Err(io::Error::new(
633            io::ErrorKind::PermissionDenied,
634            format!(
635                "SHM segment {name} owner/group/mode do not satisfy {policy:?} for uid {owner_uid}"
636            ),
637        ));
638    }
639    let actual_size = stat.st_size;
640    usize::try_from(actual_size).map_err(|_| {
641        io::Error::new(
642            io::ErrorKind::InvalidData,
643            format!("SHM segment {name} reported invalid size {actual_size}"),
644        )
645    })
646}
647
648fn validate_minimum_size(
649    name: &str,
650    actual_size: usize,
651    minimum_size: usize,
652) -> io::Result<()> {
653    if actual_size < minimum_size {
654        return Err(io::Error::new(
655            io::ErrorKind::InvalidData,
656            format!(
657                "SHM segment {name} size {actual_size} is smaller than requested mapping {minimum_size}"
658            ),
659        ));
660    }
661    Ok(())
662}
663
664/// RAII guard for a [`ShmRegion`]'s process-recoverable exclusive lock.
665///
666/// Semantic crates use this when a current-state transition must be atomic
667/// across fleet processes. Dropping the guard releases the kernel lock.
668pub struct ShmRegionLock {
669    lock_fd: OwnedFd,
670}
671
672impl Drop for ShmRegionLock {
673    fn drop(&mut self) {
674        let _ = unsafe { libc::flock(self.lock_fd.as_raw_fd(), libc::LOCK_UN) };
675    }
676}
677
678fn lock_path_exclusive(lock_path: &Path) -> io::Result<ShmRegionLock> {
679    lock_fd_exclusive(open_lock_file(lock_path)?)
680}
681
682fn open_lock_file(lock_path: &Path) -> io::Result<OwnedFd> {
683    use std::os::unix::fs::MetadataExt;
684
685    if let Some(dir) = lock_path.parent() {
686        ensure_lock_dir(dir)?;
687    }
688
689    let file = OpenOptions::new()
690        .read(true)
691        .write(true)
692        .create(true)
693        .mode(0o600)
694        .custom_flags(libc::O_CLOEXEC | libc::O_NOFOLLOW)
695        .open(lock_path)?;
696
697    // The directory check makes this unreachable, which is the reason to make
698    // it anyway: it turns a property inferred from the directory's mode into
699    // one this function establishes about the descriptor it is about to lock.
700    let uid = unsafe { libc::geteuid() };
701    let owner = file.metadata()?.uid();
702    if owner != uid {
703        return Err(io::Error::new(
704            io::ErrorKind::PermissionDenied,
705            format!(
706                "{} is owned by uid {owner} rather than {uid}",
707                lock_path.display()
708            ),
709        ));
710    }
711
712    Ok(file.into())
713}
714
715fn lock_fd_exclusive(lock_fd: OwnedFd) -> io::Result<ShmRegionLock> {
716    loop {
717        let rc = unsafe { libc::flock(lock_fd.as_raw_fd(), libc::LOCK_EX) };
718        if rc == 0 {
719            return Ok(ShmRegionLock { lock_fd });
720        }
721        let error = io::Error::last_os_error();
722        if error.kind() != io::ErrorKind::Interrupted {
723            return Err(error);
724        }
725    }
726}
727
728impl Drop for ShmRegion {
729    fn drop(&mut self) {
730        // SAFETY: ptr came from mmap of `self.len` bytes; munmap is the inverse.
731        unsafe {
732            libc::munmap(self.ptr.as_ptr().cast(), self.len);
733        }
734    }
735}
736
737// SAFETY: the underlying region is shared memory and synchronization
738// happens at the slot level (atomic seq counters); the handle itself
739// is just a pointer + length, safe to send/share.
740unsafe impl Send for ShmRegion {}
741unsafe impl Sync for ShmRegion {}
742
743/// Build the conventional name for an Orbit ring segment.
744pub fn ring_segment_name(
745    fleet_name: &str,
746    kind: u8,
747) -> String {
748    // SAFETY: `geteuid` always returns a value; no error path.
749    let uid = unsafe { libc::geteuid() };
750    ring_segment_name_for_uid(fleet_name, kind, uid)
751}
752
753/// Build the conventional name for an Orbit ring segment owned by `uid`.
754///
755/// This form is intended for inspection and lifecycle tools that need to
756/// address a user other than their own effective uid.
757pub fn ring_segment_name_for_uid(
758    fleet_name: &str,
759    kind: u8,
760    uid: u32,
761) -> String {
762    format!("/{SHM_NAMESPACE}-{fleet_name}-{kind}-{uid}")
763}
764
765/// Where a fleet's members hold their presence: `<lock dir>/orbit-<fleet>.fleet`.
766pub fn fleet_lock_path(fleet_name: &str) -> PathBuf {
767    lock_dir().join(format!("{SHM_NAMESPACE}-{fleet_name}.fleet"))
768}
769
770/// The same for another user's fleet, for lifecycle tools that address a uid
771/// other than their own.
772pub fn fleet_lock_path_for_uid(
773    fleet_name: &str,
774    uid: u32,
775) -> PathBuf {
776    lock_dir_for_uid(uid).join(format!("{SHM_NAMESPACE}-{fleet_name}.fleet"))
777}
778
779/// A process's membership in a fleet: a shared `flock` on the fleet's lock
780/// file, held for as long as this lives and released by the kernel when the
781/// process dies, however it dies. It is what [`try_lock_fleet_exclusive`]
782/// contends with, so a tool that removes the fleet's segments cannot do so
783/// while any member is alive.
784pub struct FleetMembership {
785    lock_fd: OwnedFd,
786}
787
788impl Drop for FleetMembership {
789    fn drop(&mut self) {
790        let _ = unsafe { libc::flock(self.lock_fd.as_raw_fd(), libc::LOCK_UN) };
791    }
792}
793
794/// Join the fleet's membership. Waits for a lifecycle tool that holds the
795/// exclusive lock at that moment; a clear in progress finishes first.
796pub fn join_fleet_membership(fleet_name: &str) -> io::Result<FleetMembership> {
797    let lock_fd = open_lock_file(&fleet_lock_path(fleet_name))?;
798    // SAFETY: `lock_fd` is an open descriptor owned by this call.
799    let rc = unsafe { libc::flock(lock_fd.as_raw_fd(), libc::LOCK_SH) };
800    if rc != 0 {
801        return Err(io::Error::last_os_error());
802    }
803    Ok(FleetMembership { lock_fd })
804}
805
806/// Protect an existing fleet against clearing while a diagnostic reader pins
807/// its arena. Never creates a membership file or reserves a writer lane.
808pub fn hold_existing_fleet(fleet_name: &str) -> io::Result<FleetMembership> {
809    use std::os::unix::fs::MetadataExt;
810    let path = fleet_lock_path(fleet_name);
811    let file = OpenOptions::new()
812        .read(true)
813        .custom_flags(libc::O_CLOEXEC | libc::O_NOFOLLOW)
814        .open(&path)?;
815    let metadata = file.metadata()?;
816    if !metadata.is_file() || metadata.uid() != unsafe { libc::geteuid() } {
817        return Err(io::Error::new(
818            io::ErrorKind::PermissionDenied,
819            "fleet membership file has an unexpected owner or type",
820        ));
821    }
822    let lock_fd: OwnedFd = file.into();
823    loop {
824        let rc = unsafe { libc::flock(lock_fd.as_raw_fd(), libc::LOCK_SH) };
825        if rc == 0 {
826            return Ok(FleetMembership { lock_fd });
827        }
828        let error = io::Error::last_os_error();
829        if error.kind() != io::ErrorKind::Interrupted {
830            return Err(error);
831        }
832    }
833}
834
835/// Exclusive hold on a fleet, for the tool that removes its segments.
836///
837/// `Ok(None)` means a member is alive and the fleet must not be touched;
838/// there is deliberately no way to force past it. `Ok(Some(_))` keeps the
839/// fleet closed to new members until the guard is dropped, so a removal
840/// cannot interleave with a start. Never waits.
841pub fn try_lock_fleet_exclusive(
842    fleet_name: &str,
843    uid: u32,
844) -> io::Result<Option<ShmRegionLock>> {
845    // Creating the file when no member ever joined is right: the guard then
846    // keeps a first member from starting in the middle of a removal.
847    let lock_fd = open_lock_file(&fleet_lock_path_for_uid(fleet_name, uid))?;
848    // SAFETY: `lock_fd` is an open descriptor owned by this call.
849    let rc = unsafe {
850        libc::flock(lock_fd.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB)
851    };
852    if rc == 0 {
853        return Ok(Some(ShmRegionLock { lock_fd }));
854    }
855    let error = io::Error::last_os_error();
856    if error.kind() == io::ErrorKind::WouldBlock {
857        return Ok(None);
858    }
859    Err(error)
860}
861
862/// One process's hold on one lane of a segment: an exclusive `flock` on the
863/// lane's own lock file, kept for as long as this lives and released by the
864/// kernel when the process dies, however it dies.
865///
866/// It is how a lane's owner is known to be gone without a timeout and without
867/// a PID, which another PID namespace would read wrongly: a hold that can be
868/// taken has nobody behind it.
869pub struct LaneHold {
870    lock_fd: OwnedFd,
871}
872
873/// A lane lock prepared at opening, with no path construction or file open
874/// during acquisition. Each opener owns a distinct file description.
875/// Open after fork; inherited instances cannot be used by the child.
876pub struct PreparedLaneLock {
877    fd: OwnedFd,
878    busy: AtomicBool,
879    pid: u32,
880}
881
882impl PreparedLaneLock {
883    pub fn open(
884        shm_name: &str,
885        lane: usize,
886    ) -> io::Result<Self> {
887        let name = format!("{}.lane{lane}", shm_name.trim_start_matches('/'));
888        Ok(Self {
889            fd: open_lock_file(&lock_file_path(&name))?,
890            busy: AtomicBool::new(false),
891            pid: std::process::id(),
892        })
893    }
894
895    /// One local CAS and at most one LOCK_NB syscall. No owner wait or retry.
896    pub fn try_lock(&self) -> io::Result<Option<PreparedLaneGuard<'_>>> {
897        if self.pid != std::process::id() {
898            return Err(io::Error::from(io::ErrorKind::PermissionDenied));
899        }
900        if self
901            .busy
902            .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
903            .is_err()
904        {
905            return Ok(None);
906        }
907        // SAFETY: the local flag excludes overlapping use of this description.
908        if unsafe {
909            libc::flock(self.fd.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB)
910        } == 0
911        {
912            return Ok(Some(PreparedLaneGuard { lock: self }));
913        }
914        let error = io::Error::last_os_error();
915        self.busy.store(false, Ordering::Release);
916        if error.kind() == io::ErrorKind::WouldBlock {
917            Ok(None)
918        } else {
919            Err(error)
920        }
921    }
922}
923
924pub struct PreparedLaneGuard<'a> {
925    lock: &'a PreparedLaneLock,
926}
927
928impl Drop for PreparedLaneGuard<'_> {
929    fn drop(&mut self) {
930        if self.lock.pid == std::process::id() {
931            // SAFETY: this guard owns this file description's exclusive hold.
932            unsafe {
933                libc::flock(self.lock.fd.as_raw_fd(), libc::LOCK_UN);
934            }
935            self.lock.busy.store(false, Ordering::Release);
936        }
937    }
938}
939
940impl Drop for LaneHold {
941    fn drop(&mut self) {
942        let _ = unsafe { libc::flock(self.lock_fd.as_raw_fd(), libc::LOCK_UN) };
943    }
944}
945
946/// Take lane `lane` of segment `shm_name`, if no live process holds it.
947///
948/// `Ok(None)` means a process holding it is alive. Never waits.
949pub fn try_hold_lane(
950    shm_name: &str,
951    lane: usize,
952) -> io::Result<Option<LaneHold>> {
953    let name = format!("{}.lane{lane}", shm_name.trim_start_matches('/'));
954    let lock_fd = open_lock_file(&lock_file_path(&name))?;
955    // SAFETY: `lock_fd` is an open descriptor owned by this call.
956    let rc = unsafe {
957        libc::flock(lock_fd.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB)
958    };
959    if rc == 0 {
960        return Ok(Some(LaneHold { lock_fd }));
961    }
962    let error = io::Error::last_os_error();
963    if error.kind() == io::ErrorKind::WouldBlock {
964        return Ok(None);
965    }
966    Err(error)
967}
968
969/// One process's hold on one directory-watcher slot of a segment: the same
970/// kernel-released `flock` as a [`LaneHold`], on a lock file of its own.
971///
972/// A watcher is not a lane. It serves nothing and takes no frames; it only
973/// wants to be woken when the lane directory moves. Its slot is still owned
974/// the way a lane is, so a slot a dead process left can be taken by the next
975/// one without a timeout.
976pub struct WatcherHold {
977    lock_fd: OwnedFd,
978}
979
980impl Drop for WatcherHold {
981    fn drop(&mut self) {
982        let _ = unsafe { libc::flock(self.lock_fd.as_raw_fd(), libc::LOCK_UN) };
983    }
984}
985
986/// Take watcher slot `slot` of segment `shm_name`, if no live process holds
987/// it.
988///
989/// `Ok(None)` means a process holding it is alive. Never waits.
990pub fn try_hold_watcher(
991    shm_name: &str,
992    slot: usize,
993) -> io::Result<Option<WatcherHold>> {
994    let name = format!("{}.watch{slot}", shm_name.trim_start_matches('/'));
995    let lock_fd = open_lock_file(&lock_file_path(&name))?;
996    // SAFETY: `lock_fd` is an open descriptor owned by this call.
997    let rc = unsafe {
998        libc::flock(lock_fd.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB)
999    };
1000    if rc == 0 {
1001        return Ok(Some(WatcherHold { lock_fd }));
1002    }
1003    let error = io::Error::last_os_error();
1004    if error.kind() == io::ErrorKind::WouldBlock {
1005        return Ok(None);
1006    }
1007    Err(error)
1008}
1009
1010/// Every lock file beside segment `shm_name` of `uid`: the region's own, one
1011/// per lane that was ever held ([`try_hold_lane`]) and one per watcher slot
1012/// ([`try_hold_watcher`]). What removes a segment
1013/// removes these with it; an unlocked one left behind is harmless, but it is
1014/// one more file per lane for every segment that ever existed.
1015pub fn companion_lock_files(
1016    shm_name: &str,
1017    uid: u32,
1018) -> io::Result<Vec<PathBuf>> {
1019    let base = shm_name.trim_start_matches('/');
1020    let own = format!("{base}.lock");
1021    let lane = format!("{base}.lane");
1022    let watch = format!("{base}.watch");
1023    let dir = lock_dir_for_uid(uid);
1024    let entries = match std::fs::read_dir(&dir) {
1025        Ok(entries) => entries,
1026        Err(error) if error.kind() == io::ErrorKind::NotFound => {
1027            return Ok(Vec::new());
1028        }
1029        Err(error) => return Err(error),
1030    };
1031    let mut found = Vec::new();
1032    for entry in entries {
1033        let entry = entry?;
1034        let Some(file) = entry.file_name().to_str().map(str::to_owned) else {
1035            continue;
1036        };
1037        if file == own
1038            || ((file.starts_with(&lane) || file.starts_with(&watch))
1039                && file.ends_with(".lock"))
1040        {
1041            found.push(entry.path());
1042        }
1043    }
1044    Ok(found)
1045}
1046
1047fn lock_file_path(shm_name: &str) -> PathBuf {
1048    lock_dir().join(format!("{}.lock", shm_name.trim_start_matches('/')))
1049}
1050
1051/// Where the companion lock files live.
1052///
1053/// They used to live in `/tmp` directly, as
1054/// `/tmp/orbit-{fleet}-{kind}-{uid}.lock`, opened `O_CREAT` without `O_EXCL`
1055/// and without asking who owned what the open found. `/tmp` is world-writable,
1056/// so any local user could create that file first and then hold `LOCK_EX` on it
1057/// for as long as they liked: every process in the fleet would sit in `flock` —
1058/// not fail, block — waiting for a lock it was never going to get. The SHM
1059/// segment name is uid-scoped and so cannot be squatted this way; the lock path
1060/// was not. `O_NOFOLLOW` prevented the symlink version of the trick and nothing
1061/// else.
1062///
1063/// They now live in a per-uid directory created `0700`, which a user who is not
1064/// us cannot put a file into. What such a user can still do is create the
1065/// directory first, so its owner and mode are checked on every open rather than
1066/// assumed from having created it: a directory that is not ours, or not
1067/// private, fails the open with the path in the message instead of parking the
1068/// process on a lock.
1069///
1070/// `XDG_RUNTIME_DIR` is preferred where the session provides one, because it is
1071/// already per-user and `0700` and so is not inside a world-writable directory
1072/// at all. macOS has no such variable but gives each user a private `TMPDIR`;
1073/// `/tmp` is the fallback, with the checks above carrying the weight.
1074fn lock_dir() -> PathBuf {
1075    let uid = unsafe { libc::geteuid() };
1076    lock_dir_for_uid(uid)
1077}
1078
1079/// Locate the existing per-user companion lock directory without creating it.
1080/// Tools can use its filenames as discovery hints, but must verify SHM objects.
1081pub fn lock_dir_for_uid(uid: u32) -> PathBuf {
1082    let base = std::env::var_os("XDG_RUNTIME_DIR")
1083        .map(PathBuf::from)
1084        .filter(|dir| dir.is_absolute())
1085        .unwrap_or_else(|| PathBuf::from("/tmp"));
1086
1087    base.join(format!("{SHM_NAMESPACE}-{uid}"))
1088}
1089
1090/// Creates the lock directory if it is missing and refuses it if it is not
1091/// ours. Called when a lock is actually taken rather than when a region is
1092/// opened: a region that never locks has nothing to squat, and failing its open
1093/// on a directory it does not use would hand an attacker a wider outage than
1094/// the one being closed.
1095fn ensure_lock_dir(dir: &Path) -> io::Result<()> {
1096    use std::os::unix::fs::DirBuilderExt;
1097
1098    let uid = unsafe { libc::geteuid() };
1099    match std::fs::DirBuilder::new().mode(0o700).create(dir) {
1100        Ok(()) => {}
1101        Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {}
1102        Err(error) => return Err(error),
1103    }
1104
1105    ensure_private_dir(dir, uid)
1106}
1107
1108/// Refuses a lock directory that someone else could write to.
1109///
1110/// `symlink_metadata` rather than `metadata`: a symlink pointing at a directory
1111/// we do own would otherwise pass while the lock files landed somewhere the
1112/// attacker chose.
1113fn ensure_private_dir(
1114    dir: &Path,
1115    uid: u32,
1116) -> io::Result<()> {
1117    use std::os::unix::fs::{MetadataExt, PermissionsExt};
1118
1119    let metadata = std::fs::symlink_metadata(dir)?;
1120    if !metadata.is_dir() {
1121        return Err(io::Error::new(
1122            io::ErrorKind::PermissionDenied,
1123            format!("{} is not a directory", dir.display()),
1124        ));
1125    }
1126    if metadata.uid() != uid {
1127        return Err(io::Error::new(
1128            io::ErrorKind::PermissionDenied,
1129            format!(
1130                "{} is owned by uid {} rather than {uid}; refusing to lock in a directory \
1131                 another user controls",
1132                dir.display(),
1133                metadata.uid()
1134            ),
1135        ));
1136    }
1137    if metadata.permissions().mode() & 0o077 != 0 {
1138        return Err(io::Error::new(
1139            io::ErrorKind::PermissionDenied,
1140            format!(
1141                "{} is mode {:o}; refusing to lock in a directory others can write to",
1142                dir.display(),
1143                metadata.permissions().mode() & 0o777
1144            ),
1145        ));
1146    }
1147
1148    Ok(())
1149}
1150
1151#[cfg(test)]
1152mod fleet_lock_tests {
1153    use super::{join_fleet_membership, try_lock_fleet_exclusive};
1154
1155    #[test]
1156    fn prepared_lane_excludes_threads_and_distinct_file_descriptions() {
1157        let name = format!("prepared-{}", std::process::id());
1158        let first = super::PreparedLaneLock::open(&name, 0).unwrap();
1159        let second = super::PreparedLaneLock::open(&name, 0).unwrap();
1160        let hold = first.try_lock().unwrap().unwrap();
1161        assert!(first.try_lock().unwrap().is_none());
1162        std::thread::scope(|scope| {
1163            assert!(
1164                scope
1165                    .spawn(|| first.try_lock().unwrap().is_none())
1166                    .join()
1167                    .unwrap()
1168            );
1169        });
1170        assert!(second.try_lock().unwrap().is_none());
1171        drop(hold);
1172        let other = second.try_lock().unwrap().unwrap();
1173        assert!(first.try_lock().unwrap().is_none());
1174        drop(other);
1175        drop(first);
1176        drop(second);
1177        std::fs::remove_file(super::lock_file_path(&format!("{name}.lane0")))
1178            .unwrap();
1179    }
1180
1181    /// A member alive means the fleet cannot be cleared, and there is no
1182    /// flag that says otherwise; the member going away is what opens it.
1183    #[test]
1184    fn a_member_holds_the_fleet_against_exclusive_takers() {
1185        let fleet = format!("fl{:x}", std::process::id());
1186        let uid = unsafe { libc::geteuid() };
1187
1188        let member = join_fleet_membership(&fleet).expect("join");
1189        assert!(try_lock_fleet_exclusive(&fleet, uid).expect("try").is_none());
1190
1191        drop(member);
1192        let exclusive = try_lock_fleet_exclusive(&fleet, uid).expect("try");
1193        assert!(exclusive.is_some());
1194        // And a member cannot join while a removal holds the fleet: the
1195        // shared lock would block, which is the behaviour, not a test to run.
1196        drop(exclusive);
1197        let _ =
1198            std::fs::remove_file(super::fleet_lock_path_for_uid(&fleet, uid));
1199    }
1200}