From fff674a27ddd9c7b8c485fd4f30f89d46b478347 Mon Sep 17 00:00:00 2001 From: Roberto Campesato Date: Thu, 7 May 2026 08:46:20 -0700 Subject: [PATCH] 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 Assisted-by: Claude:claude-opus-4-6 --- vmm/src/seccomp_filters.rs | 5 +++- vmm/src/uffd.rs | 53 ++++++++++++++++++++++++++++++++------ vmm/src/userfaultfd.rs | 5 ++++ 3 files changed, 54 insertions(+), 9 deletions(-) diff --git a/vmm/src/seccomp_filters.rs b/vmm/src/seccomp_filters.rs index 877ea907d..83e0683b9 100644 --- a/vmm/src/seccomp_filters.rs +++ b/vmm/src/seccomp_filters.rs @@ -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)?; diff --git a/vmm/src/uffd.rs b/vmm/src/uffd.rs index ec63fcc32..8b4a4e980 100644 --- a/vmm/src/uffd.rs +++ b/vmm/src/uffd.rs @@ -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::() == 32); -/// Create a userfaultfd file descriptor and perform the API handshake. -pub(crate) fn create(required_features: u64) -> Result { - // 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 { + 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 { + 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, diff --git a/vmm/src/userfaultfd.rs b/vmm/src/userfaultfd.rs index bbefb16bc..e3b766db6 100644 --- a/vmm/src/userfaultfd.rs +++ b/vmm/src/userfaultfd.rs @@ -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;