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