Skip to main content

vfio_sys/
lib.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4#![expect(missing_docs)]
5#![cfg(unix)]
6// UNSAFETY: Manual memory management with mmap and vfio ioctls.
7#![expect(unsafe_code)]
8
9pub mod cdev;
10pub mod iommufd;
11
12use anyhow::Context;
13use bitfield_struct::bitfield;
14use headervec::HeaderVec;
15use libc::c_void;
16use memory_range::MemoryRange;
17use pal_async::driver::Driver;
18use pal_async::timer::PolledTimer;
19use std::ffi::CString;
20use std::fs;
21use std::fs::File;
22use std::io::BufRead;
23use std::io::BufReader;
24use std::os::unix::prelude::*;
25use std::path::Path;
26use std::time::Duration;
27use vfio_bindings::bindings::vfio::VFIO_IRQ_SET_ACTION_TRIGGER;
28use vfio_bindings::bindings::vfio::VFIO_IRQ_SET_DATA_EVENTFD;
29use vfio_bindings::bindings::vfio::VFIO_IRQ_SET_DATA_NONE;
30use vfio_bindings::bindings::vfio::VFIO_PCI_MSIX_IRQ_INDEX;
31use vfio_bindings::bindings::vfio::VFIO_REGION_INFO_CAP_SPARSE_MMAP;
32use vfio_bindings::bindings::vfio::vfio_device_info;
33use vfio_bindings::bindings::vfio::vfio_group_status;
34use vfio_bindings::bindings::vfio::vfio_info_cap_header;
35use vfio_bindings::bindings::vfio::vfio_irq_info;
36use vfio_bindings::bindings::vfio::vfio_irq_set;
37use vfio_bindings::bindings::vfio::vfio_region_info;
38use vfio_bindings::bindings::vfio::vfio_region_info_cap_sparse_mmap;
39use vfio_bindings::bindings::vfio::vfio_region_sparse_mmap_area;
40
41/// Returns the host page size.
42pub fn host_page_size() -> u64 {
43    use std::sync::atomic::{AtomicU64, Ordering::Relaxed};
44    static PAGE_SIZE: AtomicU64 = const { AtomicU64::new(0) };
45
46    let page_size = PAGE_SIZE.load(Relaxed);
47    if page_size == 0 {
48        // SAFETY: sysconf(_SC_PAGESIZE) is always safe to call on Linux.
49        let raw = unsafe { libc::sysconf(libc::_SC_PAGESIZE) };
50        assert!(raw > 0, "sysconf(_SC_PAGESIZE) failed: {raw}");
51        let page_size = raw as u64;
52        PAGE_SIZE.store(page_size, Relaxed);
53        page_size
54    } else {
55        page_size
56    }
57}
58
59mod ioctl {
60    use nix::request_code_none;
61    use std::os::raw::c_char;
62    use std::os::raw::c_int;
63    use vfio_bindings::bindings::vfio::VFIO_BASE;
64    use vfio_bindings::bindings::vfio::VFIO_TYPE;
65    use vfio_bindings::bindings::vfio::vfio_device_info;
66    use vfio_bindings::bindings::vfio::vfio_group_status;
67    use vfio_bindings::bindings::vfio::vfio_iommu_type1_dma_map;
68    use vfio_bindings::bindings::vfio::vfio_iommu_type1_dma_unmap;
69    use vfio_bindings::bindings::vfio::vfio_irq_info;
70    use vfio_bindings::bindings::vfio::vfio_irq_set;
71    use vfio_bindings::bindings::vfio::vfio_region_info;
72
73    const VFIO_PRIVATE_BASE: u32 = 200;
74
75    nix::ioctl_write_int_bad!(vfio_set_iommu, request_code_none!(VFIO_TYPE, VFIO_BASE + 2));
76    nix::ioctl_read_bad!(
77        vfio_group_get_status,
78        request_code_none!(VFIO_TYPE, VFIO_BASE + 3),
79        vfio_group_status
80    );
81    nix::ioctl_write_ptr_bad!(
82        vfio_group_set_container,
83        request_code_none!(VFIO_TYPE, VFIO_BASE + 4),
84        c_int
85    );
86    nix::ioctl_write_ptr_bad!(
87        vfio_group_get_device_fd,
88        request_code_none!(VFIO_TYPE, VFIO_BASE + 6),
89        c_char
90    );
91    nix::ioctl_read_bad!(
92        vfio_device_get_info,
93        request_code_none!(VFIO_TYPE, VFIO_BASE + 7),
94        vfio_device_info
95    );
96    nix::ioctl_readwrite_bad!(
97        vfio_device_get_region_info,
98        request_code_none!(VFIO_TYPE, VFIO_BASE + 8),
99        vfio_region_info
100    );
101    nix::ioctl_readwrite_bad!(
102        vfio_device_get_irq_info,
103        request_code_none!(VFIO_TYPE, VFIO_BASE + 9),
104        vfio_irq_info
105    );
106    nix::ioctl_write_ptr_bad!(
107        vfio_device_set_irqs,
108        request_code_none!(VFIO_TYPE, VFIO_BASE + 10),
109        vfio_irq_set
110    );
111    nix::ioctl_none_bad!(
112        vfio_device_reset,
113        request_code_none!(VFIO_TYPE, VFIO_BASE + 11)
114    );
115    nix::ioctl_write_ptr_bad!(
116        vfio_group_set_keep_alive,
117        request_code_none!(VFIO_TYPE, VFIO_PRIVATE_BASE),
118        c_char
119    );
120    // VFIO_IOMMU_MAP_DMA
121    nix::ioctl_write_ptr_bad!(
122        vfio_iommu_map_dma,
123        request_code_none!(VFIO_TYPE, VFIO_BASE + 13),
124        vfio_iommu_type1_dma_map
125    );
126    // VFIO_IOMMU_UNMAP_DMA
127    nix::ioctl_readwrite_bad!(
128        vfio_iommu_unmap_dma,
129        request_code_none!(VFIO_TYPE, VFIO_BASE + 14),
130        vfio_iommu_type1_dma_unmap
131    );
132    // VFIO_DEVICE_FEATURE - _IO(VFIO_TYPE, VFIO_BASE + 17). The GET direction
133    // for the dmabuf feature returns a new dmabuf fd as the ioctl return
134    // value.
135    nix::ioctl_write_ptr_bad!(
136        vfio_device_feature_dma_buf,
137        request_code_none!(VFIO_TYPE, VFIO_BASE + 17),
138        super::VfioDeviceFeatureDmaBuf
139    );
140}
141
142/// `VFIO_DEVICE_FEATURE` direction/probe flags (`include/uapi/linux/vfio.h`).
143const VFIO_DEVICE_FEATURE_GET: u32 = 1 << 16;
144const VFIO_DEVICE_FEATURE_PROBE: u32 = 1 << 18;
145/// Feature index for exporting a device-region dmabuf for peer-to-peer DMA.
146const VFIO_DEVICE_FEATURE_DMA_BUF: u32 = 11;
147
148/// Combined `struct vfio_device_feature` header and
149/// `struct vfio_device_feature_dma_buf` payload with a single
150/// `struct vfio_region_dma_range`.
151///
152/// The kernel dmabuf-to-iommufd interconnect currently supports only
153/// `nr_ranges == 1`, so a fixed single-range struct suffices (one dmabuf per
154/// contiguous BAR area). Layout must match `include/uapi/linux/vfio.h`
155/// exactly.
156#[repr(C)]
157struct VfioDeviceFeatureDmaBuf {
158    // `struct vfio_device_feature`
159    argsz: u32,
160    flags: u32,
161    // `struct vfio_device_feature_dma_buf`
162    region_index: u32,
163    open_flags: u32,
164    dma_buf_flags: u32,
165    nr_ranges: u32,
166    // `struct vfio_region_dma_range dma_ranges[1]`
167    range_offset: u64,
168    range_length: u64,
169}
170
171/// Returns the `(st_dev, st_ino)` identity of a file descriptor.
172///
173/// Used to key device BAR areas by their intrinsic identity (the VFIO cdev
174/// inode plus a BAR-region file offset) rather than by a guest-controlled
175/// address.
176pub fn fd_identity(fd: BorrowedFd<'_>) -> std::io::Result<(u64, u64)> {
177    // SAFETY: `fstat` fully initializes the buffer on success; the fd is
178    // valid for the duration of the call.
179    let mut stat = unsafe { std::mem::zeroed::<libc::stat>() };
180    // SAFETY: `fd` is a valid file descriptor and `stat` points to a valid,
181    // correctly sized `libc::stat`.
182    let ret = unsafe { libc::fstat(fd.as_raw_fd(), &mut stat) };
183    if ret != 0 {
184        return Err(std::io::Error::last_os_error());
185    }
186    Ok((stat.st_dev as u64, stat.st_ino as u64))
187}
188
189pub struct Container {
190    file: File,
191}
192
193impl Container {
194    pub fn new() -> anyhow::Result<Self> {
195        let file = fs::OpenOptions::new()
196            .read(true)
197            .write(true)
198            .open("/dev/vfio/vfio")
199            .context("failed to open /dev/vfio/vfio")?;
200
201        Ok(Self { file })
202    }
203
204    pub fn set_iommu(&self, iommu: IommuType) -> anyhow::Result<()> {
205        // SAFETY: The file descriptor is valid.
206        unsafe {
207            ioctl::vfio_set_iommu(self.file.as_raw_fd(), iommu as i32)
208                .context("failed to set iommu")?;
209        }
210        Ok(())
211    }
212
213    /// Map a host virtual address range into the IOMMU for device DMA access.
214    ///
215    /// `iova` is the IO virtual address the device will use (typically the
216    /// guest physical address). `vaddr` is the host virtual address backing
217    /// the memory. `size` is the length in bytes. All three must be
218    /// page-aligned.
219    ///
220    /// Only valid when the container uses a Type1v2 IOMMU.
221    ///
222    /// # Safety
223    /// `vaddr` must point to valid, backed memory for `size` bytes. The
224    /// memory must not be unmapped while the IOMMU mapping is live (until
225    /// a corresponding `unmap_dma` call).
226    pub unsafe fn map_dma(
227        &self,
228        iova: u64,
229        vaddr: *const u8,
230        size: u64,
231        writable: bool,
232    ) -> anyhow::Result<()> {
233        use vfio_bindings::bindings::vfio::VFIO_DMA_MAP_FLAG_READ;
234        use vfio_bindings::bindings::vfio::VFIO_DMA_MAP_FLAG_WRITE;
235
236        let page_size = host_page_size();
237        let page_mask = page_size - 1;
238        let vaddr = vaddr as u64;
239        anyhow::ensure!(
240            iova & page_mask == 0 && vaddr & page_mask == 0 && size & page_mask == 0,
241            "VFIO DMA mapping requires page-aligned iova ({iova:#x}), vaddr ({vaddr:#x}), and size ({size:#x}), page size {page_size:#x}"
242        );
243
244        let mut flags = VFIO_DMA_MAP_FLAG_READ;
245        if writable {
246            flags |= VFIO_DMA_MAP_FLAG_WRITE;
247        }
248
249        let dma_map = vfio_bindings::bindings::vfio::vfio_iommu_type1_dma_map {
250            argsz: size_of::<vfio_bindings::bindings::vfio::vfio_iommu_type1_dma_map>() as u32,
251            flags,
252            vaddr,
253            iova,
254            size,
255        };
256        // SAFETY: The file descriptor is valid and a correctly constructed
257        // struct is being passed.
258        unsafe {
259            ioctl::vfio_iommu_map_dma(self.file.as_raw_fd(), &dma_map)
260                .context("VFIO_IOMMU_MAP_DMA failed")?;
261        }
262        Ok(())
263    }
264
265    /// Unmap a previously mapped IOVA range from the IOMMU.
266    ///
267    /// For Type1v2, the unmap range must not bisect any previous mapping:
268    /// if a mapping exists at `iova`, it must start exactly at `iova`, and
269    /// if a mapping exists at `iova + size - 1`, it must end there.
270    /// Multiple mappings may be unmapped in one call as long as these
271    /// boundary conditions hold. Gaps within the range are fine.
272    pub fn unmap_dma(&self, iova: u64, size: u64) -> anyhow::Result<()> {
273        let mut dma_unmap = vfio_bindings::bindings::vfio::vfio_iommu_type1_dma_unmap {
274            argsz: size_of::<vfio_bindings::bindings::vfio::vfio_iommu_type1_dma_unmap>() as u32,
275            flags: 0,
276            iova,
277            size,
278        };
279        // SAFETY: The file descriptor is valid and a correctly constructed
280        // struct is being passed.
281        unsafe {
282            ioctl::vfio_iommu_unmap_dma(self.file.as_raw_fd(), &mut dma_unmap)
283                .context("VFIO_IOMMU_UNMAP_DMA failed")?;
284        }
285        Ok(())
286    }
287}
288
289/// IOMMU type for VFIO container.
290///
291/// Only Type1v2 and NoIommu are supported. Type1 (v1) is a legacy interface
292/// that does not support fine-grained DMA mapping and is intentionally excluded.
293#[repr(u32)]
294pub enum IommuType {
295    Type1v2 = vfio_bindings::bindings::vfio::VFIO_TYPE1v2_IOMMU,
296    NoIommu = vfio_bindings::bindings::vfio::VFIO_NOIOMMU_IOMMU,
297}
298
299pub struct Group {
300    file: File,
301}
302
303impl Group {
304    /// Construct a `Group` from a pre-opened VFIO group file descriptor.
305    pub fn from_file(file: File) -> Self {
306        Self { file }
307    }
308
309    pub fn open(group: u64) -> anyhow::Result<Self> {
310        Self::open_path(format!("/dev/vfio/{group}").as_ref())
311    }
312
313    pub fn open_noiommu(group: u64) -> anyhow::Result<Self> {
314        Self::open_path(format!("/dev/vfio/noiommu-{group}").as_ref())
315    }
316
317    fn open_path(group: &Path) -> anyhow::Result<Self> {
318        let file = fs::OpenOptions::new()
319            .read(true)
320            .write(true)
321            .open(group)
322            .with_context(|| format!("failed to open group {}", group.display()))?;
323
324        Ok(Self { file })
325    }
326
327    pub fn find_group_for_device(device_sysfs_path: &Path) -> anyhow::Result<u64> {
328        let group = device_sysfs_path.join("iommu_group");
329        let group = fs::read_link(group).context("failed to read iommu group")?;
330        let group: u64 = group
331            .file_name()
332            .and_then(|s| s.to_str())
333            .context("invalid group link")?
334            .parse()
335            .context("failed to parse iommu group")?;
336
337        Ok(group)
338    }
339
340    pub fn open_device(&self, device_id: &str) -> anyhow::Result<Device> {
341        let id = CString::new(device_id)?;
342        // SAFETY: The file descriptor is valid and the string is null-terminated.
343        let file = unsafe {
344            let fd = ioctl::vfio_group_get_device_fd(self.file.as_raw_fd(), id.as_ptr())
345                .with_context(|| format!("failed to get device fd for {device_id}"))?;
346            File::from_raw_fd(fd)
347        };
348
349        Ok(Device { file })
350    }
351
352    pub fn set_container(&self, container: &Container) -> anyhow::Result<()> {
353        // SAFETY: The file descriptors are valid.
354        unsafe {
355            ioctl::vfio_group_set_container(self.file.as_raw_fd(), &container.file.as_raw_fd())
356                .context("failed to set container")?;
357        }
358        Ok(())
359    }
360
361    /// Try to attach this group to the given container.
362    ///
363    /// Returns `Ok(true)` if the group was successfully attached, `Ok(false)`
364    /// if the kernel rejected the pairing (EINVAL — the IOMMU domains are
365    /// incompatible), or `Err` on unexpected failures.
366    pub fn try_set_container(&self, container: &Container) -> anyhow::Result<bool> {
367        // SAFETY: The file descriptors are valid.
368        let result = unsafe {
369            ioctl::vfio_group_set_container(self.file.as_raw_fd(), &container.file.as_raw_fd())
370        };
371        match result {
372            Ok(_) => Ok(true),
373            Err(nix::errno::Errno::EINVAL) => Ok(false),
374            Err(e) => Err(e).context("failed to set container"),
375        }
376    }
377
378    pub fn status(&self) -> anyhow::Result<GroupStatus> {
379        let mut status = vfio_group_status {
380            argsz: size_of::<vfio_group_status>() as u32,
381            flags: 0,
382        };
383        // SAFETY: The file descriptor is valid and a correctly constructed struct is being passed.
384        unsafe {
385            ioctl::vfio_group_get_status(self.file.as_raw_fd(), &mut status)
386                .context("failed to get group status")?;
387        };
388        Ok(GroupStatus::from(status.flags))
389    }
390
391    /// Skip VFIO device reset when kernel is reloaded during servicing.
392    /// This feature is non-upstream version of our kernel and will be
393    /// eventually replaced with iommufd.
394    pub fn set_keep_alive(&self, device_id: &str) -> anyhow::Result<()> {
395        let id = CString::new(device_id)?;
396        // SAFETY: The file descriptor is valid and a correctly constructed struct is being passed.
397        unsafe {
398            ioctl::vfio_group_set_keep_alive(self.file.as_raw_fd(), id.as_ptr())
399                .with_context(|| format!("failed to set keep-alive for {device_id}"))?;
400        }
401        Ok(())
402    }
403}
404
405/// Retry wrapper for VFIO operations that may transiently fail
406pub struct VfioRetry<'a> {
407    driver: &'a dyn Driver,
408    device_id: &'a str,
409    sleep_duration: Duration,
410    max_retries: u32,
411}
412
413impl<'a> VfioRetry<'a> {
414    const SLEEP_DURATION: Duration = Duration::from_millis(250);
415    const MAX_RETRIES: u32 = 1;
416
417    pub fn new(driver: &'a dyn Driver, device_id: &'a str) -> Self {
418        Self {
419            driver,
420            device_id,
421            sleep_duration: Self::SLEEP_DURATION,
422            max_retries: Self::MAX_RETRIES,
423        }
424    }
425
426    /// Retry `op` when `should_retry` returns true for the error, up to
427    /// `max_retries` times with a sleep between attempts.
428    pub async fn retry<T, E>(
429        &self,
430        mut op: impl FnMut() -> Result<T, E>,
431        should_retry: impl Fn(&E) -> bool,
432        context: &str,
433    ) -> Result<T, E>
434    where
435        E: std::fmt::Display,
436    {
437        let mut attempt = 0;
438        loop {
439            match op() {
440                Ok(val) => return Ok(val),
441                Err(err) => {
442                    if attempt >= self.max_retries || !should_retry(&err) {
443                        return Err(err);
444                    }
445                    attempt += 1;
446                    tracelimit::warn_ratelimited!(
447                        device_id = self.device_id,
448                        operation = context,
449                        attempt,
450                        "retrying after transient error: {err}"
451                    );
452                }
453            }
454            PolledTimer::new(self.driver)
455                .sleep(self.sleep_duration)
456                .await;
457        }
458    }
459}
460
461#[bitfield(u32)]
462pub struct GroupStatus {
463    pub viable: bool,
464    pub container_set: bool,
465
466    #[bits(30)]
467    _reserved: u32,
468}
469
470pub struct Device {
471    file: File,
472}
473
474#[derive(Debug)]
475pub struct DeviceInfo {
476    pub flags: DeviceFlags,
477    pub num_regions: u32,
478    pub num_irqs: u32,
479}
480
481#[bitfield(u32)]
482pub struct DeviceFlags {
483    pub reset: bool,
484    pub pci: bool,
485    pub platform: bool,
486    pub amba: bool,
487    pub ccw: bool,
488    pub ap: bool,
489
490    #[bits(26)]
491    _reserved: u32,
492}
493
494#[derive(Debug)]
495pub struct RegionInfo {
496    pub flags: RegionFlags,
497    pub size: u64,
498    pub offset: u64,
499}
500
501#[bitfield(u32)]
502pub struct RegionFlags {
503    read: bool,
504    write: bool,
505    mmap: bool,
506    caps: bool,
507
508    #[bits(28)]
509    _reserved: u32,
510}
511
512#[derive(Debug)]
513pub struct IrqInfo {
514    pub flags: IrqFlags,
515    pub count: u32,
516}
517
518#[bitfield(u32)]
519pub struct IrqFlags {
520    eventfd: bool,
521    maskable: bool,
522    automasked: bool,
523    pub noresize: bool,
524
525    #[bits(28)]
526    _reserved: u32,
527}
528
529impl Device {
530    pub fn info(&self) -> anyhow::Result<DeviceInfo> {
531        let mut info = vfio_device_info {
532            argsz: size_of::<vfio_device_info>() as u32,
533            flags: 0,
534            num_regions: 0,
535            num_irqs: 0,
536        };
537        // SAFETY: The file descriptor is valid and a correctly constructed struct is being passed.
538        unsafe {
539            ioctl::vfio_device_get_info(self.file.as_raw_fd(), &mut info)
540                .context("failed to get device info")?;
541        }
542        Ok(DeviceInfo {
543            flags: DeviceFlags::from(info.flags),
544            num_regions: info.num_regions,
545            num_irqs: info.num_irqs,
546        })
547    }
548
549    pub fn region_info(&self, index: u32) -> anyhow::Result<RegionInfo> {
550        let mut info = vfio_region_info {
551            argsz: size_of::<vfio_region_info>() as u32,
552            index,
553            flags: 0,
554            cap_offset: 0,
555            size: 0,
556            offset: 0,
557        };
558        // SAFETY: The file descriptor is valid and a correctly constructed struct is being passed.
559        unsafe {
560            ioctl::vfio_device_get_region_info(self.file.as_raw_fd(), &mut info)
561                .context("failed to get region info")?;
562        };
563        Ok(RegionInfo {
564            flags: RegionFlags::from(info.flags),
565            size: info.size,
566            offset: info.offset,
567        })
568    }
569
570    /// Query the mmappable sub-regions for a VFIO region.
571    ///
572    /// If the region has a `VFIO_REGION_INFO_CAP_SPARSE_MMAP` capability,
573    /// returns the list of mmappable areas from it. If the region supports
574    /// mmap but has no sparse capability, returns a single area covering
575    /// the entire region. Returns an empty list if the region does not
576    /// support mmap.
577    pub fn region_mmap_areas(&self, index: u32) -> anyhow::Result<Vec<MemoryRange>> {
578        let mut info = vfio_region_info {
579            argsz: size_of::<vfio_region_info>() as u32,
580            index,
581            flags: 0,
582            cap_offset: 0,
583            size: 0,
584            offset: 0,
585        };
586        // SAFETY: The file descriptor is valid and a correctly constructed struct is being passed.
587        unsafe {
588            ioctl::vfio_device_get_region_info(self.file.as_raw_fd(), &mut info)
589                .context("failed to get region info")?;
590        };
591
592        let flags = RegionFlags::from(info.flags);
593
594        // If the kernel indicates capabilities are present and returned a
595        // larger argsz, re-query with a sufficiently large buffer to
596        // retrieve the capability chain.
597        if flags.caps() && info.argsz > size_of::<vfio_region_info>() as u32 {
598            let buf_size = info.argsz as usize;
599            let tail_len = buf_size - size_of::<vfio_region_info>();
600            let mut buf = HeaderVec::<vfio_region_info, u8, 0>::with_capacity(
601                vfio_region_info {
602                    argsz: buf_size as u32,
603                    index,
604                    flags: 0,
605                    cap_offset: 0,
606                    size: 0,
607                    offset: 0,
608                },
609                tail_len,
610            );
611            // SAFETY: The buffer is properly aligned and large enough per the
612            // kernel's argsz, and the fd is valid.
613            unsafe {
614                ioctl::vfio_device_get_region_info(self.file.as_raw_fd(), buf.as_mut_ptr())
615                    .context("failed to get region info with capabilities")?;
616            }
617            // Use the kernel's returned argsz rather than our pre-computed
618            // value, in case it differs.
619            let actual_tail = buf.head.argsz as usize - size_of::<vfio_region_info>();
620            // SAFETY: The kernel initialized the tail bytes via the ioctl.
621            unsafe { buf.set_tail_len(actual_tail.min(tail_len)) };
622            if let Some(areas) = parse_sparse_mmap_caps(&buf) {
623                return Ok(areas);
624            }
625        }
626
627        if flags.mmap() {
628            // The kernel can report a mmappable region whose size is not a
629            // multiple of the page size — e.g. a sub-page MMIO BAR that recent
630            // vfio-pci exposes for direct mapping. `MemoryRange` requires
631            // page-aligned bounds, and a sub-page region cannot be safely
632            // direct-mapped into guest GPA at page granularity anyway, so align
633            // the size down to the host page size and skip the region entirely
634            // if nothing remains (it stays trap-and-emulate).
635            let page_mask = host_page_size() - 1;
636            let aligned_size = info.size & !page_mask;
637            if aligned_size == 0 {
638                Ok(Vec::new())
639            } else {
640                Ok(vec![MemoryRange::new(0..aligned_size)])
641            }
642        } else {
643            Ok(Vec::new())
644        }
645    }
646
647    pub fn irq_info(&self, index: u32) -> anyhow::Result<IrqInfo> {
648        let mut info = vfio_irq_info {
649            argsz: size_of::<vfio_irq_info>() as u32,
650            index,
651            flags: 0,
652            count: 0,
653        };
654        // SAFETY: The file descriptor is valid and a correctly constructed struct is being passed.
655        unsafe {
656            ioctl::vfio_device_get_irq_info(self.file.as_raw_fd(), &mut info)
657                .context("failed to get irq info")?;
658        }
659        Ok(IrqInfo {
660            flags: IrqFlags::from(info.flags),
661            count: info.count,
662        })
663    }
664
665    pub fn map(&self, offset: u64, len: usize, write: bool) -> anyhow::Result<MappedRegion> {
666        let mut prot = libc::PROT_READ;
667        if write {
668            prot |= libc::PROT_WRITE;
669        }
670        // SAFETY: The file descriptor is valid and no address is being passed.
671        // The result is being validated.
672        let addr = unsafe {
673            libc::mmap(
674                std::ptr::null_mut(),
675                len,
676                prot,
677                libc::MAP_SHARED,
678                self.file.as_raw_fd(),
679                offset as i64,
680            )
681        };
682        if addr == libc::MAP_FAILED {
683            return Err(std::io::Error::last_os_error()).context("failed to map region");
684        }
685        Ok(MappedRegion { addr, len })
686    }
687
688    /// Returns whether the device supports exporting a region as a dmabuf for
689    /// peer-to-peer DMA (`VFIO_DEVICE_FEATURE_DMA_BUF`).
690    ///
691    /// `Ok(false)` means the feature is unavailable and the caller should fall
692    /// back to host-VA mapping. An unexpected probe failure is returned as an
693    /// error.
694    pub fn supports_dma_buf(&self) -> anyhow::Result<bool> {
695        let feature = VfioDeviceFeatureDmaBuf {
696            argsz: size_of::<VfioDeviceFeatureDmaBuf>() as u32,
697            flags: VFIO_DEVICE_FEATURE_PROBE
698                | VFIO_DEVICE_FEATURE_GET
699                | VFIO_DEVICE_FEATURE_DMA_BUF,
700            region_index: 0,
701            open_flags: 0,
702            dma_buf_flags: 0,
703            nr_ranges: 0,
704            range_offset: 0,
705            range_length: 0,
706        };
707        // SAFETY: the fd is valid and the struct is correctly sized. With the
708        // PROBE flag the kernel only reports support and does not export.
709        match unsafe { ioctl::vfio_device_feature_dma_buf(self.file.as_raw_fd(), &feature) } {
710            Ok(_) => Ok(true),
711            // The feature is not available on this kernel or device.
712            Err(nix::errno::Errno::ENOTTY | nix::errno::Errno::EOPNOTSUPP) => Ok(false),
713            Err(e) => Err(e).context("VFIO_DEVICE_FEATURE_DMA_BUF probe failed"),
714        }
715    }
716
717    /// Exports a single page-aligned range of a device region (BAR) as a
718    /// dmabuf for peer-to-peer DMA via iommufd (`VFIO_DEVICE_FEATURE_DMA_BUF`).
719    ///
720    /// `region_index` is the VFIO region (BAR) index. `offset` and `length`
721    /// are BAR-relative and must be page-aligned. The kernel interconnect
722    /// currently supports only a single range per dmabuf, so this exports
723    /// exactly one range. Returns an owned dmabuf fd.
724    pub fn export_dma_buf(
725        &self,
726        region_index: u32,
727        offset: u64,
728        length: u64,
729    ) -> anyhow::Result<OwnedFd> {
730        let feature = VfioDeviceFeatureDmaBuf {
731            argsz: size_of::<VfioDeviceFeatureDmaBuf>() as u32,
732            flags: VFIO_DEVICE_FEATURE_GET | VFIO_DEVICE_FEATURE_DMA_BUF,
733            region_index,
734            open_flags: (libc::O_RDWR | libc::O_CLOEXEC) as u32,
735            dma_buf_flags: 0,
736            nr_ranges: 1,
737            range_offset: offset,
738            range_length: length,
739        };
740        // SAFETY: the fd is valid and the struct is correctly sized and
741        // constructed. On GET the kernel returns a freshly-opened dmabuf fd as
742        // the ioctl return value.
743        let fd = unsafe {
744            ioctl::vfio_device_feature_dma_buf(self.file.as_raw_fd(), &feature)
745                .context("VFIO_DEVICE_FEATURE_DMA_BUF export failed")?
746        };
747        // SAFETY: the kernel returned a new, owned dmabuf fd.
748        Ok(unsafe { OwnedFd::from_raw_fd(fd) })
749    }
750
751    pub fn map_msix<I>(&self, start: u32, eventfd: I) -> anyhow::Result<()>
752    where
753        I: IntoIterator,
754        I::Item: AsFd,
755    {
756        // Collect the eventfds up front so we know how many MSI-X vectors to
757        // bind; holding the borrowed fds keeps them open for the ioctl.
758        let fds: Vec<_> = eventfd.into_iter().collect();
759
760        // VFIO_DEVICE_SET_IRQS takes a vfio_irq_set header immediately followed
761        // by a variable-length array of eventfd file descriptors (one per
762        // vector). Build it with a HeaderVec so the header and the fd tail are
763        // laid out contiguously and the vector count is bounded only by what
764        // the device and kernel accept.
765        // vfio_irq_set itself is not Copy (it ends in an incomplete-array
766        // member), so it cannot be a HeaderVec head; VfioIrqSetHeader mirrors
767        // its fixed 20-byte prefix and the i32 tail supplies the fd array.
768        #[repr(C)]
769        #[derive(Copy, Clone)]
770        struct VfioIrqSetHeader {
771            argsz: u32,
772            flags: u32,
773            index: u32,
774            start: u32,
775            count: u32,
776        }
777        const _: () = assert!(
778            size_of::<VfioIrqSetHeader>() == size_of::<vfio_irq_set>(),
779            "VfioIrqSetHeader must match the fixed prefix of vfio_irq_set"
780        );
781
782        let mut param = HeaderVec::<VfioIrqSetHeader, i32, 0>::with_capacity(
783            VfioIrqSetHeader {
784                argsz: 0, // set below, once the fd tail length is known
785                flags: VFIO_IRQ_SET_ACTION_TRIGGER,
786                index: VFIO_PCI_MSIX_IRQ_INDEX,
787                start,
788                count: 0,
789            },
790            fds.len(),
791        );
792        for fd in &fds {
793            param.push_tail(fd.as_fd().as_raw_fd());
794        }
795
796        // argsz spans the header plus the contiguous fd tail.
797        let argsz = param.total_byte_len() as u32;
798        param.head.argsz = argsz;
799        param.head.count = fds.len() as u32;
800        if fds.is_empty() {
801            param.head.flags |= VFIO_IRQ_SET_DATA_NONE;
802        } else {
803            param.head.flags |= VFIO_IRQ_SET_DATA_EVENTFD;
804        }
805
806        // SAFETY: The file descriptor is valid. HeaderVec lays out the header
807        // and fd tail contiguously exactly as vfio_irq_set expects, and argsz
808        // spans the whole buffer, so the pointer is valid for the ioctl read.
809        unsafe {
810            ioctl::vfio_device_set_irqs(self.file.as_raw_fd(), param.as_ptr().cast())
811                .context("failed to set msi-x trigger")?;
812        }
813        Ok(())
814    }
815
816    /// Disable MSI-X for this device, tearing down all eventfd bindings.
817    ///
818    /// VFIO does not support disabling a subset of MSI-X vectors via DATA_NONE:
819    /// per `vfio_pci_set_msi_trigger` in the kernel, the only teardown form is
820    /// ACTION_TRIGGER | DATA_NONE with `count == 0`, which disables MSI-X
821    /// entirely. (A non-zero count with DATA_NONE is instead interpreted as a
822    /// loopback signal request that fires each vector's eventfd and unmaps
823    /// nothing.) This therefore always disables all vectors, and the caller
824    /// must only invoke it when MSI-X is currently enabled — otherwise the
825    /// kernel returns EINVAL.
826    pub fn unmap_msix(&self) -> anyhow::Result<()> {
827        let header = vfio_irq_set {
828            argsz: size_of::<vfio_irq_set>() as u32,
829            flags: VFIO_IRQ_SET_ACTION_TRIGGER | VFIO_IRQ_SET_DATA_NONE,
830            index: VFIO_PCI_MSIX_IRQ_INDEX,
831            start: 0,
832            count: 0,
833            data: Default::default(),
834        };
835
836        // SAFETY: The file descriptor is valid; header constructed per VFIO spec.
837        unsafe {
838            ioctl::vfio_device_set_irqs(self.file.as_raw_fd(), &header)
839                .context("failed to unmap msix vectors")?;
840        }
841        Ok(())
842    }
843
844    /// Reset the device via VFIO_DEVICE_RESET.
845    ///
846    /// Not all devices support reset — check `DeviceInfo::flags.reset()`
847    /// first. Returns an error if the ioctl fails.
848    pub fn reset(&self) -> anyhow::Result<()> {
849        // SAFETY: The file descriptor is valid.
850        unsafe {
851            ioctl::vfio_device_reset(self.file.as_raw_fd()).context("VFIO_DEVICE_RESET failed")?;
852        }
853        Ok(())
854    }
855
856    /// Returns the underlying device file, for direct positional I/O on config
857    /// space and BAR regions (`read_at`/`write_at`).
858    ///
859    /// Prefer this over `AsRef::<File>::as_ref` when the `Device` is held
860    /// behind an `Arc`: `Arc<Device>` also implements `AsRef<Device>`, so a
861    /// bare `.as_ref()` there would resolve to `&Device` rather than `&File`.
862    pub fn file(&self) -> &File {
863        &self.file
864    }
865}
866
867/// Walk the VFIO capability chain in a region info buffer and extract sparse
868/// mmap areas from any `VFIO_REGION_INFO_CAP_SPARSE_MMAP` capability.
869///
870/// Returns `Some(areas)` if the sparse mmap capability is present (even if
871/// empty), or `None` if it is absent.
872fn parse_sparse_mmap_caps(buf: &HeaderVec<vfio_region_info, u8, 0>) -> Option<Vec<MemoryRange>> {
873    let mut offset = buf.head.cap_offset as usize;
874
875    // SAFETY: HeaderVec guarantees head + tail are contiguous.
876    let bytes =
877        unsafe { std::slice::from_raw_parts(buf.as_ptr().cast::<u8>(), buf.total_byte_len()) };
878
879    while offset != 0 {
880        if offset + size_of::<vfio_info_cap_header>() > bytes.len() {
881            tracing::warn!(offset, "VFIO cap header extends beyond buffer");
882            break;
883        }
884
885        // SAFETY: Bounds checked above. The kernel places capabilities at
886        // aligned offsets within the buffer.
887        let header = unsafe { &*bytes.as_ptr().add(offset).cast::<vfio_info_cap_header>() };
888
889        if header.id as u32 == VFIO_REGION_INFO_CAP_SPARSE_MMAP {
890            if offset + size_of::<vfio_region_info_cap_sparse_mmap>() > bytes.len() {
891                tracing::warn!("VFIO sparse mmap cap truncated");
892                break;
893            }
894            // SAFETY: Bounds checked above; repr(C) struct at kernel-aligned offset.
895            let cap = unsafe {
896                &*bytes
897                    .as_ptr()
898                    .add(offset)
899                    .cast::<vfio_region_info_cap_sparse_mmap>()
900            };
901            let n = cap.nr_areas as usize;
902            let areas_end = offset
903                + size_of::<vfio_region_info_cap_sparse_mmap>()
904                + n * size_of::<vfio_region_sparse_mmap_area>();
905            if areas_end > bytes.len() {
906                tracing::warn!(n, "VFIO sparse mmap areas extend beyond buffer");
907                break;
908            }
909            // SAFETY: Bounds checked; flexible array immediately follows the fixed fields.
910            let areas = unsafe { cap.areas.as_slice(n) };
911            return Some(
912                areas
913                    .iter()
914                    .filter(|a| a.size > 0)
915                    .map(|a| MemoryRange::new(a.offset..a.offset + a.size))
916                    .collect(),
917            );
918        }
919
920        offset = header.next as usize;
921    }
922
923    // No sparse mmap cap found.
924    None
925}
926
927impl AsRef<File> for Device {
928    fn as_ref(&self) -> &File {
929        &self.file
930    }
931}
932
933impl AsFd for Device {
934    fn as_fd(&self) -> BorrowedFd<'_> {
935        self.file.as_fd()
936    }
937}
938
939/// Find the Linux irq number for the MSI-X `index` of the PCI device `pci_id`.
940pub fn find_msix_irq(pci_id: &str, index: u32) -> anyhow::Result<u32> {
941    let buffered = BufReader::new(File::open("/proc/interrupts")?);
942
943    let id = format!("vfio-msix[{}]({})", index, pci_id);
944    let match_str = buffered
945        .lines()
946        .map_while(Result::ok)
947        .find(|line| line.contains(&id))
948        .with_context(|| format!("cannot find interrupt {id} in /proc/interrupts"))?;
949
950    // irq format is: <irq#:> cpu# <irq name>
951    let irq = match_str.trim_start().split(':').next().unwrap();
952    let irq: u32 = irq
953        .parse()
954        .with_context(|| format!("unexpected irq format {}. Expecting 'irq#:'", irq))?;
955
956    Ok(irq)
957}
958
959pub fn print_relevant_params() {
960    #[derive(Debug)]
961    struct Param {
962        _name: &'static str,
963        _value: Option<String>,
964    }
965
966    let vfio_params = [
967        "/sys/module/vfio/parameters/enable_unsafe_noiommu_mode",
968        "/sys/module/driver/parameters/async_probe",
969    ]
970    .iter()
971    .map(|path| Param {
972        _name: path,
973        _value: fs::read_to_string(path).ok().map(|s| s.trim().to_string()),
974    })
975    .collect::<Vec<_>>();
976
977    tracing::debug!(
978        vfio_params = ?vfio_params,
979        "Relevant VFIO module parameters"
980    );
981}
982
983pub struct MappedRegion {
984    addr: *mut c_void,
985    len: usize,
986}
987
988// SAFETY: The result of an mmap is safe to share amongst threads.
989unsafe impl Send for MappedRegion {}
990// SAFETY: The result of an mmap is safe to share amongst threads.
991unsafe impl Sync for MappedRegion {}
992
993impl MappedRegion {
994    pub fn as_ptr(&self) -> *mut c_void {
995        self.addr
996    }
997
998    pub fn len(&self) -> usize {
999        self.len
1000    }
1001
1002    pub fn read_u32(&self, offset: usize) -> u32 {
1003        assert_eq!(offset % 4, 0);
1004        assert!(offset.saturating_add(4) <= self.len);
1005        // SAFETY: We have validated that the offset is inside the region.
1006        unsafe { std::ptr::read_volatile(self.addr.byte_add(offset).cast()) }
1007    }
1008
1009    pub fn read_u64(&self, offset: usize) -> u64 {
1010        assert_eq!(offset % 8, 0);
1011        assert!(offset.saturating_add(8) <= self.len);
1012        // SAFETY: We have validated that the offset is inside the region.
1013        unsafe { std::ptr::read_volatile(self.addr.byte_add(offset).cast()) }
1014    }
1015
1016    pub fn write_u32(&self, offset: usize, data: u32) {
1017        assert_eq!(offset % 4, 0);
1018        assert!(offset.saturating_add(4) <= self.len);
1019        // SAFETY: We have validated that the offset is inside the region.
1020        unsafe {
1021            std::ptr::write_volatile(self.addr.byte_add(offset).cast(), data);
1022        }
1023    }
1024
1025    pub fn write_u64(&self, offset: usize, data: u64) {
1026        assert_eq!(offset % 8, 0);
1027        assert!(offset.saturating_add(8) <= self.len);
1028        // SAFETY: We have validated that the offset is inside the region.
1029        unsafe {
1030            std::ptr::write_volatile(self.addr.byte_add(offset).cast(), data);
1031        }
1032    }
1033}
1034
1035impl Drop for MappedRegion {
1036    fn drop(&mut self) {
1037        // SAFETY: The address and length are a valid mmap result.
1038        unsafe {
1039            libc::munmap(self.addr, self.len);
1040        }
1041    }
1042}