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};
45use std::sync::{Arc, OnceLock};
46
47use crate::process::ProcessOwner;
48
49mod group;
50mod section;
51pub use group::GroupAccess;
52
53pub const SHM_NAMESPACE: &str = "orbit";
55
56#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
67pub enum ShmAccessPolicy {
68 #[default]
70 OwnerOnly,
71 GroupRead { gid: u32 },
73 GroupReadWrite { gid: u32 },
75}
76
77impl ShmAccessPolicy {
78 pub const fn mode(self) -> u32 {
79 match self {
80 Self::OwnerOnly => 0o600,
81 Self::GroupRead { .. } => 0o640,
82 Self::GroupReadWrite { .. } => 0o660,
83 }
84 }
85
86 pub const fn gid(self) -> Option<u32> {
87 match self {
88 Self::OwnerOnly => None,
89 Self::GroupRead { gid } | Self::GroupReadWrite { gid } => Some(gid),
90 }
91 }
92}
93
94#[derive(Clone, Copy, Debug, Eq, PartialEq)]
101pub enum ShmValidation {
102 Missing,
104 Valid { actual_size: usize },
106}
107
108pub struct ShmRegion {
112 name: CString,
113 lock_path: PathBuf,
114 process_lock: bool,
116 section: OnceLock<Arc<section::Section>>,
120 lock_dir: OnceLock<OwnedFd>,
127 ptr: NonNull<u8>,
128 len: usize,
129 created: bool,
133}
134
135impl ShmRegion {
136 pub fn validate_existing(
146 name: &str,
147 minimum_size: usize,
148 ) -> io::Result<ShmValidation> {
149 Self::validate_existing_with_policy(
150 name,
151 minimum_size,
152 unsafe { libc::geteuid() },
153 ShmAccessPolicy::default(),
154 )
155 }
156
157 pub fn validate_existing_with_policy(
160 name: &str,
161 minimum_size: usize,
162 owner_uid: u32,
163 policy: ShmAccessPolicy,
164 ) -> io::Result<ShmValidation> {
165 let cname = CString::new(name).map_err(|_| {
166 io::Error::new(io::ErrorKind::InvalidInput, "shm name has nul byte")
167 })?;
168 let raw_fd = loop {
169 let fd = unsafe {
173 libc::shm_open(cname.as_ptr(), libc::O_RDONLY, 0o600)
174 };
175 if fd >= 0 {
176 break fd;
177 }
178 let error = io::Error::last_os_error();
179 if error.kind() == io::ErrorKind::Interrupted {
180 continue;
181 }
182 if error.raw_os_error() == Some(libc::ENOENT) {
183 return Ok(ShmValidation::Missing);
184 }
185 return Err(error);
186 };
187 let fd = unsafe { OwnedFd::from_raw_fd(raw_fd) };
190 let actual_size = shm_object_size(&fd, name, owner_uid, policy)?;
191 validate_minimum_size(name, actual_size, minimum_size)?;
192 Ok(ShmValidation::Valid { actual_size })
193 }
194
195 pub(crate) fn open_existing_read_only(
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, false)
208 }
209
210 pub fn open_existing_read_write(
213 name: &str,
214 minimum_size: usize,
215 owner_uid: u32,
216 policy: ShmAccessPolicy,
217 ) -> io::Result<Self> {
218 Self::open_existing(name, minimum_size, owner_uid, policy, true)
219 }
220
221 fn open_existing(
222 name: &str,
223 minimum_size: usize,
224 owner_uid: u32,
225 policy: ShmAccessPolicy,
226 writable: bool,
227 ) -> io::Result<Self> {
228 let cname = CString::new(name).map_err(|_| {
229 io::Error::new(io::ErrorKind::InvalidInput, "shm name has nul byte")
230 })?;
231 let raw_fd = loop {
232 let fd = unsafe {
237 libc::shm_open(
238 cname.as_ptr(),
239 if writable { libc::O_RDWR } else { libc::O_RDONLY },
240 0o600,
241 )
242 };
243 if fd >= 0 {
244 break fd;
245 }
246 let error = io::Error::last_os_error();
247 if error.kind() == io::ErrorKind::Interrupted {
248 continue;
249 }
250 return Err(error);
251 };
252 let fd = unsafe { OwnedFd::from_raw_fd(raw_fd) };
255 let actual_size = shm_object_size(&fd, name, owner_uid, policy)?;
256 validate_minimum_size(name, actual_size, minimum_size)?;
257
258 let ptr = unsafe {
263 libc::mmap(
264 std::ptr::null_mut(),
265 actual_size,
266 libc::PROT_READ | if writable { libc::PROT_WRITE } else { 0 },
267 libc::MAP_SHARED,
268 fd.as_raw_fd(),
269 0,
270 )
271 };
272 if ptr == libc::MAP_FAILED {
273 return Err(io::Error::last_os_error());
274 }
275 let ptr = NonNull::new(ptr.cast::<u8>())
277 .expect("mmap returned non-null on success");
278
279 Ok(Self {
280 lock_path: lock_dir_for_uid(owner_uid)
281 .join(format!("{}.lock", name.trim_start_matches('/'))),
282 name: cname,
283 process_lock: false,
284 lock_dir: OnceLock::new(),
285 section: OnceLock::new(),
286 ptr,
287 len: actual_size,
288 created: false,
289 })
290 }
291
292 pub fn open_or_create(
301 name: &str,
302 size: usize,
303 ) -> io::Result<Self> {
304 Self::open_or_create_with_policy(name, size, ShmAccessPolicy::default())
305 }
306
307 pub fn open_or_create_with_policy(
311 name: &str,
312 size: usize,
313 policy: ShmAccessPolicy,
314 ) -> io::Result<Self> {
315 let (region, initialization_lock) =
316 Self::open_or_create_inner(name, size, false, policy)?;
317 debug_assert!(initialization_lock.is_none());
318 Ok(region)
319 }
320
321 pub fn open_or_create_locked(
325 name: &str,
326 size: usize,
327 ) -> io::Result<(Self, ShmRegionLock)> {
328 Self::open_or_create_locked_with_policy(
329 name,
330 size,
331 ShmAccessPolicy::default(),
332 )
333 }
334
335 pub fn open_existing_locked(
340 name: &str,
341 size: usize,
342 ) -> io::Result<Option<(Self, ShmRegionLock)>> {
343 match Self::open_inner(
344 name,
345 size,
346 true,
347 ShmAccessPolicy::default(),
348 false,
349 ) {
350 Ok((region, lock)) => Ok(Some((
351 region,
352 lock.expect(
353 "locked SHM open must return its initialization lock",
354 ),
355 ))),
356 Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None),
357 Err(error) => Err(error),
358 }
359 }
360
361 pub fn open_or_create_locked_with_policy(
364 name: &str,
365 size: usize,
366 policy: ShmAccessPolicy,
367 ) -> io::Result<(Self, ShmRegionLock)> {
368 let (region, initialization_lock) =
369 Self::open_or_create_inner(name, size, true, policy)?;
370 Ok((
371 region,
372 initialization_lock
373 .expect("locked SHM open must return its initialization lock"),
374 ))
375 }
376
377 fn open_or_create_inner(
378 name: &str,
379 size: usize,
380 process_lock: bool,
381 policy: ShmAccessPolicy,
382 ) -> io::Result<(Self, Option<ShmRegionLock>)> {
383 Self::open_inner(name, size, process_lock, policy, true)
384 }
385
386 fn open_inner(
389 name: &str,
390 size: usize,
391 process_lock: bool,
392 policy: ShmAccessPolicy,
393 create: bool,
394 ) -> io::Result<(Self, Option<ShmRegionLock>)> {
395 let cname = CString::new(name).map_err(|_| {
396 io::Error::new(io::ErrorKind::InvalidInput, "shm name has nul byte")
397 })?;
398 let lock_path = lock_file_path(name);
399 let initialization_lock = if process_lock {
400 Some(lock_path_exclusive(&lock_path)?)
401 } else {
402 None
403 };
404
405 #[cfg(target_os = "macos")]
408 let can_create = create
409 && policy.gid().is_none_or(|gid| gid == unsafe { libc::getegid() });
410 #[cfg(target_os = "macos")]
411 let creation_mode = policy.mode();
412 #[cfg(not(target_os = "macos"))]
414 let (can_create, creation_mode) = (create, 0o600u32);
415 #[cfg(not(target_vendor = "apple"))]
417 let creation_mode = creation_mode as libc::mode_t;
418 let (raw_fd, created) = unsafe {
420 let fd = if can_create {
422 libc::shm_open(
423 cname.as_ptr(),
424 libc::O_RDWR | libc::O_CREAT | libc::O_EXCL,
425 creation_mode,
426 )
427 } else {
428 libc::shm_open(cname.as_ptr(), libc::O_RDWR, 0)
429 };
430 if fd >= 0 {
431 (fd, can_create)
432 } else {
433 let err = io::Error::last_os_error();
435 if !create && err.raw_os_error() == Some(libc::ENOENT) {
436 return Err(io::Error::new(
437 io::ErrorKind::NotFound,
438 "no SHM object has this name",
439 ));
440 }
441 if !can_create && err.raw_os_error() == Some(libc::ENOENT) {
442 return Err(io::Error::new(
443 io::ErrorKind::InvalidInput,
444 "creating group-shared SHM requires the selected effective gid",
445 ));
446 }
447 if err.raw_os_error() != Some(libc::EEXIST) {
448 return Err(err);
449 }
450 let fd = libc::shm_open(cname.as_ptr(), libc::O_RDWR, 0o600);
451 if fd < 0 {
452 return Err(io::Error::last_os_error());
453 }
454 (fd, false)
455 }
456 };
457 let fd = unsafe { OwnedFd::from_raw_fd(raw_fd) };
460
461 #[cfg(not(target_os = "macos"))]
462 if created && let Err(error) = configure_new_shm(&fd, policy) {
463 let _ = unsafe { libc::shm_unlink(cname.as_ptr()) };
464 return Err(error);
465 }
466
467 let owner_uid = unsafe { libc::geteuid() };
470 if let Err(error) = shm_object_size(&fd, name, owner_uid, policy) {
471 if created {
472 let _ = unsafe { libc::shm_unlink(cname.as_ptr()) };
473 }
474 return Err(error);
475 }
476
477 if created {
479 let rc =
481 unsafe { libc::ftruncate(fd.as_raw_fd(), size as libc::off_t) };
482 if rc != 0 {
483 let err = io::Error::last_os_error();
484 let _ = unsafe { libc::shm_unlink(cname.as_ptr()) };
485 return Err(err);
486 }
487 }
488
489 let actual_size = match shm_object_size(&fd, name, owner_uid, policy) {
494 Ok(actual_size) => actual_size,
495 Err(error) => {
496 if created {
497 let _ = unsafe { libc::shm_unlink(cname.as_ptr()) };
498 }
499 return Err(error);
500 }
501 };
502 if let Err(error) = validate_minimum_size(name, actual_size, size) {
503 if created {
504 let _ = unsafe { libc::shm_unlink(cname.as_ptr()) };
505 }
506 return Err(error);
507 }
508
509 let ptr = unsafe {
512 libc::mmap(
513 std::ptr::null_mut(),
514 size,
515 libc::PROT_READ | libc::PROT_WRITE,
516 libc::MAP_SHARED,
517 fd.as_raw_fd(),
518 0,
519 )
520 };
521
522 if ptr == libc::MAP_FAILED {
523 let err = io::Error::last_os_error();
524 if created {
525 let _ = unsafe { libc::shm_unlink(cname.as_ptr()) };
526 }
527 return Err(err);
528 }
529
530 let ptr = NonNull::new(ptr.cast::<u8>())
532 .expect("mmap returned non-null on success");
533
534 Ok((
535 Self {
536 name: cname,
537 lock_path,
538 process_lock,
539 lock_dir: OnceLock::new(),
540 section: OnceLock::new(),
541 ptr,
542 len: size,
543 created,
544 },
545 initialization_lock,
546 ))
547 }
548
549 pub fn as_ptr(&self) -> *mut u8 {
551 self.ptr.as_ptr()
552 }
553
554 pub fn len(&self) -> usize {
556 self.len
557 }
558
559 pub fn is_empty(&self) -> bool {
560 self.len == 0
561 }
562
563 pub fn created(&self) -> bool {
567 self.created
568 }
569
570 pub fn lock_exclusive(&self) -> io::Result<ShmRegionLock> {
575 self.lock(libc::LOCK_EX)
576 }
577
578 pub(crate) fn lock_shared(&self) -> io::Result<ShmRegionLock> {
579 self.lock(libc::LOCK_SH)
580 }
581
582 fn lock(
583 &self,
584 operation: libc::c_int,
585 ) -> io::Result<ShmRegionLock> {
586 if !self.process_lock {
587 return Err(io::Error::new(
588 io::ErrorKind::InvalidInput,
589 "SHM region was opened without a process lock",
590 ));
591 }
592 #[cfg(feature = "bench-timing")]
593 let opening = std::time::Instant::now();
594 let section = self.section()?;
595 #[cfg(feature = "bench-timing")]
596 let opened = std::time::Instant::now();
597 let guard = match section {
598 Some(section) => {
599 let exclusive = operation == libc::LOCK_EX;
600 section.acquire(exclusive, || self.open_own_lock_file())?;
601 ShmRegionLock::kept(section, exclusive)
602 }
603 None => lock_fd(self.open_own_lock_file()?, operation)?,
604 };
605 #[cfg(feature = "bench-timing")]
606 let guard = {
607 let flock = opened.elapsed();
608 let mut guard = guard;
609 guard.timing = Some(LockTiming {
610 open: opened.duration_since(opening),
611 flock,
612 });
613 guard
614 };
615 Ok(guard)
616 }
617
618 fn section(&self) -> io::Result<Option<Arc<section::Section>>> {
621 if let Some(section) = self.section.get() {
622 return Ok(section.is_current().then(|| Arc::clone(section)));
623 }
624 let fresh =
625 Arc::new(section::Section::new(self.open_own_lock_file()?)?);
626 let kept = self.section.get_or_init(|| fresh);
628 Ok(kept.is_current().then(|| Arc::clone(kept)))
629 }
630
631 fn open_own_lock_file(&self) -> io::Result<OwnedFd> {
634 let (Some(dir), Some(name)) =
635 (self.lock_path.parent(), self.lock_path.file_name())
636 else {
637 return open_lock_file(&self.lock_path);
638 };
639 let verified = match self.lock_dir.get() {
640 Some(verified) => verified,
641 None => {
642 let _ = self.lock_dir.set(open_lock_dir(dir)?);
644 self.lock_dir.get().expect("lock directory was just set")
645 }
646 };
647 match open_lock_file_in(verified, name, &self.lock_path) {
648 Err(error) if error.kind() == io::ErrorKind::NotFound => {
651 open_lock_file(&self.lock_path)
652 }
653 opened => opened,
654 }
655 }
656
657 #[cfg(test)]
658 pub(crate) fn try_lock_exclusive(&self) -> io::Result<ShmRegionLock> {
659 if !self.process_lock {
660 return Err(io::Error::new(
661 io::ErrorKind::InvalidInput,
662 "SHM region was opened without a process lock",
663 ));
664 }
665 let lock_fd = open_lock_file(&self.lock_path)?;
666 let rc = unsafe {
667 libc::flock(lock_fd.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB)
668 };
669 if rc == 0 {
670 Ok(ShmRegionLock::new(lock_fd))
671 } else {
672 Err(io::Error::last_os_error())
673 }
674 }
675
676 pub fn unlink(&self) -> io::Result<()> {
680 let rc = unsafe { libc::shm_unlink(self.name.as_ptr()) };
682 let shm_error = if rc != 0 {
683 let err = io::Error::last_os_error();
684 if err.raw_os_error() == Some(libc::ENOENT) {
686 None
687 } else {
688 Some(err)
689 }
690 } else {
691 None
692 };
693 let mut lock_error = match std::fs::remove_file(&self.lock_path) {
694 Ok(()) => None,
695 Err(error) if error.kind() == io::ErrorKind::NotFound => None,
696 Err(error) => Some(error),
697 };
698 let name = self.name.to_string_lossy();
699 let uid = unsafe { libc::geteuid() };
701 match companion_lock_files(&name, uid) {
702 Ok(files) => {
703 for file in files {
704 if let Err(error) = std::fs::remove_file(&file)
705 && error.kind() != io::ErrorKind::NotFound
706 {
707 lock_error.get_or_insert(error);
708 }
709 }
710 }
711 Err(error) => {
712 lock_error.get_or_insert(error);
713 }
714 }
715 if let Some(error) = shm_error.or(lock_error) {
716 return Err(error);
717 }
718 Ok(())
719 }
720}
721
722#[cfg(not(target_os = "macos"))]
723fn configure_new_shm(
724 fd: &OwnedFd,
725 policy: ShmAccessPolicy,
726) -> io::Result<()> {
727 let Some(gid) = policy.gid() else {
728 return Ok(());
729 };
730 if gid == !0 {
731 return Err(io::Error::new(
732 io::ErrorKind::InvalidInput,
733 "invalid SHM group id",
734 ));
735 }
736 if unsafe { libc::fchown(fd.as_raw_fd(), !0, gid) } != 0 {
738 return Err(io::Error::last_os_error());
739 }
740 if unsafe { libc::fchmod(fd.as_raw_fd(), policy.mode() as _) } != 0 {
742 return Err(io::Error::last_os_error());
743 }
744 Ok(())
745}
746
747fn shm_object_size(
748 fd: &OwnedFd,
749 name: &str,
750 owner_uid: u32,
751 policy: ShmAccessPolicy,
752) -> io::Result<usize> {
753 let mut stat = std::mem::MaybeUninit::<libc::stat>::uninit();
754 let stat_rc = unsafe { libc::fstat(fd.as_raw_fd(), stat.as_mut_ptr()) };
756 if stat_rc != 0 {
757 return Err(io::Error::last_os_error());
758 }
759 let stat = unsafe { stat.assume_init() };
761 let mode = u64::from(stat.st_mode) & 0o7777;
762 if stat.st_uid != owner_uid
763 || policy.gid().is_some_and(|gid| gid != stat.st_gid)
764 || mode & !u64::from(policy.mode()) != 0
765 {
766 return Err(io::Error::new(
767 io::ErrorKind::PermissionDenied,
768 format!(
769 "SHM segment {name} owner/group/mode do not satisfy {policy:?} for uid {owner_uid}"
770 ),
771 ));
772 }
773 let actual_size = stat.st_size;
774 usize::try_from(actual_size).map_err(|_| {
775 io::Error::new(
776 io::ErrorKind::InvalidData,
777 format!("SHM segment {name} reported invalid size {actual_size}"),
778 )
779 })
780}
781
782fn validate_minimum_size(
783 name: &str,
784 actual_size: usize,
785 minimum_size: usize,
786) -> io::Result<()> {
787 if actual_size < minimum_size {
788 return Err(io::Error::new(
789 io::ErrorKind::InvalidData,
790 format!(
791 "SHM segment {name} size {actual_size} is smaller than requested mapping {minimum_size}"
792 ),
793 ));
794 }
795 Ok(())
796}
797
798pub struct ShmRegionLock {
803 held: Held,
804 #[cfg(feature = "bench-timing")]
805 timing: Option<LockTiming>,
806}
807
808#[cfg(feature = "bench-timing")]
809#[derive(Clone, Copy)]
810pub struct LockTiming {
811 pub open: std::time::Duration,
812 pub flock: std::time::Duration,
813}
814
815enum Held {
816 Own(OwnedFd),
818 Kept { section: Arc<section::Section>, exclusive: bool },
820}
821
822impl ShmRegionLock {
823 fn new(lock_fd: OwnedFd) -> Self {
824 Self {
825 held: Held::Own(lock_fd),
826 #[cfg(feature = "bench-timing")]
827 timing: None,
828 }
829 }
830
831 fn kept(
832 section: Arc<section::Section>,
833 exclusive: bool,
834 ) -> Self {
835 Self {
836 held: Held::Kept { section, exclusive },
837 #[cfg(feature = "bench-timing")]
838 timing: None,
839 }
840 }
841
842 #[cfg(feature = "bench-timing")]
843 pub fn timing(&self) -> Option<LockTiming> {
844 self.timing
845 }
846}
847
848impl Drop for ShmRegionLock {
849 fn drop(&mut self) {
850 match &self.held {
851 Held::Own(lock_fd) => {
852 let _ =
854 unsafe { libc::flock(lock_fd.as_raw_fd(), libc::LOCK_UN) };
855 }
856 Held::Kept { section, exclusive } => section.release(*exclusive),
857 }
858 }
859}
860
861fn lock_path_exclusive(lock_path: &Path) -> io::Result<ShmRegionLock> {
862 lock_fd_exclusive(open_lock_file(lock_path)?)
863}
864
865fn open_lock_file(lock_path: &Path) -> io::Result<OwnedFd> {
866 if let Some(dir) = lock_path.parent() {
867 ensure_lock_dir(dir)?;
868 }
869
870 let file = OpenOptions::new()
871 .read(true)
872 .write(true)
873 .create(true)
874 .mode(0o600)
875 .custom_flags(libc::O_CLOEXEC | libc::O_NOFOLLOW)
876 .open(lock_path)?;
877
878 ensure_own_lock_file(&file, lock_path)?;
879 Ok(file.into())
880}
881
882fn ensure_own_lock_file(
888 file: &std::fs::File,
889 lock_path: &Path,
890) -> io::Result<()> {
891 use std::os::unix::fs::MetadataExt;
892
893 let uid = unsafe { libc::geteuid() };
894 let owner = file.metadata()?.uid();
895 if owner != uid {
896 return Err(io::Error::new(
897 io::ErrorKind::PermissionDenied,
898 format!(
899 "{} is owned by uid {owner} rather than {uid}",
900 lock_path.display()
901 ),
902 ));
903 }
904 Ok(())
905}
906
907fn open_lock_dir(dir: &Path) -> io::Result<OwnedFd> {
913 ensure_lock_dir(dir)?;
914 let opened = OpenOptions::new()
915 .read(true)
916 .custom_flags(libc::O_DIRECTORY | libc::O_CLOEXEC | libc::O_NOFOLLOW)
917 .open(dir)?;
918 let uid = unsafe { libc::geteuid() };
919 ensure_private(&opened.metadata()?, dir, uid)?;
920 Ok(opened.into())
921}
922
923fn open_lock_file_in(
925 dir: &OwnedFd,
926 name: &std::ffi::OsStr,
927 lock_path: &Path,
928) -> io::Result<OwnedFd> {
929 use std::os::unix::ffi::OsStrExt;
930
931 let name = CString::new(name.as_bytes()).map_err(|_| {
932 io::Error::new(io::ErrorKind::InvalidInput, "lock name has nul byte")
933 })?;
934 let fd = loop {
935 let fd = unsafe {
937 libc::openat(
938 dir.as_raw_fd(),
939 name.as_ptr(),
940 libc::O_RDWR
941 | libc::O_CREAT
942 | libc::O_CLOEXEC
943 | libc::O_NOFOLLOW,
944 0o600 as libc::c_uint,
945 )
946 };
947 if fd >= 0 {
948 break fd;
949 }
950 let error = io::Error::last_os_error();
951 if error.kind() != io::ErrorKind::Interrupted {
952 return Err(error);
953 }
954 };
955 let file = std::fs::File::from(unsafe { OwnedFd::from_raw_fd(fd) });
957 ensure_own_lock_file(&file, lock_path)?;
958 Ok(file.into())
959}
960
961fn lock_fd_exclusive(lock_fd: OwnedFd) -> io::Result<ShmRegionLock> {
962 self::lock_fd(lock_fd, libc::LOCK_EX)
963}
964
965fn lock_fd(
966 lock_fd: OwnedFd,
967 operation: libc::c_int,
968) -> io::Result<ShmRegionLock> {
969 loop {
970 let rc = unsafe { libc::flock(lock_fd.as_raw_fd(), operation) };
971 if rc == 0 {
972 return Ok(ShmRegionLock::new(lock_fd));
973 }
974 let error = io::Error::last_os_error();
975 if error.kind() != io::ErrorKind::Interrupted {
976 return Err(error);
977 }
978 }
979}
980
981impl Drop for ShmRegion {
982 fn drop(&mut self) {
983 unsafe {
985 libc::munmap(self.ptr.as_ptr().cast(), self.len);
986 }
987 }
988}
989
990unsafe impl Send for ShmRegion {}
994unsafe impl Sync for ShmRegion {}
995
996pub fn ring_segment_name(
998 fleet_name: &str,
999 kind: u8,
1000) -> String {
1001 let uid = unsafe { libc::geteuid() };
1003 ring_segment_name_for_uid(fleet_name, kind, uid)
1004}
1005
1006pub fn ring_segment_name_for_uid(
1011 fleet_name: &str,
1012 kind: u8,
1013 uid: u32,
1014) -> String {
1015 format!("/{SHM_NAMESPACE}-{fleet_name}-{kind}-{uid}")
1016}
1017
1018pub fn fleet_lock_path(fleet_name: &str) -> PathBuf {
1020 lock_dir().join(format!("{SHM_NAMESPACE}-{fleet_name}.fleet"))
1021}
1022
1023pub fn fleet_lock_path_for_uid(
1026 fleet_name: &str,
1027 uid: u32,
1028) -> PathBuf {
1029 lock_dir_for_uid(uid).join(format!("{SHM_NAMESPACE}-{fleet_name}.fleet"))
1030}
1031
1032pub struct FleetMembership {
1038 lock_fd: OwnedFd,
1039}
1040
1041impl Drop for FleetMembership {
1042 fn drop(&mut self) {
1043 let _ = unsafe { libc::flock(self.lock_fd.as_raw_fd(), libc::LOCK_UN) };
1044 }
1045}
1046
1047pub fn join_fleet_membership(fleet_name: &str) -> io::Result<FleetMembership> {
1050 let lock_fd = open_lock_file(&fleet_lock_path(fleet_name))?;
1051 let rc = unsafe { libc::flock(lock_fd.as_raw_fd(), libc::LOCK_SH) };
1053 if rc != 0 {
1054 return Err(io::Error::last_os_error());
1055 }
1056 Ok(FleetMembership { lock_fd })
1057}
1058
1059pub fn hold_existing_fleet(fleet_name: &str) -> io::Result<FleetMembership> {
1062 use std::os::unix::fs::MetadataExt;
1063 let path = fleet_lock_path(fleet_name);
1064 let file = OpenOptions::new()
1065 .read(true)
1066 .custom_flags(libc::O_CLOEXEC | libc::O_NOFOLLOW)
1067 .open(&path)?;
1068 let metadata = file.metadata()?;
1069 if !metadata.is_file() || metadata.uid() != unsafe { libc::geteuid() } {
1070 return Err(io::Error::new(
1071 io::ErrorKind::PermissionDenied,
1072 "fleet membership file has an unexpected owner or type",
1073 ));
1074 }
1075 let lock_fd: OwnedFd = file.into();
1076 loop {
1077 let rc = unsafe { libc::flock(lock_fd.as_raw_fd(), libc::LOCK_SH) };
1078 if rc == 0 {
1079 return Ok(FleetMembership { lock_fd });
1080 }
1081 let error = io::Error::last_os_error();
1082 if error.kind() != io::ErrorKind::Interrupted {
1083 return Err(error);
1084 }
1085 }
1086}
1087
1088pub fn try_lock_fleet_exclusive(
1095 fleet_name: &str,
1096 uid: u32,
1097) -> io::Result<Option<ShmRegionLock>> {
1098 let lock_fd = open_lock_file(&fleet_lock_path_for_uid(fleet_name, uid))?;
1101 let rc = unsafe {
1103 libc::flock(lock_fd.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB)
1104 };
1105 if rc == 0 {
1106 return Ok(Some(ShmRegionLock::new(lock_fd)));
1107 }
1108 let error = io::Error::last_os_error();
1109 if error.kind() == io::ErrorKind::WouldBlock {
1110 return Ok(None);
1111 }
1112 Err(error)
1113}
1114
1115pub struct LaneHold {
1123 lock_fd: OwnedFd,
1124}
1125
1126pub struct PreparedLaneLock {
1130 fd: OwnedFd,
1131 busy: AtomicBool,
1132 owner: ProcessOwner,
1133}
1134
1135impl PreparedLaneLock {
1136 pub fn open(
1137 shm_name: &str,
1138 lane: usize,
1139 ) -> io::Result<Self> {
1140 let name = format!("{}.lane{lane}", shm_name.trim_start_matches('/'));
1141 Ok(Self {
1142 fd: open_lock_file(&lock_file_path(&name))?,
1143 busy: AtomicBool::new(false),
1144 owner: ProcessOwner::current()?,
1145 })
1146 }
1147
1148 pub fn try_lock(&self) -> io::Result<Option<PreparedLaneGuard<'_>>> {
1150 if !self.owner.is_current() {
1151 return Err(io::Error::from(io::ErrorKind::PermissionDenied));
1152 }
1153 if self
1154 .busy
1155 .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
1156 .is_err()
1157 {
1158 return Ok(None);
1159 }
1160 if unsafe {
1162 libc::flock(self.fd.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB)
1163 } == 0
1164 {
1165 return Ok(Some(PreparedLaneGuard { lock: self }));
1166 }
1167 let error = io::Error::last_os_error();
1168 self.busy.store(false, Ordering::Release);
1169 if error.kind() == io::ErrorKind::WouldBlock {
1170 Ok(None)
1171 } else {
1172 Err(error)
1173 }
1174 }
1175}
1176
1177pub struct PreparedLaneGuard<'a> {
1178 lock: &'a PreparedLaneLock,
1179}
1180
1181impl Drop for PreparedLaneGuard<'_> {
1182 fn drop(&mut self) {
1183 if self.lock.owner.is_current() {
1184 unsafe {
1186 libc::flock(self.lock.fd.as_raw_fd(), libc::LOCK_UN);
1187 }
1188 self.lock.busy.store(false, Ordering::Release);
1189 }
1190 }
1191}
1192
1193impl Drop for LaneHold {
1194 fn drop(&mut self) {
1195 let _ = unsafe { libc::flock(self.lock_fd.as_raw_fd(), libc::LOCK_UN) };
1196 }
1197}
1198
1199pub fn try_hold_lane(
1203 shm_name: &str,
1204 lane: usize,
1205) -> io::Result<Option<LaneHold>> {
1206 let name = format!("{}.lane{lane}", shm_name.trim_start_matches('/'));
1207 let lock_fd = open_lock_file(&lock_file_path(&name))?;
1208 let rc = unsafe {
1210 libc::flock(lock_fd.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB)
1211 };
1212 if rc == 0 {
1213 return Ok(Some(LaneHold { lock_fd }));
1214 }
1215 let error = io::Error::last_os_error();
1216 if error.kind() == io::ErrorKind::WouldBlock {
1217 return Ok(None);
1218 }
1219 Err(error)
1220}
1221
1222pub fn wait_lane_free(
1228 shm_name: &str,
1229 lane: usize,
1230) -> io::Result<()> {
1231 let name = format!("{}.lane{lane}", shm_name.trim_start_matches('/'));
1232 wait_free(&open_lock_file(&lock_file_path(&name))?)
1233}
1234
1235pub(crate) fn wait_free(lock_fd: &OwnedFd) -> io::Result<()> {
1236 loop {
1237 if unsafe { libc::flock(lock_fd.as_raw_fd(), libc::LOCK_EX) } == 0 {
1239 return Ok(());
1240 }
1241 let error = io::Error::last_os_error();
1242 if error.kind() != io::ErrorKind::Interrupted {
1243 return Err(error);
1244 }
1245 }
1246}
1247
1248pub struct WatcherHold {
1256 lock_fd: OwnedFd,
1257}
1258
1259impl Drop for WatcherHold {
1260 fn drop(&mut self) {
1261 let _ = unsafe { libc::flock(self.lock_fd.as_raw_fd(), libc::LOCK_UN) };
1262 }
1263}
1264
1265pub fn try_hold_watcher(
1270 shm_name: &str,
1271 slot: usize,
1272) -> io::Result<Option<WatcherHold>> {
1273 let name = format!("{}.watch{slot}", shm_name.trim_start_matches('/'));
1274 let lock_fd = open_lock_file(&lock_file_path(&name))?;
1275 let rc = unsafe {
1277 libc::flock(lock_fd.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB)
1278 };
1279 if rc == 0 {
1280 return Ok(Some(WatcherHold { lock_fd }));
1281 }
1282 let error = io::Error::last_os_error();
1283 if error.kind() == io::ErrorKind::WouldBlock {
1284 return Ok(None);
1285 }
1286 Err(error)
1287}
1288
1289pub fn try_hold_adopter(shm_name: &str) -> io::Result<Option<WatcherHold>> {
1294 let name = format!("{}.watch-adopter", shm_name.trim_start_matches('/'));
1295 let lock_fd = open_lock_file(&lock_file_path(&name))?;
1296 let rc = unsafe {
1298 libc::flock(lock_fd.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB)
1299 };
1300 if rc == 0 {
1301 return Ok(Some(WatcherHold { lock_fd }));
1302 }
1303 let error = io::Error::last_os_error();
1304 if error.kind() == io::ErrorKind::WouldBlock {
1305 return Ok(None);
1306 }
1307 Err(error)
1308}
1309
1310pub fn companion_lock_files(
1316 shm_name: &str,
1317 uid: u32,
1318) -> io::Result<Vec<PathBuf>> {
1319 let base = shm_name.trim_start_matches('/');
1320 let own = format!("{base}.lock");
1321 let lane = format!("{base}.lane");
1322 let watch = format!("{base}.watch");
1323 let dir = lock_dir_for_uid(uid);
1324 let entries = match std::fs::read_dir(&dir) {
1325 Ok(entries) => entries,
1326 Err(error) if error.kind() == io::ErrorKind::NotFound => {
1327 return Ok(Vec::new());
1328 }
1329 Err(error) => return Err(error),
1330 };
1331 let mut found = Vec::new();
1332 for entry in entries {
1333 let entry = entry?;
1334 let Some(file) = entry.file_name().to_str().map(str::to_owned) else {
1335 continue;
1336 };
1337 if file == own
1338 || ((file.starts_with(&lane) || file.starts_with(&watch))
1339 && file.ends_with(".lock"))
1340 {
1341 found.push(entry.path());
1342 }
1343 }
1344 Ok(found)
1345}
1346
1347fn lock_file_path(shm_name: &str) -> PathBuf {
1348 lock_dir().join(format!("{}.lock", shm_name.trim_start_matches('/')))
1349}
1350
1351fn lock_dir() -> PathBuf {
1374 let uid = unsafe { libc::geteuid() };
1375 lock_dir_for_uid(uid)
1376}
1377
1378pub fn lock_dir_for_uid(uid: u32) -> PathBuf {
1382 Path::new("/tmp").join(format!("{SHM_NAMESPACE}-{uid}"))
1383}
1384
1385fn ensure_lock_dir(dir: &Path) -> io::Result<()> {
1391 use std::os::unix::fs::DirBuilderExt;
1392
1393 let uid = unsafe { libc::geteuid() };
1394 match std::fs::DirBuilder::new().mode(0o700).create(dir) {
1395 Ok(()) => {}
1396 Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {}
1397 Err(error) => return Err(error),
1398 }
1399
1400 ensure_private_dir(dir, uid)
1401}
1402
1403fn ensure_private_dir(
1409 dir: &Path,
1410 uid: u32,
1411) -> io::Result<()> {
1412 ensure_private(&std::fs::symlink_metadata(dir)?, dir, uid)
1413}
1414
1415fn ensure_private(
1417 metadata: &std::fs::Metadata,
1418 dir: &Path,
1419 uid: u32,
1420) -> io::Result<()> {
1421 use std::os::unix::fs::{MetadataExt, PermissionsExt};
1422
1423 if !metadata.is_dir() {
1424 return Err(io::Error::new(
1425 io::ErrorKind::PermissionDenied,
1426 format!("{} is not a directory", dir.display()),
1427 ));
1428 }
1429 if metadata.uid() != uid {
1430 return Err(io::Error::new(
1431 io::ErrorKind::PermissionDenied,
1432 format!(
1433 "{} is owned by uid {} rather than {uid}; refusing to lock in a directory \
1434 another user controls",
1435 dir.display(),
1436 metadata.uid()
1437 ),
1438 ));
1439 }
1440 if metadata.permissions().mode() & 0o077 != 0 {
1441 return Err(io::Error::new(
1442 io::ErrorKind::PermissionDenied,
1443 format!(
1444 "{} is mode {:o}; refusing to lock in a directory others can write to",
1445 dir.display(),
1446 metadata.permissions().mode() & 0o777
1447 ),
1448 ));
1449 }
1450
1451 Ok(())
1452}
1453
1454#[cfg(test)]
1455#[path = "../tests/shm/identity.rs"]
1456mod lock_identity_tests;
1457
1458#[cfg(test)]
1459mod fleet_lock_tests {
1460 use super::{join_fleet_membership, try_lock_fleet_exclusive};
1461
1462 #[test]
1463 fn forked_prepared_guard_does_not_unlock_the_parent() {
1464 let name = format!("fork-lock-{}", std::process::id());
1465 let lock = super::PreparedLaneLock::open(&name, 0).unwrap();
1466 let guard = lock.try_lock().unwrap().unwrap();
1467 let child = unsafe { libc::fork() };
1470 assert!(child >= 0, "fork failed");
1471 if child == 0 {
1472 drop(guard);
1473 let rejected = lock.try_lock().is_err();
1474 let still_held = super::PreparedLaneLock::open(&name, 0)
1475 .and_then(|fresh| {
1476 fresh.try_lock().map(|result| result.is_none())
1477 })
1478 .unwrap_or(false);
1479 let valid = rejected && still_held && lock.try_lock().is_err();
1480 unsafe { libc::_exit(i32::from(!valid)) };
1482 }
1483 let mut status = 0;
1484 assert_eq!(unsafe { libc::waitpid(child, &mut status, 0) }, child);
1486 assert!(libc::WIFEXITED(status));
1487 assert_eq!(libc::WEXITSTATUS(status), 0);
1488 drop(guard);
1489 drop(lock);
1490 std::fs::remove_file(super::lock_file_path(&format!("{name}.lane0")))
1491 .unwrap();
1492 }
1493
1494 #[test]
1495 fn prepared_lane_excludes_threads_and_distinct_file_descriptions() {
1496 let name = format!("prepared-{}", std::process::id());
1497 let first = super::PreparedLaneLock::open(&name, 0).unwrap();
1498 let second = super::PreparedLaneLock::open(&name, 0).unwrap();
1499 let hold = first.try_lock().unwrap().unwrap();
1500 assert!(first.try_lock().unwrap().is_none());
1501 std::thread::scope(|scope| {
1502 assert!(
1503 scope
1504 .spawn(|| first.try_lock().unwrap().is_none())
1505 .join()
1506 .unwrap()
1507 );
1508 });
1509 assert!(second.try_lock().unwrap().is_none());
1510 drop(hold);
1511 let other = second.try_lock().unwrap().unwrap();
1512 assert!(first.try_lock().unwrap().is_none());
1513 drop(other);
1514 drop(first);
1515 drop(second);
1516 std::fs::remove_file(super::lock_file_path(&format!("{name}.lane0")))
1517 .unwrap();
1518 }
1519
1520 #[test]
1523 fn a_member_holds_the_fleet_against_exclusive_takers() {
1524 let fleet = format!("fl{:x}", std::process::id());
1525 let uid = unsafe { libc::geteuid() };
1526
1527 let member = join_fleet_membership(&fleet).expect("join");
1528 assert!(try_lock_fleet_exclusive(&fleet, uid).expect("try").is_none());
1529
1530 drop(member);
1531 let exclusive = try_lock_fleet_exclusive(&fleet, uid).expect("try");
1532 assert!(exclusive.is_some());
1533 drop(exclusive);
1536 let _ =
1537 std::fs::remove_file(super::fleet_lock_path_for_uid(&fleet, uid));
1538 }
1539}