Skip to main content

vfio_sys/
iommufd.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Bindings for the Linux iommufd subsystem (`/dev/iommu`).
5//!
6//! Provides safe wrappers around iommufd ioctls for:
7//! - IOAS allocation and DMA mapping (`IOMMU_IOAS_ALLOC`, `IOMMU_IOAS_MAP`,
8//!   `IOMMU_IOAS_MAP_FILE`, `IOMMU_IOAS_UNMAP`)
9//! - Hardware page table management (`IOMMU_HWPT_ALLOC`, `IOMMU_HWPT_INVALIDATE`)
10//! - Hardware info query (`IOMMU_GET_HW_INFO`)
11//! - Virtual IOMMU objects (`IOMMU_VIOMMU_ALLOC`, `IOMMU_VDEVICE_ALLOC`,
12//!   `IOMMU_VEVENTQ_ALLOC`)
13//!
14//! The IOAS path supports identity DMA mapping (Phase 4). The HWPT/vIOMMU
15//! path supports nested stage 1 translation for VFIO passthrough (Phase 5).
16
17use anyhow::Context as _;
18use std::fs;
19use std::os::unix::prelude::*;
20use zerocopy::FromBytes;
21use zerocopy::Immutable;
22use zerocopy::IntoBytes;
23use zerocopy::KnownLayout;
24
25/// iommufd ioctl type character (';' = 0x3B).
26const IOMMUFD_TYPE: u8 = b';';
27
28/// Base command number for iommufd ioctls.
29const IOMMUFD_CMD_BASE: u8 = 0x80;
30
31// Command numbers (IOMMUFD_CMD_BASE + offset).
32const IOMMUFD_CMD_DESTROY: u8 = IOMMUFD_CMD_BASE;
33const IOMMUFD_CMD_IOAS_ALLOC: u8 = IOMMUFD_CMD_BASE + 1;
34const IOMMUFD_CMD_IOAS_MAP: u8 = IOMMUFD_CMD_BASE + 5;
35const IOMMUFD_CMD_IOAS_UNMAP: u8 = IOMMUFD_CMD_BASE + 6;
36const IOMMUFD_CMD_HWPT_ALLOC: u8 = IOMMUFD_CMD_BASE + 9;
37const IOMMUFD_CMD_IOAS_MAP_FILE: u8 = IOMMUFD_CMD_BASE + 15;
38const IOMMUFD_CMD_GET_HW_INFO: u8 = IOMMUFD_CMD_BASE + 0x0a;
39const IOMMUFD_CMD_HWPT_INVALIDATE: u8 = IOMMUFD_CMD_BASE + 0x0d;
40const IOMMUFD_CMD_VIOMMU_ALLOC: u8 = IOMMUFD_CMD_BASE + 0x10;
41const IOMMUFD_CMD_VDEVICE_ALLOC: u8 = IOMMUFD_CMD_BASE + 0x11;
42const IOMMUFD_CMD_VEVENTQ_ALLOC: u8 = IOMMUFD_CMD_BASE + 0x13;
43
44/// Flags for `IOMMU_IOAS_MAP`.
45pub const IOMMU_IOAS_MAP_FIXED_IOVA: u32 = 1 << 0;
46pub const IOMMU_IOAS_MAP_WRITEABLE: u32 = 1 << 1;
47pub const IOMMU_IOAS_MAP_READABLE: u32 = 1 << 2;
48
49mod ioctl {
50    use nix::request_code_none;
51
52    // IOMMUFD ioctls use _IO (no direction, just type + nr).
53    // The kernel defines them as _IO(IOMMUFD_TYPE, cmd_nr).
54    nix::ioctl_readwrite_bad!(
55        iommu_destroy,
56        request_code_none!(
57            super::IOMMUFD_TYPE as u32,
58            super::IOMMUFD_CMD_DESTROY as u32
59        ),
60        super::IommuDestroy
61    );
62    nix::ioctl_readwrite_bad!(
63        iommu_ioas_alloc,
64        request_code_none!(
65            super::IOMMUFD_TYPE as u32,
66            super::IOMMUFD_CMD_IOAS_ALLOC as u32
67        ),
68        super::IommuIoasAlloc
69    );
70    nix::ioctl_readwrite_bad!(
71        iommu_ioas_map,
72        request_code_none!(
73            super::IOMMUFD_TYPE as u32,
74            super::IOMMUFD_CMD_IOAS_MAP as u32
75        ),
76        super::IommuIoasMap
77    );
78    nix::ioctl_readwrite_bad!(
79        iommu_ioas_map_file,
80        request_code_none!(
81            super::IOMMUFD_TYPE as u32,
82            super::IOMMUFD_CMD_IOAS_MAP_FILE as u32
83        ),
84        super::IommuIoasMapFile
85    );
86    nix::ioctl_readwrite_bad!(
87        iommu_ioas_unmap,
88        request_code_none!(
89            super::IOMMUFD_TYPE as u32,
90            super::IOMMUFD_CMD_IOAS_UNMAP as u32
91        ),
92        super::IommuIoasUnmap
93    );
94    nix::ioctl_readwrite_bad!(
95        iommu_hwpt_alloc,
96        request_code_none!(
97            super::IOMMUFD_TYPE as u32,
98            super::IOMMUFD_CMD_HWPT_ALLOC as u32
99        ),
100        super::IommuHwptAlloc
101    );
102    nix::ioctl_readwrite_bad!(
103        iommu_get_hw_info,
104        request_code_none!(
105            super::IOMMUFD_TYPE as u32,
106            super::IOMMUFD_CMD_GET_HW_INFO as u32
107        ),
108        super::IommuGetHwInfo
109    );
110    nix::ioctl_readwrite_bad!(
111        iommu_hwpt_invalidate,
112        request_code_none!(
113            super::IOMMUFD_TYPE as u32,
114            super::IOMMUFD_CMD_HWPT_INVALIDATE as u32
115        ),
116        super::IommuHwptInvalidate
117    );
118    nix::ioctl_readwrite_bad!(
119        iommu_viommu_alloc,
120        request_code_none!(
121            super::IOMMUFD_TYPE as u32,
122            super::IOMMUFD_CMD_VIOMMU_ALLOC as u32
123        ),
124        super::IommuViommuAlloc
125    );
126    nix::ioctl_readwrite_bad!(
127        iommu_vdevice_alloc,
128        request_code_none!(
129            super::IOMMUFD_TYPE as u32,
130            super::IOMMUFD_CMD_VDEVICE_ALLOC as u32
131        ),
132        super::IommuVdeviceAlloc
133    );
134    nix::ioctl_readwrite_bad!(
135        iommu_veventq_alloc,
136        request_code_none!(
137            super::IOMMUFD_TYPE as u32,
138            super::IOMMUFD_CMD_VEVENTQ_ALLOC as u32
139        ),
140        super::IommuVeventqAlloc
141    );
142}
143
144// Kernel ABI structs — must match `include/uapi/linux/iommufd.h` exactly.
145
146#[repr(C)]
147struct IommuDestroy {
148    size: u32,
149    id: u32,
150}
151
152#[repr(C)]
153struct IommuIoasAlloc {
154    size: u32,
155    flags: u32,
156    out_ioas_id: u32,
157}
158
159#[repr(C)]
160struct IommuIoasMap {
161    size: u32,
162    flags: u32,
163    ioas_id: u32,
164    __reserved: u32,
165    user_va: u64,
166    length: u64,
167    iova: u64,
168}
169
170#[repr(C)]
171struct IommuIoasMapFile {
172    size: u32,
173    flags: u32,
174    ioas_id: u32,
175    fd: i32,
176    start: u64,
177    length: u64,
178    iova: u64,
179}
180
181#[repr(C)]
182struct IommuIoasUnmap {
183    size: u32,
184    ioas_id: u32,
185    iova: u64,
186    length: u64,
187}
188
189// --- HWPT allocation ---
190
191/// Flags for `IOMMU_HWPT_ALLOC`.
192pub const IOMMU_HWPT_ALLOC_NEST_PARENT: u32 = 1 << 0;
193
194/// HWPT data type: no type-specific data.
195pub const IOMMU_HWPT_DATA_NONE: u32 = 0;
196/// HWPT data type: ARM SMMUv3 (nested STE DW0-1).
197pub const IOMMU_HWPT_DATA_ARM_SMMUV3: u32 = 2;
198
199#[repr(C)]
200struct IommuHwptAlloc {
201    size: u32,
202    flags: u32,
203    dev_id: u32,
204    pt_id: u32,
205    out_hwpt_id: u32,
206    __reserved: u32,
207    data_type: u32,
208    data_len: u32,
209    data_uptr: u64,
210    fault_id: u32,
211    __reserved2: u32,
212}
213
214/// ARM SMMUv3 nested HWPT data: the first two double words of the STE.
215///
216/// Passed via `data_uptr` when `data_type == IOMMU_HWPT_DATA_ARM_SMMUV3`.
217/// The kernel validates the STE fields and programs the host IOMMU.
218#[repr(C)]
219pub struct IommuHwptArmSmmuv3 {
220    pub ste: [u64; 2],
221}
222
223// --- Hardware info query ---
224
225/// HW info type: ARM SMMUv3.
226pub const IOMMU_HW_INFO_TYPE_ARM_SMMUV3: u32 = 2;
227
228#[repr(C)]
229struct IommuGetHwInfo {
230    size: u32,
231    flags: u32,
232    dev_id: u32,
233    data_len: u32,
234    data_uptr: u64,
235    out_data_type: u32,
236    out_max_pasid_log2: u8,
237    __reserved: [u8; 3],
238    out_capabilities: u64,
239}
240
241/// ARM SMMUv3 hardware information returned by `IOMMU_GET_HW_INFO`.
242///
243/// Contains the physical IOMMU's IDR register values. The VMM uses
244/// these to cap the virtual SMMU's advertised capabilities.
245#[repr(C)]
246pub struct IommuHwInfoArmSmmuv3 {
247    pub flags: u32,
248    pub __reserved: u32,
249    pub idr: [u32; 6],
250    pub iidr: u32,
251    pub aidr: u32,
252}
253
254// --- HWPT invalidation ---
255
256/// Invalidation data type for ARM SMMUv3 (via vIOMMU).
257pub const IOMMU_VIOMMU_INVALIDATE_DATA_ARM_SMMUV3: u32 = 1;
258
259#[repr(C)]
260struct IommuHwptInvalidate {
261    size: u32,
262    hwpt_id: u32,
263    data_uptr: u64,
264    data_type: u32,
265    entry_len: u32,
266    entry_num: u32,
267    __reserved: u32,
268}
269
270/// Error from [`IommufdCtx::hwpt_invalidate`], pairing the underlying ioctl
271/// errno with the kernel's reported handled-entry count.
272#[derive(Debug, thiserror::Error)]
273#[error("IOMMU_HWPT_INVALIDATE failed (kernel handled {handled} entries)")]
274pub struct HwptInvalidateError {
275    /// The underlying `IOMMU_HWPT_INVALIDATE` ioctl errno.
276    #[source]
277    pub errno: nix::errno::Errno,
278    /// The kernel's in/out `entry_num` after the failed call: the number of
279    /// leading entries it reports as handled before the failure. See the
280    /// caveat on [`IommufdCtx::hwpt_invalidate`] — this is unreliable for early
281    /// failures and may equal the input count.
282    pub handled: u32,
283}
284
285// --- Virtual IOMMU ---
286
287/// vIOMMU type: ARM SMMUv3.
288pub const IOMMU_VIOMMU_TYPE_ARM_SMMUV3: u32 = 1;
289
290/// Outcome of [`IommufdCtx::viommu_alloc`].
291#[derive(Debug, Copy, Clone, PartialEq, Eq)]
292pub enum ViommuAlloc {
293    /// The kernel-assigned vIOMMU object ID.
294    Allocated(u32),
295    /// The nesting parent belongs to a different physical IOMMU than the
296    /// device. Callers may probe another candidate parent.
297    Incompatible,
298}
299
300#[repr(C)]
301struct IommuViommuAlloc {
302    size: u32,
303    flags: u32,
304    r#type: u32,
305    dev_id: u32,
306    hwpt_id: u32,
307    out_viommu_id: u32,
308    data_len: u32,
309    __reserved: u32,
310    data_uptr: u64,
311}
312
313// --- Virtual device ---
314
315#[repr(C)]
316struct IommuVdeviceAlloc {
317    size: u32,
318    viommu_id: u32,
319    dev_id: u32,
320    out_vdevice_id: u32,
321    virt_id: u64,
322}
323
324// --- Virtual event queue ---
325
326/// vEVENTQ type: ARM SMMUv3.
327pub const IOMMU_VEVENTQ_TYPE_ARM_SMMUV3: u32 = 1;
328
329/// `IommufdVeventHeader::flags`: the queue lost one or more events before this
330/// one. No event data follows a header carrying this flag.
331pub const IOMMU_VEVENTQ_FLAG_LOST_EVENTS: u32 = 1 << 0;
332
333#[repr(C)]
334struct IommuVeventqAlloc {
335    size: u32,
336    flags: u32,
337    viommu_id: u32,
338    r#type: u32,
339    veventq_depth: u32,
340    out_veventq_id: u32,
341    out_veventq_fd: u32,
342    __reserved: u32,
343}
344
345/// Header for each event in a vEVENTQ fd read.
346///
347/// `sequence` is monotonic over `[0, i32::MAX]`, wrapping back to 0. A gap of
348/// more than 1 between adjacent headers means the intervening events were lost.
349#[repr(C)]
350#[derive(Copy, Clone, FromBytes, Immutable, IntoBytes, KnownLayout)]
351pub struct IommufdVeventHeader {
352    pub flags: u32,
353    pub sequence: u32,
354}
355
356/// ARM SMMUv3 virtual event record (256-bit, little-endian).
357///
358/// Follows an `IommufdVeventHeader` in the vEVENTQ fd read stream. The four
359/// quadwords are a verbatim SMMUv3 Event queue record, except that the kernel
360/// has rewritten the StreamID field to the virtual StreamID the vDevice was
361/// allocated with.
362#[repr(C)]
363#[derive(Copy, Clone, FromBytes, Immutable, IntoBytes, KnownLayout)]
364pub struct IommuVeventArmSmmuv3 {
365    pub evt: [u64; 4],
366}
367
368/// An open iommufd file descriptor (`/dev/iommu`).
369///
370/// Wraps the fd and provides safe methods for the iommufd ioctls needed
371/// to allocate an IOAS and map/unmap host memory into it.
372pub struct IommufdCtx {
373    file: fs::File,
374}
375
376impl IommufdCtx {
377    /// Open `/dev/iommu` and return a new iommufd context.
378    pub fn new() -> anyhow::Result<Self> {
379        let file = fs::OpenOptions::new()
380            .read(true)
381            .write(true)
382            .open("/dev/iommu")
383            .context("failed to open /dev/iommu")?;
384        Ok(Self { file })
385    }
386
387    /// Wrap an existing iommufd file descriptor.
388    pub fn from_file(file: fs::File) -> Self {
389        Self { file }
390    }
391
392    /// Allocate a new IO Address Space (IOAS).
393    ///
394    /// Returns the kernel-assigned IOAS object ID.
395    pub fn ioas_alloc(&self) -> anyhow::Result<u32> {
396        let mut cmd = IommuIoasAlloc {
397            size: size_of::<IommuIoasAlloc>() as u32,
398            flags: 0,
399            out_ioas_id: 0,
400        };
401        // SAFETY: fd is valid, struct is correctly sized and zeroed.
402        unsafe {
403            ioctl::iommu_ioas_alloc(self.file.as_raw_fd(), &mut cmd)
404                .context("IOMMU_IOAS_ALLOC failed")?;
405        }
406        Ok(cmd.out_ioas_id)
407    }
408
409    /// Map a user VA range into an IOAS at a fixed IOVA.
410    ///
411    /// `ioas_id` is the IOAS to map into. `iova` is the fixed IO virtual
412    /// address. `user_va` is the host virtual address of the backing memory.
413    /// `length` is the size in bytes (must be page-aligned).
414    ///
415    /// # Safety
416    /// `user_va` must point to valid, backed memory for `length` bytes.
417    /// The memory must remain mapped for the lifetime of this IOAS mapping.
418    pub unsafe fn ioas_map(
419        &self,
420        ioas_id: u32,
421        iova: u64,
422        user_va: u64,
423        length: u64,
424        writable: bool,
425    ) -> anyhow::Result<()> {
426        let mut flags = IOMMU_IOAS_MAP_FIXED_IOVA | IOMMU_IOAS_MAP_READABLE;
427        if writable {
428            flags |= IOMMU_IOAS_MAP_WRITEABLE;
429        }
430        let mut cmd = IommuIoasMap {
431            size: size_of::<IommuIoasMap>() as u32,
432            flags,
433            ioas_id,
434            __reserved: 0,
435            user_va,
436            length,
437            iova,
438        };
439        // SAFETY: fd is valid, struct correctly constructed. Caller
440        // guarantees user_va is backed and stable.
441        unsafe {
442            ioctl::iommu_ioas_map(self.file.as_raw_fd(), &mut cmd)
443                .context("IOMMU_IOAS_MAP failed")?;
444        }
445        Ok(())
446    }
447
448    /// Map a file/memfd range into an IOAS at a fixed IOVA via
449    /// `IOMMU_IOAS_MAP_FILE`.
450    ///
451    /// Unlike [`Self::ioas_map`], the kernel pins the backing folios directly
452    /// from `fd`, so no host VA is required. `start` is the byte offset within
453    /// the file; like [`Self::ioas_map`], both `start` and `length` must be
454    /// page-aligned. Requires a kernel with `IOMMU_IOAS_MAP_FILE` (Linux
455    /// 6.13+).
456    pub fn ioas_map_file(
457        &self,
458        ioas_id: u32,
459        iova: u64,
460        fd: RawFd,
461        start: u64,
462        length: u64,
463        writable: bool,
464    ) -> anyhow::Result<()> {
465        let mut flags = IOMMU_IOAS_MAP_FIXED_IOVA | IOMMU_IOAS_MAP_READABLE;
466        if writable {
467            flags |= IOMMU_IOAS_MAP_WRITEABLE;
468        }
469        let mut cmd = IommuIoasMapFile {
470            size: size_of::<IommuIoasMapFile>() as u32,
471            flags,
472            ioas_id,
473            fd,
474            start,
475            length,
476            iova,
477        };
478        // SAFETY: the iommufd fd is valid and the struct is correctly sized and
479        // constructed. `fd` is only read during the ioctl.
480        unsafe {
481            ioctl::iommu_ioas_map_file(self.file.as_raw_fd(), &mut cmd)
482                .context("IOMMU_IOAS_MAP_FILE failed")?;
483        }
484        Ok(())
485    }
486
487    /// Unmap an IOVA range from an IOAS.
488    ///
489    /// Returns the number of bytes actually unmapped.
490    pub fn ioas_unmap(&self, ioas_id: u32, iova: u64, length: u64) -> anyhow::Result<u64> {
491        let mut cmd = IommuIoasUnmap {
492            size: size_of::<IommuIoasUnmap>() as u32,
493            ioas_id,
494            iova,
495            length,
496        };
497        // SAFETY: fd is valid, struct correctly constructed.
498        unsafe {
499            ioctl::iommu_ioas_unmap(self.file.as_raw_fd(), &mut cmd)
500                .context("IOMMU_IOAS_UNMAP failed")?;
501        }
502        Ok(cmd.length)
503    }
504
505    /// Destroy an iommufd object by its ID.
506    pub fn destroy(&self, id: u32) -> anyhow::Result<()> {
507        let mut cmd = IommuDestroy {
508            size: size_of::<IommuDestroy>() as u32,
509            id,
510        };
511        // SAFETY: fd is valid, struct correctly constructed.
512        unsafe {
513            ioctl::iommu_destroy(self.file.as_raw_fd(), &mut cmd)
514                .context("IOMMU_DESTROY failed")?;
515        }
516        Ok(())
517    }
518
519    /// Allocate a hardware page table (HWPT).
520    ///
521    /// For a **nesting parent** (S2): set `flags = IOMMU_HWPT_ALLOC_NEST_PARENT`,
522    /// `pt_id` = IOAS ID, `data_type = IOMMU_HWPT_DATA_NONE`.
523    ///
524    /// For a **nested child** (S1): set `flags = 0`, `pt_id` = parent HWPT ID
525    /// or vIOMMU ID, `data_type = IOMMU_HWPT_DATA_ARM_SMMUV3`, and pass the
526    /// STE data via `data`.
527    ///
528    /// Returns the kernel-assigned HWPT object ID.
529    pub fn hwpt_alloc(
530        &self,
531        flags: u32,
532        dev_id: u32,
533        pt_id: u32,
534        data_type: u32,
535        data: Option<&IommuHwptArmSmmuv3>,
536    ) -> anyhow::Result<u32> {
537        let (data_uptr, data_len) = match data {
538            Some(data) => (
539                std::ptr::from_ref(data) as u64,
540                size_of::<IommuHwptArmSmmuv3>() as u32,
541            ),
542            None => (0, 0),
543        };
544        let mut cmd = IommuHwptAlloc {
545            size: size_of::<IommuHwptAlloc>() as u32,
546            flags,
547            dev_id,
548            pt_id,
549            out_hwpt_id: 0,
550            __reserved: 0,
551            data_type,
552            data_len,
553            data_uptr,
554            fault_id: 0,
555            __reserved2: 0,
556        };
557        // SAFETY: the fd is valid and `cmd` is correctly constructed.
558        // `data_uptr`/`data_len` are derived from the optional live `data`
559        // borrow (or null/zero when absent), so the kernel reads only within a
560        // valid, fully-initialized `#[repr(C)]` buffer.
561        unsafe {
562            ioctl::iommu_hwpt_alloc(self.file.as_raw_fd(), &mut cmd)
563                .context("IOMMU_HWPT_ALLOC failed")?;
564        }
565        Ok(cmd.out_hwpt_id)
566    }
567
568    /// Query hardware information for a device's IOMMU.
569    ///
570    /// Returns `(out_data_type, out_capabilities)`. The type-specific data is
571    /// written into `out_info`.
572    pub fn get_hw_info(
573        &self,
574        dev_id: u32,
575        out_info: &mut IommuHwInfoArmSmmuv3,
576    ) -> anyhow::Result<(u32, u64)> {
577        let mut cmd = IommuGetHwInfo {
578            size: size_of::<IommuGetHwInfo>() as u32,
579            flags: 0,
580            dev_id,
581            data_len: size_of::<IommuHwInfoArmSmmuv3>() as u32,
582            data_uptr: std::ptr::from_mut(out_info) as u64,
583            out_data_type: 0,
584            out_max_pasid_log2: 0,
585            __reserved: [0; 3],
586            out_capabilities: 0,
587        };
588        // SAFETY: the fd is valid and `cmd` is correctly constructed.
589        // `data_uptr`/`data_len` are derived from the live, exclusively
590        // borrowed `out_info`, so the kernel writes at most
591        // `size_of::<IommuHwInfoArmSmmuv3>()` bytes into a valid buffer. Every
592        // field of that `#[repr(C)]` struct is an integer, so any bytes the
593        // kernel writes form a valid value.
594        unsafe {
595            ioctl::iommu_get_hw_info(self.file.as_raw_fd(), &mut cmd)
596                .context("IOMMU_GET_HW_INFO failed")?;
597        }
598        Ok((cmd.out_data_type, cmd.out_capabilities))
599    }
600
601    /// Invalidate IOMMU caches via a nested HWPT or vIOMMU.
602    ///
603    /// `hwpt_id` is a nested HWPT ID or vIOMMU ID. Each entry in `entries` is a
604    /// raw 128-bit invalidation command as a `[qw0, qw1]` quadword pair; the
605    /// kernel parses the opcode and operands per `data_type`.
606    ///
607    /// On full success returns the number of entries handled (always
608    /// `entries.len()`). On failure returns a [`HwptInvalidateError`] carrying
609    /// the kernel's in/out `entry_num` — the count of leading entries it
610    /// reports as handled before the failure — so the caller can locate the
611    /// offending entry. The kernel writes `entry_num` back even on error.
612    ///
613    /// Caveat: `entry_num` is only meaningful when the kernel reached its
614    /// per-entry processing loop. For an early failure (notably `-ENOMEM`
615    /// allocating the kernel-side scratch array) the field is left at the
616    /// input count, so [`HwptInvalidateError::handled`] can equal
617    /// `entries.len()` despite nothing being handled. Callers must treat
618    /// `handled >= entries.len()` on the error path as "position unknown".
619    pub fn hwpt_invalidate(
620        &self,
621        hwpt_id: u32,
622        data_type: u32,
623        entries: &[[u64; 2]],
624    ) -> Result<u32, HwptInvalidateError> {
625        let entry_num = u32::try_from(entries.len()).map_err(|_| HwptInvalidateError {
626            errno: nix::errno::Errno::EINVAL,
627            handled: 0,
628        })?;
629        let mut cmd = IommuHwptInvalidate {
630            size: size_of::<IommuHwptInvalidate>() as u32,
631            hwpt_id,
632            data_uptr: entries.as_ptr() as u64,
633            data_type,
634            entry_len: size_of::<[u64; 2]>() as u32,
635            entry_num,
636            __reserved: 0,
637        };
638        // SAFETY: the fd is valid and `cmd` is correctly constructed.
639        // `data_uptr`/`entry_len`/`entry_num` are derived from the live
640        // `entries` slice, so the kernel reads only within a valid,
641        // fully-initialized array.
642        let res = unsafe { ioctl::iommu_hwpt_invalidate(self.file.as_raw_fd(), &mut cmd) };
643        // The kernel writes `entry_num` (the number of entries it handled) back
644        // even on failure, so read it regardless of the ioctl result.
645        match res {
646            Ok(_) => Ok(cmd.entry_num),
647            Err(errno) => Err(HwptInvalidateError {
648                errno,
649                handled: cmd.entry_num,
650            }),
651        }
652    }
653
654    /// Allocate a virtual IOMMU (vIOMMU).
655    ///
656    /// `viommu_type` should be `IOMMU_VIOMMU_TYPE_ARM_SMMUV3` for SMMUv3.
657    /// `dev_id` is a device bound to the physical IOMMU backing this vIOMMU.
658    /// `hwpt_id` is the nesting parent HWPT to associate with.
659    ///
660    /// Returns [`ViommuAlloc::Incompatible`] when the nesting parent and device
661    /// belong to different physical SMMUs. For this wrapper all generic fields
662    /// are fixed to valid Arm SMMUv3 values and `hwpt_id` is supplied by
663    /// [`IommufdCtx::hwpt_alloc`] with `IOMMU_HWPT_ALLOC_NEST_PARENT`; after
664    /// those core checks, the Arm driver returns `EINVAL` only when the parent
665    /// domain's SMMU differs from the device's SMMU.
666    pub fn viommu_alloc(
667        &self,
668        viommu_type: u32,
669        dev_id: u32,
670        hwpt_id: u32,
671    ) -> anyhow::Result<ViommuAlloc> {
672        let mut cmd = IommuViommuAlloc {
673            size: size_of::<IommuViommuAlloc>() as u32,
674            flags: 0,
675            r#type: viommu_type,
676            dev_id,
677            hwpt_id,
678            out_viommu_id: 0,
679            data_len: 0,
680            __reserved: 0,
681            data_uptr: 0,
682        };
683        // SAFETY: fd is valid, struct correctly constructed.
684        let r = unsafe { ioctl::iommu_viommu_alloc(self.file.as_raw_fd(), &mut cmd) };
685        match r {
686            Ok(_) => Ok(ViommuAlloc::Allocated(cmd.out_viommu_id)),
687            Err(nix::errno::Errno::EINVAL) => Ok(ViommuAlloc::Incompatible),
688            Err(err) => Err(err).context("IOMMU_VIOMMU_ALLOC failed"),
689        }
690    }
691
692    /// Allocate a virtual device (vDevice) on a vIOMMU.
693    ///
694    /// `virt_id` is the virtual stream ID (e.g., guest BDF for SMMUv3).
695    ///
696    /// Returns the kernel-assigned vDevice object ID.
697    pub fn vdevice_alloc(&self, viommu_id: u32, dev_id: u32, virt_id: u64) -> anyhow::Result<u32> {
698        let mut cmd = IommuVdeviceAlloc {
699            size: size_of::<IommuVdeviceAlloc>() as u32,
700            viommu_id,
701            dev_id,
702            out_vdevice_id: 0,
703            virt_id,
704        };
705        // SAFETY: fd is valid, struct correctly constructed.
706        unsafe {
707            ioctl::iommu_vdevice_alloc(self.file.as_raw_fd(), &mut cmd)
708                .context("IOMMU_VDEVICE_ALLOC failed")?;
709        }
710        Ok(cmd.out_vdevice_id)
711    }
712
713    /// Allocate a virtual event queue (vEVENTQ) on a vIOMMU.
714    ///
715    /// `veventq_type` should be `IOMMU_VEVENTQ_TYPE_ARM_SMMUV3` for SMMUv3.
716    /// `depth` is the maximum number of events in the queue.
717    ///
718    /// Returns `(veventq_id, veventq_fd)`. The fd is an eventfd-style file
719    /// descriptor that can be polled for fault events.
720    pub fn veventq_alloc(
721        &self,
722        viommu_id: u32,
723        veventq_type: u32,
724        depth: u32,
725    ) -> anyhow::Result<(u32, fs::File)> {
726        let mut cmd = IommuVeventqAlloc {
727            size: size_of::<IommuVeventqAlloc>() as u32,
728            flags: 0,
729            viommu_id,
730            r#type: veventq_type,
731            veventq_depth: depth,
732            out_veventq_id: 0,
733            out_veventq_fd: 0,
734            __reserved: 0,
735        };
736        // SAFETY: fd is valid, struct correctly constructed.
737        unsafe {
738            ioctl::iommu_veventq_alloc(self.file.as_raw_fd(), &mut cmd)
739                .context("IOMMU_VEVENTQ_ALLOC failed")?;
740        }
741        // SAFETY: kernel returned a valid fd in out_veventq_fd.
742        let veventq_file = unsafe { fs::File::from_raw_fd(cmd.out_veventq_fd as RawFd) };
743        Ok((cmd.out_veventq_id, veventq_file))
744    }
745}
746
747impl AsFd for IommufdCtx {
748    fn as_fd(&self) -> BorrowedFd<'_> {
749        self.file.as_fd()
750    }
751}
752
753impl AsRawFd for IommufdCtx {
754    fn as_raw_fd(&self) -> RawFd {
755        self.file.as_raw_fd()
756    }
757}