Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions src/core/dm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,11 @@ impl DM {
hdr.version[1] = ioctl_version.1;
hdr.version[2] = ioctl_version.2;

// Clear event_nr for ioctls that don't support udev cookies to avoid EINVAL
if !dmi::ioctl_uses_udev_cookie(ioctl) {
hdr.event_nr = 0;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Begin udev sync transaction and set DM_UDEV_PRIMARY_SOURCE_FLAG
// if ioctl command generates uevents.
let sync = UdevSync::begin(hdr, ioctl)?;
Expand Down Expand Up @@ -1142,4 +1147,66 @@ mod tests {
dm.device_remove(&DevId::Name(&name), DmOptions::default())
.unwrap();
}

use crate::core::dm_flags::DmUdevFlags;

/// Build DmOptions that fully disable udev synchronization.
/// Setting all DM_UDEV_DISABLE_* flags together with DM_UDEV_DISABLE_LIBRARY_FALLBACK
/// tells the kernel and the UdevSync layer to skip udev cookie handling entirely.
fn no_udev_dm_options() -> DmOptions {
DmOptions::default().set_udev_flags(
DmUdevFlags::DM_UDEV_DISABLE_LIBRARY_FALLBACK
| DmUdevFlags::DM_UDEV_DISABLE_SUBSYSTEM_RULES_FLAG
| DmUdevFlags::DM_UDEV_DISABLE_DISK_RULES_FLAG
| DmUdevFlags::DM_UDEV_DISABLE_OTHER_RULES_FLAG
| DmUdevFlags::DM_UDEV_DISABLE_DM_RULES_FLAG,
)
}

/// The test verifies that DM operations work correctly when udev
/// synchronization is fully disabled, simulating the dm-verity device
/// creation flow in a no-udev guest VM environment.
///
/// It simulates the dm-verity device lifecycle:
/// device_create -> table_load -> device_suspend(resume) -> device_info -> device_remove
///
/// All operations use no-udev DmOptions. Uses a read-only error target
/// (no real block device needed).
#[test]
fn sudo_test_no_udev_device_lifecycle() {
let dm = DM::new().unwrap();
let name = test_name("no-udev-lifecycle").expect("is valid DM name");
let id = DevId::Name(&name);

let ro_opts = no_udev_dm_options().set_flags(DmFlags::DM_READONLY);
let opts = no_udev_dm_options();

// Step 1: Create device (read-only, no-udev)
let create_info = dm.device_create(&name, None, ro_opts).unwrap();
assert_eq!(create_info.name(), Some(&*name));

// Step 2: Load a trivial error target table
let table = vec![(
0u64,
1024u64, // 512KB in 512-byte sectors
"error".into(),
"".into(),
)];
dm.table_load(&id, &table, ro_opts).unwrap();

// Step 3: Resume device (activate the table)
// device_suspend without DM_SUSPEND flag = resume
dm.device_suspend(&id, opts).unwrap();

// Step 4: Verify device is active via device_info
// device_info uses DmOptions::default(), but DM_DEV_STATUS_CMD
// does not require udev sync, so it works in no-udev environments
let info = dm.device_info(&id).unwrap();
assert_eq!(info.name(), Some(&*name));
// Device should not be suspended after resume
assert!(!info.flags().contains(DmFlags::DM_SUSPEND));

// Step 5: Remove device with no-udev options
dm.device_remove(&id, no_udev_dm_options()).unwrap();
}
}
10 changes: 10 additions & 0 deletions src/core/dm_ioctl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,13 @@ pub(crate) fn ioctl_to_version(ioctl: u8) -> (u32, u32, u32) {
unreachable!("Unknown device-mapper ioctl command: {}", ioctl);
}
}

/// Returns true for ioctls whose `event_nr` field carries a udev cookie /
/// udev flags and is honored by the kernel for udev synchronization.
#[inline]
pub(crate) fn ioctl_uses_udev_cookie(ioctl: u8) -> bool {
matches!(
ioctl as u32,
DM_DEV_REMOVE_CMD | DM_DEV_RENAME_CMD | DM_DEV_SUSPEND_CMD
)
}
91 changes: 78 additions & 13 deletions src/core/dm_udev_sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -293,23 +293,48 @@ pub mod sync_semaphore {
/// Allocate a SysV semaphore according to the device-mapper udev cookie
/// protocol and set the initial state of the semaphore counter.
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(),
)));
// First check if this ioctl command requires udev synchronization.
// Only REMOVE, RENAME, and SUSPEND (non-suspended) operations need it.
let requires_sync = dmi::ioctl_uses_udev_cookie(ioctl)
&& *SYSV_SEM_SUPPORTED
&& (hdr.flags & DmFlags::DM_SUSPEND.bits()) == 0;
// If this operation doesn't require udev sync, return immediately
if !requires_sync {
// Strip any udev flags the caller put in event_nr; without a
// semaphore behind them the kernel would otherwise wait for a
// udev completion that will never arrive.
hdr.event_nr = 0;
return Ok(UdevSync {
cookie: 0,
semid: None,
});
}

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,
});
}
// Check if udev integration has been explicitly disabled via flags.
let udev_disabled = {
let event_flags = hdr.event_nr >> dmi::DM_UDEV_FLAGS_SHIFT;
(event_flags & DmUdevFlags::DM_UDEV_DISABLE_LIBRARY_FALLBACK.bits()) != 0
};

// If udev is disabled (via DM_UDEV_DISABLE_LIBRARY_FALLBACK) or udev
// daemon is not running, gracefully degrade: skip semaphore allocation,
// clear any stale cookie flags from event_nr, and return an inactive
// UdevSync. This allows the library to function in environments without
// udev (e.g. minimal VMs, containers) without requiring every caller
// to explicitly set DM_UDEV_DISABLE_LIBRARY_FALLBACK.
if udev_disabled || !udev_running() {
// Clear event_nr to prevent sending non-zero cookie flags to the
// kernel when we cannot actually perform udev synchronization.
// A non-zero event_nr without a matching semaphore would cause the
// kernel to wait indefinitely for udevd to process the cookie.
hdr.event_nr = 0;
return Ok(UdevSync {
cookie: 0,
semid: None,
});
}
Comment thread
Apokleos marked this conversation as resolved.

// Udev sync is required and udev is available, allocate semaphore
let (base_cookie, semid) = notify_sem_create()?;

// Encode the primary source flag and the random base cookie value into
Expand Down Expand Up @@ -479,6 +504,46 @@ pub mod sync_semaphore {
);
assert!(sync.end(DmFlags::empty().bits()).is_ok());
}

/// Verify that the full no_udev_dm_options() flag combination causes all
/// udev-sync commands to return inactive UdevSync. This is an end-to-end
/// validation of the use case.
#[test]
fn test_udevsync_no_udev_options_all_sync_cmds() {
let no_udev_flags = DmUdevFlags::DM_UDEV_DISABLE_LIBRARY_FALLBACK
| DmUdevFlags::DM_UDEV_DISABLE_SUBSYSTEM_RULES_FLAG
| DmUdevFlags::DM_UDEV_DISABLE_DISK_RULES_FLAG
| DmUdevFlags::DM_UDEV_DISABLE_OTHER_RULES_FLAG
| DmUdevFlags::DM_UDEV_DISABLE_DM_RULES_FLAG;

let event_nr = no_udev_flags.bits() << dmi::DM_UDEV_FLAGS_SHIFT;

for ioctl_cmd in [
dmi::DM_DEV_REMOVE_CMD as u8,
dmi::DM_DEV_RENAME_CMD as u8,
dmi::DM_DEV_SUSPEND_CMD as u8, // resume (no DM_SUSPEND flag)
] {
let mut hdr: dmi::Struct_dm_ioctl = devicemapper_sys::dm_ioctl {
..Default::default()
};
hdr.event_nr = event_nr;

let sync = UdevSync::begin(&mut hdr, ioctl_cmd).unwrap();
assert_eq!(
sync.cookie, 0,
"ioctl {ioctl_cmd}: no_udev flags should produce inactive UdevSync"
);
assert_eq!(
sync.semid, None,
"ioctl {ioctl_cmd}: no semaphore should be allocated"
);
assert_eq!(
hdr.event_nr, 0,
"ioctl {ioctl_cmd}: event_nr must be cleared"
);
assert!(sync.end(DmFlags::empty().bits()).is_ok());
}
}
}
}
#[cfg(target_os = "android")]
Expand Down