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