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_existing_locked(
336 name: &str,
337 size: usize,
338 ) -> io::Result<Option<(Self, ShmRegionLock)>> {
339 match Self::open_inner(
340 name,
341 size,
342 true,
343 ShmAccessPolicy::default(),
344 false,
345 ) {
346 Ok((region, lock)) => Ok(Some((
347 region,
348 lock.expect(
349 "locked SHM open must return its initialization lock",
350 ),
351 ))),
352 Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None),
353 Err(error) => Err(error),
354 }
355 }
356
357 pub fn open_or_create_locked_with_policy(
360 name: &str,
361 size: usize,
362 policy: ShmAccessPolicy,
363 ) -> io::Result<(Self, ShmRegionLock)> {
364 let (region, initialization_lock) =
365 Self::open_or_create_inner(name, size, true, policy)?;
366 Ok((
367 region,
368 initialization_lock
369 .expect("locked SHM open must return its initialization lock"),
370 ))
371 }
372
373 fn open_or_create_inner(
374 name: &str,
375 size: usize,
376 process_lock: bool,
377 policy: ShmAccessPolicy,
378 ) -> io::Result<(Self, Option<ShmRegionLock>)> {
379 Self::open_inner(name, size, process_lock, policy, true)
380 }
381
382 fn open_inner(
385 name: &str,
386 size: usize,
387 process_lock: bool,
388 policy: ShmAccessPolicy,
389 create: bool,
390 ) -> io::Result<(Self, Option<ShmRegionLock>)> {
391 let cname = CString::new(name).map_err(|_| {
392 io::Error::new(io::ErrorKind::InvalidInput, "shm name has nul byte")
393 })?;
394 let lock_path = lock_file_path(name);
395 let initialization_lock = if process_lock {
396 Some(lock_path_exclusive(&lock_path)?)
397 } else {
398 None
399 };
400
401 #[cfg(target_os = "macos")]
404 let can_create = create
405 && policy.gid().is_none_or(|gid| gid == unsafe { libc::getegid() });
406 #[cfg(target_os = "macos")]
407 let creation_mode = policy.mode();
408 #[cfg(not(target_os = "macos"))]
410 let (can_create, creation_mode) = (create, 0o600u32);
411 #[cfg(not(target_vendor = "apple"))]
413 let creation_mode = creation_mode as libc::mode_t;
414 let (raw_fd, created) = unsafe {
416 let fd = if can_create {
418 libc::shm_open(
419 cname.as_ptr(),
420 libc::O_RDWR | libc::O_CREAT | libc::O_EXCL,
421 creation_mode,
422 )
423 } else {
424 libc::shm_open(cname.as_ptr(), libc::O_RDWR, 0)
425 };
426 if fd >= 0 {
427 (fd, can_create)
428 } else {
429 let err = io::Error::last_os_error();
431 if !create && err.raw_os_error() == Some(libc::ENOENT) {
432 return Err(io::Error::new(
433 io::ErrorKind::NotFound,
434 "no SHM object has this name",
435 ));
436 }
437 if !can_create && err.raw_os_error() == Some(libc::ENOENT) {
438 return Err(io::Error::new(
439 io::ErrorKind::InvalidInput,
440 "creating group-shared SHM requires the selected effective gid",
441 ));
442 }
443 if err.raw_os_error() != Some(libc::EEXIST) {
444 return Err(err);
445 }
446 let fd = libc::shm_open(cname.as_ptr(), libc::O_RDWR, 0o600);
447 if fd < 0 {
448 return Err(io::Error::last_os_error());
449 }
450 (fd, false)
451 }
452 };
453 let fd = unsafe { OwnedFd::from_raw_fd(raw_fd) };
456
457 #[cfg(not(target_os = "macos"))]
458 if created && let Err(error) = configure_new_shm(&fd, policy) {
459 let _ = unsafe { libc::shm_unlink(cname.as_ptr()) };
460 return Err(error);
461 }
462
463 let owner_uid = unsafe { libc::geteuid() };
466 if let Err(error) = shm_object_size(&fd, name, owner_uid, policy) {
467 if created {
468 let _ = unsafe { libc::shm_unlink(cname.as_ptr()) };
469 }
470 return Err(error);
471 }
472
473 if created {
475 let rc =
477 unsafe { libc::ftruncate(fd.as_raw_fd(), size as libc::off_t) };
478 if rc != 0 {
479 let err = io::Error::last_os_error();
480 let _ = unsafe { libc::shm_unlink(cname.as_ptr()) };
481 return Err(err);
482 }
483 }
484
485 let actual_size = match shm_object_size(&fd, name, owner_uid, policy) {
490 Ok(actual_size) => actual_size,
491 Err(error) => {
492 if created {
493 let _ = unsafe { libc::shm_unlink(cname.as_ptr()) };
494 }
495 return Err(error);
496 }
497 };
498 if let Err(error) = validate_minimum_size(name, actual_size, size) {
499 if created {
500 let _ = unsafe { libc::shm_unlink(cname.as_ptr()) };
501 }
502 return Err(error);
503 }
504
505 let ptr = unsafe {
508 libc::mmap(
509 std::ptr::null_mut(),
510 size,
511 libc::PROT_READ | libc::PROT_WRITE,
512 libc::MAP_SHARED,
513 fd.as_raw_fd(),
514 0,
515 )
516 };
517
518 if ptr == libc::MAP_FAILED {
519 let err = io::Error::last_os_error();
520 if created {
521 let _ = unsafe { libc::shm_unlink(cname.as_ptr()) };
522 }
523 return Err(err);
524 }
525
526 let ptr = NonNull::new(ptr.cast::<u8>())
528 .expect("mmap returned non-null on success");
529
530 Ok((
531 Self {
532 name: cname,
533 lock_path,
534 process_lock,
535 lock_dir: OnceLock::new(),
536 ptr,
537 len: size,
538 created,
539 },
540 initialization_lock,
541 ))
542 }
543
544 pub fn as_ptr(&self) -> *mut u8 {
546 self.ptr.as_ptr()
547 }
548
549 pub fn len(&self) -> usize {
551 self.len
552 }
553
554 pub fn is_empty(&self) -> bool {
555 self.len == 0
556 }
557
558 pub fn created(&self) -> bool {
562 self.created
563 }
564
565 pub fn lock_exclusive(&self) -> io::Result<ShmRegionLock> {
570 if !self.process_lock {
571 return Err(io::Error::new(
572 io::ErrorKind::InvalidInput,
573 "SHM region was opened without a process lock",
574 ));
575 }
576 lock_fd_exclusive(self.open_own_lock_file()?)
577 }
578
579 fn open_own_lock_file(&self) -> io::Result<OwnedFd> {
582 let (Some(dir), Some(name)) =
583 (self.lock_path.parent(), self.lock_path.file_name())
584 else {
585 return open_lock_file(&self.lock_path);
586 };
587 let verified = match self.lock_dir.get() {
588 Some(verified) => verified,
589 None => {
590 let _ = self.lock_dir.set(open_lock_dir(dir)?);
592 self.lock_dir.get().expect("lock directory was just set")
593 }
594 };
595 match open_lock_file_in(verified, name, &self.lock_path) {
596 Err(error) if error.kind() == io::ErrorKind::NotFound => {
599 open_lock_file(&self.lock_path)
600 }
601 opened => opened,
602 }
603 }
604
605 #[cfg(test)]
606 pub(crate) fn try_lock_exclusive(&self) -> io::Result<ShmRegionLock> {
607 if !self.process_lock {
608 return Err(io::Error::new(
609 io::ErrorKind::InvalidInput,
610 "SHM region was opened without a process lock",
611 ));
612 }
613 let lock_fd = open_lock_file(&self.lock_path)?;
614 let rc = unsafe {
615 libc::flock(lock_fd.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB)
616 };
617 if rc == 0 {
618 Ok(ShmRegionLock { lock_fd })
619 } else {
620 Err(io::Error::last_os_error())
621 }
622 }
623
624 pub fn unlink(&self) -> io::Result<()> {
628 let rc = unsafe { libc::shm_unlink(self.name.as_ptr()) };
630 let shm_error = if rc != 0 {
631 let err = io::Error::last_os_error();
632 if err.raw_os_error() == Some(libc::ENOENT) {
634 None
635 } else {
636 Some(err)
637 }
638 } else {
639 None
640 };
641 let mut lock_error = match std::fs::remove_file(&self.lock_path) {
642 Ok(()) => None,
643 Err(error) if error.kind() == io::ErrorKind::NotFound => None,
644 Err(error) => Some(error),
645 };
646 let name = self.name.to_string_lossy();
647 let uid = unsafe { libc::geteuid() };
649 match companion_lock_files(&name, uid) {
650 Ok(files) => {
651 for file in files {
652 if let Err(error) = std::fs::remove_file(&file)
653 && error.kind() != io::ErrorKind::NotFound
654 {
655 lock_error.get_or_insert(error);
656 }
657 }
658 }
659 Err(error) => {
660 lock_error.get_or_insert(error);
661 }
662 }
663 if let Some(error) = shm_error.or(lock_error) {
664 return Err(error);
665 }
666 Ok(())
667 }
668}
669
670#[cfg(not(target_os = "macos"))]
671fn configure_new_shm(
672 fd: &OwnedFd,
673 policy: ShmAccessPolicy,
674) -> io::Result<()> {
675 let Some(gid) = policy.gid() else {
676 return Ok(());
677 };
678 if gid == !0 {
679 return Err(io::Error::new(
680 io::ErrorKind::InvalidInput,
681 "invalid SHM group id",
682 ));
683 }
684 if unsafe { libc::fchown(fd.as_raw_fd(), !0, gid) } != 0 {
686 return Err(io::Error::last_os_error());
687 }
688 if unsafe { libc::fchmod(fd.as_raw_fd(), policy.mode() as _) } != 0 {
690 return Err(io::Error::last_os_error());
691 }
692 Ok(())
693}
694
695fn shm_object_size(
696 fd: &OwnedFd,
697 name: &str,
698 owner_uid: u32,
699 policy: ShmAccessPolicy,
700) -> io::Result<usize> {
701 let mut stat = std::mem::MaybeUninit::<libc::stat>::uninit();
702 let stat_rc = unsafe { libc::fstat(fd.as_raw_fd(), stat.as_mut_ptr()) };
704 if stat_rc != 0 {
705 return Err(io::Error::last_os_error());
706 }
707 let stat = unsafe { stat.assume_init() };
709 let mode = u64::from(stat.st_mode) & 0o7777;
710 if stat.st_uid != owner_uid
711 || policy.gid().is_some_and(|gid| gid != stat.st_gid)
712 || mode & !u64::from(policy.mode()) != 0
713 {
714 return Err(io::Error::new(
715 io::ErrorKind::PermissionDenied,
716 format!(
717 "SHM segment {name} owner/group/mode do not satisfy {policy:?} for uid {owner_uid}"
718 ),
719 ));
720 }
721 let actual_size = stat.st_size;
722 usize::try_from(actual_size).map_err(|_| {
723 io::Error::new(
724 io::ErrorKind::InvalidData,
725 format!("SHM segment {name} reported invalid size {actual_size}"),
726 )
727 })
728}
729
730fn validate_minimum_size(
731 name: &str,
732 actual_size: usize,
733 minimum_size: usize,
734) -> io::Result<()> {
735 if actual_size < minimum_size {
736 return Err(io::Error::new(
737 io::ErrorKind::InvalidData,
738 format!(
739 "SHM segment {name} size {actual_size} is smaller than requested mapping {minimum_size}"
740 ),
741 ));
742 }
743 Ok(())
744}
745
746pub struct ShmRegionLock {
751 lock_fd: OwnedFd,
752}
753
754impl Drop for ShmRegionLock {
755 fn drop(&mut self) {
756 let _ = unsafe { libc::flock(self.lock_fd.as_raw_fd(), libc::LOCK_UN) };
757 }
758}
759
760fn lock_path_exclusive(lock_path: &Path) -> io::Result<ShmRegionLock> {
761 lock_fd_exclusive(open_lock_file(lock_path)?)
762}
763
764fn open_lock_file(lock_path: &Path) -> io::Result<OwnedFd> {
765 if let Some(dir) = lock_path.parent() {
766 ensure_lock_dir(dir)?;
767 }
768
769 let file = OpenOptions::new()
770 .read(true)
771 .write(true)
772 .create(true)
773 .mode(0o600)
774 .custom_flags(libc::O_CLOEXEC | libc::O_NOFOLLOW)
775 .open(lock_path)?;
776
777 ensure_own_lock_file(&file, lock_path)?;
778 Ok(file.into())
779}
780
781fn ensure_own_lock_file(
787 file: &std::fs::File,
788 lock_path: &Path,
789) -> io::Result<()> {
790 use std::os::unix::fs::MetadataExt;
791
792 let uid = unsafe { libc::geteuid() };
793 let owner = file.metadata()?.uid();
794 if owner != uid {
795 return Err(io::Error::new(
796 io::ErrorKind::PermissionDenied,
797 format!(
798 "{} is owned by uid {owner} rather than {uid}",
799 lock_path.display()
800 ),
801 ));
802 }
803 Ok(())
804}
805
806fn open_lock_dir(dir: &Path) -> io::Result<OwnedFd> {
812 ensure_lock_dir(dir)?;
813 let opened = OpenOptions::new()
814 .read(true)
815 .custom_flags(libc::O_DIRECTORY | libc::O_CLOEXEC | libc::O_NOFOLLOW)
816 .open(dir)?;
817 let uid = unsafe { libc::geteuid() };
818 ensure_private(&opened.metadata()?, dir, uid)?;
819 Ok(opened.into())
820}
821
822fn open_lock_file_in(
824 dir: &OwnedFd,
825 name: &std::ffi::OsStr,
826 lock_path: &Path,
827) -> io::Result<OwnedFd> {
828 use std::os::unix::ffi::OsStrExt;
829
830 let name = CString::new(name.as_bytes()).map_err(|_| {
831 io::Error::new(io::ErrorKind::InvalidInput, "lock name has nul byte")
832 })?;
833 let fd = loop {
834 let fd = unsafe {
836 libc::openat(
837 dir.as_raw_fd(),
838 name.as_ptr(),
839 libc::O_RDWR
840 | libc::O_CREAT
841 | libc::O_CLOEXEC
842 | libc::O_NOFOLLOW,
843 0o600 as libc::c_uint,
844 )
845 };
846 if fd >= 0 {
847 break fd;
848 }
849 let error = io::Error::last_os_error();
850 if error.kind() != io::ErrorKind::Interrupted {
851 return Err(error);
852 }
853 };
854 let file = std::fs::File::from(unsafe { OwnedFd::from_raw_fd(fd) });
856 ensure_own_lock_file(&file, lock_path)?;
857 Ok(file.into())
858}
859
860fn lock_fd_exclusive(lock_fd: OwnedFd) -> io::Result<ShmRegionLock> {
861 loop {
862 let rc = unsafe { libc::flock(lock_fd.as_raw_fd(), libc::LOCK_EX) };
863 if rc == 0 {
864 return Ok(ShmRegionLock { lock_fd });
865 }
866 let error = io::Error::last_os_error();
867 if error.kind() != io::ErrorKind::Interrupted {
868 return Err(error);
869 }
870 }
871}
872
873impl Drop for ShmRegion {
874 fn drop(&mut self) {
875 unsafe {
877 libc::munmap(self.ptr.as_ptr().cast(), self.len);
878 }
879 }
880}
881
882unsafe impl Send for ShmRegion {}
886unsafe impl Sync for ShmRegion {}
887
888pub fn ring_segment_name(
890 fleet_name: &str,
891 kind: u8,
892) -> String {
893 let uid = unsafe { libc::geteuid() };
895 ring_segment_name_for_uid(fleet_name, kind, uid)
896}
897
898pub fn ring_segment_name_for_uid(
903 fleet_name: &str,
904 kind: u8,
905 uid: u32,
906) -> String {
907 format!("/{SHM_NAMESPACE}-{fleet_name}-{kind}-{uid}")
908}
909
910pub fn fleet_lock_path(fleet_name: &str) -> PathBuf {
912 lock_dir().join(format!("{SHM_NAMESPACE}-{fleet_name}.fleet"))
913}
914
915pub fn fleet_lock_path_for_uid(
918 fleet_name: &str,
919 uid: u32,
920) -> PathBuf {
921 lock_dir_for_uid(uid).join(format!("{SHM_NAMESPACE}-{fleet_name}.fleet"))
922}
923
924pub struct FleetMembership {
930 lock_fd: OwnedFd,
931}
932
933impl Drop for FleetMembership {
934 fn drop(&mut self) {
935 let _ = unsafe { libc::flock(self.lock_fd.as_raw_fd(), libc::LOCK_UN) };
936 }
937}
938
939pub fn join_fleet_membership(fleet_name: &str) -> io::Result<FleetMembership> {
942 let lock_fd = open_lock_file(&fleet_lock_path(fleet_name))?;
943 let rc = unsafe { libc::flock(lock_fd.as_raw_fd(), libc::LOCK_SH) };
945 if rc != 0 {
946 return Err(io::Error::last_os_error());
947 }
948 Ok(FleetMembership { lock_fd })
949}
950
951pub fn hold_existing_fleet(fleet_name: &str) -> io::Result<FleetMembership> {
954 use std::os::unix::fs::MetadataExt;
955 let path = fleet_lock_path(fleet_name);
956 let file = OpenOptions::new()
957 .read(true)
958 .custom_flags(libc::O_CLOEXEC | libc::O_NOFOLLOW)
959 .open(&path)?;
960 let metadata = file.metadata()?;
961 if !metadata.is_file() || metadata.uid() != unsafe { libc::geteuid() } {
962 return Err(io::Error::new(
963 io::ErrorKind::PermissionDenied,
964 "fleet membership file has an unexpected owner or type",
965 ));
966 }
967 let lock_fd: OwnedFd = file.into();
968 loop {
969 let rc = unsafe { libc::flock(lock_fd.as_raw_fd(), libc::LOCK_SH) };
970 if rc == 0 {
971 return Ok(FleetMembership { lock_fd });
972 }
973 let error = io::Error::last_os_error();
974 if error.kind() != io::ErrorKind::Interrupted {
975 return Err(error);
976 }
977 }
978}
979
980pub fn try_lock_fleet_exclusive(
987 fleet_name: &str,
988 uid: u32,
989) -> io::Result<Option<ShmRegionLock>> {
990 let lock_fd = open_lock_file(&fleet_lock_path_for_uid(fleet_name, uid))?;
993 let rc = unsafe {
995 libc::flock(lock_fd.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB)
996 };
997 if rc == 0 {
998 return Ok(Some(ShmRegionLock { lock_fd }));
999 }
1000 let error = io::Error::last_os_error();
1001 if error.kind() == io::ErrorKind::WouldBlock {
1002 return Ok(None);
1003 }
1004 Err(error)
1005}
1006
1007pub struct LaneHold {
1015 lock_fd: OwnedFd,
1016}
1017
1018pub struct PreparedLaneLock {
1022 fd: OwnedFd,
1023 busy: AtomicBool,
1024 pid: u32,
1025}
1026
1027impl PreparedLaneLock {
1028 pub fn open(
1029 shm_name: &str,
1030 lane: usize,
1031 ) -> io::Result<Self> {
1032 let name = format!("{}.lane{lane}", shm_name.trim_start_matches('/'));
1033 Ok(Self {
1034 fd: open_lock_file(&lock_file_path(&name))?,
1035 busy: AtomicBool::new(false),
1036 pid: std::process::id(),
1037 })
1038 }
1039
1040 pub fn try_lock(&self) -> io::Result<Option<PreparedLaneGuard<'_>>> {
1042 if self.pid != std::process::id() {
1043 return Err(io::Error::from(io::ErrorKind::PermissionDenied));
1044 }
1045 if self
1046 .busy
1047 .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
1048 .is_err()
1049 {
1050 return Ok(None);
1051 }
1052 if unsafe {
1054 libc::flock(self.fd.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB)
1055 } == 0
1056 {
1057 return Ok(Some(PreparedLaneGuard { lock: self }));
1058 }
1059 let error = io::Error::last_os_error();
1060 self.busy.store(false, Ordering::Release);
1061 if error.kind() == io::ErrorKind::WouldBlock {
1062 Ok(None)
1063 } else {
1064 Err(error)
1065 }
1066 }
1067}
1068
1069pub struct PreparedLaneGuard<'a> {
1070 lock: &'a PreparedLaneLock,
1071}
1072
1073impl Drop for PreparedLaneGuard<'_> {
1074 fn drop(&mut self) {
1075 if self.lock.pid == std::process::id() {
1076 unsafe {
1078 libc::flock(self.lock.fd.as_raw_fd(), libc::LOCK_UN);
1079 }
1080 self.lock.busy.store(false, Ordering::Release);
1081 }
1082 }
1083}
1084
1085impl Drop for LaneHold {
1086 fn drop(&mut self) {
1087 let _ = unsafe { libc::flock(self.lock_fd.as_raw_fd(), libc::LOCK_UN) };
1088 }
1089}
1090
1091pub fn try_hold_lane(
1095 shm_name: &str,
1096 lane: usize,
1097) -> io::Result<Option<LaneHold>> {
1098 let name = format!("{}.lane{lane}", shm_name.trim_start_matches('/'));
1099 let lock_fd = open_lock_file(&lock_file_path(&name))?;
1100 let rc = unsafe {
1102 libc::flock(lock_fd.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB)
1103 };
1104 if rc == 0 {
1105 return Ok(Some(LaneHold { lock_fd }));
1106 }
1107 let error = io::Error::last_os_error();
1108 if error.kind() == io::ErrorKind::WouldBlock {
1109 return Ok(None);
1110 }
1111 Err(error)
1112}
1113
1114pub fn wait_lane_free(
1120 shm_name: &str,
1121 lane: usize,
1122) -> io::Result<()> {
1123 let name = format!("{}.lane{lane}", shm_name.trim_start_matches('/'));
1124 wait_free(&open_lock_file(&lock_file_path(&name))?)
1125}
1126
1127pub(crate) fn wait_free(lock_fd: &OwnedFd) -> io::Result<()> {
1128 loop {
1129 if unsafe { libc::flock(lock_fd.as_raw_fd(), libc::LOCK_EX) } == 0 {
1131 return Ok(());
1132 }
1133 let error = io::Error::last_os_error();
1134 if error.kind() != io::ErrorKind::Interrupted {
1135 return Err(error);
1136 }
1137 }
1138}
1139
1140pub struct WatcherHold {
1148 lock_fd: OwnedFd,
1149}
1150
1151impl Drop for WatcherHold {
1152 fn drop(&mut self) {
1153 let _ = unsafe { libc::flock(self.lock_fd.as_raw_fd(), libc::LOCK_UN) };
1154 }
1155}
1156
1157pub fn try_hold_watcher(
1162 shm_name: &str,
1163 slot: usize,
1164) -> io::Result<Option<WatcherHold>> {
1165 let name = format!("{}.watch{slot}", shm_name.trim_start_matches('/'));
1166 let lock_fd = open_lock_file(&lock_file_path(&name))?;
1167 let rc = unsafe {
1169 libc::flock(lock_fd.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB)
1170 };
1171 if rc == 0 {
1172 return Ok(Some(WatcherHold { lock_fd }));
1173 }
1174 let error = io::Error::last_os_error();
1175 if error.kind() == io::ErrorKind::WouldBlock {
1176 return Ok(None);
1177 }
1178 Err(error)
1179}
1180
1181pub fn try_hold_adopter(shm_name: &str) -> io::Result<Option<WatcherHold>> {
1186 let name = format!("{}.watch-adopter", shm_name.trim_start_matches('/'));
1187 let lock_fd = open_lock_file(&lock_file_path(&name))?;
1188 let rc = unsafe {
1190 libc::flock(lock_fd.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB)
1191 };
1192 if rc == 0 {
1193 return Ok(Some(WatcherHold { lock_fd }));
1194 }
1195 let error = io::Error::last_os_error();
1196 if error.kind() == io::ErrorKind::WouldBlock {
1197 return Ok(None);
1198 }
1199 Err(error)
1200}
1201
1202pub fn companion_lock_files(
1208 shm_name: &str,
1209 uid: u32,
1210) -> io::Result<Vec<PathBuf>> {
1211 let base = shm_name.trim_start_matches('/');
1212 let own = format!("{base}.lock");
1213 let lane = format!("{base}.lane");
1214 let watch = format!("{base}.watch");
1215 let dir = lock_dir_for_uid(uid);
1216 let entries = match std::fs::read_dir(&dir) {
1217 Ok(entries) => entries,
1218 Err(error) if error.kind() == io::ErrorKind::NotFound => {
1219 return Ok(Vec::new());
1220 }
1221 Err(error) => return Err(error),
1222 };
1223 let mut found = Vec::new();
1224 for entry in entries {
1225 let entry = entry?;
1226 let Some(file) = entry.file_name().to_str().map(str::to_owned) else {
1227 continue;
1228 };
1229 if file == own
1230 || ((file.starts_with(&lane) || file.starts_with(&watch))
1231 && file.ends_with(".lock"))
1232 {
1233 found.push(entry.path());
1234 }
1235 }
1236 Ok(found)
1237}
1238
1239fn lock_file_path(shm_name: &str) -> PathBuf {
1240 lock_dir().join(format!("{}.lock", shm_name.trim_start_matches('/')))
1241}
1242
1243fn lock_dir() -> PathBuf {
1266 let uid = unsafe { libc::geteuid() };
1267 lock_dir_for_uid(uid)
1268}
1269
1270pub fn lock_dir_for_uid(uid: u32) -> PathBuf {
1274 Path::new("/tmp").join(format!("{SHM_NAMESPACE}-{uid}"))
1275}
1276
1277fn ensure_lock_dir(dir: &Path) -> io::Result<()> {
1283 use std::os::unix::fs::DirBuilderExt;
1284
1285 let uid = unsafe { libc::geteuid() };
1286 match std::fs::DirBuilder::new().mode(0o700).create(dir) {
1287 Ok(()) => {}
1288 Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {}
1289 Err(error) => return Err(error),
1290 }
1291
1292 ensure_private_dir(dir, uid)
1293}
1294
1295fn ensure_private_dir(
1301 dir: &Path,
1302 uid: u32,
1303) -> io::Result<()> {
1304 ensure_private(&std::fs::symlink_metadata(dir)?, dir, uid)
1305}
1306
1307fn ensure_private(
1309 metadata: &std::fs::Metadata,
1310 dir: &Path,
1311 uid: u32,
1312) -> io::Result<()> {
1313 use std::os::unix::fs::{MetadataExt, PermissionsExt};
1314
1315 if !metadata.is_dir() {
1316 return Err(io::Error::new(
1317 io::ErrorKind::PermissionDenied,
1318 format!("{} is not a directory", dir.display()),
1319 ));
1320 }
1321 if metadata.uid() != uid {
1322 return Err(io::Error::new(
1323 io::ErrorKind::PermissionDenied,
1324 format!(
1325 "{} is owned by uid {} rather than {uid}; refusing to lock in a directory \
1326 another user controls",
1327 dir.display(),
1328 metadata.uid()
1329 ),
1330 ));
1331 }
1332 if metadata.permissions().mode() & 0o077 != 0 {
1333 return Err(io::Error::new(
1334 io::ErrorKind::PermissionDenied,
1335 format!(
1336 "{} is mode {:o}; refusing to lock in a directory others can write to",
1337 dir.display(),
1338 metadata.permissions().mode() & 0o777
1339 ),
1340 ));
1341 }
1342
1343 Ok(())
1344}
1345
1346#[cfg(test)]
1347#[path = "../tests/shm/identity.rs"]
1348mod lock_identity_tests;
1349
1350#[cfg(test)]
1351mod fleet_lock_tests {
1352 use super::{join_fleet_membership, try_lock_fleet_exclusive};
1353
1354 #[test]
1355 fn prepared_lane_excludes_threads_and_distinct_file_descriptions() {
1356 let name = format!("prepared-{}", std::process::id());
1357 let first = super::PreparedLaneLock::open(&name, 0).unwrap();
1358 let second = super::PreparedLaneLock::open(&name, 0).unwrap();
1359 let hold = first.try_lock().unwrap().unwrap();
1360 assert!(first.try_lock().unwrap().is_none());
1361 std::thread::scope(|scope| {
1362 assert!(
1363 scope
1364 .spawn(|| first.try_lock().unwrap().is_none())
1365 .join()
1366 .unwrap()
1367 );
1368 });
1369 assert!(second.try_lock().unwrap().is_none());
1370 drop(hold);
1371 let other = second.try_lock().unwrap().unwrap();
1372 assert!(first.try_lock().unwrap().is_none());
1373 drop(other);
1374 drop(first);
1375 drop(second);
1376 std::fs::remove_file(super::lock_file_path(&format!("{name}.lane0")))
1377 .unwrap();
1378 }
1379
1380 #[test]
1383 fn a_member_holds_the_fleet_against_exclusive_takers() {
1384 let fleet = format!("fl{:x}", std::process::id());
1385 let uid = unsafe { libc::geteuid() };
1386
1387 let member = join_fleet_membership(&fleet).expect("join");
1388 assert!(try_lock_fleet_exclusive(&fleet, uid).expect("try").is_none());
1389
1390 drop(member);
1391 let exclusive = try_lock_fleet_exclusive(&fleet, uid).expect("try");
1392 assert!(exclusive.is_some());
1393 drop(exclusive);
1396 let _ =
1397 std::fs::remove_file(super::fleet_lock_path_for_uid(&fleet, uid));
1398 }
1399}