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    /// Policy-aware creation with the existing owner-local initialization lock.
332    /// Group permissions do not make that lock or fleet membership cross-user.
333    pub fn open_or_create_locked_with_policy(
334        name: &str,
335        size: usize,
336        policy: ShmAccessPolicy,
337    ) -> io::Result<(Self, ShmRegionLock)> {
338        let (region, initialization_lock) =
339            Self::open_or_create_inner(name, size, true, policy)?;
340        Ok((
341            region,
342            initialization_lock
343                .expect("locked SHM open must return its initialization lock"),
344        ))
345    }
346
347    fn open_or_create_inner(
348        name: &str,
349        size: usize,
350        process_lock: bool,
351        policy: ShmAccessPolicy,
352    ) -> io::Result<(Self, Option<ShmRegionLock>)> {
353        let cname = CString::new(name).map_err(|_| {
354            io::Error::new(io::ErrorKind::InvalidInput, "shm name has nul byte")
355        })?;
356        let lock_path = lock_file_path(name);
357        let initialization_lock = if process_lock {
358            Some(lock_path_exclusive(&lock_path)?)
359        } else {
360            None
361        };
362
363        // Darwin cannot fchmod/fchown a POSIX SHM descriptor. Grant only the
364        // intended group at creation; attaching a matching existing object is OK.
365        #[cfg(target_os = "macos")]
366        let can_create =
367            policy.gid().is_none_or(|gid| gid == unsafe { libc::getegid() });
368        #[cfg(target_os = "macos")]
369        let creation_mode = policy.mode();
370        // Elsewhere, don't expose the object to its initial group before chown.
371        #[cfg(not(target_os = "macos"))]
372        let (can_create, creation_mode) = (true, 0o600u32);
373        // Apple declares shm_open as variadic, so its mode stays promoted to u32.
374        #[cfg(not(target_vendor = "apple"))]
375        let creation_mode = creation_mode as libc::mode_t;
376        // Try create-exclusive first; if it already exists, open.
377        let (raw_fd, created) = unsafe {
378            // SAFETY: passing a valid C string and well-known POSIX flags.
379            let fd = if can_create {
380                libc::shm_open(
381                    cname.as_ptr(),
382                    libc::O_RDWR | libc::O_CREAT | libc::O_EXCL,
383                    creation_mode,
384                )
385            } else {
386                libc::shm_open(cname.as_ptr(), libc::O_RDWR, 0)
387            };
388            if fd >= 0 {
389                (fd, can_create)
390            } else {
391                // Could be EEXIST (already created by a peer) or another error.
392                let err = io::Error::last_os_error();
393                if !can_create && err.raw_os_error() == Some(libc::ENOENT) {
394                    return Err(io::Error::new(
395                        io::ErrorKind::InvalidInput,
396                        "creating group-shared SHM requires the selected effective gid",
397                    ));
398                }
399                if err.raw_os_error() != Some(libc::EEXIST) {
400                    return Err(err);
401                }
402                let fd = libc::shm_open(cname.as_ptr(), libc::O_RDWR, 0o600);
403                if fd < 0 {
404                    return Err(io::Error::last_os_error());
405                }
406                (fd, false)
407            }
408        };
409        // SAFETY: `raw_fd` was returned by `shm_open` and is now uniquely
410        // owned by this scope.
411        let fd = unsafe { OwnedFd::from_raw_fd(raw_fd) };
412
413        #[cfg(not(target_os = "macos"))]
414        if created && let Err(error) = configure_new_shm(&fd, policy) {
415            let _ = unsafe { libc::shm_unlink(cname.as_ptr()) };
416            return Err(error);
417        }
418
419        // Verify ownership and permissions before ftruncate or mmap. Only a
420        // newly created object is ours to unlink on initialization failure.
421        let owner_uid = unsafe { libc::geteuid() };
422        if let Err(error) = shm_object_size(&fd, name, owner_uid, policy) {
423            if created {
424                let _ = unsafe { libc::shm_unlink(cname.as_ptr()) };
425            }
426            return Err(error);
427        }
428
429        // Size the segment on first creation.
430        if created {
431            // SAFETY: fd is a valid POSIX fd we just received.
432            let rc =
433                unsafe { libc::ftruncate(fd.as_raw_fd(), size as libc::off_t) };
434            if rc != 0 {
435                let err = io::Error::last_os_error();
436                let _ = unsafe { libc::shm_unlink(cname.as_ptr()) };
437                return Err(err);
438            }
439        }
440
441        // Never mmap beyond the real SHM object: access past it can raise
442        // SIGBUS. A larger reported size is valid on platforms (notably
443        // macOS) that page-round POSIX SHM objects; callers verify their
444        // own ABI metadata after mapping.
445        let actual_size = match shm_object_size(&fd, name, owner_uid, policy) {
446            Ok(actual_size) => actual_size,
447            Err(error) => {
448                if created {
449                    let _ = unsafe { libc::shm_unlink(cname.as_ptr()) };
450                }
451                return Err(error);
452            }
453        };
454        if let Err(error) = validate_minimum_size(name, actual_size, size) {
455            if created {
456                let _ = unsafe { libc::shm_unlink(cname.as_ptr()) };
457            }
458            return Err(error);
459        }
460
461        // Memory-map the segment.
462        // SAFETY: fd valid, size positive, flags well-known.
463        let ptr = unsafe {
464            libc::mmap(
465                std::ptr::null_mut(),
466                size,
467                libc::PROT_READ | libc::PROT_WRITE,
468                libc::MAP_SHARED,
469                fd.as_raw_fd(),
470                0,
471            )
472        };
473
474        if ptr == libc::MAP_FAILED {
475            let err = io::Error::last_os_error();
476            if created {
477                let _ = unsafe { libc::shm_unlink(cname.as_ptr()) };
478            }
479            return Err(err);
480        }
481
482        // SAFETY: mmap returned a non-null pointer (we just checked).
483        let ptr = NonNull::new(ptr.cast::<u8>())
484            .expect("mmap returned non-null on success");
485
486        Ok((
487            Self {
488                name: cname,
489                lock_path,
490                process_lock,
491                lock_dir: OnceLock::new(),
492                ptr,
493                len: size,
494                created,
495            },
496            initialization_lock,
497        ))
498    }
499
500    /// Raw mapped pointer to the start of the region.
501    pub fn as_ptr(&self) -> *mut u8 {
502        self.ptr.as_ptr()
503    }
504
505    /// Length of the mapped region (the `size` passed to `open_or_create`).
506    pub fn len(&self) -> usize {
507        self.len
508    }
509
510    pub fn is_empty(&self) -> bool {
511        self.len == 0
512    }
513
514    /// True when this handle was the one that created the segment.
515    /// Useful for picking which process performs first-time
516    /// initialization of the header.
517    pub fn created(&self) -> bool {
518        self.created
519    }
520
521    /// Acquire an exclusive cross-process lock tied to this SHM name.
522    ///
523    /// `flock` ownership is held by the kernel and is released when a process
524    /// exits or the descriptor closes, including abnormal termination.
525    pub fn lock_exclusive(&self) -> io::Result<ShmRegionLock> {
526        if !self.process_lock {
527            return Err(io::Error::new(
528                io::ErrorKind::InvalidInput,
529                "SHM region was opened without a process lock",
530            ));
531        }
532        lock_fd_exclusive(self.open_own_lock_file()?)
533    }
534
535    /// A new description of this region's lock file, for one critical
536    /// section, opened inside the directory verified for this region.
537    fn open_own_lock_file(&self) -> io::Result<OwnedFd> {
538        let (Some(dir), Some(name)) =
539            (self.lock_path.parent(), self.lock_path.file_name())
540        else {
541            return open_lock_file(&self.lock_path);
542        };
543        let verified = match self.lock_dir.get() {
544            Some(verified) => verified,
545            None => {
546                // Two first sections may both open it; one descriptor stays.
547                let _ = self.lock_dir.set(open_lock_dir(dir)?);
548                self.lock_dir.get().expect("lock directory was just set")
549            }
550        };
551        match open_lock_file_in(verified, name, &self.lock_path) {
552            // The directory was removed under a live region. The path is
553            // created and verified again for this section.
554            Err(error) if error.kind() == io::ErrorKind::NotFound => {
555                open_lock_file(&self.lock_path)
556            }
557            opened => opened,
558        }
559    }
560
561    #[cfg(test)]
562    pub(crate) fn try_lock_exclusive(&self) -> io::Result<ShmRegionLock> {
563        if !self.process_lock {
564            return Err(io::Error::new(
565                io::ErrorKind::InvalidInput,
566                "SHM region was opened without a process lock",
567            ));
568        }
569        let lock_fd = open_lock_file(&self.lock_path)?;
570        let rc = unsafe {
571            libc::flock(lock_fd.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB)
572        };
573        if rc == 0 {
574            Ok(ShmRegionLock { lock_fd })
575        } else {
576            Err(io::Error::last_os_error())
577        }
578    }
579
580    /// Remove the underlying segment name. Existing mappings stay
581    /// valid until each process drops its `ShmRegion`. Use only on
582    /// shutdown / fleet teardown by the process that owns lifecycle.
583    pub fn unlink(&self) -> io::Result<()> {
584        // SAFETY: name is a valid C string.
585        let rc = unsafe { libc::shm_unlink(self.name.as_ptr()) };
586        let shm_error = if rc != 0 {
587            let err = io::Error::last_os_error();
588            // ENOENT is fine — segment was already unlinked.
589            if err.raw_os_error() == Some(libc::ENOENT) {
590                None
591            } else {
592                Some(err)
593            }
594        } else {
595            None
596        };
597        let mut lock_error = match std::fs::remove_file(&self.lock_path) {
598            Ok(()) => None,
599            Err(error) if error.kind() == io::ErrorKind::NotFound => None,
600            Err(error) => Some(error),
601        };
602        let name = self.name.to_string_lossy();
603        // SAFETY: a plain syscall with no arguments.
604        let uid = unsafe { libc::geteuid() };
605        match companion_lock_files(&name, uid) {
606            Ok(files) => {
607                for file in files {
608                    if let Err(error) = std::fs::remove_file(&file)
609                        && error.kind() != io::ErrorKind::NotFound
610                    {
611                        lock_error.get_or_insert(error);
612                    }
613                }
614            }
615            Err(error) => {
616                lock_error.get_or_insert(error);
617            }
618        }
619        if let Some(error) = shm_error.or(lock_error) {
620            return Err(error);
621        }
622        Ok(())
623    }
624}
625
626#[cfg(not(target_os = "macos"))]
627fn configure_new_shm(
628    fd: &OwnedFd,
629    policy: ShmAccessPolicy,
630) -> io::Result<()> {
631    let Some(gid) = policy.gid() else {
632        return Ok(());
633    };
634    if gid == !0 {
635        return Err(io::Error::new(
636            io::ErrorKind::InvalidInput,
637            "invalid SHM group id",
638        ));
639    }
640    // SAFETY: this is our new, still owner-only object. Preserve its uid.
641    if unsafe { libc::fchown(fd.as_raw_fd(), !0, gid) } != 0 {
642        return Err(io::Error::last_os_error());
643    }
644    // Explicit group-sharing policy, applied only after group ownership is set.
645    if unsafe { libc::fchmod(fd.as_raw_fd(), policy.mode() as _) } != 0 {
646        return Err(io::Error::last_os_error());
647    }
648    Ok(())
649}
650
651fn shm_object_size(
652    fd: &OwnedFd,
653    name: &str,
654    owner_uid: u32,
655    policy: ShmAccessPolicy,
656) -> io::Result<usize> {
657    let mut stat = std::mem::MaybeUninit::<libc::stat>::uninit();
658    // SAFETY: `fd` is valid and `stat` points to writable storage.
659    let stat_rc = unsafe { libc::fstat(fd.as_raw_fd(), stat.as_mut_ptr()) };
660    if stat_rc != 0 {
661        return Err(io::Error::last_os_error());
662    }
663    // SAFETY: `fstat` succeeded and initialized the structure.
664    let stat = unsafe { stat.assume_init() };
665    let mode = u64::from(stat.st_mode) & 0o7777;
666    if stat.st_uid != owner_uid
667        || policy.gid().is_some_and(|gid| gid != stat.st_gid)
668        || mode & !u64::from(policy.mode()) != 0
669    {
670        return Err(io::Error::new(
671            io::ErrorKind::PermissionDenied,
672            format!(
673                "SHM segment {name} owner/group/mode do not satisfy {policy:?} for uid {owner_uid}"
674            ),
675        ));
676    }
677    let actual_size = stat.st_size;
678    usize::try_from(actual_size).map_err(|_| {
679        io::Error::new(
680            io::ErrorKind::InvalidData,
681            format!("SHM segment {name} reported invalid size {actual_size}"),
682        )
683    })
684}
685
686fn validate_minimum_size(
687    name: &str,
688    actual_size: usize,
689    minimum_size: usize,
690) -> io::Result<()> {
691    if actual_size < minimum_size {
692        return Err(io::Error::new(
693            io::ErrorKind::InvalidData,
694            format!(
695                "SHM segment {name} size {actual_size} is smaller than requested mapping {minimum_size}"
696            ),
697        ));
698    }
699    Ok(())
700}
701
702/// RAII guard for a [`ShmRegion`]'s process-recoverable exclusive lock.
703///
704/// Semantic crates use this when a current-state transition must be atomic
705/// across fleet processes. Dropping the guard releases the kernel lock.
706pub struct ShmRegionLock {
707    lock_fd: OwnedFd,
708}
709
710impl Drop for ShmRegionLock {
711    fn drop(&mut self) {
712        let _ = unsafe { libc::flock(self.lock_fd.as_raw_fd(), libc::LOCK_UN) };
713    }
714}
715
716fn lock_path_exclusive(lock_path: &Path) -> io::Result<ShmRegionLock> {
717    lock_fd_exclusive(open_lock_file(lock_path)?)
718}
719
720fn open_lock_file(lock_path: &Path) -> io::Result<OwnedFd> {
721    if let Some(dir) = lock_path.parent() {
722        ensure_lock_dir(dir)?;
723    }
724
725    let file = OpenOptions::new()
726        .read(true)
727        .write(true)
728        .create(true)
729        .mode(0o600)
730        .custom_flags(libc::O_CLOEXEC | libc::O_NOFOLLOW)
731        .open(lock_path)?;
732
733    ensure_own_lock_file(&file, lock_path)?;
734    Ok(file.into())
735}
736
737/// Refuses a lock file this user does not own.
738///
739/// The directory check makes this unreachable, which is the reason to make
740/// it anyway: it turns a property inferred from the directory's mode into
741/// one established about the descriptor that is about to be locked.
742fn ensure_own_lock_file(
743    file: &std::fs::File,
744    lock_path: &Path,
745) -> io::Result<()> {
746    use std::os::unix::fs::MetadataExt;
747
748    let uid = unsafe { libc::geteuid() };
749    let owner = file.metadata()?.uid();
750    if owner != uid {
751        return Err(io::Error::new(
752            io::ErrorKind::PermissionDenied,
753            format!(
754                "{} is owned by uid {owner} rather than {uid}",
755                lock_path.display()
756            ),
757        ));
758    }
759    Ok(())
760}
761
762/// Create and verify the lock directory, and hold it by descriptor.
763///
764/// The path is checked as before. The same properties are then established
765/// about the descriptor, so that what later opens resolve against is the
766/// directory that was verified and not whatever the path names by then.
767fn open_lock_dir(dir: &Path) -> io::Result<OwnedFd> {
768    ensure_lock_dir(dir)?;
769    let opened = OpenOptions::new()
770        .read(true)
771        .custom_flags(libc::O_DIRECTORY | libc::O_CLOEXEC | libc::O_NOFOLLOW)
772        .open(dir)?;
773    let uid = unsafe { libc::geteuid() };
774    ensure_private(&opened.metadata()?, dir, uid)?;
775    Ok(opened.into())
776}
777
778/// Open a lock file by name inside a verified lock directory.
779fn open_lock_file_in(
780    dir: &OwnedFd,
781    name: &std::ffi::OsStr,
782    lock_path: &Path,
783) -> io::Result<OwnedFd> {
784    use std::os::unix::ffi::OsStrExt;
785
786    let name = CString::new(name.as_bytes()).map_err(|_| {
787        io::Error::new(io::ErrorKind::InvalidInput, "lock name has nul byte")
788    })?;
789    let fd = loop {
790        // SAFETY: `dir` is an open directory and `name` a valid C string.
791        let fd = unsafe {
792            libc::openat(
793                dir.as_raw_fd(),
794                name.as_ptr(),
795                libc::O_RDWR
796                    | libc::O_CREAT
797                    | libc::O_CLOEXEC
798                    | libc::O_NOFOLLOW,
799                0o600 as libc::c_uint,
800            )
801        };
802        if fd >= 0 {
803            break fd;
804        }
805        let error = io::Error::last_os_error();
806        if error.kind() != io::ErrorKind::Interrupted {
807            return Err(error);
808        }
809    };
810    // SAFETY: `openat` returned a descriptor this function now owns.
811    let file = std::fs::File::from(unsafe { OwnedFd::from_raw_fd(fd) });
812    ensure_own_lock_file(&file, lock_path)?;
813    Ok(file.into())
814}
815
816fn lock_fd_exclusive(lock_fd: OwnedFd) -> io::Result<ShmRegionLock> {
817    loop {
818        let rc = unsafe { libc::flock(lock_fd.as_raw_fd(), libc::LOCK_EX) };
819        if rc == 0 {
820            return Ok(ShmRegionLock { lock_fd });
821        }
822        let error = io::Error::last_os_error();
823        if error.kind() != io::ErrorKind::Interrupted {
824            return Err(error);
825        }
826    }
827}
828
829impl Drop for ShmRegion {
830    fn drop(&mut self) {
831        // SAFETY: ptr came from mmap of `self.len` bytes; munmap is the inverse.
832        unsafe {
833            libc::munmap(self.ptr.as_ptr().cast(), self.len);
834        }
835    }
836}
837
838// SAFETY: the underlying region is shared memory and synchronization
839// happens at the slot level (atomic seq counters); the handle itself
840// is just a pointer + length, safe to send/share.
841unsafe impl Send for ShmRegion {}
842unsafe impl Sync for ShmRegion {}
843
844/// Build the conventional name for an Orbit ring segment.
845pub fn ring_segment_name(
846    fleet_name: &str,
847    kind: u8,
848) -> String {
849    // SAFETY: `geteuid` always returns a value; no error path.
850    let uid = unsafe { libc::geteuid() };
851    ring_segment_name_for_uid(fleet_name, kind, uid)
852}
853
854/// Build the conventional name for an Orbit ring segment owned by `uid`.
855///
856/// This form is intended for inspection and lifecycle tools that need to
857/// address a user other than their own effective uid.
858pub fn ring_segment_name_for_uid(
859    fleet_name: &str,
860    kind: u8,
861    uid: u32,
862) -> String {
863    format!("/{SHM_NAMESPACE}-{fleet_name}-{kind}-{uid}")
864}
865
866/// Where a fleet's members hold their presence: `<lock dir>/orbit-<fleet>.fleet`.
867pub fn fleet_lock_path(fleet_name: &str) -> PathBuf {
868    lock_dir().join(format!("{SHM_NAMESPACE}-{fleet_name}.fleet"))
869}
870
871/// The same for another user's fleet, for lifecycle tools that address a uid
872/// other than their own.
873pub fn fleet_lock_path_for_uid(
874    fleet_name: &str,
875    uid: u32,
876) -> PathBuf {
877    lock_dir_for_uid(uid).join(format!("{SHM_NAMESPACE}-{fleet_name}.fleet"))
878}
879
880/// A process's membership in a fleet: a shared `flock` on the fleet's lock
881/// file, held for as long as this lives and released by the kernel when the
882/// process dies, however it dies. It is what [`try_lock_fleet_exclusive`]
883/// contends with, so a tool that removes the fleet's segments cannot do so
884/// while any member is alive.
885pub struct FleetMembership {
886    lock_fd: OwnedFd,
887}
888
889impl Drop for FleetMembership {
890    fn drop(&mut self) {
891        let _ = unsafe { libc::flock(self.lock_fd.as_raw_fd(), libc::LOCK_UN) };
892    }
893}
894
895/// Join the fleet's membership. Waits for a lifecycle tool that holds the
896/// exclusive lock at that moment; a clear in progress finishes first.
897pub fn join_fleet_membership(fleet_name: &str) -> io::Result<FleetMembership> {
898    let lock_fd = open_lock_file(&fleet_lock_path(fleet_name))?;
899    // SAFETY: `lock_fd` is an open descriptor owned by this call.
900    let rc = unsafe { libc::flock(lock_fd.as_raw_fd(), libc::LOCK_SH) };
901    if rc != 0 {
902        return Err(io::Error::last_os_error());
903    }
904    Ok(FleetMembership { lock_fd })
905}
906
907/// Protect an existing fleet against clearing while a diagnostic reader pins
908/// its arena. Never creates a membership file or reserves a writer lane.
909pub fn hold_existing_fleet(fleet_name: &str) -> io::Result<FleetMembership> {
910    use std::os::unix::fs::MetadataExt;
911    let path = fleet_lock_path(fleet_name);
912    let file = OpenOptions::new()
913        .read(true)
914        .custom_flags(libc::O_CLOEXEC | libc::O_NOFOLLOW)
915        .open(&path)?;
916    let metadata = file.metadata()?;
917    if !metadata.is_file() || metadata.uid() != unsafe { libc::geteuid() } {
918        return Err(io::Error::new(
919            io::ErrorKind::PermissionDenied,
920            "fleet membership file has an unexpected owner or type",
921        ));
922    }
923    let lock_fd: OwnedFd = file.into();
924    loop {
925        let rc = unsafe { libc::flock(lock_fd.as_raw_fd(), libc::LOCK_SH) };
926        if rc == 0 {
927            return Ok(FleetMembership { lock_fd });
928        }
929        let error = io::Error::last_os_error();
930        if error.kind() != io::ErrorKind::Interrupted {
931            return Err(error);
932        }
933    }
934}
935
936/// Exclusive hold on a fleet, for the tool that removes its segments.
937///
938/// `Ok(None)` means a member is alive and the fleet must not be touched;
939/// there is deliberately no way to force past it. `Ok(Some(_))` keeps the
940/// fleet closed to new members until the guard is dropped, so a removal
941/// cannot interleave with a start. Never waits.
942pub fn try_lock_fleet_exclusive(
943    fleet_name: &str,
944    uid: u32,
945) -> io::Result<Option<ShmRegionLock>> {
946    // Creating the file when no member ever joined is right: the guard then
947    // keeps a first member from starting in the middle of a removal.
948    let lock_fd = open_lock_file(&fleet_lock_path_for_uid(fleet_name, uid))?;
949    // SAFETY: `lock_fd` is an open descriptor owned by this call.
950    let rc = unsafe {
951        libc::flock(lock_fd.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB)
952    };
953    if rc == 0 {
954        return Ok(Some(ShmRegionLock { lock_fd }));
955    }
956    let error = io::Error::last_os_error();
957    if error.kind() == io::ErrorKind::WouldBlock {
958        return Ok(None);
959    }
960    Err(error)
961}
962
963/// One process's hold on one lane of a segment: an exclusive `flock` on the
964/// lane's own lock file, kept for as long as this lives and released by the
965/// kernel when the process dies, however it dies.
966///
967/// It is how a lane's owner is known to be gone without a timeout and without
968/// a PID, which another PID namespace would read wrongly: a hold that can be
969/// taken has nobody behind it.
970pub struct LaneHold {
971    lock_fd: OwnedFd,
972}
973
974/// A lane lock prepared at opening, with no path construction or file open
975/// during acquisition. Each opener owns a distinct file description.
976/// Open after fork; inherited instances cannot be used by the child.
977pub struct PreparedLaneLock {
978    fd: OwnedFd,
979    busy: AtomicBool,
980    pid: u32,
981}
982
983impl PreparedLaneLock {
984    pub fn open(
985        shm_name: &str,
986        lane: usize,
987    ) -> io::Result<Self> {
988        let name = format!("{}.lane{lane}", shm_name.trim_start_matches('/'));
989        Ok(Self {
990            fd: open_lock_file(&lock_file_path(&name))?,
991            busy: AtomicBool::new(false),
992            pid: std::process::id(),
993        })
994    }
995
996    /// One local CAS and at most one LOCK_NB syscall. No owner wait or retry.
997    pub fn try_lock(&self) -> io::Result<Option<PreparedLaneGuard<'_>>> {
998        if self.pid != std::process::id() {
999            return Err(io::Error::from(io::ErrorKind::PermissionDenied));
1000        }
1001        if self
1002            .busy
1003            .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
1004            .is_err()
1005        {
1006            return Ok(None);
1007        }
1008        // SAFETY: the local flag excludes overlapping use of this description.
1009        if unsafe {
1010            libc::flock(self.fd.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB)
1011        } == 0
1012        {
1013            return Ok(Some(PreparedLaneGuard { lock: self }));
1014        }
1015        let error = io::Error::last_os_error();
1016        self.busy.store(false, Ordering::Release);
1017        if error.kind() == io::ErrorKind::WouldBlock {
1018            Ok(None)
1019        } else {
1020            Err(error)
1021        }
1022    }
1023}
1024
1025pub struct PreparedLaneGuard<'a> {
1026    lock: &'a PreparedLaneLock,
1027}
1028
1029impl Drop for PreparedLaneGuard<'_> {
1030    fn drop(&mut self) {
1031        if self.lock.pid == std::process::id() {
1032            // SAFETY: this guard owns this file description's exclusive hold.
1033            unsafe {
1034                libc::flock(self.lock.fd.as_raw_fd(), libc::LOCK_UN);
1035            }
1036            self.lock.busy.store(false, Ordering::Release);
1037        }
1038    }
1039}
1040
1041impl Drop for LaneHold {
1042    fn drop(&mut self) {
1043        let _ = unsafe { libc::flock(self.lock_fd.as_raw_fd(), libc::LOCK_UN) };
1044    }
1045}
1046
1047/// Take lane `lane` of segment `shm_name`, if no live process holds it.
1048///
1049/// `Ok(None)` means a process holding it is alive. Never waits.
1050pub fn try_hold_lane(
1051    shm_name: &str,
1052    lane: usize,
1053) -> io::Result<Option<LaneHold>> {
1054    let name = format!("{}.lane{lane}", shm_name.trim_start_matches('/'));
1055    let lock_fd = open_lock_file(&lock_file_path(&name))?;
1056    // SAFETY: `lock_fd` is an open descriptor owned by this call.
1057    let rc = unsafe {
1058        libc::flock(lock_fd.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB)
1059    };
1060    if rc == 0 {
1061        return Ok(Some(LaneHold { lock_fd }));
1062    }
1063    let error = io::Error::last_os_error();
1064    if error.kind() == io::ErrorKind::WouldBlock {
1065        return Ok(None);
1066    }
1067    Err(error)
1068}
1069
1070/// One process's hold on one directory-watcher slot of a segment: the same
1071/// kernel-released `flock` as a [`LaneHold`], on a lock file of its own.
1072///
1073/// A watcher is not a lane. It serves nothing and takes no frames; it only
1074/// wants to be woken when the lane directory moves. Its slot is still owned
1075/// the way a lane is, so a slot a dead process left can be taken by the next
1076/// one without a timeout.
1077pub struct WatcherHold {
1078    lock_fd: OwnedFd,
1079}
1080
1081impl Drop for WatcherHold {
1082    fn drop(&mut self) {
1083        let _ = unsafe { libc::flock(self.lock_fd.as_raw_fd(), libc::LOCK_UN) };
1084    }
1085}
1086
1087/// Take watcher slot `slot` of segment `shm_name`, if no live process holds
1088/// it.
1089///
1090/// `Ok(None)` means a process holding it is alive. Never waits.
1091pub fn try_hold_watcher(
1092    shm_name: &str,
1093    slot: usize,
1094) -> io::Result<Option<WatcherHold>> {
1095    let name = format!("{}.watch{slot}", shm_name.trim_start_matches('/'));
1096    let lock_fd = open_lock_file(&lock_file_path(&name))?;
1097    // SAFETY: `lock_fd` is an open descriptor owned by this call.
1098    let rc = unsafe {
1099        libc::flock(lock_fd.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB)
1100    };
1101    if rc == 0 {
1102        return Ok(Some(WatcherHold { lock_fd }));
1103    }
1104    let error = io::Error::last_os_error();
1105    if error.kind() == io::ErrorKind::WouldBlock {
1106        return Ok(None);
1107    }
1108    Err(error)
1109}
1110
1111/// Every lock file beside segment `shm_name` of `uid`: the region's own, one
1112/// per lane that was ever held ([`try_hold_lane`]) and one per watcher slot
1113/// ([`try_hold_watcher`]). What removes a segment
1114/// removes these with it; an unlocked one left behind is harmless, but it is
1115/// one more file per lane for every segment that ever existed.
1116pub fn companion_lock_files(
1117    shm_name: &str,
1118    uid: u32,
1119) -> io::Result<Vec<PathBuf>> {
1120    let base = shm_name.trim_start_matches('/');
1121    let own = format!("{base}.lock");
1122    let lane = format!("{base}.lane");
1123    let watch = format!("{base}.watch");
1124    let dir = lock_dir_for_uid(uid);
1125    let entries = match std::fs::read_dir(&dir) {
1126        Ok(entries) => entries,
1127        Err(error) if error.kind() == io::ErrorKind::NotFound => {
1128            return Ok(Vec::new());
1129        }
1130        Err(error) => return Err(error),
1131    };
1132    let mut found = Vec::new();
1133    for entry in entries {
1134        let entry = entry?;
1135        let Some(file) = entry.file_name().to_str().map(str::to_owned) else {
1136            continue;
1137        };
1138        if file == own
1139            || ((file.starts_with(&lane) || file.starts_with(&watch))
1140                && file.ends_with(".lock"))
1141        {
1142            found.push(entry.path());
1143        }
1144    }
1145    Ok(found)
1146}
1147
1148fn lock_file_path(shm_name: &str) -> PathBuf {
1149    lock_dir().join(format!("{}.lock", shm_name.trim_start_matches('/')))
1150}
1151
1152/// Where the companion lock files live.
1153///
1154/// They used to live in `/tmp` directly, as
1155/// `/tmp/orbit-{fleet}-{kind}-{uid}.lock`, opened `O_CREAT` without `O_EXCL`
1156/// and without asking who owned what the open found. `/tmp` is world-writable,
1157/// so any local user could create that file first and then hold `LOCK_EX` on it
1158/// for as long as they liked: every process in the fleet would sit in `flock` —
1159/// not fail, block — waiting for a lock it was never going to get. The SHM
1160/// segment name is uid-scoped and so cannot be squatted this way; the lock path
1161/// was not. `O_NOFOLLOW` prevented the symlink version of the trick and nothing
1162/// else.
1163///
1164/// They now live in a per-uid directory created `0700`, which a user who is not
1165/// us cannot put a file into. What such a user can still do is create the
1166/// directory first, so its owner and mode are checked on every open rather than
1167/// assumed from having created it: a directory that is not ours, or not
1168/// private, fails the open with the path in the message instead of parking the
1169/// process on a lock.
1170///
1171/// The root is part of the owner-only SHM namespace, not a session preference.
1172/// Every process addressing the same SHM name must open the same lock inode,
1173/// including after environment scrubbing and from another login session.
1174fn lock_dir() -> PathBuf {
1175    let uid = unsafe { libc::geteuid() };
1176    lock_dir_for_uid(uid)
1177}
1178
1179/// Locate the existing per-user companion lock directory without creating it.
1180/// Tools can use its filenames as discovery hints, but must verify SHM objects.
1181/// `XDG_RUNTIME_DIR` and temporary-directory variables never change this identity.
1182pub fn lock_dir_for_uid(uid: u32) -> PathBuf {
1183    Path::new("/tmp").join(format!("{SHM_NAMESPACE}-{uid}"))
1184}
1185
1186/// Creates the lock directory if it is missing and refuses it if it is not
1187/// ours. Called when a lock is actually taken rather than when a region is
1188/// opened: a region that never locks has nothing to squat, and failing its open
1189/// on a directory it does not use would hand an attacker a wider outage than
1190/// the one being closed.
1191fn ensure_lock_dir(dir: &Path) -> io::Result<()> {
1192    use std::os::unix::fs::DirBuilderExt;
1193
1194    let uid = unsafe { libc::geteuid() };
1195    match std::fs::DirBuilder::new().mode(0o700).create(dir) {
1196        Ok(()) => {}
1197        Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {}
1198        Err(error) => return Err(error),
1199    }
1200
1201    ensure_private_dir(dir, uid)
1202}
1203
1204/// Refuses a lock directory that someone else could write to.
1205///
1206/// `symlink_metadata` rather than `metadata`: a symlink pointing at a directory
1207/// we do own would otherwise pass while the lock files landed somewhere the
1208/// attacker chose.
1209fn ensure_private_dir(
1210    dir: &Path,
1211    uid: u32,
1212) -> io::Result<()> {
1213    ensure_private(&std::fs::symlink_metadata(dir)?, dir, uid)
1214}
1215
1216/// The same refusal, about metadata already taken from a path or a descriptor.
1217fn ensure_private(
1218    metadata: &std::fs::Metadata,
1219    dir: &Path,
1220    uid: u32,
1221) -> io::Result<()> {
1222    use std::os::unix::fs::{MetadataExt, PermissionsExt};
1223
1224    if !metadata.is_dir() {
1225        return Err(io::Error::new(
1226            io::ErrorKind::PermissionDenied,
1227            format!("{} is not a directory", dir.display()),
1228        ));
1229    }
1230    if metadata.uid() != uid {
1231        return Err(io::Error::new(
1232            io::ErrorKind::PermissionDenied,
1233            format!(
1234                "{} is owned by uid {} rather than {uid}; refusing to lock in a directory \
1235                 another user controls",
1236                dir.display(),
1237                metadata.uid()
1238            ),
1239        ));
1240    }
1241    if metadata.permissions().mode() & 0o077 != 0 {
1242        return Err(io::Error::new(
1243            io::ErrorKind::PermissionDenied,
1244            format!(
1245                "{} is mode {:o}; refusing to lock in a directory others can write to",
1246                dir.display(),
1247                metadata.permissions().mode() & 0o777
1248            ),
1249        ));
1250    }
1251
1252    Ok(())
1253}
1254
1255#[cfg(test)]
1256#[path = "../tests/shm/identity.rs"]
1257mod lock_identity_tests;
1258
1259#[cfg(test)]
1260mod fleet_lock_tests {
1261    use super::{join_fleet_membership, try_lock_fleet_exclusive};
1262
1263    #[test]
1264    fn prepared_lane_excludes_threads_and_distinct_file_descriptions() {
1265        let name = format!("prepared-{}", std::process::id());
1266        let first = super::PreparedLaneLock::open(&name, 0).unwrap();
1267        let second = super::PreparedLaneLock::open(&name, 0).unwrap();
1268        let hold = first.try_lock().unwrap().unwrap();
1269        assert!(first.try_lock().unwrap().is_none());
1270        std::thread::scope(|scope| {
1271            assert!(
1272                scope
1273                    .spawn(|| first.try_lock().unwrap().is_none())
1274                    .join()
1275                    .unwrap()
1276            );
1277        });
1278        assert!(second.try_lock().unwrap().is_none());
1279        drop(hold);
1280        let other = second.try_lock().unwrap().unwrap();
1281        assert!(first.try_lock().unwrap().is_none());
1282        drop(other);
1283        drop(first);
1284        drop(second);
1285        std::fs::remove_file(super::lock_file_path(&format!("{name}.lane0")))
1286            .unwrap();
1287    }
1288
1289    /// A member alive means the fleet cannot be cleared, and there is no
1290    /// flag that says otherwise; the member going away is what opens it.
1291    #[test]
1292    fn a_member_holds_the_fleet_against_exclusive_takers() {
1293        let fleet = format!("fl{:x}", std::process::id());
1294        let uid = unsafe { libc::geteuid() };
1295
1296        let member = join_fleet_membership(&fleet).expect("join");
1297        assert!(try_lock_fleet_exclusive(&fleet, uid).expect("try").is_none());
1298
1299        drop(member);
1300        let exclusive = try_lock_fleet_exclusive(&fleet, uid).expect("try");
1301        assert!(exclusive.is_some());
1302        // And a member cannot join while a removal holds the fleet: the
1303        // shared lock would block, which is the behaviour, not a test to run.
1304        drop(exclusive);
1305        let _ =
1306            std::fs::remove_file(super::fleet_lock_path_for_uid(&fleet, uid));
1307    }
1308}