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