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
47pub const SHM_NAMESPACE: &str = "orbit";
49
50#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
61pub enum ShmAccessPolicy {
62 #[default]
64 OwnerOnly,
65 GroupRead { gid: u32 },
67 GroupReadWrite { gid: u32 },
69}
70
71impl ShmAccessPolicy {
72 pub const fn mode(self) -> u32 {
73 match self {
74 Self::OwnerOnly => 0o600,
75 Self::GroupRead { .. } => 0o640,
76 Self::GroupReadWrite { .. } => 0o660,
77 }
78 }
79
80 pub const fn gid(self) -> Option<u32> {
81 match self {
82 Self::OwnerOnly => None,
83 Self::GroupRead { gid } | Self::GroupReadWrite { gid } => Some(gid),
84 }
85 }
86}
87
88#[derive(Clone, Copy, Debug, Eq, PartialEq)]
95pub enum ShmValidation {
96 Missing,
98 Valid { actual_size: usize },
100}
101
102pub struct ShmRegion {
106 name: CString,
107 lock_path: PathBuf,
108 process_lock: bool,
114 ptr: NonNull<u8>,
115 len: usize,
116 created: bool,
120}
121
122impl ShmRegion {
123 pub fn validate_existing(
133 name: &str,
134 minimum_size: usize,
135 ) -> io::Result<ShmValidation> {
136 Self::validate_existing_with_policy(
137 name,
138 minimum_size,
139 unsafe { libc::geteuid() },
140 ShmAccessPolicy::default(),
141 )
142 }
143
144 pub fn validate_existing_with_policy(
147 name: &str,
148 minimum_size: usize,
149 owner_uid: u32,
150 policy: ShmAccessPolicy,
151 ) -> io::Result<ShmValidation> {
152 let cname = CString::new(name).map_err(|_| {
153 io::Error::new(io::ErrorKind::InvalidInput, "shm name has nul byte")
154 })?;
155 let raw_fd = loop {
156 let fd = unsafe {
160 libc::shm_open(cname.as_ptr(), libc::O_RDONLY, 0o600)
161 };
162 if fd >= 0 {
163 break fd;
164 }
165 let error = io::Error::last_os_error();
166 if error.kind() == io::ErrorKind::Interrupted {
167 continue;
168 }
169 if error.raw_os_error() == Some(libc::ENOENT) {
170 return Ok(ShmValidation::Missing);
171 }
172 return Err(error);
173 };
174 let fd = unsafe { OwnedFd::from_raw_fd(raw_fd) };
177 let actual_size = shm_object_size(&fd, name, owner_uid, policy)?;
178 validate_minimum_size(name, actual_size, minimum_size)?;
179 Ok(ShmValidation::Valid { actual_size })
180 }
181
182 pub(crate) fn open_existing_read_only(
189 name: &str,
190 minimum_size: usize,
191 owner_uid: u32,
192 policy: ShmAccessPolicy,
193 ) -> io::Result<Self> {
194 Self::open_existing(name, minimum_size, owner_uid, policy, false)
195 }
196
197 pub fn open_existing_read_write(
200 name: &str,
201 minimum_size: usize,
202 owner_uid: u32,
203 policy: ShmAccessPolicy,
204 ) -> io::Result<Self> {
205 Self::open_existing(name, minimum_size, owner_uid, policy, true)
206 }
207
208 fn open_existing(
209 name: &str,
210 minimum_size: usize,
211 owner_uid: u32,
212 policy: ShmAccessPolicy,
213 writable: bool,
214 ) -> io::Result<Self> {
215 let cname = CString::new(name).map_err(|_| {
216 io::Error::new(io::ErrorKind::InvalidInput, "shm name has nul byte")
217 })?;
218 let raw_fd = loop {
219 let fd = unsafe {
224 libc::shm_open(
225 cname.as_ptr(),
226 if writable { libc::O_RDWR } else { libc::O_RDONLY },
227 0o600,
228 )
229 };
230 if fd >= 0 {
231 break fd;
232 }
233 let error = io::Error::last_os_error();
234 if error.kind() == io::ErrorKind::Interrupted {
235 continue;
236 }
237 return Err(error);
238 };
239 let fd = unsafe { OwnedFd::from_raw_fd(raw_fd) };
242 let actual_size = shm_object_size(&fd, name, owner_uid, policy)?;
243 validate_minimum_size(name, actual_size, minimum_size)?;
244
245 let ptr = unsafe {
250 libc::mmap(
251 std::ptr::null_mut(),
252 actual_size,
253 libc::PROT_READ | if writable { libc::PROT_WRITE } else { 0 },
254 libc::MAP_SHARED,
255 fd.as_raw_fd(),
256 0,
257 )
258 };
259 if ptr == libc::MAP_FAILED {
260 return Err(io::Error::last_os_error());
261 }
262 let ptr = NonNull::new(ptr.cast::<u8>())
264 .expect("mmap returned non-null on success");
265
266 Ok(Self {
267 lock_path: lock_dir_for_uid(owner_uid)
268 .join(format!("{}.lock", name.trim_start_matches('/'))),
269 name: cname,
270 process_lock: false,
271 ptr,
272 len: actual_size,
273 created: false,
274 })
275 }
276
277 pub fn open_or_create(
286 name: &str,
287 size: usize,
288 ) -> io::Result<Self> {
289 Self::open_or_create_with_policy(name, size, ShmAccessPolicy::default())
290 }
291
292 pub fn open_or_create_with_policy(
296 name: &str,
297 size: usize,
298 policy: ShmAccessPolicy,
299 ) -> io::Result<Self> {
300 let (region, initialization_lock) =
301 Self::open_or_create_inner(name, size, false, policy)?;
302 debug_assert!(initialization_lock.is_none());
303 Ok(region)
304 }
305
306 pub fn open_or_create_locked(
310 name: &str,
311 size: usize,
312 ) -> io::Result<(Self, ShmRegionLock)> {
313 Self::open_or_create_locked_with_policy(
314 name,
315 size,
316 ShmAccessPolicy::default(),
317 )
318 }
319
320 pub fn open_or_create_locked_with_policy(
323 name: &str,
324 size: usize,
325 policy: ShmAccessPolicy,
326 ) -> io::Result<(Self, ShmRegionLock)> {
327 let (region, initialization_lock) =
328 Self::open_or_create_inner(name, size, true, policy)?;
329 Ok((
330 region,
331 initialization_lock
332 .expect("locked SHM open must return its initialization lock"),
333 ))
334 }
335
336 fn open_or_create_inner(
337 name: &str,
338 size: usize,
339 process_lock: bool,
340 policy: ShmAccessPolicy,
341 ) -> io::Result<(Self, Option<ShmRegionLock>)> {
342 let cname = CString::new(name).map_err(|_| {
343 io::Error::new(io::ErrorKind::InvalidInput, "shm name has nul byte")
344 })?;
345 let lock_path = lock_file_path(name);
346 let initialization_lock = if process_lock {
347 Some(lock_path_exclusive(&lock_path)?)
348 } else {
349 None
350 };
351
352 #[cfg(target_os = "macos")]
355 let can_create =
356 policy.gid().is_none_or(|gid| gid == unsafe { libc::getegid() });
357 #[cfg(target_os = "macos")]
358 let creation_mode = policy.mode();
359 #[cfg(not(target_os = "macos"))]
361 let (can_create, creation_mode) = (true, 0o600u32);
362 #[cfg(not(target_vendor = "apple"))]
364 let creation_mode = creation_mode as libc::mode_t;
365 let (raw_fd, created) = unsafe {
367 let fd = if can_create {
369 libc::shm_open(
370 cname.as_ptr(),
371 libc::O_RDWR | libc::O_CREAT | libc::O_EXCL,
372 creation_mode,
373 )
374 } else {
375 libc::shm_open(cname.as_ptr(), libc::O_RDWR, 0)
376 };
377 if fd >= 0 {
378 (fd, can_create)
379 } else {
380 let err = io::Error::last_os_error();
382 if !can_create && err.raw_os_error() == Some(libc::ENOENT) {
383 return Err(io::Error::new(
384 io::ErrorKind::InvalidInput,
385 "creating group-shared SHM requires the selected effective gid",
386 ));
387 }
388 if err.raw_os_error() != Some(libc::EEXIST) {
389 return Err(err);
390 }
391 let fd = libc::shm_open(cname.as_ptr(), libc::O_RDWR, 0o600);
392 if fd < 0 {
393 return Err(io::Error::last_os_error());
394 }
395 (fd, false)
396 }
397 };
398 let fd = unsafe { OwnedFd::from_raw_fd(raw_fd) };
401
402 #[cfg(not(target_os = "macos"))]
403 if created && let Err(error) = configure_new_shm(&fd, policy) {
404 let _ = unsafe { libc::shm_unlink(cname.as_ptr()) };
405 return Err(error);
406 }
407
408 let owner_uid = unsafe { libc::geteuid() };
411 if let Err(error) = shm_object_size(&fd, name, owner_uid, policy) {
412 if created {
413 let _ = unsafe { libc::shm_unlink(cname.as_ptr()) };
414 }
415 return Err(error);
416 }
417
418 if created {
420 let rc =
422 unsafe { libc::ftruncate(fd.as_raw_fd(), size as libc::off_t) };
423 if rc != 0 {
424 let err = io::Error::last_os_error();
425 let _ = unsafe { libc::shm_unlink(cname.as_ptr()) };
426 return Err(err);
427 }
428 }
429
430 let actual_size = match shm_object_size(&fd, name, owner_uid, policy) {
435 Ok(actual_size) => actual_size,
436 Err(error) => {
437 if created {
438 let _ = unsafe { libc::shm_unlink(cname.as_ptr()) };
439 }
440 return Err(error);
441 }
442 };
443 if let Err(error) = validate_minimum_size(name, actual_size, size) {
444 if created {
445 let _ = unsafe { libc::shm_unlink(cname.as_ptr()) };
446 }
447 return Err(error);
448 }
449
450 let ptr = unsafe {
453 libc::mmap(
454 std::ptr::null_mut(),
455 size,
456 libc::PROT_READ | libc::PROT_WRITE,
457 libc::MAP_SHARED,
458 fd.as_raw_fd(),
459 0,
460 )
461 };
462
463 if ptr == libc::MAP_FAILED {
464 let err = io::Error::last_os_error();
465 if created {
466 let _ = unsafe { libc::shm_unlink(cname.as_ptr()) };
467 }
468 return Err(err);
469 }
470
471 let ptr = NonNull::new(ptr.cast::<u8>())
473 .expect("mmap returned non-null on success");
474
475 Ok((
476 Self {
477 name: cname,
478 lock_path,
479 process_lock,
480 ptr,
481 len: size,
482 created,
483 },
484 initialization_lock,
485 ))
486 }
487
488 pub fn as_ptr(&self) -> *mut u8 {
490 self.ptr.as_ptr()
491 }
492
493 pub fn len(&self) -> usize {
495 self.len
496 }
497
498 pub fn is_empty(&self) -> bool {
499 self.len == 0
500 }
501
502 pub fn created(&self) -> bool {
506 self.created
507 }
508
509 pub fn lock_exclusive(&self) -> io::Result<ShmRegionLock> {
514 if !self.process_lock {
515 return Err(io::Error::new(
516 io::ErrorKind::InvalidInput,
517 "SHM region was opened without a process lock",
518 ));
519 }
520 lock_path_exclusive(&self.lock_path)
521 }
522
523 #[cfg(test)]
524 pub(crate) fn try_lock_exclusive(&self) -> io::Result<ShmRegionLock> {
525 if !self.process_lock {
526 return Err(io::Error::new(
527 io::ErrorKind::InvalidInput,
528 "SHM region was opened without a process lock",
529 ));
530 }
531 let lock_fd = open_lock_file(&self.lock_path)?;
532 let rc = unsafe {
533 libc::flock(lock_fd.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB)
534 };
535 if rc == 0 {
536 Ok(ShmRegionLock { lock_fd })
537 } else {
538 Err(io::Error::last_os_error())
539 }
540 }
541
542 pub fn unlink(&self) -> io::Result<()> {
546 let rc = unsafe { libc::shm_unlink(self.name.as_ptr()) };
548 let shm_error = if rc != 0 {
549 let err = io::Error::last_os_error();
550 if err.raw_os_error() == Some(libc::ENOENT) {
552 None
553 } else {
554 Some(err)
555 }
556 } else {
557 None
558 };
559 let mut lock_error = match std::fs::remove_file(&self.lock_path) {
560 Ok(()) => None,
561 Err(error) if error.kind() == io::ErrorKind::NotFound => None,
562 Err(error) => Some(error),
563 };
564 let name = self.name.to_string_lossy();
565 let uid = unsafe { libc::geteuid() };
567 match companion_lock_files(&name, uid) {
568 Ok(files) => {
569 for file in files {
570 if let Err(error) = std::fs::remove_file(&file)
571 && error.kind() != io::ErrorKind::NotFound
572 {
573 lock_error.get_or_insert(error);
574 }
575 }
576 }
577 Err(error) => {
578 lock_error.get_or_insert(error);
579 }
580 }
581 if let Some(error) = shm_error.or(lock_error) {
582 return Err(error);
583 }
584 Ok(())
585 }
586}
587
588#[cfg(not(target_os = "macos"))]
589fn configure_new_shm(
590 fd: &OwnedFd,
591 policy: ShmAccessPolicy,
592) -> io::Result<()> {
593 let Some(gid) = policy.gid() else {
594 return Ok(());
595 };
596 if gid == !0 {
597 return Err(io::Error::new(
598 io::ErrorKind::InvalidInput,
599 "invalid SHM group id",
600 ));
601 }
602 if unsafe { libc::fchown(fd.as_raw_fd(), !0, gid) } != 0 {
604 return Err(io::Error::last_os_error());
605 }
606 if unsafe { libc::fchmod(fd.as_raw_fd(), policy.mode() as _) } != 0 {
608 return Err(io::Error::last_os_error());
609 }
610 Ok(())
611}
612
613fn shm_object_size(
614 fd: &OwnedFd,
615 name: &str,
616 owner_uid: u32,
617 policy: ShmAccessPolicy,
618) -> io::Result<usize> {
619 let mut stat = std::mem::MaybeUninit::<libc::stat>::uninit();
620 let stat_rc = unsafe { libc::fstat(fd.as_raw_fd(), stat.as_mut_ptr()) };
622 if stat_rc != 0 {
623 return Err(io::Error::last_os_error());
624 }
625 let stat = unsafe { stat.assume_init() };
627 let mode = u64::from(stat.st_mode) & 0o7777;
628 if stat.st_uid != owner_uid
629 || policy.gid().is_some_and(|gid| gid != stat.st_gid)
630 || mode & !u64::from(policy.mode()) != 0
631 {
632 return Err(io::Error::new(
633 io::ErrorKind::PermissionDenied,
634 format!(
635 "SHM segment {name} owner/group/mode do not satisfy {policy:?} for uid {owner_uid}"
636 ),
637 ));
638 }
639 let actual_size = stat.st_size;
640 usize::try_from(actual_size).map_err(|_| {
641 io::Error::new(
642 io::ErrorKind::InvalidData,
643 format!("SHM segment {name} reported invalid size {actual_size}"),
644 )
645 })
646}
647
648fn validate_minimum_size(
649 name: &str,
650 actual_size: usize,
651 minimum_size: usize,
652) -> io::Result<()> {
653 if actual_size < minimum_size {
654 return Err(io::Error::new(
655 io::ErrorKind::InvalidData,
656 format!(
657 "SHM segment {name} size {actual_size} is smaller than requested mapping {minimum_size}"
658 ),
659 ));
660 }
661 Ok(())
662}
663
664pub struct ShmRegionLock {
669 lock_fd: OwnedFd,
670}
671
672impl Drop for ShmRegionLock {
673 fn drop(&mut self) {
674 let _ = unsafe { libc::flock(self.lock_fd.as_raw_fd(), libc::LOCK_UN) };
675 }
676}
677
678fn lock_path_exclusive(lock_path: &Path) -> io::Result<ShmRegionLock> {
679 lock_fd_exclusive(open_lock_file(lock_path)?)
680}
681
682fn open_lock_file(lock_path: &Path) -> io::Result<OwnedFd> {
683 use std::os::unix::fs::MetadataExt;
684
685 if let Some(dir) = lock_path.parent() {
686 ensure_lock_dir(dir)?;
687 }
688
689 let file = OpenOptions::new()
690 .read(true)
691 .write(true)
692 .create(true)
693 .mode(0o600)
694 .custom_flags(libc::O_CLOEXEC | libc::O_NOFOLLOW)
695 .open(lock_path)?;
696
697 let uid = unsafe { libc::geteuid() };
701 let owner = file.metadata()?.uid();
702 if owner != uid {
703 return Err(io::Error::new(
704 io::ErrorKind::PermissionDenied,
705 format!(
706 "{} is owned by uid {owner} rather than {uid}",
707 lock_path.display()
708 ),
709 ));
710 }
711
712 Ok(file.into())
713}
714
715fn lock_fd_exclusive(lock_fd: OwnedFd) -> io::Result<ShmRegionLock> {
716 loop {
717 let rc = unsafe { libc::flock(lock_fd.as_raw_fd(), libc::LOCK_EX) };
718 if rc == 0 {
719 return Ok(ShmRegionLock { lock_fd });
720 }
721 let error = io::Error::last_os_error();
722 if error.kind() != io::ErrorKind::Interrupted {
723 return Err(error);
724 }
725 }
726}
727
728impl Drop for ShmRegion {
729 fn drop(&mut self) {
730 unsafe {
732 libc::munmap(self.ptr.as_ptr().cast(), self.len);
733 }
734 }
735}
736
737unsafe impl Send for ShmRegion {}
741unsafe impl Sync for ShmRegion {}
742
743pub fn ring_segment_name(
745 fleet_name: &str,
746 kind: u8,
747) -> String {
748 let uid = unsafe { libc::geteuid() };
750 ring_segment_name_for_uid(fleet_name, kind, uid)
751}
752
753pub fn ring_segment_name_for_uid(
758 fleet_name: &str,
759 kind: u8,
760 uid: u32,
761) -> String {
762 format!("/{SHM_NAMESPACE}-{fleet_name}-{kind}-{uid}")
763}
764
765pub fn fleet_lock_path(fleet_name: &str) -> PathBuf {
767 lock_dir().join(format!("{SHM_NAMESPACE}-{fleet_name}.fleet"))
768}
769
770pub fn fleet_lock_path_for_uid(
773 fleet_name: &str,
774 uid: u32,
775) -> PathBuf {
776 lock_dir_for_uid(uid).join(format!("{SHM_NAMESPACE}-{fleet_name}.fleet"))
777}
778
779pub struct FleetMembership {
785 lock_fd: OwnedFd,
786}
787
788impl Drop for FleetMembership {
789 fn drop(&mut self) {
790 let _ = unsafe { libc::flock(self.lock_fd.as_raw_fd(), libc::LOCK_UN) };
791 }
792}
793
794pub fn join_fleet_membership(fleet_name: &str) -> io::Result<FleetMembership> {
797 let lock_fd = open_lock_file(&fleet_lock_path(fleet_name))?;
798 let rc = unsafe { libc::flock(lock_fd.as_raw_fd(), libc::LOCK_SH) };
800 if rc != 0 {
801 return Err(io::Error::last_os_error());
802 }
803 Ok(FleetMembership { lock_fd })
804}
805
806pub fn hold_existing_fleet(fleet_name: &str) -> io::Result<FleetMembership> {
809 use std::os::unix::fs::MetadataExt;
810 let path = fleet_lock_path(fleet_name);
811 let file = OpenOptions::new()
812 .read(true)
813 .custom_flags(libc::O_CLOEXEC | libc::O_NOFOLLOW)
814 .open(&path)?;
815 let metadata = file.metadata()?;
816 if !metadata.is_file() || metadata.uid() != unsafe { libc::geteuid() } {
817 return Err(io::Error::new(
818 io::ErrorKind::PermissionDenied,
819 "fleet membership file has an unexpected owner or type",
820 ));
821 }
822 let lock_fd: OwnedFd = file.into();
823 loop {
824 let rc = unsafe { libc::flock(lock_fd.as_raw_fd(), libc::LOCK_SH) };
825 if rc == 0 {
826 return Ok(FleetMembership { lock_fd });
827 }
828 let error = io::Error::last_os_error();
829 if error.kind() != io::ErrorKind::Interrupted {
830 return Err(error);
831 }
832 }
833}
834
835pub fn try_lock_fleet_exclusive(
842 fleet_name: &str,
843 uid: u32,
844) -> io::Result<Option<ShmRegionLock>> {
845 let lock_fd = open_lock_file(&fleet_lock_path_for_uid(fleet_name, uid))?;
848 let rc = unsafe {
850 libc::flock(lock_fd.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB)
851 };
852 if rc == 0 {
853 return Ok(Some(ShmRegionLock { lock_fd }));
854 }
855 let error = io::Error::last_os_error();
856 if error.kind() == io::ErrorKind::WouldBlock {
857 return Ok(None);
858 }
859 Err(error)
860}
861
862pub struct LaneHold {
870 lock_fd: OwnedFd,
871}
872
873pub struct PreparedLaneLock {
877 fd: OwnedFd,
878 busy: AtomicBool,
879 pid: u32,
880}
881
882impl PreparedLaneLock {
883 pub fn open(
884 shm_name: &str,
885 lane: usize,
886 ) -> io::Result<Self> {
887 let name = format!("{}.lane{lane}", shm_name.trim_start_matches('/'));
888 Ok(Self {
889 fd: open_lock_file(&lock_file_path(&name))?,
890 busy: AtomicBool::new(false),
891 pid: std::process::id(),
892 })
893 }
894
895 pub fn try_lock(&self) -> io::Result<Option<PreparedLaneGuard<'_>>> {
897 if self.pid != std::process::id() {
898 return Err(io::Error::from(io::ErrorKind::PermissionDenied));
899 }
900 if self
901 .busy
902 .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
903 .is_err()
904 {
905 return Ok(None);
906 }
907 if unsafe {
909 libc::flock(self.fd.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB)
910 } == 0
911 {
912 return Ok(Some(PreparedLaneGuard { lock: self }));
913 }
914 let error = io::Error::last_os_error();
915 self.busy.store(false, Ordering::Release);
916 if error.kind() == io::ErrorKind::WouldBlock {
917 Ok(None)
918 } else {
919 Err(error)
920 }
921 }
922}
923
924pub struct PreparedLaneGuard<'a> {
925 lock: &'a PreparedLaneLock,
926}
927
928impl Drop for PreparedLaneGuard<'_> {
929 fn drop(&mut self) {
930 if self.lock.pid == std::process::id() {
931 unsafe {
933 libc::flock(self.lock.fd.as_raw_fd(), libc::LOCK_UN);
934 }
935 self.lock.busy.store(false, Ordering::Release);
936 }
937 }
938}
939
940impl Drop for LaneHold {
941 fn drop(&mut self) {
942 let _ = unsafe { libc::flock(self.lock_fd.as_raw_fd(), libc::LOCK_UN) };
943 }
944}
945
946pub fn try_hold_lane(
950 shm_name: &str,
951 lane: usize,
952) -> io::Result<Option<LaneHold>> {
953 let name = format!("{}.lane{lane}", shm_name.trim_start_matches('/'));
954 let lock_fd = open_lock_file(&lock_file_path(&name))?;
955 let rc = unsafe {
957 libc::flock(lock_fd.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB)
958 };
959 if rc == 0 {
960 return Ok(Some(LaneHold { lock_fd }));
961 }
962 let error = io::Error::last_os_error();
963 if error.kind() == io::ErrorKind::WouldBlock {
964 return Ok(None);
965 }
966 Err(error)
967}
968
969pub struct WatcherHold {
977 lock_fd: OwnedFd,
978}
979
980impl Drop for WatcherHold {
981 fn drop(&mut self) {
982 let _ = unsafe { libc::flock(self.lock_fd.as_raw_fd(), libc::LOCK_UN) };
983 }
984}
985
986pub fn try_hold_watcher(
991 shm_name: &str,
992 slot: usize,
993) -> io::Result<Option<WatcherHold>> {
994 let name = format!("{}.watch{slot}", shm_name.trim_start_matches('/'));
995 let lock_fd = open_lock_file(&lock_file_path(&name))?;
996 let rc = unsafe {
998 libc::flock(lock_fd.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB)
999 };
1000 if rc == 0 {
1001 return Ok(Some(WatcherHold { lock_fd }));
1002 }
1003 let error = io::Error::last_os_error();
1004 if error.kind() == io::ErrorKind::WouldBlock {
1005 return Ok(None);
1006 }
1007 Err(error)
1008}
1009
1010pub fn companion_lock_files(
1016 shm_name: &str,
1017 uid: u32,
1018) -> io::Result<Vec<PathBuf>> {
1019 let base = shm_name.trim_start_matches('/');
1020 let own = format!("{base}.lock");
1021 let lane = format!("{base}.lane");
1022 let watch = format!("{base}.watch");
1023 let dir = lock_dir_for_uid(uid);
1024 let entries = match std::fs::read_dir(&dir) {
1025 Ok(entries) => entries,
1026 Err(error) if error.kind() == io::ErrorKind::NotFound => {
1027 return Ok(Vec::new());
1028 }
1029 Err(error) => return Err(error),
1030 };
1031 let mut found = Vec::new();
1032 for entry in entries {
1033 let entry = entry?;
1034 let Some(file) = entry.file_name().to_str().map(str::to_owned) else {
1035 continue;
1036 };
1037 if file == own
1038 || ((file.starts_with(&lane) || file.starts_with(&watch))
1039 && file.ends_with(".lock"))
1040 {
1041 found.push(entry.path());
1042 }
1043 }
1044 Ok(found)
1045}
1046
1047fn lock_file_path(shm_name: &str) -> PathBuf {
1048 lock_dir().join(format!("{}.lock", shm_name.trim_start_matches('/')))
1049}
1050
1051fn lock_dir() -> PathBuf {
1075 let uid = unsafe { libc::geteuid() };
1076 lock_dir_for_uid(uid)
1077}
1078
1079pub fn lock_dir_for_uid(uid: u32) -> PathBuf {
1082 let base = std::env::var_os("XDG_RUNTIME_DIR")
1083 .map(PathBuf::from)
1084 .filter(|dir| dir.is_absolute())
1085 .unwrap_or_else(|| PathBuf::from("/tmp"));
1086
1087 base.join(format!("{SHM_NAMESPACE}-{uid}"))
1088}
1089
1090fn ensure_lock_dir(dir: &Path) -> io::Result<()> {
1096 use std::os::unix::fs::DirBuilderExt;
1097
1098 let uid = unsafe { libc::geteuid() };
1099 match std::fs::DirBuilder::new().mode(0o700).create(dir) {
1100 Ok(()) => {}
1101 Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {}
1102 Err(error) => return Err(error),
1103 }
1104
1105 ensure_private_dir(dir, uid)
1106}
1107
1108fn ensure_private_dir(
1114 dir: &Path,
1115 uid: u32,
1116) -> io::Result<()> {
1117 use std::os::unix::fs::{MetadataExt, PermissionsExt};
1118
1119 let metadata = std::fs::symlink_metadata(dir)?;
1120 if !metadata.is_dir() {
1121 return Err(io::Error::new(
1122 io::ErrorKind::PermissionDenied,
1123 format!("{} is not a directory", dir.display()),
1124 ));
1125 }
1126 if metadata.uid() != uid {
1127 return Err(io::Error::new(
1128 io::ErrorKind::PermissionDenied,
1129 format!(
1130 "{} is owned by uid {} rather than {uid}; refusing to lock in a directory \
1131 another user controls",
1132 dir.display(),
1133 metadata.uid()
1134 ),
1135 ));
1136 }
1137 if metadata.permissions().mode() & 0o077 != 0 {
1138 return Err(io::Error::new(
1139 io::ErrorKind::PermissionDenied,
1140 format!(
1141 "{} is mode {:o}; refusing to lock in a directory others can write to",
1142 dir.display(),
1143 metadata.permissions().mode() & 0o777
1144 ),
1145 ));
1146 }
1147
1148 Ok(())
1149}
1150
1151#[cfg(test)]
1152mod fleet_lock_tests {
1153 use super::{join_fleet_membership, try_lock_fleet_exclusive};
1154
1155 #[test]
1156 fn prepared_lane_excludes_threads_and_distinct_file_descriptions() {
1157 let name = format!("prepared-{}", std::process::id());
1158 let first = super::PreparedLaneLock::open(&name, 0).unwrap();
1159 let second = super::PreparedLaneLock::open(&name, 0).unwrap();
1160 let hold = first.try_lock().unwrap().unwrap();
1161 assert!(first.try_lock().unwrap().is_none());
1162 std::thread::scope(|scope| {
1163 assert!(
1164 scope
1165 .spawn(|| first.try_lock().unwrap().is_none())
1166 .join()
1167 .unwrap()
1168 );
1169 });
1170 assert!(second.try_lock().unwrap().is_none());
1171 drop(hold);
1172 let other = second.try_lock().unwrap().unwrap();
1173 assert!(first.try_lock().unwrap().is_none());
1174 drop(other);
1175 drop(first);
1176 drop(second);
1177 std::fs::remove_file(super::lock_file_path(&format!("{name}.lane0")))
1178 .unwrap();
1179 }
1180
1181 #[test]
1184 fn a_member_holds_the_fleet_against_exclusive_takers() {
1185 let fleet = format!("fl{:x}", std::process::id());
1186 let uid = unsafe { libc::geteuid() };
1187
1188 let member = join_fleet_membership(&fleet).expect("join");
1189 assert!(try_lock_fleet_exclusive(&fleet, uid).expect("try").is_none());
1190
1191 drop(member);
1192 let exclusive = try_lock_fleet_exclusive(&fleet, uid).expect("try");
1193 assert!(exclusive.is_some());
1194 drop(exclusive);
1197 let _ =
1198 std::fs::remove_file(super::fleet_lock_path_for_uid(&fleet, uid));
1199 }
1200}