#![cfg(unix)]
use std::ffi::CString;
use std::fs::OpenOptions;
use std::io;
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
use std::os::unix::fs::OpenOptionsExt;
use std::path::{Path, PathBuf};
use std::ptr::NonNull;
pub const SHM_NAMESPACE: &str = "orbit";
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum ShmAccessPolicy {
#[default]
OwnerOnly,
GroupRead { gid: u32 },
GroupReadWrite { gid: u32 }
}
impl ShmAccessPolicy {
pub const fn mode(self) -> u32 {
match self {
Self::OwnerOnly => 0o600,
Self::GroupRead { .. } => 0o640,
Self::GroupReadWrite { .. } => 0o660
}
}
pub const fn gid(self) -> Option<u32> {
match self {
Self::OwnerOnly => None,
Self::GroupRead { gid } | Self::GroupReadWrite { gid } => Some(gid)
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ShmValidation {
Missing,
Valid { actual_size: usize }
}
pub struct ShmRegion {
name: CString,
lock_path: PathBuf,
process_lock: bool,
ptr: NonNull<u8>,
len: usize,
created: bool
}
impl ShmRegion {
pub fn validate_existing(
name: &str,
minimum_size: usize
) -> io::Result<ShmValidation> {
Self::validate_existing_with_policy(
name,
minimum_size,
unsafe { libc::geteuid() },
ShmAccessPolicy::default()
)
}
pub fn validate_existing_with_policy(
name: &str,
minimum_size: usize,
owner_uid: u32,
policy: ShmAccessPolicy
) -> io::Result<ShmValidation> {
let cname = CString::new(name)
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "shm name has nul byte"))?;
let raw_fd = loop {
let fd = unsafe { libc::shm_open(cname.as_ptr(), libc::O_RDONLY, 0o600) };
if fd >= 0 {
break fd;
}
let error = io::Error::last_os_error();
if error.kind() == io::ErrorKind::Interrupted {
continue;
}
if error.raw_os_error() == Some(libc::ENOENT) {
return Ok(ShmValidation::Missing);
}
return Err(error);
};
let fd = unsafe { OwnedFd::from_raw_fd(raw_fd) };
let actual_size = shm_object_size(&fd, name, owner_uid, policy)?;
validate_minimum_size(name, actual_size, minimum_size)?;
Ok(ShmValidation::Valid { actual_size })
}
pub(crate) fn open_existing_read_only(
name: &str,
minimum_size: usize,
owner_uid: u32,
policy: ShmAccessPolicy
) -> io::Result<Self> {
let cname = CString::new(name)
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "shm name has nul byte"))?;
let raw_fd = loop {
let fd = unsafe { libc::shm_open(cname.as_ptr(), libc::O_RDONLY, 0o600) };
if fd >= 0 {
break fd;
}
let error = io::Error::last_os_error();
if error.kind() == io::ErrorKind::Interrupted {
continue;
}
return Err(error);
};
let fd = unsafe { OwnedFd::from_raw_fd(raw_fd) };
let actual_size = shm_object_size(&fd, name, owner_uid, policy)?;
validate_minimum_size(name, actual_size, minimum_size)?;
let ptr = unsafe {
libc::mmap(
std::ptr::null_mut(),
actual_size,
libc::PROT_READ,
libc::MAP_SHARED,
fd.as_raw_fd(),
0
)
};
if ptr == libc::MAP_FAILED {
return Err(io::Error::last_os_error());
}
let ptr = NonNull::new(ptr.cast::<u8>()).expect("mmap returned non-null on success");
Ok(Self {
lock_path: lock_file_path(name),
name: cname,
process_lock: false,
ptr,
len: actual_size,
created: false
})
}
pub fn open_or_create(
name: &str,
size: usize
) -> io::Result<Self> {
Self::open_or_create_with_policy(name, size, ShmAccessPolicy::default())
}
pub fn open_or_create_with_policy(
name: &str,
size: usize,
policy: ShmAccessPolicy
) -> io::Result<Self> {
let (region, initialization_lock) = Self::open_or_create_inner(name, size, false, policy)?;
debug_assert!(initialization_lock.is_none());
Ok(region)
}
pub fn open_or_create_locked(
name: &str,
size: usize
) -> io::Result<(Self, ShmRegionLock)> {
Self::open_or_create_locked_with_policy(name, size, ShmAccessPolicy::default())
}
pub fn open_or_create_locked_with_policy(
name: &str,
size: usize,
policy: ShmAccessPolicy
) -> io::Result<(Self, ShmRegionLock)> {
let (region, initialization_lock) = Self::open_or_create_inner(name, size, true, policy)?;
Ok((
region,
initialization_lock.expect("locked SHM open must return its initialization lock")
))
}
fn open_or_create_inner(
name: &str,
size: usize,
process_lock: bool,
policy: ShmAccessPolicy
) -> io::Result<(Self, Option<ShmRegionLock>)> {
let cname = CString::new(name)
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "shm name has nul byte"))?;
let lock_path = lock_file_path(name);
let initialization_lock =
if process_lock { Some(lock_path_exclusive(&lock_path)?) } else { None };
#[cfg(target_os = "macos")]
let can_create = policy.gid().is_none_or(|gid| gid == unsafe { libc::getegid() });
#[cfg(target_os = "macos")]
let creation_mode = policy.mode();
#[cfg(not(target_os = "macos"))]
let (can_create, creation_mode) = (true, 0o600u32);
let (raw_fd, created) = unsafe {
let fd = if can_create {
libc::shm_open(
cname.as_ptr(),
libc::O_RDWR | libc::O_CREAT | libc::O_EXCL,
creation_mode
)
} else {
libc::shm_open(cname.as_ptr(), libc::O_RDWR, 0)
};
if fd >= 0 {
(fd, can_create)
} else {
let err = io::Error::last_os_error();
if !can_create && err.raw_os_error() == Some(libc::ENOENT) {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"creating group-shared SHM requires the selected effective gid"
));
}
if err.raw_os_error() != Some(libc::EEXIST) {
return Err(err);
}
let fd = libc::shm_open(cname.as_ptr(), libc::O_RDWR, 0o600);
if fd < 0 {
return Err(io::Error::last_os_error());
}
(fd, false)
}
};
let fd = unsafe { OwnedFd::from_raw_fd(raw_fd) };
#[cfg(not(target_os = "macos"))]
if created && let Err(error) = configure_new_shm(&fd, policy) {
let _ = unsafe { libc::shm_unlink(cname.as_ptr()) };
return Err(error);
}
let owner_uid = unsafe { libc::geteuid() };
if let Err(error) = shm_object_size(&fd, name, owner_uid, policy) {
if created {
let _ = unsafe { libc::shm_unlink(cname.as_ptr()) };
}
return Err(error);
}
if created {
let rc = unsafe { libc::ftruncate(fd.as_raw_fd(), size as libc::off_t) };
if rc != 0 {
let err = io::Error::last_os_error();
let _ = unsafe { libc::shm_unlink(cname.as_ptr()) };
return Err(err);
}
}
let actual_size = match shm_object_size(&fd, name, owner_uid, policy) {
Ok(actual_size) => actual_size,
Err(error) => {
if created {
let _ = unsafe { libc::shm_unlink(cname.as_ptr()) };
}
return Err(error);
}
};
if let Err(error) = validate_minimum_size(name, actual_size, size) {
if created {
let _ = unsafe { libc::shm_unlink(cname.as_ptr()) };
}
return Err(error);
}
let ptr = unsafe {
libc::mmap(
std::ptr::null_mut(),
size,
libc::PROT_READ | libc::PROT_WRITE,
libc::MAP_SHARED,
fd.as_raw_fd(),
0
)
};
if ptr == libc::MAP_FAILED {
let err = io::Error::last_os_error();
if created {
let _ = unsafe { libc::shm_unlink(cname.as_ptr()) };
}
return Err(err);
}
let ptr = NonNull::new(ptr.cast::<u8>()).expect("mmap returned non-null on success");
Ok((
Self { name: cname, lock_path, process_lock, ptr, len: size, created },
initialization_lock
))
}
pub fn as_ptr(&self) -> *mut u8 {
self.ptr.as_ptr()
}
pub fn len(&self) -> usize {
self.len
}
pub fn is_empty(&self) -> bool {
self.len == 0
}
pub fn created(&self) -> bool {
self.created
}
pub fn lock_exclusive(&self) -> io::Result<ShmRegionLock> {
if !self.process_lock {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"SHM region was opened without a process lock"
));
}
lock_path_exclusive(&self.lock_path)
}
#[cfg(test)]
pub(crate) fn try_lock_exclusive(&self) -> io::Result<ShmRegionLock> {
if !self.process_lock {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"SHM region was opened without a process lock"
));
}
let lock_fd = open_lock_file(&self.lock_path)?;
let rc = unsafe { libc::flock(lock_fd.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
if rc == 0 { Ok(ShmRegionLock { lock_fd }) } else { Err(io::Error::last_os_error()) }
}
pub fn unlink(&self) -> io::Result<()> {
let rc = unsafe { libc::shm_unlink(self.name.as_ptr()) };
let shm_error = if rc != 0 {
let err = io::Error::last_os_error();
if err.raw_os_error() == Some(libc::ENOENT) { None } else { Some(err) }
} else {
None
};
let mut lock_error = match std::fs::remove_file(&self.lock_path) {
Ok(()) => None,
Err(error) if error.kind() == io::ErrorKind::NotFound => None,
Err(error) => Some(error)
};
let name = self.name.to_string_lossy();
let uid = unsafe { libc::geteuid() };
match companion_lock_files(&name, uid) {
Ok(files) => {
for file in files {
if let Err(error) = std::fs::remove_file(&file)
&& error.kind() != io::ErrorKind::NotFound
{
lock_error.get_or_insert(error);
}
}
}
Err(error) => {
lock_error.get_or_insert(error);
}
}
if let Some(error) = shm_error.or(lock_error) {
return Err(error);
}
Ok(())
}
}
#[cfg(not(target_os = "macos"))]
fn configure_new_shm(
fd: &OwnedFd,
policy: ShmAccessPolicy
) -> io::Result<()> {
let Some(gid) = policy.gid() else {
return Ok(());
};
if gid == !0 {
return Err(io::Error::new(io::ErrorKind::InvalidInput, "invalid SHM group id"));
}
if unsafe { libc::fchown(fd.as_raw_fd(), !0, gid) } != 0 {
return Err(io::Error::last_os_error());
}
if unsafe { libc::fchmod(fd.as_raw_fd(), policy.mode() as _) } != 0 {
return Err(io::Error::last_os_error());
}
Ok(())
}
fn shm_object_size(
fd: &OwnedFd,
name: &str,
owner_uid: u32,
policy: ShmAccessPolicy
) -> io::Result<usize> {
let mut stat = std::mem::MaybeUninit::<libc::stat>::uninit();
let stat_rc = unsafe { libc::fstat(fd.as_raw_fd(), stat.as_mut_ptr()) };
if stat_rc != 0 {
return Err(io::Error::last_os_error());
}
let stat = unsafe { stat.assume_init() };
let mode = u64::from(stat.st_mode) & 0o7777;
if stat.st_uid != owner_uid
|| policy.gid().is_some_and(|gid| gid != stat.st_gid)
|| mode & !u64::from(policy.mode()) != 0
{
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
format!(
"SHM segment {name} owner/group/mode do not satisfy {policy:?} for uid {owner_uid}"
)
));
}
let actual_size = stat.st_size;
usize::try_from(actual_size).map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("SHM segment {name} reported invalid size {actual_size}")
)
})
}
fn validate_minimum_size(
name: &str,
actual_size: usize,
minimum_size: usize
) -> io::Result<()> {
if actual_size < minimum_size {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"SHM segment {name} size {actual_size} is smaller than requested mapping {minimum_size}"
)
));
}
Ok(())
}
pub struct ShmRegionLock {
lock_fd: OwnedFd
}
impl Drop for ShmRegionLock {
fn drop(&mut self) {
let _ = unsafe { libc::flock(self.lock_fd.as_raw_fd(), libc::LOCK_UN) };
}
}
fn lock_path_exclusive(lock_path: &Path) -> io::Result<ShmRegionLock> {
lock_fd_exclusive(open_lock_file(lock_path)?)
}
fn open_lock_file(lock_path: &Path) -> io::Result<OwnedFd> {
use std::os::unix::fs::MetadataExt;
if let Some(dir) = lock_path.parent() {
ensure_lock_dir(dir)?;
}
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.mode(0o600)
.custom_flags(libc::O_CLOEXEC | libc::O_NOFOLLOW)
.open(lock_path)?;
let uid = unsafe { libc::geteuid() };
let owner = file.metadata()?.uid();
if owner != uid {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
format!("{} is owned by uid {owner} rather than {uid}", lock_path.display())
));
}
Ok(file.into())
}
fn lock_fd_exclusive(lock_fd: OwnedFd) -> io::Result<ShmRegionLock> {
loop {
let rc = unsafe { libc::flock(lock_fd.as_raw_fd(), libc::LOCK_EX) };
if rc == 0 {
return Ok(ShmRegionLock { lock_fd });
}
let error = io::Error::last_os_error();
if error.kind() != io::ErrorKind::Interrupted {
return Err(error);
}
}
}
impl Drop for ShmRegion {
fn drop(&mut self) {
unsafe {
libc::munmap(self.ptr.as_ptr().cast(), self.len);
}
}
}
unsafe impl Send for ShmRegion {}
unsafe impl Sync for ShmRegion {}
pub fn ring_segment_name(
fleet_name: &str,
kind: u8
) -> String {
let uid = unsafe { libc::geteuid() };
ring_segment_name_for_uid(fleet_name, kind, uid)
}
pub fn ring_segment_name_for_uid(
fleet_name: &str,
kind: u8,
uid: u32
) -> String {
format!("/{SHM_NAMESPACE}-{fleet_name}-{kind}-{uid}")
}
pub fn fleet_lock_path(fleet_name: &str) -> PathBuf {
lock_dir().join(format!("{SHM_NAMESPACE}-{fleet_name}.fleet"))
}
pub fn fleet_lock_path_for_uid(
fleet_name: &str,
uid: u32
) -> PathBuf {
lock_dir_for_uid(uid).join(format!("{SHM_NAMESPACE}-{fleet_name}.fleet"))
}
pub struct FleetMembership {
lock_fd: OwnedFd
}
impl Drop for FleetMembership {
fn drop(&mut self) {
let _ = unsafe { libc::flock(self.lock_fd.as_raw_fd(), libc::LOCK_UN) };
}
}
pub fn join_fleet_membership(fleet_name: &str) -> io::Result<FleetMembership> {
let lock_fd = open_lock_file(&fleet_lock_path(fleet_name))?;
let rc = unsafe { libc::flock(lock_fd.as_raw_fd(), libc::LOCK_SH) };
if rc != 0 {
return Err(io::Error::last_os_error());
}
Ok(FleetMembership { lock_fd })
}
pub fn try_lock_fleet_exclusive(
fleet_name: &str,
uid: u32
) -> io::Result<Option<ShmRegionLock>> {
let lock_fd = open_lock_file(&fleet_lock_path_for_uid(fleet_name, uid))?;
let rc = unsafe { libc::flock(lock_fd.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
if rc == 0 {
return Ok(Some(ShmRegionLock { lock_fd }));
}
let error = io::Error::last_os_error();
if error.kind() == io::ErrorKind::WouldBlock {
return Ok(None);
}
Err(error)
}
pub struct LaneHold {
lock_fd: OwnedFd
}
impl Drop for LaneHold {
fn drop(&mut self) {
let _ = unsafe { libc::flock(self.lock_fd.as_raw_fd(), libc::LOCK_UN) };
}
}
pub fn try_hold_lane(
shm_name: &str,
lane: usize
) -> io::Result<Option<LaneHold>> {
let name = format!("{}.lane{lane}", shm_name.trim_start_matches('/'));
let lock_fd = open_lock_file(&lock_file_path(&name))?;
let rc = unsafe { libc::flock(lock_fd.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
if rc == 0 {
return Ok(Some(LaneHold { lock_fd }));
}
let error = io::Error::last_os_error();
if error.kind() == io::ErrorKind::WouldBlock {
return Ok(None);
}
Err(error)
}
pub fn companion_lock_files(
shm_name: &str,
uid: u32
) -> io::Result<Vec<PathBuf>> {
let base = shm_name.trim_start_matches('/');
let own = format!("{base}.lock");
let lane = format!("{base}.lane");
let dir = lock_dir_for_uid(uid);
let entries = match std::fs::read_dir(&dir) {
Ok(entries) => entries,
Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(error) => return Err(error)
};
let mut found = Vec::new();
for entry in entries {
let entry = entry?;
let Some(file) = entry.file_name().to_str().map(str::to_owned) else {
continue;
};
if file == own || (file.starts_with(&lane) && file.ends_with(".lock")) {
found.push(entry.path());
}
}
Ok(found)
}
fn lock_file_path(shm_name: &str) -> PathBuf {
lock_dir().join(format!("{}.lock", shm_name.trim_start_matches('/')))
}
fn lock_dir() -> PathBuf {
let uid = unsafe { libc::geteuid() };
lock_dir_for_uid(uid)
}
fn lock_dir_for_uid(uid: u32) -> PathBuf {
let base = std::env::var_os("XDG_RUNTIME_DIR")
.map(PathBuf::from)
.filter(|dir| dir.is_absolute())
.unwrap_or_else(|| PathBuf::from("/tmp"));
base.join(format!("{SHM_NAMESPACE}-{uid}"))
}
fn ensure_lock_dir(dir: &Path) -> io::Result<()> {
use std::os::unix::fs::DirBuilderExt;
let uid = unsafe { libc::geteuid() };
match std::fs::DirBuilder::new().mode(0o700).create(dir) {
Ok(()) => {}
Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {}
Err(error) => return Err(error)
}
ensure_private_dir(dir, uid)
}
fn ensure_private_dir(
dir: &Path,
uid: u32
) -> io::Result<()> {
use std::os::unix::fs::{MetadataExt, PermissionsExt};
let metadata = std::fs::symlink_metadata(dir)?;
if !metadata.is_dir() {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
format!("{} is not a directory", dir.display())
));
}
if metadata.uid() != uid {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
format!(
"{} is owned by uid {} rather than {uid}; refusing to lock in a directory \
another user controls",
dir.display(),
metadata.uid()
)
));
}
if metadata.permissions().mode() & 0o077 != 0 {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
format!(
"{} is mode {:o}; refusing to lock in a directory others can write to",
dir.display(),
metadata.permissions().mode() & 0o777
)
));
}
Ok(())
}
#[cfg(test)]
mod fleet_lock_tests {
use super::{join_fleet_membership, try_lock_fleet_exclusive};
#[test]
fn a_member_holds_the_fleet_against_exclusive_takers() {
let fleet = format!("fl{:x}", std::process::id());
let uid = unsafe { libc::geteuid() };
let member = join_fleet_membership(&fleet).expect("join");
assert!(try_lock_fleet_exclusive(&fleet, uid).expect("try").is_none());
drop(member);
let exclusive = try_lock_fleet_exclusive(&fleet, uid).expect("try");
assert!(exclusive.is_some());
drop(exclusive);
let _ = std::fs::remove_file(super::fleet_lock_path_for_uid(&fleet, uid));
}
}