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