1#![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;
45use std::sync::atomic::{AtomicBool, Ordering};
46
47mod group;
48pub use group::GroupAccess;
49
50pub const SHM_NAMESPACE: &str = "orbit";
52
53#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
64pub enum ShmAccessPolicy {
65 #[default]
67 OwnerOnly,
68 GroupRead { gid: u32 },
70 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
98pub enum ShmValidation {
99 Missing,
101 Valid { actual_size: usize },
103}
104
105pub struct ShmRegion {
109 name: CString,
110 lock_path: PathBuf,
111 process_lock: bool,
117 ptr: NonNull<u8>,
118 len: usize,
119 created: bool,
123}
124
125impl ShmRegion {
126 pub fn validate_existing(
136 name: &str,
137 minimum_size: usize,
138 ) -> io::Result<ShmValidation> {
139 Self::validate_existing_with_policy(
140 name,
141 minimum_size,
142 unsafe { libc::geteuid() },
143 ShmAccessPolicy::default(),
144 )
145 }
146
147 pub fn validate_existing_with_policy(
150 name: &str,
151 minimum_size: usize,
152 owner_uid: u32,
153 policy: ShmAccessPolicy,
154 ) -> io::Result<ShmValidation> {
155 let cname = CString::new(name).map_err(|_| {
156 io::Error::new(io::ErrorKind::InvalidInput, "shm name has nul byte")
157 })?;
158 let raw_fd = loop {
159 let fd = unsafe {
163 libc::shm_open(cname.as_ptr(), libc::O_RDONLY, 0o600)
164 };
165 if fd >= 0 {
166 break fd;
167 }
168 let error = io::Error::last_os_error();
169 if error.kind() == io::ErrorKind::Interrupted {
170 continue;
171 }
172 if error.raw_os_error() == Some(libc::ENOENT) {
173 return Ok(ShmValidation::Missing);
174 }
175 return Err(error);
176 };
177 let fd = unsafe { OwnedFd::from_raw_fd(raw_fd) };
180 let actual_size = shm_object_size(&fd, name, owner_uid, policy)?;
181 validate_minimum_size(name, actual_size, minimum_size)?;
182 Ok(ShmValidation::Valid { actual_size })
183 }
184
185 pub(crate) fn open_existing_read_only(
192 name: &str,
193 minimum_size: usize,
194 owner_uid: u32,
195 policy: ShmAccessPolicy,
196 ) -> io::Result<Self> {
197 Self::open_existing(name, minimum_size, owner_uid, policy, false)
198 }
199
200 pub fn open_existing_read_write(
203 name: &str,
204 minimum_size: usize,
205 owner_uid: u32,
206 policy: ShmAccessPolicy,
207 ) -> io::Result<Self> {
208 Self::open_existing(name, minimum_size, owner_uid, policy, true)
209 }
210
211 fn open_existing(
212 name: &str,
213 minimum_size: usize,
214 owner_uid: u32,
215 policy: ShmAccessPolicy,
216 writable: bool,
217 ) -> io::Result<Self> {
218 let cname = CString::new(name).map_err(|_| {
219 io::Error::new(io::ErrorKind::InvalidInput, "shm name has nul byte")
220 })?;
221 let raw_fd = loop {
222 let fd = unsafe {
227 libc::shm_open(
228 cname.as_ptr(),
229 if writable { libc::O_RDWR } else { libc::O_RDONLY },
230 0o600,
231 )
232 };
233 if fd >= 0 {
234 break fd;
235 }
236 let error = io::Error::last_os_error();
237 if error.kind() == io::ErrorKind::Interrupted {
238 continue;
239 }
240 return Err(error);
241 };
242 let fd = unsafe { OwnedFd::from_raw_fd(raw_fd) };
245 let actual_size = shm_object_size(&fd, name, owner_uid, policy)?;
246 validate_minimum_size(name, actual_size, minimum_size)?;
247
248 let ptr = unsafe {
253 libc::mmap(
254 std::ptr::null_mut(),
255 actual_size,
256 libc::PROT_READ | if writable { libc::PROT_WRITE } else { 0 },
257 libc::MAP_SHARED,
258 fd.as_raw_fd(),
259 0,
260 )
261 };
262 if ptr == libc::MAP_FAILED {
263 return Err(io::Error::last_os_error());
264 }
265 let ptr = NonNull::new(ptr.cast::<u8>())
267 .expect("mmap returned non-null on success");
268
269 Ok(Self {
270 lock_path: lock_dir_for_uid(owner_uid)
271 .join(format!("{}.lock", name.trim_start_matches('/'))),
272 name: cname,
273 process_lock: false,
274 ptr,
275 len: actual_size,
276 created: false,
277 })
278 }
279
280 pub fn open_or_create(
289 name: &str,
290 size: usize,
291 ) -> io::Result<Self> {
292 Self::open_or_create_with_policy(name, size, ShmAccessPolicy::default())
293 }
294
295 pub fn open_or_create_with_policy(
299 name: &str,
300 size: usize,
301 policy: ShmAccessPolicy,
302 ) -> io::Result<Self> {
303 let (region, initialization_lock) =
304 Self::open_or_create_inner(name, size, false, policy)?;
305 debug_assert!(initialization_lock.is_none());
306 Ok(region)
307 }
308
309 pub fn open_or_create_locked(
313 name: &str,
314 size: usize,
315 ) -> io::Result<(Self, ShmRegionLock)> {
316 Self::open_or_create_locked_with_policy(
317 name,
318 size,
319 ShmAccessPolicy::default(),
320 )
321 }
322
323 pub fn open_or_create_locked_with_policy(
326 name: &str,
327 size: usize,
328 policy: ShmAccessPolicy,
329 ) -> io::Result<(Self, ShmRegionLock)> {
330 let (region, initialization_lock) =
331 Self::open_or_create_inner(name, size, true, policy)?;
332 Ok((
333 region,
334 initialization_lock
335 .expect("locked SHM open must return its initialization lock"),
336 ))
337 }
338
339 fn open_or_create_inner(
340 name: &str,
341 size: usize,
342 process_lock: bool,
343 policy: ShmAccessPolicy,
344 ) -> io::Result<(Self, Option<ShmRegionLock>)> {
345 let cname = CString::new(name).map_err(|_| {
346 io::Error::new(io::ErrorKind::InvalidInput, "shm name has nul byte")
347 })?;
348 let lock_path = lock_file_path(name);
349 let initialization_lock = if process_lock {
350 Some(lock_path_exclusive(&lock_path)?)
351 } else {
352 None
353 };
354
355 #[cfg(target_os = "macos")]
358 let can_create =
359 policy.gid().is_none_or(|gid| gid == unsafe { libc::getegid() });
360 #[cfg(target_os = "macos")]
361 let creation_mode = policy.mode();
362 #[cfg(not(target_os = "macos"))]
364 let (can_create, creation_mode) = (true, 0o600u32);
365 #[cfg(not(target_vendor = "apple"))]
367 let creation_mode = creation_mode as libc::mode_t;
368 let (raw_fd, created) = unsafe {
370 let fd = if can_create {
372 libc::shm_open(
373 cname.as_ptr(),
374 libc::O_RDWR | libc::O_CREAT | libc::O_EXCL,
375 creation_mode,
376 )
377 } else {
378 libc::shm_open(cname.as_ptr(), libc::O_RDWR, 0)
379 };
380 if fd >= 0 {
381 (fd, can_create)
382 } else {
383 let err = io::Error::last_os_error();
385 if !can_create && err.raw_os_error() == Some(libc::ENOENT) {
386 return Err(io::Error::new(
387 io::ErrorKind::InvalidInput,
388 "creating group-shared SHM requires the selected effective gid",
389 ));
390 }
391 if err.raw_os_error() != Some(libc::EEXIST) {
392 return Err(err);
393 }
394 let fd = libc::shm_open(cname.as_ptr(), libc::O_RDWR, 0o600);
395 if fd < 0 {
396 return Err(io::Error::last_os_error());
397 }
398 (fd, false)
399 }
400 };
401 let fd = unsafe { OwnedFd::from_raw_fd(raw_fd) };
404
405 #[cfg(not(target_os = "macos"))]
406 if created && let Err(error) = configure_new_shm(&fd, policy) {
407 let _ = unsafe { libc::shm_unlink(cname.as_ptr()) };
408 return Err(error);
409 }
410
411 let owner_uid = unsafe { libc::geteuid() };
414 if let Err(error) = shm_object_size(&fd, name, owner_uid, policy) {
415 if created {
416 let _ = unsafe { libc::shm_unlink(cname.as_ptr()) };
417 }
418 return Err(error);
419 }
420
421 if created {
423 let rc =
425 unsafe { libc::ftruncate(fd.as_raw_fd(), size as libc::off_t) };
426 if rc != 0 {
427 let err = io::Error::last_os_error();
428 let _ = unsafe { libc::shm_unlink(cname.as_ptr()) };
429 return Err(err);
430 }
431 }
432
433 let actual_size = match shm_object_size(&fd, name, owner_uid, policy) {
438 Ok(actual_size) => actual_size,
439 Err(error) => {
440 if created {
441 let _ = unsafe { libc::shm_unlink(cname.as_ptr()) };
442 }
443 return Err(error);
444 }
445 };
446 if let Err(error) = validate_minimum_size(name, actual_size, size) {
447 if created {
448 let _ = unsafe { libc::shm_unlink(cname.as_ptr()) };
449 }
450 return Err(error);
451 }
452
453 let ptr = unsafe {
456 libc::mmap(
457 std::ptr::null_mut(),
458 size,
459 libc::PROT_READ | libc::PROT_WRITE,
460 libc::MAP_SHARED,
461 fd.as_raw_fd(),
462 0,
463 )
464 };
465
466 if ptr == libc::MAP_FAILED {
467 let err = io::Error::last_os_error();
468 if created {
469 let _ = unsafe { libc::shm_unlink(cname.as_ptr()) };
470 }
471 return Err(err);
472 }
473
474 let ptr = NonNull::new(ptr.cast::<u8>())
476 .expect("mmap returned non-null on success");
477
478 Ok((
479 Self {
480 name: cname,
481 lock_path,
482 process_lock,
483 ptr,
484 len: size,
485 created,
486 },
487 initialization_lock,
488 ))
489 }
490
491 pub fn as_ptr(&self) -> *mut u8 {
493 self.ptr.as_ptr()
494 }
495
496 pub fn len(&self) -> usize {
498 self.len
499 }
500
501 pub fn is_empty(&self) -> bool {
502 self.len == 0
503 }
504
505 pub fn created(&self) -> bool {
509 self.created
510 }
511
512 pub fn lock_exclusive(&self) -> io::Result<ShmRegionLock> {
517 if !self.process_lock {
518 return Err(io::Error::new(
519 io::ErrorKind::InvalidInput,
520 "SHM region was opened without a process lock",
521 ));
522 }
523 lock_path_exclusive(&self.lock_path)
524 }
525
526 #[cfg(test)]
527 pub(crate) fn try_lock_exclusive(&self) -> io::Result<ShmRegionLock> {
528 if !self.process_lock {
529 return Err(io::Error::new(
530 io::ErrorKind::InvalidInput,
531 "SHM region was opened without a process lock",
532 ));
533 }
534 let lock_fd = open_lock_file(&self.lock_path)?;
535 let rc = unsafe {
536 libc::flock(lock_fd.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB)
537 };
538 if rc == 0 {
539 Ok(ShmRegionLock { lock_fd })
540 } else {
541 Err(io::Error::last_os_error())
542 }
543 }
544
545 pub fn unlink(&self) -> io::Result<()> {
549 let rc = unsafe { libc::shm_unlink(self.name.as_ptr()) };
551 let shm_error = if rc != 0 {
552 let err = io::Error::last_os_error();
553 if err.raw_os_error() == Some(libc::ENOENT) {
555 None
556 } else {
557 Some(err)
558 }
559 } else {
560 None
561 };
562 let mut lock_error = match std::fs::remove_file(&self.lock_path) {
563 Ok(()) => None,
564 Err(error) if error.kind() == io::ErrorKind::NotFound => None,
565 Err(error) => Some(error),
566 };
567 let name = self.name.to_string_lossy();
568 let uid = unsafe { libc::geteuid() };
570 match companion_lock_files(&name, uid) {
571 Ok(files) => {
572 for file in files {
573 if let Err(error) = std::fs::remove_file(&file)
574 && error.kind() != io::ErrorKind::NotFound
575 {
576 lock_error.get_or_insert(error);
577 }
578 }
579 }
580 Err(error) => {
581 lock_error.get_or_insert(error);
582 }
583 }
584 if let Some(error) = shm_error.or(lock_error) {
585 return Err(error);
586 }
587 Ok(())
588 }
589}
590
591#[cfg(not(target_os = "macos"))]
592fn configure_new_shm(
593 fd: &OwnedFd,
594 policy: ShmAccessPolicy,
595) -> io::Result<()> {
596 let Some(gid) = policy.gid() else {
597 return Ok(());
598 };
599 if gid == !0 {
600 return Err(io::Error::new(
601 io::ErrorKind::InvalidInput,
602 "invalid SHM group id",
603 ));
604 }
605 if unsafe { libc::fchown(fd.as_raw_fd(), !0, gid) } != 0 {
607 return Err(io::Error::last_os_error());
608 }
609 if unsafe { libc::fchmod(fd.as_raw_fd(), policy.mode() as _) } != 0 {
611 return Err(io::Error::last_os_error());
612 }
613 Ok(())
614}
615
616fn shm_object_size(
617 fd: &OwnedFd,
618 name: &str,
619 owner_uid: u32,
620 policy: ShmAccessPolicy,
621) -> io::Result<usize> {
622 let mut stat = std::mem::MaybeUninit::<libc::stat>::uninit();
623 let stat_rc = unsafe { libc::fstat(fd.as_raw_fd(), stat.as_mut_ptr()) };
625 if stat_rc != 0 {
626 return Err(io::Error::last_os_error());
627 }
628 let stat = unsafe { stat.assume_init() };
630 let mode = u64::from(stat.st_mode) & 0o7777;
631 if stat.st_uid != owner_uid
632 || policy.gid().is_some_and(|gid| gid != stat.st_gid)
633 || mode & !u64::from(policy.mode()) != 0
634 {
635 return Err(io::Error::new(
636 io::ErrorKind::PermissionDenied,
637 format!(
638 "SHM segment {name} owner/group/mode do not satisfy {policy:?} for uid {owner_uid}"
639 ),
640 ));
641 }
642 let actual_size = stat.st_size;
643 usize::try_from(actual_size).map_err(|_| {
644 io::Error::new(
645 io::ErrorKind::InvalidData,
646 format!("SHM segment {name} reported invalid size {actual_size}"),
647 )
648 })
649}
650
651fn validate_minimum_size(
652 name: &str,
653 actual_size: usize,
654 minimum_size: usize,
655) -> io::Result<()> {
656 if actual_size < minimum_size {
657 return Err(io::Error::new(
658 io::ErrorKind::InvalidData,
659 format!(
660 "SHM segment {name} size {actual_size} is smaller than requested mapping {minimum_size}"
661 ),
662 ));
663 }
664 Ok(())
665}
666
667pub struct ShmRegionLock {
672 lock_fd: OwnedFd,
673}
674
675impl Drop for ShmRegionLock {
676 fn drop(&mut self) {
677 let _ = unsafe { libc::flock(self.lock_fd.as_raw_fd(), libc::LOCK_UN) };
678 }
679}
680
681fn lock_path_exclusive(lock_path: &Path) -> io::Result<ShmRegionLock> {
682 lock_fd_exclusive(open_lock_file(lock_path)?)
683}
684
685fn open_lock_file(lock_path: &Path) -> io::Result<OwnedFd> {
686 use std::os::unix::fs::MetadataExt;
687
688 if let Some(dir) = lock_path.parent() {
689 ensure_lock_dir(dir)?;
690 }
691
692 let file = OpenOptions::new()
693 .read(true)
694 .write(true)
695 .create(true)
696 .mode(0o600)
697 .custom_flags(libc::O_CLOEXEC | libc::O_NOFOLLOW)
698 .open(lock_path)?;
699
700 let uid = unsafe { libc::geteuid() };
704 let owner = file.metadata()?.uid();
705 if owner != uid {
706 return Err(io::Error::new(
707 io::ErrorKind::PermissionDenied,
708 format!(
709 "{} is owned by uid {owner} rather than {uid}",
710 lock_path.display()
711 ),
712 ));
713 }
714
715 Ok(file.into())
716}
717
718fn lock_fd_exclusive(lock_fd: OwnedFd) -> io::Result<ShmRegionLock> {
719 loop {
720 let rc = unsafe { libc::flock(lock_fd.as_raw_fd(), libc::LOCK_EX) };
721 if rc == 0 {
722 return Ok(ShmRegionLock { lock_fd });
723 }
724 let error = io::Error::last_os_error();
725 if error.kind() != io::ErrorKind::Interrupted {
726 return Err(error);
727 }
728 }
729}
730
731impl Drop for ShmRegion {
732 fn drop(&mut self) {
733 unsafe {
735 libc::munmap(self.ptr.as_ptr().cast(), self.len);
736 }
737 }
738}
739
740unsafe impl Send for ShmRegion {}
744unsafe impl Sync for ShmRegion {}
745
746pub fn ring_segment_name(
748 fleet_name: &str,
749 kind: u8,
750) -> String {
751 let uid = unsafe { libc::geteuid() };
753 ring_segment_name_for_uid(fleet_name, kind, uid)
754}
755
756pub fn ring_segment_name_for_uid(
761 fleet_name: &str,
762 kind: u8,
763 uid: u32,
764) -> String {
765 format!("/{SHM_NAMESPACE}-{fleet_name}-{kind}-{uid}")
766}
767
768pub fn fleet_lock_path(fleet_name: &str) -> PathBuf {
770 lock_dir().join(format!("{SHM_NAMESPACE}-{fleet_name}.fleet"))
771}
772
773pub fn fleet_lock_path_for_uid(
776 fleet_name: &str,
777 uid: u32,
778) -> PathBuf {
779 lock_dir_for_uid(uid).join(format!("{SHM_NAMESPACE}-{fleet_name}.fleet"))
780}
781
782pub struct FleetMembership {
788 lock_fd: OwnedFd,
789}
790
791impl Drop for FleetMembership {
792 fn drop(&mut self) {
793 let _ = unsafe { libc::flock(self.lock_fd.as_raw_fd(), libc::LOCK_UN) };
794 }
795}
796
797pub fn join_fleet_membership(fleet_name: &str) -> io::Result<FleetMembership> {
800 let lock_fd = open_lock_file(&fleet_lock_path(fleet_name))?;
801 let rc = unsafe { libc::flock(lock_fd.as_raw_fd(), libc::LOCK_SH) };
803 if rc != 0 {
804 return Err(io::Error::last_os_error());
805 }
806 Ok(FleetMembership { lock_fd })
807}
808
809pub fn hold_existing_fleet(fleet_name: &str) -> io::Result<FleetMembership> {
812 use std::os::unix::fs::MetadataExt;
813 let path = fleet_lock_path(fleet_name);
814 let file = OpenOptions::new()
815 .read(true)
816 .custom_flags(libc::O_CLOEXEC | libc::O_NOFOLLOW)
817 .open(&path)?;
818 let metadata = file.metadata()?;
819 if !metadata.is_file() || metadata.uid() != unsafe { libc::geteuid() } {
820 return Err(io::Error::new(
821 io::ErrorKind::PermissionDenied,
822 "fleet membership file has an unexpected owner or type",
823 ));
824 }
825 let lock_fd: OwnedFd = file.into();
826 loop {
827 let rc = unsafe { libc::flock(lock_fd.as_raw_fd(), libc::LOCK_SH) };
828 if rc == 0 {
829 return Ok(FleetMembership { lock_fd });
830 }
831 let error = io::Error::last_os_error();
832 if error.kind() != io::ErrorKind::Interrupted {
833 return Err(error);
834 }
835 }
836}
837
838pub fn try_lock_fleet_exclusive(
845 fleet_name: &str,
846 uid: u32,
847) -> io::Result<Option<ShmRegionLock>> {
848 let lock_fd = open_lock_file(&fleet_lock_path_for_uid(fleet_name, uid))?;
851 let rc = unsafe {
853 libc::flock(lock_fd.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB)
854 };
855 if rc == 0 {
856 return Ok(Some(ShmRegionLock { lock_fd }));
857 }
858 let error = io::Error::last_os_error();
859 if error.kind() == io::ErrorKind::WouldBlock {
860 return Ok(None);
861 }
862 Err(error)
863}
864
865pub struct LaneHold {
873 lock_fd: OwnedFd,
874}
875
876pub struct PreparedLaneLock {
880 fd: OwnedFd,
881 busy: AtomicBool,
882 pid: u32,
883}
884
885impl PreparedLaneLock {
886 pub fn open(
887 shm_name: &str,
888 lane: usize,
889 ) -> io::Result<Self> {
890 let name = format!("{}.lane{lane}", shm_name.trim_start_matches('/'));
891 Ok(Self {
892 fd: open_lock_file(&lock_file_path(&name))?,
893 busy: AtomicBool::new(false),
894 pid: std::process::id(),
895 })
896 }
897
898 pub fn try_lock(&self) -> io::Result<Option<PreparedLaneGuard<'_>>> {
900 if self.pid != std::process::id() {
901 return Err(io::Error::from(io::ErrorKind::PermissionDenied));
902 }
903 if self
904 .busy
905 .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
906 .is_err()
907 {
908 return Ok(None);
909 }
910 if unsafe {
912 libc::flock(self.fd.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB)
913 } == 0
914 {
915 return Ok(Some(PreparedLaneGuard { lock: self }));
916 }
917 let error = io::Error::last_os_error();
918 self.busy.store(false, Ordering::Release);
919 if error.kind() == io::ErrorKind::WouldBlock {
920 Ok(None)
921 } else {
922 Err(error)
923 }
924 }
925}
926
927pub struct PreparedLaneGuard<'a> {
928 lock: &'a PreparedLaneLock,
929}
930
931impl Drop for PreparedLaneGuard<'_> {
932 fn drop(&mut self) {
933 if self.lock.pid == std::process::id() {
934 unsafe {
936 libc::flock(self.lock.fd.as_raw_fd(), libc::LOCK_UN);
937 }
938 self.lock.busy.store(false, Ordering::Release);
939 }
940 }
941}
942
943impl Drop for LaneHold {
944 fn drop(&mut self) {
945 let _ = unsafe { libc::flock(self.lock_fd.as_raw_fd(), libc::LOCK_UN) };
946 }
947}
948
949pub fn try_hold_lane(
953 shm_name: &str,
954 lane: usize,
955) -> io::Result<Option<LaneHold>> {
956 let name = format!("{}.lane{lane}", shm_name.trim_start_matches('/'));
957 let lock_fd = open_lock_file(&lock_file_path(&name))?;
958 let rc = unsafe {
960 libc::flock(lock_fd.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB)
961 };
962 if rc == 0 {
963 return Ok(Some(LaneHold { lock_fd }));
964 }
965 let error = io::Error::last_os_error();
966 if error.kind() == io::ErrorKind::WouldBlock {
967 return Ok(None);
968 }
969 Err(error)
970}
971
972pub struct WatcherHold {
980 lock_fd: OwnedFd,
981}
982
983impl Drop for WatcherHold {
984 fn drop(&mut self) {
985 let _ = unsafe { libc::flock(self.lock_fd.as_raw_fd(), libc::LOCK_UN) };
986 }
987}
988
989pub fn try_hold_watcher(
994 shm_name: &str,
995 slot: usize,
996) -> io::Result<Option<WatcherHold>> {
997 let name = format!("{}.watch{slot}", shm_name.trim_start_matches('/'));
998 let lock_fd = open_lock_file(&lock_file_path(&name))?;
999 let rc = unsafe {
1001 libc::flock(lock_fd.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB)
1002 };
1003 if rc == 0 {
1004 return Ok(Some(WatcherHold { lock_fd }));
1005 }
1006 let error = io::Error::last_os_error();
1007 if error.kind() == io::ErrorKind::WouldBlock {
1008 return Ok(None);
1009 }
1010 Err(error)
1011}
1012
1013pub fn companion_lock_files(
1019 shm_name: &str,
1020 uid: u32,
1021) -> io::Result<Vec<PathBuf>> {
1022 let base = shm_name.trim_start_matches('/');
1023 let own = format!("{base}.lock");
1024 let lane = format!("{base}.lane");
1025 let watch = format!("{base}.watch");
1026 let dir = lock_dir_for_uid(uid);
1027 let entries = match std::fs::read_dir(&dir) {
1028 Ok(entries) => entries,
1029 Err(error) if error.kind() == io::ErrorKind::NotFound => {
1030 return Ok(Vec::new());
1031 }
1032 Err(error) => return Err(error),
1033 };
1034 let mut found = Vec::new();
1035 for entry in entries {
1036 let entry = entry?;
1037 let Some(file) = entry.file_name().to_str().map(str::to_owned) else {
1038 continue;
1039 };
1040 if file == own
1041 || ((file.starts_with(&lane) || file.starts_with(&watch))
1042 && file.ends_with(".lock"))
1043 {
1044 found.push(entry.path());
1045 }
1046 }
1047 Ok(found)
1048}
1049
1050fn lock_file_path(shm_name: &str) -> PathBuf {
1051 lock_dir().join(format!("{}.lock", shm_name.trim_start_matches('/')))
1052}
1053
1054fn lock_dir() -> PathBuf {
1078 let uid = unsafe { libc::geteuid() };
1079 lock_dir_for_uid(uid)
1080}
1081
1082pub fn lock_dir_for_uid(uid: u32) -> PathBuf {
1085 let base = std::env::var_os("XDG_RUNTIME_DIR")
1086 .map(PathBuf::from)
1087 .filter(|dir| dir.is_absolute())
1088 .unwrap_or_else(|| PathBuf::from("/tmp"));
1089
1090 base.join(format!("{SHM_NAMESPACE}-{uid}"))
1091}
1092
1093fn ensure_lock_dir(dir: &Path) -> io::Result<()> {
1099 use std::os::unix::fs::DirBuilderExt;
1100
1101 let uid = unsafe { libc::geteuid() };
1102 match std::fs::DirBuilder::new().mode(0o700).create(dir) {
1103 Ok(()) => {}
1104 Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {}
1105 Err(error) => return Err(error),
1106 }
1107
1108 ensure_private_dir(dir, uid)
1109}
1110
1111fn ensure_private_dir(
1117 dir: &Path,
1118 uid: u32,
1119) -> io::Result<()> {
1120 use std::os::unix::fs::{MetadataExt, PermissionsExt};
1121
1122 let metadata = std::fs::symlink_metadata(dir)?;
1123 if !metadata.is_dir() {
1124 return Err(io::Error::new(
1125 io::ErrorKind::PermissionDenied,
1126 format!("{} is not a directory", dir.display()),
1127 ));
1128 }
1129 if metadata.uid() != uid {
1130 return Err(io::Error::new(
1131 io::ErrorKind::PermissionDenied,
1132 format!(
1133 "{} is owned by uid {} rather than {uid}; refusing to lock in a directory \
1134 another user controls",
1135 dir.display(),
1136 metadata.uid()
1137 ),
1138 ));
1139 }
1140 if metadata.permissions().mode() & 0o077 != 0 {
1141 return Err(io::Error::new(
1142 io::ErrorKind::PermissionDenied,
1143 format!(
1144 "{} is mode {:o}; refusing to lock in a directory others can write to",
1145 dir.display(),
1146 metadata.permissions().mode() & 0o777
1147 ),
1148 ));
1149 }
1150
1151 Ok(())
1152}
1153
1154#[cfg(test)]
1155mod fleet_lock_tests {
1156 use super::{join_fleet_membership, try_lock_fleet_exclusive};
1157
1158 #[test]
1159 fn prepared_lane_excludes_threads_and_distinct_file_descriptions() {
1160 let name = format!("prepared-{}", std::process::id());
1161 let first = super::PreparedLaneLock::open(&name, 0).unwrap();
1162 let second = super::PreparedLaneLock::open(&name, 0).unwrap();
1163 let hold = first.try_lock().unwrap().unwrap();
1164 assert!(first.try_lock().unwrap().is_none());
1165 std::thread::scope(|scope| {
1166 assert!(
1167 scope
1168 .spawn(|| first.try_lock().unwrap().is_none())
1169 .join()
1170 .unwrap()
1171 );
1172 });
1173 assert!(second.try_lock().unwrap().is_none());
1174 drop(hold);
1175 let other = second.try_lock().unwrap().unwrap();
1176 assert!(first.try_lock().unwrap().is_none());
1177 drop(other);
1178 drop(first);
1179 drop(second);
1180 std::fs::remove_file(super::lock_file_path(&format!("{name}.lane0")))
1181 .unwrap();
1182 }
1183
1184 #[test]
1187 fn a_member_holds_the_fleet_against_exclusive_takers() {
1188 let fleet = format!("fl{:x}", std::process::id());
1189 let uid = unsafe { libc::geteuid() };
1190
1191 let member = join_fleet_membership(&fleet).expect("join");
1192 assert!(try_lock_fleet_exclusive(&fleet, uid).expect("try").is_none());
1193
1194 drop(member);
1195 let exclusive = try_lock_fleet_exclusive(&fleet, uid).expect("try");
1196 assert!(exclusive.is_some());
1197 drop(exclusive);
1200 let _ =
1201 std::fs::remove_file(super::fleet_lock_path_for_uid(&fleet, uid));
1202 }
1203}