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