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::OnceLock;
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 lock_dir: OnceLock<OwnedFd>,
124 ptr: NonNull<u8>,
125 len: usize,
126 created: bool,
130}
131
132impl ShmRegion {
133 pub fn validate_existing(
143 name: &str,
144 minimum_size: usize,
145 ) -> io::Result<ShmValidation> {
146 Self::validate_existing_with_policy(
147 name,
148 minimum_size,
149 unsafe { libc::geteuid() },
150 ShmAccessPolicy::default(),
151 )
152 }
153
154 pub fn validate_existing_with_policy(
157 name: &str,
158 minimum_size: usize,
159 owner_uid: u32,
160 policy: ShmAccessPolicy,
161 ) -> io::Result<ShmValidation> {
162 let cname = CString::new(name).map_err(|_| {
163 io::Error::new(io::ErrorKind::InvalidInput, "shm name has nul byte")
164 })?;
165 let raw_fd = loop {
166 let fd = unsafe {
170 libc::shm_open(cname.as_ptr(), libc::O_RDONLY, 0o600)
171 };
172 if fd >= 0 {
173 break fd;
174 }
175 let error = io::Error::last_os_error();
176 if error.kind() == io::ErrorKind::Interrupted {
177 continue;
178 }
179 if error.raw_os_error() == Some(libc::ENOENT) {
180 return Ok(ShmValidation::Missing);
181 }
182 return Err(error);
183 };
184 let fd = unsafe { OwnedFd::from_raw_fd(raw_fd) };
187 let actual_size = shm_object_size(&fd, name, owner_uid, policy)?;
188 validate_minimum_size(name, actual_size, minimum_size)?;
189 Ok(ShmValidation::Valid { actual_size })
190 }
191
192 pub(crate) fn open_existing_read_only(
199 name: &str,
200 minimum_size: usize,
201 owner_uid: u32,
202 policy: ShmAccessPolicy,
203 ) -> io::Result<Self> {
204 Self::open_existing(name, minimum_size, owner_uid, policy, false)
205 }
206
207 pub fn open_existing_read_write(
210 name: &str,
211 minimum_size: usize,
212 owner_uid: u32,
213 policy: ShmAccessPolicy,
214 ) -> io::Result<Self> {
215 Self::open_existing(name, minimum_size, owner_uid, policy, true)
216 }
217
218 fn open_existing(
219 name: &str,
220 minimum_size: usize,
221 owner_uid: u32,
222 policy: ShmAccessPolicy,
223 writable: bool,
224 ) -> io::Result<Self> {
225 let cname = CString::new(name).map_err(|_| {
226 io::Error::new(io::ErrorKind::InvalidInput, "shm name has nul byte")
227 })?;
228 let raw_fd = loop {
229 let fd = unsafe {
234 libc::shm_open(
235 cname.as_ptr(),
236 if writable { libc::O_RDWR } else { libc::O_RDONLY },
237 0o600,
238 )
239 };
240 if fd >= 0 {
241 break fd;
242 }
243 let error = io::Error::last_os_error();
244 if error.kind() == io::ErrorKind::Interrupted {
245 continue;
246 }
247 return Err(error);
248 };
249 let fd = unsafe { OwnedFd::from_raw_fd(raw_fd) };
252 let actual_size = shm_object_size(&fd, name, owner_uid, policy)?;
253 validate_minimum_size(name, actual_size, minimum_size)?;
254
255 let ptr = unsafe {
260 libc::mmap(
261 std::ptr::null_mut(),
262 actual_size,
263 libc::PROT_READ | if writable { libc::PROT_WRITE } else { 0 },
264 libc::MAP_SHARED,
265 fd.as_raw_fd(),
266 0,
267 )
268 };
269 if ptr == libc::MAP_FAILED {
270 return Err(io::Error::last_os_error());
271 }
272 let ptr = NonNull::new(ptr.cast::<u8>())
274 .expect("mmap returned non-null on success");
275
276 Ok(Self {
277 lock_path: lock_dir_for_uid(owner_uid)
278 .join(format!("{}.lock", name.trim_start_matches('/'))),
279 name: cname,
280 process_lock: false,
281 lock_dir: OnceLock::new(),
282 ptr,
283 len: actual_size,
284 created: false,
285 })
286 }
287
288 pub fn open_or_create(
297 name: &str,
298 size: usize,
299 ) -> io::Result<Self> {
300 Self::open_or_create_with_policy(name, size, ShmAccessPolicy::default())
301 }
302
303 pub fn open_or_create_with_policy(
307 name: &str,
308 size: usize,
309 policy: ShmAccessPolicy,
310 ) -> io::Result<Self> {
311 let (region, initialization_lock) =
312 Self::open_or_create_inner(name, size, false, policy)?;
313 debug_assert!(initialization_lock.is_none());
314 Ok(region)
315 }
316
317 pub fn open_or_create_locked(
321 name: &str,
322 size: usize,
323 ) -> io::Result<(Self, ShmRegionLock)> {
324 Self::open_or_create_locked_with_policy(
325 name,
326 size,
327 ShmAccessPolicy::default(),
328 )
329 }
330
331 pub fn open_or_create_locked_with_policy(
334 name: &str,
335 size: usize,
336 policy: ShmAccessPolicy,
337 ) -> io::Result<(Self, ShmRegionLock)> {
338 let (region, initialization_lock) =
339 Self::open_or_create_inner(name, size, true, policy)?;
340 Ok((
341 region,
342 initialization_lock
343 .expect("locked SHM open must return its initialization lock"),
344 ))
345 }
346
347 fn open_or_create_inner(
348 name: &str,
349 size: usize,
350 process_lock: bool,
351 policy: ShmAccessPolicy,
352 ) -> io::Result<(Self, Option<ShmRegionLock>)> {
353 let cname = CString::new(name).map_err(|_| {
354 io::Error::new(io::ErrorKind::InvalidInput, "shm name has nul byte")
355 })?;
356 let lock_path = lock_file_path(name);
357 let initialization_lock = if process_lock {
358 Some(lock_path_exclusive(&lock_path)?)
359 } else {
360 None
361 };
362
363 #[cfg(target_os = "macos")]
366 let can_create =
367 policy.gid().is_none_or(|gid| gid == unsafe { libc::getegid() });
368 #[cfg(target_os = "macos")]
369 let creation_mode = policy.mode();
370 #[cfg(not(target_os = "macos"))]
372 let (can_create, creation_mode) = (true, 0o600u32);
373 #[cfg(not(target_vendor = "apple"))]
375 let creation_mode = creation_mode as libc::mode_t;
376 let (raw_fd, created) = unsafe {
378 let fd = if can_create {
380 libc::shm_open(
381 cname.as_ptr(),
382 libc::O_RDWR | libc::O_CREAT | libc::O_EXCL,
383 creation_mode,
384 )
385 } else {
386 libc::shm_open(cname.as_ptr(), libc::O_RDWR, 0)
387 };
388 if fd >= 0 {
389 (fd, can_create)
390 } else {
391 let err = io::Error::last_os_error();
393 if !can_create && err.raw_os_error() == Some(libc::ENOENT) {
394 return Err(io::Error::new(
395 io::ErrorKind::InvalidInput,
396 "creating group-shared SHM requires the selected effective gid",
397 ));
398 }
399 if err.raw_os_error() != Some(libc::EEXIST) {
400 return Err(err);
401 }
402 let fd = libc::shm_open(cname.as_ptr(), libc::O_RDWR, 0o600);
403 if fd < 0 {
404 return Err(io::Error::last_os_error());
405 }
406 (fd, false)
407 }
408 };
409 let fd = unsafe { OwnedFd::from_raw_fd(raw_fd) };
412
413 #[cfg(not(target_os = "macos"))]
414 if created && let Err(error) = configure_new_shm(&fd, policy) {
415 let _ = unsafe { libc::shm_unlink(cname.as_ptr()) };
416 return Err(error);
417 }
418
419 let owner_uid = unsafe { libc::geteuid() };
422 if let Err(error) = shm_object_size(&fd, name, owner_uid, policy) {
423 if created {
424 let _ = unsafe { libc::shm_unlink(cname.as_ptr()) };
425 }
426 return Err(error);
427 }
428
429 if created {
431 let rc =
433 unsafe { libc::ftruncate(fd.as_raw_fd(), size as libc::off_t) };
434 if rc != 0 {
435 let err = io::Error::last_os_error();
436 let _ = unsafe { libc::shm_unlink(cname.as_ptr()) };
437 return Err(err);
438 }
439 }
440
441 let actual_size = match shm_object_size(&fd, name, owner_uid, policy) {
446 Ok(actual_size) => actual_size,
447 Err(error) => {
448 if created {
449 let _ = unsafe { libc::shm_unlink(cname.as_ptr()) };
450 }
451 return Err(error);
452 }
453 };
454 if let Err(error) = validate_minimum_size(name, actual_size, size) {
455 if created {
456 let _ = unsafe { libc::shm_unlink(cname.as_ptr()) };
457 }
458 return Err(error);
459 }
460
461 let ptr = unsafe {
464 libc::mmap(
465 std::ptr::null_mut(),
466 size,
467 libc::PROT_READ | libc::PROT_WRITE,
468 libc::MAP_SHARED,
469 fd.as_raw_fd(),
470 0,
471 )
472 };
473
474 if ptr == libc::MAP_FAILED {
475 let err = io::Error::last_os_error();
476 if created {
477 let _ = unsafe { libc::shm_unlink(cname.as_ptr()) };
478 }
479 return Err(err);
480 }
481
482 let ptr = NonNull::new(ptr.cast::<u8>())
484 .expect("mmap returned non-null on success");
485
486 Ok((
487 Self {
488 name: cname,
489 lock_path,
490 process_lock,
491 lock_dir: OnceLock::new(),
492 ptr,
493 len: size,
494 created,
495 },
496 initialization_lock,
497 ))
498 }
499
500 pub fn as_ptr(&self) -> *mut u8 {
502 self.ptr.as_ptr()
503 }
504
505 pub fn len(&self) -> usize {
507 self.len
508 }
509
510 pub fn is_empty(&self) -> bool {
511 self.len == 0
512 }
513
514 pub fn created(&self) -> bool {
518 self.created
519 }
520
521 pub fn lock_exclusive(&self) -> io::Result<ShmRegionLock> {
526 if !self.process_lock {
527 return Err(io::Error::new(
528 io::ErrorKind::InvalidInput,
529 "SHM region was opened without a process lock",
530 ));
531 }
532 lock_fd_exclusive(self.open_own_lock_file()?)
533 }
534
535 fn open_own_lock_file(&self) -> io::Result<OwnedFd> {
538 let (Some(dir), Some(name)) =
539 (self.lock_path.parent(), self.lock_path.file_name())
540 else {
541 return open_lock_file(&self.lock_path);
542 };
543 let verified = match self.lock_dir.get() {
544 Some(verified) => verified,
545 None => {
546 let _ = self.lock_dir.set(open_lock_dir(dir)?);
548 self.lock_dir.get().expect("lock directory was just set")
549 }
550 };
551 match open_lock_file_in(verified, name, &self.lock_path) {
552 Err(error) if error.kind() == io::ErrorKind::NotFound => {
555 open_lock_file(&self.lock_path)
556 }
557 opened => opened,
558 }
559 }
560
561 #[cfg(test)]
562 pub(crate) fn try_lock_exclusive(&self) -> io::Result<ShmRegionLock> {
563 if !self.process_lock {
564 return Err(io::Error::new(
565 io::ErrorKind::InvalidInput,
566 "SHM region was opened without a process lock",
567 ));
568 }
569 let lock_fd = open_lock_file(&self.lock_path)?;
570 let rc = unsafe {
571 libc::flock(lock_fd.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB)
572 };
573 if rc == 0 {
574 Ok(ShmRegionLock { lock_fd })
575 } else {
576 Err(io::Error::last_os_error())
577 }
578 }
579
580 pub fn unlink(&self) -> io::Result<()> {
584 let rc = unsafe { libc::shm_unlink(self.name.as_ptr()) };
586 let shm_error = if rc != 0 {
587 let err = io::Error::last_os_error();
588 if err.raw_os_error() == Some(libc::ENOENT) {
590 None
591 } else {
592 Some(err)
593 }
594 } else {
595 None
596 };
597 let mut lock_error = match std::fs::remove_file(&self.lock_path) {
598 Ok(()) => None,
599 Err(error) if error.kind() == io::ErrorKind::NotFound => None,
600 Err(error) => Some(error),
601 };
602 let name = self.name.to_string_lossy();
603 let uid = unsafe { libc::geteuid() };
605 match companion_lock_files(&name, uid) {
606 Ok(files) => {
607 for file in files {
608 if let Err(error) = std::fs::remove_file(&file)
609 && error.kind() != io::ErrorKind::NotFound
610 {
611 lock_error.get_or_insert(error);
612 }
613 }
614 }
615 Err(error) => {
616 lock_error.get_or_insert(error);
617 }
618 }
619 if let Some(error) = shm_error.or(lock_error) {
620 return Err(error);
621 }
622 Ok(())
623 }
624}
625
626#[cfg(not(target_os = "macos"))]
627fn configure_new_shm(
628 fd: &OwnedFd,
629 policy: ShmAccessPolicy,
630) -> io::Result<()> {
631 let Some(gid) = policy.gid() else {
632 return Ok(());
633 };
634 if gid == !0 {
635 return Err(io::Error::new(
636 io::ErrorKind::InvalidInput,
637 "invalid SHM group id",
638 ));
639 }
640 if unsafe { libc::fchown(fd.as_raw_fd(), !0, gid) } != 0 {
642 return Err(io::Error::last_os_error());
643 }
644 if unsafe { libc::fchmod(fd.as_raw_fd(), policy.mode() as _) } != 0 {
646 return Err(io::Error::last_os_error());
647 }
648 Ok(())
649}
650
651fn shm_object_size(
652 fd: &OwnedFd,
653 name: &str,
654 owner_uid: u32,
655 policy: ShmAccessPolicy,
656) -> io::Result<usize> {
657 let mut stat = std::mem::MaybeUninit::<libc::stat>::uninit();
658 let stat_rc = unsafe { libc::fstat(fd.as_raw_fd(), stat.as_mut_ptr()) };
660 if stat_rc != 0 {
661 return Err(io::Error::last_os_error());
662 }
663 let stat = unsafe { stat.assume_init() };
665 let mode = u64::from(stat.st_mode) & 0o7777;
666 if stat.st_uid != owner_uid
667 || policy.gid().is_some_and(|gid| gid != stat.st_gid)
668 || mode & !u64::from(policy.mode()) != 0
669 {
670 return Err(io::Error::new(
671 io::ErrorKind::PermissionDenied,
672 format!(
673 "SHM segment {name} owner/group/mode do not satisfy {policy:?} for uid {owner_uid}"
674 ),
675 ));
676 }
677 let actual_size = stat.st_size;
678 usize::try_from(actual_size).map_err(|_| {
679 io::Error::new(
680 io::ErrorKind::InvalidData,
681 format!("SHM segment {name} reported invalid size {actual_size}"),
682 )
683 })
684}
685
686fn validate_minimum_size(
687 name: &str,
688 actual_size: usize,
689 minimum_size: usize,
690) -> io::Result<()> {
691 if actual_size < minimum_size {
692 return Err(io::Error::new(
693 io::ErrorKind::InvalidData,
694 format!(
695 "SHM segment {name} size {actual_size} is smaller than requested mapping {minimum_size}"
696 ),
697 ));
698 }
699 Ok(())
700}
701
702pub struct ShmRegionLock {
707 lock_fd: OwnedFd,
708}
709
710impl Drop for ShmRegionLock {
711 fn drop(&mut self) {
712 let _ = unsafe { libc::flock(self.lock_fd.as_raw_fd(), libc::LOCK_UN) };
713 }
714}
715
716fn lock_path_exclusive(lock_path: &Path) -> io::Result<ShmRegionLock> {
717 lock_fd_exclusive(open_lock_file(lock_path)?)
718}
719
720fn open_lock_file(lock_path: &Path) -> io::Result<OwnedFd> {
721 if let Some(dir) = lock_path.parent() {
722 ensure_lock_dir(dir)?;
723 }
724
725 let file = OpenOptions::new()
726 .read(true)
727 .write(true)
728 .create(true)
729 .mode(0o600)
730 .custom_flags(libc::O_CLOEXEC | libc::O_NOFOLLOW)
731 .open(lock_path)?;
732
733 ensure_own_lock_file(&file, lock_path)?;
734 Ok(file.into())
735}
736
737fn ensure_own_lock_file(
743 file: &std::fs::File,
744 lock_path: &Path,
745) -> io::Result<()> {
746 use std::os::unix::fs::MetadataExt;
747
748 let uid = unsafe { libc::geteuid() };
749 let owner = file.metadata()?.uid();
750 if owner != uid {
751 return Err(io::Error::new(
752 io::ErrorKind::PermissionDenied,
753 format!(
754 "{} is owned by uid {owner} rather than {uid}",
755 lock_path.display()
756 ),
757 ));
758 }
759 Ok(())
760}
761
762fn open_lock_dir(dir: &Path) -> io::Result<OwnedFd> {
768 ensure_lock_dir(dir)?;
769 let opened = OpenOptions::new()
770 .read(true)
771 .custom_flags(libc::O_DIRECTORY | libc::O_CLOEXEC | libc::O_NOFOLLOW)
772 .open(dir)?;
773 let uid = unsafe { libc::geteuid() };
774 ensure_private(&opened.metadata()?, dir, uid)?;
775 Ok(opened.into())
776}
777
778fn open_lock_file_in(
780 dir: &OwnedFd,
781 name: &std::ffi::OsStr,
782 lock_path: &Path,
783) -> io::Result<OwnedFd> {
784 use std::os::unix::ffi::OsStrExt;
785
786 let name = CString::new(name.as_bytes()).map_err(|_| {
787 io::Error::new(io::ErrorKind::InvalidInput, "lock name has nul byte")
788 })?;
789 let fd = loop {
790 let fd = unsafe {
792 libc::openat(
793 dir.as_raw_fd(),
794 name.as_ptr(),
795 libc::O_RDWR
796 | libc::O_CREAT
797 | libc::O_CLOEXEC
798 | libc::O_NOFOLLOW,
799 0o600 as libc::c_uint,
800 )
801 };
802 if fd >= 0 {
803 break fd;
804 }
805 let error = io::Error::last_os_error();
806 if error.kind() != io::ErrorKind::Interrupted {
807 return Err(error);
808 }
809 };
810 let file = std::fs::File::from(unsafe { OwnedFd::from_raw_fd(fd) });
812 ensure_own_lock_file(&file, lock_path)?;
813 Ok(file.into())
814}
815
816fn lock_fd_exclusive(lock_fd: OwnedFd) -> io::Result<ShmRegionLock> {
817 loop {
818 let rc = unsafe { libc::flock(lock_fd.as_raw_fd(), libc::LOCK_EX) };
819 if rc == 0 {
820 return Ok(ShmRegionLock { lock_fd });
821 }
822 let error = io::Error::last_os_error();
823 if error.kind() != io::ErrorKind::Interrupted {
824 return Err(error);
825 }
826 }
827}
828
829impl Drop for ShmRegion {
830 fn drop(&mut self) {
831 unsafe {
833 libc::munmap(self.ptr.as_ptr().cast(), self.len);
834 }
835 }
836}
837
838unsafe impl Send for ShmRegion {}
842unsafe impl Sync for ShmRegion {}
843
844pub fn ring_segment_name(
846 fleet_name: &str,
847 kind: u8,
848) -> String {
849 let uid = unsafe { libc::geteuid() };
851 ring_segment_name_for_uid(fleet_name, kind, uid)
852}
853
854pub fn ring_segment_name_for_uid(
859 fleet_name: &str,
860 kind: u8,
861 uid: u32,
862) -> String {
863 format!("/{SHM_NAMESPACE}-{fleet_name}-{kind}-{uid}")
864}
865
866pub fn fleet_lock_path(fleet_name: &str) -> PathBuf {
868 lock_dir().join(format!("{SHM_NAMESPACE}-{fleet_name}.fleet"))
869}
870
871pub fn fleet_lock_path_for_uid(
874 fleet_name: &str,
875 uid: u32,
876) -> PathBuf {
877 lock_dir_for_uid(uid).join(format!("{SHM_NAMESPACE}-{fleet_name}.fleet"))
878}
879
880pub struct FleetMembership {
886 lock_fd: OwnedFd,
887}
888
889impl Drop for FleetMembership {
890 fn drop(&mut self) {
891 let _ = unsafe { libc::flock(self.lock_fd.as_raw_fd(), libc::LOCK_UN) };
892 }
893}
894
895pub fn join_fleet_membership(fleet_name: &str) -> io::Result<FleetMembership> {
898 let lock_fd = open_lock_file(&fleet_lock_path(fleet_name))?;
899 let rc = unsafe { libc::flock(lock_fd.as_raw_fd(), libc::LOCK_SH) };
901 if rc != 0 {
902 return Err(io::Error::last_os_error());
903 }
904 Ok(FleetMembership { lock_fd })
905}
906
907pub fn hold_existing_fleet(fleet_name: &str) -> io::Result<FleetMembership> {
910 use std::os::unix::fs::MetadataExt;
911 let path = fleet_lock_path(fleet_name);
912 let file = OpenOptions::new()
913 .read(true)
914 .custom_flags(libc::O_CLOEXEC | libc::O_NOFOLLOW)
915 .open(&path)?;
916 let metadata = file.metadata()?;
917 if !metadata.is_file() || metadata.uid() != unsafe { libc::geteuid() } {
918 return Err(io::Error::new(
919 io::ErrorKind::PermissionDenied,
920 "fleet membership file has an unexpected owner or type",
921 ));
922 }
923 let lock_fd: OwnedFd = file.into();
924 loop {
925 let rc = unsafe { libc::flock(lock_fd.as_raw_fd(), libc::LOCK_SH) };
926 if rc == 0 {
927 return Ok(FleetMembership { lock_fd });
928 }
929 let error = io::Error::last_os_error();
930 if error.kind() != io::ErrorKind::Interrupted {
931 return Err(error);
932 }
933 }
934}
935
936pub fn try_lock_fleet_exclusive(
943 fleet_name: &str,
944 uid: u32,
945) -> io::Result<Option<ShmRegionLock>> {
946 let lock_fd = open_lock_file(&fleet_lock_path_for_uid(fleet_name, uid))?;
949 let rc = unsafe {
951 libc::flock(lock_fd.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB)
952 };
953 if rc == 0 {
954 return Ok(Some(ShmRegionLock { lock_fd }));
955 }
956 let error = io::Error::last_os_error();
957 if error.kind() == io::ErrorKind::WouldBlock {
958 return Ok(None);
959 }
960 Err(error)
961}
962
963pub struct LaneHold {
971 lock_fd: OwnedFd,
972}
973
974pub struct PreparedLaneLock {
978 fd: OwnedFd,
979 busy: AtomicBool,
980 pid: u32,
981}
982
983impl PreparedLaneLock {
984 pub fn open(
985 shm_name: &str,
986 lane: usize,
987 ) -> io::Result<Self> {
988 let name = format!("{}.lane{lane}", shm_name.trim_start_matches('/'));
989 Ok(Self {
990 fd: open_lock_file(&lock_file_path(&name))?,
991 busy: AtomicBool::new(false),
992 pid: std::process::id(),
993 })
994 }
995
996 pub fn try_lock(&self) -> io::Result<Option<PreparedLaneGuard<'_>>> {
998 if self.pid != std::process::id() {
999 return Err(io::Error::from(io::ErrorKind::PermissionDenied));
1000 }
1001 if self
1002 .busy
1003 .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
1004 .is_err()
1005 {
1006 return Ok(None);
1007 }
1008 if unsafe {
1010 libc::flock(self.fd.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB)
1011 } == 0
1012 {
1013 return Ok(Some(PreparedLaneGuard { lock: self }));
1014 }
1015 let error = io::Error::last_os_error();
1016 self.busy.store(false, Ordering::Release);
1017 if error.kind() == io::ErrorKind::WouldBlock {
1018 Ok(None)
1019 } else {
1020 Err(error)
1021 }
1022 }
1023}
1024
1025pub struct PreparedLaneGuard<'a> {
1026 lock: &'a PreparedLaneLock,
1027}
1028
1029impl Drop for PreparedLaneGuard<'_> {
1030 fn drop(&mut self) {
1031 if self.lock.pid == std::process::id() {
1032 unsafe {
1034 libc::flock(self.lock.fd.as_raw_fd(), libc::LOCK_UN);
1035 }
1036 self.lock.busy.store(false, Ordering::Release);
1037 }
1038 }
1039}
1040
1041impl Drop for LaneHold {
1042 fn drop(&mut self) {
1043 let _ = unsafe { libc::flock(self.lock_fd.as_raw_fd(), libc::LOCK_UN) };
1044 }
1045}
1046
1047pub fn try_hold_lane(
1051 shm_name: &str,
1052 lane: usize,
1053) -> io::Result<Option<LaneHold>> {
1054 let name = format!("{}.lane{lane}", shm_name.trim_start_matches('/'));
1055 let lock_fd = open_lock_file(&lock_file_path(&name))?;
1056 let rc = unsafe {
1058 libc::flock(lock_fd.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB)
1059 };
1060 if rc == 0 {
1061 return Ok(Some(LaneHold { lock_fd }));
1062 }
1063 let error = io::Error::last_os_error();
1064 if error.kind() == io::ErrorKind::WouldBlock {
1065 return Ok(None);
1066 }
1067 Err(error)
1068}
1069
1070pub struct WatcherHold {
1078 lock_fd: OwnedFd,
1079}
1080
1081impl Drop for WatcherHold {
1082 fn drop(&mut self) {
1083 let _ = unsafe { libc::flock(self.lock_fd.as_raw_fd(), libc::LOCK_UN) };
1084 }
1085}
1086
1087pub fn try_hold_watcher(
1092 shm_name: &str,
1093 slot: usize,
1094) -> io::Result<Option<WatcherHold>> {
1095 let name = format!("{}.watch{slot}", shm_name.trim_start_matches('/'));
1096 let lock_fd = open_lock_file(&lock_file_path(&name))?;
1097 let rc = unsafe {
1099 libc::flock(lock_fd.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB)
1100 };
1101 if rc == 0 {
1102 return Ok(Some(WatcherHold { lock_fd }));
1103 }
1104 let error = io::Error::last_os_error();
1105 if error.kind() == io::ErrorKind::WouldBlock {
1106 return Ok(None);
1107 }
1108 Err(error)
1109}
1110
1111pub fn companion_lock_files(
1117 shm_name: &str,
1118 uid: u32,
1119) -> io::Result<Vec<PathBuf>> {
1120 let base = shm_name.trim_start_matches('/');
1121 let own = format!("{base}.lock");
1122 let lane = format!("{base}.lane");
1123 let watch = format!("{base}.watch");
1124 let dir = lock_dir_for_uid(uid);
1125 let entries = match std::fs::read_dir(&dir) {
1126 Ok(entries) => entries,
1127 Err(error) if error.kind() == io::ErrorKind::NotFound => {
1128 return Ok(Vec::new());
1129 }
1130 Err(error) => return Err(error),
1131 };
1132 let mut found = Vec::new();
1133 for entry in entries {
1134 let entry = entry?;
1135 let Some(file) = entry.file_name().to_str().map(str::to_owned) else {
1136 continue;
1137 };
1138 if file == own
1139 || ((file.starts_with(&lane) || file.starts_with(&watch))
1140 && file.ends_with(".lock"))
1141 {
1142 found.push(entry.path());
1143 }
1144 }
1145 Ok(found)
1146}
1147
1148fn lock_file_path(shm_name: &str) -> PathBuf {
1149 lock_dir().join(format!("{}.lock", shm_name.trim_start_matches('/')))
1150}
1151
1152fn lock_dir() -> PathBuf {
1175 let uid = unsafe { libc::geteuid() };
1176 lock_dir_for_uid(uid)
1177}
1178
1179pub fn lock_dir_for_uid(uid: u32) -> PathBuf {
1183 Path::new("/tmp").join(format!("{SHM_NAMESPACE}-{uid}"))
1184}
1185
1186fn ensure_lock_dir(dir: &Path) -> io::Result<()> {
1192 use std::os::unix::fs::DirBuilderExt;
1193
1194 let uid = unsafe { libc::geteuid() };
1195 match std::fs::DirBuilder::new().mode(0o700).create(dir) {
1196 Ok(()) => {}
1197 Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {}
1198 Err(error) => return Err(error),
1199 }
1200
1201 ensure_private_dir(dir, uid)
1202}
1203
1204fn ensure_private_dir(
1210 dir: &Path,
1211 uid: u32,
1212) -> io::Result<()> {
1213 ensure_private(&std::fs::symlink_metadata(dir)?, dir, uid)
1214}
1215
1216fn ensure_private(
1218 metadata: &std::fs::Metadata,
1219 dir: &Path,
1220 uid: u32,
1221) -> io::Result<()> {
1222 use std::os::unix::fs::{MetadataExt, PermissionsExt};
1223
1224 if !metadata.is_dir() {
1225 return Err(io::Error::new(
1226 io::ErrorKind::PermissionDenied,
1227 format!("{} is not a directory", dir.display()),
1228 ));
1229 }
1230 if metadata.uid() != uid {
1231 return Err(io::Error::new(
1232 io::ErrorKind::PermissionDenied,
1233 format!(
1234 "{} is owned by uid {} rather than {uid}; refusing to lock in a directory \
1235 another user controls",
1236 dir.display(),
1237 metadata.uid()
1238 ),
1239 ));
1240 }
1241 if metadata.permissions().mode() & 0o077 != 0 {
1242 return Err(io::Error::new(
1243 io::ErrorKind::PermissionDenied,
1244 format!(
1245 "{} is mode {:o}; refusing to lock in a directory others can write to",
1246 dir.display(),
1247 metadata.permissions().mode() & 0o777
1248 ),
1249 ));
1250 }
1251
1252 Ok(())
1253}
1254
1255#[cfg(test)]
1256#[path = "../tests/shm/identity.rs"]
1257mod lock_identity_tests;
1258
1259#[cfg(test)]
1260mod fleet_lock_tests {
1261 use super::{join_fleet_membership, try_lock_fleet_exclusive};
1262
1263 #[test]
1264 fn prepared_lane_excludes_threads_and_distinct_file_descriptions() {
1265 let name = format!("prepared-{}", std::process::id());
1266 let first = super::PreparedLaneLock::open(&name, 0).unwrap();
1267 let second = super::PreparedLaneLock::open(&name, 0).unwrap();
1268 let hold = first.try_lock().unwrap().unwrap();
1269 assert!(first.try_lock().unwrap().is_none());
1270 std::thread::scope(|scope| {
1271 assert!(
1272 scope
1273 .spawn(|| first.try_lock().unwrap().is_none())
1274 .join()
1275 .unwrap()
1276 );
1277 });
1278 assert!(second.try_lock().unwrap().is_none());
1279 drop(hold);
1280 let other = second.try_lock().unwrap().unwrap();
1281 assert!(first.try_lock().unwrap().is_none());
1282 drop(other);
1283 drop(first);
1284 drop(second);
1285 std::fs::remove_file(super::lock_file_path(&format!("{name}.lane0")))
1286 .unwrap();
1287 }
1288
1289 #[test]
1292 fn a_member_holds_the_fleet_against_exclusive_takers() {
1293 let fleet = format!("fl{:x}", std::process::id());
1294 let uid = unsafe { libc::geteuid() };
1295
1296 let member = join_fleet_membership(&fleet).expect("join");
1297 assert!(try_lock_fleet_exclusive(&fleet, uid).expect("try").is_none());
1298
1299 drop(member);
1300 let exclusive = try_lock_fleet_exclusive(&fleet, uid).expect("try");
1301 assert!(exclusive.is_some());
1302 drop(exclusive);
1305 let _ =
1306 std::fs::remove_file(super::fleet_lock_path_for_uid(&fleet, uid));
1307 }
1308}