vmm: prefer /dev/userfaultfd over the syscall

Prefer /dev/userfaultfd (Linux 6.1+) over the userfaultfd(2) syscall
for obtaining userfaultfd file descriptors.  The device path bypasses
the capable(CAP_SYS_PTRACE) and vm.unprivileged_userfaultfd sysctl
checks that block the syscall in user-namespaced containers, using
file permissions instead.

Falls back to the syscall on older kernels or when the device node
does not exist.

Signed-off-by: Roberto Campesato <render@metalabs.org>
Assisted-by: Claude:claude-opus-4-6
This commit is contained in:
Roberto Campesato
2026-05-07 08:46:20 -07:00
committed by Rob Bradford
parent df1809c1fa
commit fff674a27d
3 changed files with 54 additions and 9 deletions

View File

@@ -25,7 +25,9 @@ use vhost::vhost_kern::vhost_binding::{
VHOST_VDPA_SET_STATUS, VHOST_VDPA_SET_VRING_ENABLE, VHOST_VDPA_SUSPEND,
};
use crate::userfaultfd::{UFFDIO_API, UFFDIO_COPY, UFFDIO_REGISTER, UFFDIO_WAKE};
use crate::userfaultfd::{
UFFDIO_API, UFFDIO_COPY, UFFDIO_REGISTER, UFFDIO_WAKE, USERFAULTFD_IOC_NEW,
};
#[derive(Copy, Clone)]
pub enum Thread {
@@ -421,6 +423,7 @@ fn create_vmm_ioctl_seccomp_rule_common(
and![Cond::new(1, ArgLen::Dword, Eq, UFFDIO_COPY)?],
and![Cond::new(1, ArgLen::Dword, Eq, UFFDIO_REGISTER)?],
and![Cond::new(1, ArgLen::Dword, Eq, UFFDIO_WAKE)?],
and![Cond::new(1, ArgLen::Dword, Eq, USERFAULTFD_IOC_NEW)?],
];
let hypervisor_rules = create_vmm_ioctl_seccomp_rule_hypervisor(hypervisor_type)?;

View File

@@ -4,14 +4,16 @@
//! Minimal userfaultfd bindings for demand-paged snapshot restore.
//!
//! Uses the `userfaultfd(2)` syscall (available since Linux 4.3) to create a
//! fault descriptor, then `UFFDIO_API` / `UFFDIO_REGISTER` / `UFFDIO_COPY`
//! Prefers `/dev/userfaultfd` (Linux 6.1+) over the `userfaultfd(2)` syscall
//! to create a fault descriptor, falling back to the syscall when the device
//! is unavailable. Then uses `UFFDIO_API` / `UFFDIO_REGISTER` / `UFFDIO_COPY`
//! ioctls to handle page faults from a background thread.
//!
//! Unlike an mmap(MAP_PRIVATE) overlay approach, UFFD does not replace the
//! original memory mapping, so it remains compatible with VFIO device
//! passthrough and shared-memory-backed guest RAM.
use std::fs::OpenOptions;
use std::io::Error;
use std::mem;
use std::os::fd::{AsRawFd, BorrowedFd, FromRawFd, OwnedFd, RawFd};
@@ -61,15 +63,50 @@ pub(crate) struct UffdMsg {
const _: () = assert!(mem::size_of::<UffdMsg>() == 32);
/// Create a userfaultfd file descriptor and perform the API handshake.
pub(crate) fn create(required_features: u64) -> Result<OwnedFd, Error> {
// SAFETY: `userfaultfd` syscall with O_CLOEXEC | O_NONBLOCK flags.
let fd = unsafe { libc::syscall(libc::SYS_userfaultfd, libc::O_CLOEXEC | libc::O_NONBLOCK) };
/// Try to obtain a userfaultfd via /dev/userfaultfd (Linux 6.1+).
///
/// This bypasses the capability and sysctl checks that gate the syscall,
/// requiring only file permissions on the device node.
fn try_dev_userfaultfd() -> Result<OwnedFd, Error> {
let dev = OpenOptions::new()
.read(true)
.write(true)
.open("/dev/userfaultfd")?;
let flags = libc::O_CLOEXEC | libc::O_NONBLOCK;
// SAFETY: USERFAULTFD_IOC_NEW on a valid /dev/userfaultfd fd returns a new
// userfaultfd file descriptor.
let fd = unsafe {
libc::ioctl(
dev.as_raw_fd(),
userfaultfd::USERFAULTFD_IOC_NEW as libc::Ioctl,
flags,
)
};
if fd < 0 {
return Err(Error::last_os_error());
}
// SAFETY: the syscall returned a valid fd above.
let fd = unsafe { OwnedFd::from_raw_fd(fd as RawFd) };
// SAFETY: the ioctl returned a valid fd above.
Ok(unsafe { OwnedFd::from_raw_fd(fd) })
}
/// Create a userfaultfd file descriptor and perform the API handshake.
///
/// Prefers `/dev/userfaultfd` (no capability/sysctl requirements, just file
/// permissions) and falls back to the `userfaultfd(2)` syscall.
pub(crate) fn create(required_features: u64) -> Result<OwnedFd, Error> {
let fd = match try_dev_userfaultfd() {
Ok(fd) => fd,
Err(_) => {
// SAFETY: `userfaultfd` syscall with O_CLOEXEC | O_NONBLOCK flags.
let raw =
unsafe { libc::syscall(libc::SYS_userfaultfd, libc::O_CLOEXEC | libc::O_NONBLOCK) };
if raw < 0 {
return Err(Error::last_os_error());
}
// SAFETY: the syscall returned a valid fd above.
unsafe { OwnedFd::from_raw_fd(raw as RawFd) }
}
};
let mut api = UffdioApi {
api: userfaultfd::UFFD_API,

View File

@@ -26,6 +26,11 @@ const _: () = assert!(UFFDIO_REGISTER <= u32::MAX as u64);
const _: () = assert!(UFFDIO_COPY <= u32::MAX as u64);
const _: () = assert!(UFFDIO_WAKE <= u32::MAX as u64);
// /dev/userfaultfd ioctl: _IO(0xAA, 0x00)
pub const USERFAULTFD_IOC_NEW: u64 = 0x0000_AA00;
const _: () = assert!(USERFAULTFD_IOC_NEW == ioctl_ioc(0, 0xAA, 0x00, 0));
const _: () = assert!(USERFAULTFD_IOC_NEW <= u32::MAX as u64);
pub const UFFD_API: u64 = 0xAA;
pub const UFFDIO_REGISTER_MODE_MISSING: u64 = 1;
pub const UFFD_EVENT_PAGEFAULT: u8 = 0x12;