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