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