use cfg_if::cfg_if;
use crate::{core::dm_ioctl as dmi, result::DmResult};
pub trait UdevSyncAction {
fn begin(hdr: &mut dmi::Struct_dm_ioctl, ioctl: u8) -> DmResult<UdevSync>;
fn end(self, flags: u32) -> DmResult<()>;
fn cancel(self);
fn is_active(&self) -> bool;
}
#[cfg(not(target_os = "android"))]
pub mod sync_semaphore {
use nix::libc::{
c_int,
key_t,
sembuf,
semctl as libc_semctl,
semget as libc_semget,
semop as libc_semop,
EEXIST,
ENOMEM,
ENOSPC,
IPC_CREAT,
IPC_EXCL,
IPC_NOWAIT,
IPC_RMID,
};
use nix::unistd::{access, AccessFlags};
use rand::RngExt;
use retry::{delay::NoDelay, retry, OperationResult};
use std::{io, path::Path, sync::LazyLock};
use crate::core::sysvsem::seminfo;
use crate::{
core::dm_flags::{DmFlags, DmUdevFlags},
core::sysvsem::{semun, GETVAL, SEM_INFO, SETVAL},
core::{dm_ioctl as dmi, errors},
result::{DmError, DmResult},
};
use super::UdevSyncAction;
const COOKIE_MODE: i32 = 0o600;
impl DmError {
fn udev_sync_error_from_os() -> DmError {
DmError::Core(errors::Error::UdevSync(
io::Error::last_os_error().to_string(),
))
}
}
static SYSV_SEM_SUPPORTED: LazyLock<bool> = LazyLock::new(sysv_sem_supported);
fn sysv_sem_supported() -> bool {
let mut info: seminfo = Default::default();
let arg = semun { __buf: &mut info };
match semctl(0, 0, SEM_INFO, Some(arg)) {
Ok(maxid) if maxid < 0 => {
warn!(concat!(
"Kernel not configured for System V IPC semaphores.",
"Disabling udev notifications."
));
false
}
Err(err) => {
error!(
concat!(
"Error retrieving System V semaphore limits: {}.",
"Disabling udev notifications."
),
err
);
false
}
Ok(_) => {
if info.semmsl > 0 && info.semmni > 0 && info.semmns > 0 {
if info.semmsl < 1000 || info.semmni < 1000 || info.semmns < 1000 {
warn!(concat!(
"Low System V IPC semaphore limits detected: consider ",
"increasing values in /proc/sys/kernel/sem to avoid exhaustion."
));
}
true
} else {
false
}
}
}
}
const UDEV_SOCKET_PATH: &str = "/run/udev/control";
fn udev_running() -> bool {
matches!(
access(Path::new(UDEV_SOCKET_PATH), AccessFlags::F_OK),
Ok(())
)
}
fn semget(key: i32, nsems: i32, semflg: i32) -> Result<i32, std::io::Error> {
let semid = unsafe { libc_semget(key as key_t, nsems as c_int, semflg as c_int) };
match semid {
i if i < 0 => Err(io::Error::last_os_error()),
_ => Ok(semid),
}
}
fn semctl_cmd_allowed(cmd: i32) -> Result<(), std::io::Error> {
match cmd {
IPC_RMID | GETVAL | SETVAL | SEM_INFO => Ok(()),
_ => Err(io::Error::from(io::ErrorKind::Unsupported)),
}
}
fn semctl(
semid: i32,
semnum: i32,
cmd: i32,
semun: Option<semun>,
) -> Result<i32, std::io::Error> {
semctl_cmd_allowed(cmd)?;
let semun = semun.unwrap_or_default();
let r = unsafe { libc_semctl(semid as c_int, semnum as c_int, cmd as c_int, semun) };
match r {
i if i < 0 => Err(io::Error::last_os_error()),
_ => Ok(r),
}
}
fn generate_semaphore_cookie() -> OperationResult<(u32, i32), std::io::Error> {
let mut base_cookie = 0u16;
while base_cookie == 0 {
base_cookie = rand::rng().random::<u16>();
}
let cookie = (dmi::DM_COOKIE_MAGIC << dmi::DM_UDEV_FLAGS_SHIFT) | base_cookie as u32;
match semget(cookie as i32, 1, COOKIE_MODE | IPC_CREAT | IPC_EXCL) {
Ok(semid) => OperationResult::Ok((cookie, semid)),
Err(err) => match err.raw_os_error() {
Some(ENOMEM) => OperationResult::Err(err),
Some(ENOSPC) => OperationResult::Err(err),
Some(EEXIST) => OperationResult::Retry(err),
_ => OperationResult::Err(err),
},
}
}
fn notify_sem_create() -> DmResult<(u32, i32)> {
let (cookie, semid) = match retry(NoDelay.take(4), generate_semaphore_cookie) {
Ok((cookie, semid)) => (cookie, semid),
Err(err) => {
error!("Failed to generate udev notification semaphore: {err}");
return Err(DmError::Core(errors::Error::UdevSync(err.to_string())));
}
};
let sem_arg: semun = semun { val: 1 };
if let Err(err) = semctl(semid, 0, SETVAL, Some(sem_arg)) {
error!("Failed to initialize udev notification semaphore: {err}");
if let Err(err2) = notify_sem_destroy(cookie, semid) {
error!("Failed to clean up udev notification semaphore: {err2}");
}
return Err(DmError::Core(errors::Error::UdevSync(err.to_string())));
}
match semctl(semid, 0, GETVAL, None) {
Ok(1) => Ok((cookie, semid)),
_ => {
error!(
"Initialization of udev notification semaphore returned inconsistent value."
);
Err(DmError::udev_sync_error_from_os())
}
}
}
fn notify_sem_destroy(cookie: u32, semid: i32) -> DmResult<()> {
if let Err(err) = semctl(semid, 0, IPC_RMID, None) {
error!("Failed to remove udev synchronization semaphore {semid} for cookie {cookie}");
return Err(DmError::Core(errors::Error::UdevSync(err.to_string())));
};
Ok(())
}
fn notify_sem_inc(cookie: u32, semid: i32) -> DmResult<()> {
let mut sb = sembuf {
sem_num: 0,
sem_op: 1,
sem_flg: 0,
};
let r = unsafe { libc_semop(semid, &mut sb, 1) };
match r {
i if i < 0 => {
error!(
"Failed to increment udev synchronization semaphore {semid} for cookie {cookie}"
);
Err(DmError::udev_sync_error_from_os())
}
_ => Ok(()),
}
}
fn notify_sem_dec(cookie: u32, semid: i32) -> DmResult<()> {
let mut sb = sembuf {
sem_num: 0,
sem_op: -1,
sem_flg: IPC_NOWAIT as i16,
};
let r = unsafe { libc_semop(semid, &mut sb, 1) };
match r {
i if i < 0 => {
error!(
"Failed to decrement udev synchronization semaphore {semid} for cookie {cookie}"
);
Err(DmError::udev_sync_error_from_os())
}
_ => Ok(()),
}
}
fn notify_sem_wait(cookie: u32, semid: i32) -> DmResult<()> {
if let Err(err) = notify_sem_dec(cookie, semid) {
error!(
concat!(
"Failed to set initial state for notification ",
"semaphore identified by cookie value {}: {}"
),
cookie, err
);
if let Err(err2) = notify_sem_destroy(cookie, semid) {
error!("Failed to clean up udev notification semaphore: {err2}");
}
}
let mut sb = sembuf {
sem_num: 0,
sem_op: 0,
sem_flg: 0,
};
let r = unsafe { libc_semop(semid, &mut sb, 1) };
match r {
i if i < 0 => {
error!("Failed to wait on notification semaphore {semid} for cookie {cookie}");
Err(DmError::udev_sync_error_from_os())
}
_ => Ok(()),
}
}
#[derive(Debug)]
pub struct UdevSync {
cookie: u32,
semid: Option<i32>,
}
impl UdevSyncAction for UdevSync {
fn begin(hdr: &mut dmi::Struct_dm_ioctl, ioctl: u8) -> DmResult<Self> {
if !udev_running() {
return Err(DmError::Core(errors::Error::UdevSync(
"Udev daemon is not running: unable to create devices.".to_string(),
)));
}
match ioctl as u32 {
dmi::DM_DEV_REMOVE_CMD | dmi::DM_DEV_RENAME_CMD | dmi::DM_DEV_SUSPEND_CMD
if *SYSV_SEM_SUPPORTED && (hdr.flags & DmFlags::DM_SUSPEND.bits()) == 0 => {}
_ => {
return Ok(UdevSync {
cookie: 0,
semid: None,
});
}
};
let (base_cookie, semid) = notify_sem_create()?;
hdr.event_nr |= (DmUdevFlags::DM_UDEV_PRIMARY_SOURCE_FLAG.bits()
<< dmi::DM_UDEV_FLAGS_SHIFT)
| (base_cookie & !dmi::DM_UDEV_FLAGS_MASK);
debug!(
"Created UdevSync {{ cookie: {}, semid: {} }}",
hdr.event_nr, semid
);
if let Err(err) = notify_sem_inc(hdr.event_nr, semid) {
error!("Failed to set udev notification semaphore initial state: {err}");
if let Err(err2) = notify_sem_destroy(hdr.event_nr, semid) {
error!("Failed to clean up udev notification semaphore: {err2}");
}
return Err(err);
}
Ok(UdevSync {
cookie: hdr.event_nr,
semid: Some(semid),
})
}
fn end(self, flags: u32) -> DmResult<()> {
if self.is_active() {
let semid = self.semid.expect("active UdevSync must have valid semid");
if (flags & DmFlags::DM_UEVENT_GENERATED.bits()) == 0 {
if let Err(err) = notify_sem_dec(self.cookie, semid) {
error!("Failed to clear notification semaphore state: {err}");
if let Err(err2) = notify_sem_destroy(self.cookie, semid) {
error!("Failed to clean up notification semaphore: {err2}");
}
return Err(err);
}
}
trace!("Waiting on {self:?}");
notify_sem_wait(self.cookie, semid)?;
trace!("Destroying {self:?}");
if let Err(err) = notify_sem_destroy(self.cookie, semid) {
error!("Failed to clean up notification semaphore: {err}");
}
}
Ok(())
}
fn cancel(self) {
if self.is_active() {
let semid = self.semid.expect("active UdevSync must have valid semid");
trace!("Canceling {self:?}");
if let Err(err) = notify_sem_destroy(self.cookie, semid) {
error!("Failed to clean up notification semaphore: {err}");
}
}
}
fn is_active(&self) -> bool {
self.cookie != 0 && self.semid.is_some()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::dm_flags::DmUdevFlags;
const IPC_TEST_KEY: i32 = 0x444d5253;
#[test]
fn test_semget_invalid_nsems() {
assert!(semget(0, -1, 0).is_err());
}
#[test]
fn test_semget_create_destroy() {
assert_matches!(semget(IPC_TEST_KEY, 1, IPC_CREAT | IPC_EXCL),
Ok(semid) => assert!(semctl(semid, 0, IPC_RMID, None).is_ok()));
}
#[test]
fn test_notify_sem_create_destroy() {
assert_matches!(notify_sem_create(),
Ok((cookie, semid)) => assert!(notify_sem_destroy(cookie, semid).is_ok()));
}
#[test]
fn test_udevsync_non_primary_source() {
let mut hdr: dmi::Struct_dm_ioctl = devicemapper_sys::dm_ioctl {
..Default::default()
};
let sync = UdevSync::begin(&mut hdr, dmi::DM_TABLE_STATUS_CMD as u8).unwrap();
assert_eq!(sync.cookie, 0);
assert_eq!(sync.semid, None);
assert_eq!(hdr.event_nr, 0);
assert!(sync.end(DmFlags::empty().bits()).is_ok());
}
#[test]
fn test_udevsync_non_primary_source_cancel() {
let mut hdr: dmi::Struct_dm_ioctl = devicemapper_sys::dm_ioctl {
..Default::default()
};
let sync = UdevSync::begin(&mut hdr, dmi::DM_TABLE_STATUS_CMD as u8).unwrap();
assert_eq!(sync.cookie, 0);
assert_eq!(sync.semid, None);
assert_eq!(hdr.event_nr, 0);
sync.cancel();
}
#[test]
fn test_udevsync_primary_source_end() {
let mut hdr: dmi::Struct_dm_ioctl = devicemapper_sys::dm_ioctl {
..Default::default()
};
let sync = UdevSync::begin(&mut hdr, dmi::DM_DEV_REMOVE_CMD as u8).unwrap();
assert_ne!((sync.cookie & !dmi::DM_UDEV_FLAGS_MASK), 0);
assert!(sync.semid.unwrap() >= 0);
assert!(notify_sem_dec(sync.cookie, sync.semid.unwrap()).is_ok());
assert_eq!(
(hdr.event_nr >> dmi::DM_UDEV_FLAGS_SHIFT)
& DmUdevFlags::DM_UDEV_PRIMARY_SOURCE_FLAG.bits(),
DmUdevFlags::DM_UDEV_PRIMARY_SOURCE_FLAG.bits()
);
assert!(sync.end(DmFlags::DM_UEVENT_GENERATED.bits()).is_ok());
}
#[test]
fn test_udevsync_primary_source_cancel() {
let mut hdr: dmi::Struct_dm_ioctl = devicemapper_sys::dm_ioctl {
..Default::default()
};
let sync = UdevSync::begin(&mut hdr, dmi::DM_DEV_REMOVE_CMD as u8).unwrap();
assert_ne!((sync.cookie & !dmi::DM_UDEV_FLAGS_MASK), 0);
assert!(sync.semid.unwrap() >= 0);
assert_eq!(
(hdr.event_nr >> dmi::DM_UDEV_FLAGS_SHIFT)
& DmUdevFlags::DM_UDEV_PRIMARY_SOURCE_FLAG.bits(),
DmUdevFlags::DM_UDEV_PRIMARY_SOURCE_FLAG.bits()
);
sync.cancel();
}
#[test]
fn test_udevsync_primary_source_end_no_uevent() {
let mut hdr: dmi::Struct_dm_ioctl = devicemapper_sys::dm_ioctl {
..Default::default()
};
let sync = UdevSync::begin(&mut hdr, dmi::DM_DEV_REMOVE_CMD as u8).unwrap();
assert_ne!((sync.cookie & !dmi::DM_UDEV_FLAGS_MASK), 0);
assert!(sync.semid.unwrap() >= 0);
assert_eq!(
(hdr.event_nr >> dmi::DM_UDEV_FLAGS_SHIFT)
& DmUdevFlags::DM_UDEV_PRIMARY_SOURCE_FLAG.bits(),
DmUdevFlags::DM_UDEV_PRIMARY_SOURCE_FLAG.bits()
);
assert!(sync.end(DmFlags::empty().bits()).is_ok());
}
}
}
#[cfg(target_os = "android")]
pub mod sync_noop {
use super::UdevSyncAction;
use crate::{core::dm_ioctl as dmi, result::DmResult};
#[derive(Debug)]
pub struct UdevSync {
cookie: u32,
semid: Option<i32>,
}
impl UdevSyncAction for UdevSync {
fn begin(hdr: &mut dmi::Struct_dm_ioctl, ioctl: u8) -> DmResult<Self> {
debug!("Created noop UdevSync {{ cookie: {}, semid: {} }}", 0, -1);
Ok(UdevSync {
cookie: 0,
semid: None,
})
}
fn end(self, _flags: u32) -> DmResult<()> {
trace!("Destroying noop {:?}", self);
Ok(())
}
fn cancel(self) {
trace!("Canceling noop {:?}", self);
}
fn is_active(&self) -> bool {
false
}
}
}
cfg_if! {
if #[cfg(target_os = "android")] {
pub use self::sync_noop::UdevSync;
} else {
pub use self::sync_semaphore::UdevSync;
}
}