Skip to main content

pci_core/
dma.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! DMA target for PCI devices.
5//!
6//! [`DmaTarget`] bundles a device's [`GuestMemory`] (for DMA reads/writes)
7//! and [`MsiTarget`] (for MSI interrupt delivery) into a single type.
8//!
9//! In hardware, both DMA and MSI are bus-mastered transactions identified
10//! by the device's Requester ID (RID). This type ensures the two always
11//! carry a consistent device identity. For SR-IOV devices, calling
12//! [`DmaTarget::with_rid_offset`] derives both DMA and MSI targets for a
13//! specific VF in a single operation — you can't accidentally end up
14//! with mismatched identities.
15
16use crate::bus_range::AssignedBusRange;
17use crate::msi::MsiConnection;
18use crate::msi::MsiTarget;
19use guestmem::GuestMemory;
20use std::any::Any;
21use std::sync::Arc;
22
23/// A trait for IOMMU backends that produce per-device guest memory.
24///
25/// Implemented by SMMU (and future VT-d, AMD-Vi, etc.). The factory is
26/// shared across all devices behind the same IOMMU instance.
27pub trait DmaTargetIommu: Send + Sync + 'static {
28    /// Create a [`GuestMemory`] for a requester-ID offset relative to the
29    /// device's secondary bus.
30    ///
31    /// The RID is resolved as `(secondary << 8) + rid_offset` on each access,
32    /// so it tracks the live bus assignment. A plain `devfn` is just an
33    /// offset in `0..=0xff`; SR-IOV VFs use larger offsets that carry into
34    /// the bus byte.
35    fn guest_memory_for_rid_offset(&self, rid_offset: u16) -> GuestMemory;
36}
37
38/// How a device's DMA relates to host passthrough (VFIO assignment).
39///
40/// This is the only IOMMU-related view exposed to consumers of
41/// [`DmaTarget`]. It deliberately says nothing about how guest memory is
42/// translated (that is the underlying [`DmaTargetIommu`] factory); it only
43/// answers "can this device be handed to the host for passthrough, and if
44/// so, how does the assignment backend attach to the IOMMU?".
45pub enum DmaPassthrough<'a> {
46    /// No IOMMU, or none relevant to passthrough — host assignment allowed.
47    Allowed,
48    /// Behind a software/emulated IOMMU that cannot program the host IOMMU
49    /// for passthrough DMA — host assignment rejected.
50    SoftwareBlocked,
51    /// Behind a hardware-nestable IOMMU — host assignment allowed. The
52    /// opaque handle lets the assignment backend (which knows the concrete
53    /// IOMMU type) downcast and attach to the emulated IOMMU for nested
54    /// stage-1 translation.
55    HardwareNestable(&'a (dyn Any + Send + Sync)),
56}
57
58/// Private representation of a [`DmaTarget`]'s passthrough disposition.
59#[derive(Clone)]
60enum Passthrough {
61    /// Host assignment allowed (no IOMMU, or none relevant).
62    Allowed,
63    /// Behind a software/emulated IOMMU — host assignment rejected.
64    SoftwareBlocked,
65    /// Behind a hardware-nestable IOMMU — the opaque handle is downcast by
66    /// the assignment backend to its arch-specific nesting context.
67    HardwareNestable(Arc<dyn Any + Send + Sync>),
68}
69
70/// Everything a PCI device needs for bus-mastered transactions: DMA
71/// memory access and MSI interrupt delivery.
72///
73/// Most devices only need [`guest_memory`](Self::guest_memory) and
74/// [`msi_target`](Self::msi_target). SR-IOV PFs additionally call
75/// [`with_rid_offset`](Self::with_rid_offset) when creating VFs.
76#[derive(Clone)]
77pub struct DmaTarget {
78    /// This target's requester-ID offset from the secondary bus. Held so
79    /// [`with_rid_offset`](Self::with_rid_offset) can derive a target at a
80    /// further offset relative to this one.
81    rid_offset: u16,
82    guest_memory: GuestMemory,
83    msi_target: MsiTarget,
84    /// When an IOMMU is present, produces per-device GuestMemory
85    /// instances with distinct stream/context table entries.
86    iommu: Option<Arc<dyn DmaTargetIommu>>,
87    /// The device's host-passthrough disposition (see [`DmaPassthrough`]).
88    passthrough: Passthrough,
89}
90
91impl DmaTarget {
92    /// Creates a DMA target with no IOMMU.
93    ///
94    /// `bus_range` and `devfn` set the device's requester-ID identity, shared
95    /// by both the DMA and MSI sides. The MSI backend is taken (late-bound)
96    /// from `msi`. Since there is no IOMMU, all targets derived from this one
97    /// share the same guest memory; [`with_rid_offset`](Self::with_rid_offset)
98    /// only updates the MSI identity.
99    pub fn new(
100        bus_range: AssignedBusRange,
101        devfn: u8,
102        guest_memory: GuestMemory,
103        msi: &MsiConnection,
104    ) -> Self {
105        let msi_target = msi.msi_target(bus_range, devfn);
106        Self {
107            rid_offset: devfn as u16,
108            guest_memory,
109            msi_target,
110            iommu: None,
111            passthrough: Passthrough::Allowed,
112        }
113    }
114
115    /// Creates a DMA target backed by a software/emulated IOMMU.
116    ///
117    /// The base (function-`devfn`) translating guest memory is derived from
118    /// `iommu`; per-VF memory is produced by
119    /// [`with_rid_offset`](Self::with_rid_offset). The MSI backend is taken
120    /// (late-bound) from `msi`.
121    ///
122    /// The device is marked [`DmaPassthrough::SoftwareBlocked`]: a
123    /// software/emulated IOMMU cannot program the host IOMMU, so host
124    /// passthrough (VFIO assignment) is rejected. Use
125    /// [`with_nestable_iommu`](Self::with_nestable_iommu) for a
126    /// hardware-nestable IOMMU that permits passthrough.
127    pub fn with_iommu(
128        bus_range: AssignedBusRange,
129        devfn: u8,
130        iommu: Arc<dyn DmaTargetIommu>,
131        msi: &MsiConnection,
132    ) -> Self {
133        Self::iommu_backed(bus_range, devfn, iommu, msi, Passthrough::SoftwareBlocked)
134    }
135
136    /// Creates a DMA target backed by a hardware-nestable IOMMU.
137    ///
138    /// Like [`with_iommu`](Self::with_iommu), but marks the device
139    /// [`DmaPassthrough::HardwareNestable`] with an opaque `handle` that the
140    /// host-assignment backend downcasts to its arch-specific nesting context.
141    /// Accel-capable IOMMUs (e.g. an SMMU that programs the host IOMMU for
142    /// nested stage-1 translation) use this to permit VFIO passthrough despite
143    /// wrapping guest memory with a translating target; the `handle` carries
144    /// everything the backend needs to wire the device into the emulated IOMMU.
145    ///
146    /// Supplying the nesting `handle` together with the `iommu` is what makes a
147    /// [`DmaPassthrough::HardwareNestable`] target impossible to construct
148    /// without a backing IOMMU.
149    pub fn with_nestable_iommu(
150        bus_range: AssignedBusRange,
151        devfn: u8,
152        iommu: Arc<dyn DmaTargetIommu>,
153        handle: Arc<dyn Any + Send + Sync>,
154        msi: &MsiConnection,
155    ) -> Self {
156        Self::iommu_backed(
157            bus_range,
158            devfn,
159            iommu,
160            msi,
161            Passthrough::HardwareNestable(handle),
162        )
163    }
164
165    /// Shared constructor for the IOMMU-backed cases: derives the base
166    /// translating memory and MSI identity, then stamps the passthrough
167    /// disposition.
168    fn iommu_backed(
169        bus_range: AssignedBusRange,
170        devfn: u8,
171        iommu: Arc<dyn DmaTargetIommu>,
172        msi: &MsiConnection,
173        passthrough: Passthrough,
174    ) -> Self {
175        let guest_memory = iommu.guest_memory_for_rid_offset(devfn as u16);
176        let msi_target = msi.msi_target(bus_range, devfn);
177        Self {
178            rid_offset: devfn as u16,
179            guest_memory,
180            msi_target,
181            iommu: Some(iommu),
182            passthrough,
183        }
184    }
185
186    /// Returns the guest memory for DMA from this device.
187    pub fn guest_memory(&self) -> &GuestMemory {
188        &self.guest_memory
189    }
190
191    /// Returns the MSI target for interrupt delivery from this device.
192    pub fn msi_target(&self) -> &MsiTarget {
193        &self.msi_target
194    }
195
196    /// The device's host-passthrough disposition.
197    ///
198    /// Used by host-assignment backends (e.g. the VFIO resolver) to decide
199    /// whether the device may be passed through and, when it is behind a
200    /// hardware-nestable IOMMU, to obtain the opaque handle they downcast to
201    /// attach to the emulated IOMMU.
202    pub fn passthrough(&self) -> DmaPassthrough<'_> {
203        match &self.passthrough {
204            Passthrough::Allowed => DmaPassthrough::Allowed,
205            Passthrough::SoftwareBlocked => DmaPassthrough::SoftwareBlocked,
206            Passthrough::HardwareNestable(handle) => {
207                DmaPassthrough::HardwareNestable(handle.as_ref())
208            }
209        }
210    }
211
212    /// Derives a DMA target offset by `delta` from this one in RID space.
213    ///
214    /// This is the SR-IOV VF derivation primitive: given a PF's target, its
215    /// `i`th VF is `delta = VF_Offset + i * VF_Stride` away. Offsets stack,
216    /// so deriving from an already-derived target accumulates. Both the DMA
217    /// and MSI identity are derived in lockstep and resolved at use time as
218    /// `(secondary << 8) + offset` against the live bus assignment, so VF
219    /// targets can be derived before the bus is programmed.
220    pub fn with_rid_offset(&self, delta: u16) -> DmaTarget {
221        let rid_offset = self.rid_offset.wrapping_add(delta);
222        DmaTarget {
223            rid_offset,
224            guest_memory: match &self.iommu {
225                Some(factory) => factory.guest_memory_for_rid_offset(rid_offset),
226                None => self.guest_memory.clone(),
227            },
228            msi_target: self.msi_target.with_rid_offset(rid_offset),
229            iommu: self.iommu.clone(),
230            passthrough: self.passthrough.clone(),
231        }
232    }
233}
234
235#[cfg(test)]
236mod tests {
237    use super::*;
238    use crate::bus_range::AssignedBusRange;
239    use crate::msi::MsiConnection;
240    use crate::msi::SignalMsi;
241    use parking_lot::Mutex;
242    use std::sync::Arc;
243
244    /// Records the requester IDs signaled through an `MsiTarget`, so tests
245    /// can observe the MSI identity derived by `with_rid_offset`.
246    struct RecordingSignalMsi {
247        calls: Mutex<Vec<Option<u32>>>,
248    }
249
250    impl RecordingSignalMsi {
251        fn new() -> Arc<Self> {
252            Arc::new(Self {
253                calls: Mutex::new(Vec::new()),
254            })
255        }
256
257        fn pop(&self) -> Option<u32> {
258            self.calls.lock().pop().flatten()
259        }
260    }
261
262    impl SignalMsi for RecordingSignalMsi {
263        fn signal_msi(&self, devid: Option<u32>, _address: u64, _data: u32) {
264            self.calls.lock().push(devid);
265        }
266    }
267
268    /// Records the `rid_offset` passed to the IOMMU factory and hands back a
269    /// distinct `GuestMemory` for each call so tests can confirm the derived
270    /// target uses the IOMMU-provided memory.
271    struct RecordingIommu {
272        rid_offset_calls: Mutex<Vec<u16>>,
273    }
274
275    impl RecordingIommu {
276        fn new() -> Arc<Self> {
277            Arc::new(Self {
278                rid_offset_calls: Mutex::new(Vec::new()),
279            })
280        }
281    }
282
283    impl DmaTargetIommu for RecordingIommu {
284        fn guest_memory_for_rid_offset(&self, rid_offset: u16) -> GuestMemory {
285            self.rid_offset_calls.lock().push(rid_offset);
286            // A distinct, non-empty allocation marks this as IOMMU-provided.
287            GuestMemory::allocate(0x2000)
288        }
289    }
290
291    #[test]
292    fn new_has_no_iommu() {
293        let msi_conn = MsiConnection::new();
294        let target = DmaTarget::new(AssignedBusRange::new(), 0, GuestMemory::empty(), &msi_conn);
295        assert!(matches!(target.passthrough(), DmaPassthrough::Allowed));
296        assert!(target.iommu.is_none());
297    }
298
299    #[test]
300    fn with_rid_offset_no_iommu_shares_memory_and_updates_msi() {
301        let bus_range = AssignedBusRange::new();
302        bus_range.set_bus_range(5, 10);
303        let msi_conn = MsiConnection::new();
304        let recorder = RecordingSignalMsi::new();
305        msi_conn.connect(recorder.clone());
306
307        let gm = GuestMemory::allocate(0x1000);
308        let target = DmaTarget::new(bus_range.clone(), 0, gm.clone(), &msi_conn);
309
310        let derived = target.with_rid_offset(0x18); // function 0x18 on the secondary bus
311
312        // No IOMMU: the guest memory is shared. Write through the original
313        // and observe it through the derived target.
314        target.guest_memory().write_at(0, &[0xAB]).unwrap();
315        let mut buf = [0u8];
316        derived.guest_memory().read_at(0, &mut buf).unwrap();
317        assert_eq!(buf[0], 0xAB);
318
319        // The MSI identity is derived from the offset: bus 5 (secondary) | offset.
320        assert!(matches!(derived.passthrough(), DmaPassthrough::Allowed));
321        derived.msi_target().signal_msi(0xFEE0_0000, 0);
322        assert_eq!(recorder.pop().unwrap(), (5 << 8) | 0x18);
323    }
324
325    #[test]
326    fn with_rid_offset_stacks() {
327        let bus_range = AssignedBusRange::new();
328        bus_range.set_bus_range(5, 10);
329        let msi_conn = MsiConnection::new();
330        let recorder = RecordingSignalMsi::new();
331        msi_conn.connect(recorder.clone());
332
333        let target = DmaTarget::new(bus_range.clone(), 0, GuestMemory::empty(), &msi_conn);
334
335        // Offsets accumulate: 0x10 then 0x08 lands at 0x18.
336        let derived = target.with_rid_offset(0x10).with_rid_offset(0x08);
337        derived.msi_target().signal_msi(0xFEE0_0000, 0);
338        assert_eq!(recorder.pop().unwrap(), (5 << 8) | 0x18);
339    }
340
341    #[test]
342    fn with_rid_offset_iommu_derives_memory_and_msi_together() {
343        let bus_range = AssignedBusRange::new();
344        bus_range.set_bus_range(5, 10);
345        let msi_conn = MsiConnection::new();
346        let recorder = RecordingSignalMsi::new();
347        msi_conn.connect(recorder.clone());
348
349        let iommu = RecordingIommu::new();
350        let target = DmaTarget::with_iommu(bus_range.clone(), 0, iommu.clone(), &msi_conn);
351        assert!(matches!(
352            target.passthrough(),
353            DmaPassthrough::SoftwareBlocked
354        ));
355
356        let derived = target.with_rid_offset(0x18);
357
358        // The base target asked the factory for offset 0 (devfn 0); deriving
359        // offset 0x18 asks for that offset.
360        assert_eq!(*iommu.rid_offset_calls.lock(), vec![0, 0x18]);
361        // The derived target uses the IOMMU-provided 0x2000 allocation: an
362        // access past the empty base memory succeeds.
363        derived.guest_memory().write_at(0x1500, &[0xCD]).unwrap();
364        let mut buf = [0u8];
365        derived.guest_memory().read_at(0x1500, &mut buf).unwrap();
366        assert_eq!(buf[0], 0xCD);
367        assert!(matches!(
368            derived.passthrough(),
369            DmaPassthrough::SoftwareBlocked
370        ));
371
372        derived.msi_target().signal_msi(0xFEE0_0000, 0);
373        assert_eq!(recorder.pop().unwrap(), (5 << 8) | 0x18);
374    }
375
376    #[test]
377    fn with_rid_offset_iommu_cross_bus() {
378        let bus_range = AssignedBusRange::new();
379        bus_range.set_bus_range(5, 10);
380        let msi_conn = MsiConnection::new();
381        let recorder = RecordingSignalMsi::new();
382        msi_conn.connect(recorder.clone());
383
384        let iommu = RecordingIommu::new();
385        let target = DmaTarget::with_iommu(bus_range.clone(), 0, iommu.clone(), &msi_conn);
386
387        // Offset (2 << 8) | 0x0A lands on bus 7 (secondary 5 + 2), devfn 0x0A.
388        let offset: u16 = (2 << 8) | 0x0A;
389        let derived = target.with_rid_offset(offset);
390
391        // The base construction asked for offset 0 first, then the VF offset.
392        assert_eq!(*iommu.rid_offset_calls.lock(), vec![0, offset]);
393        // The derived target uses the IOMMU-provided 0x2000 allocation: an
394        // access past the empty base memory succeeds.
395        derived.guest_memory().write_at(0x1500, &[0xCD]).unwrap();
396        let mut buf = [0u8];
397        derived.guest_memory().read_at(0x1500, &mut buf).unwrap();
398        assert_eq!(buf[0], 0xCD);
399        assert!(matches!(
400            derived.passthrough(),
401            DmaPassthrough::SoftwareBlocked
402        ));
403
404        derived.msi_target().signal_msi(0xFEE0_0000, 0);
405        assert_eq!(recorder.pop().unwrap(), (7 << 8) | 0x0A);
406    }
407
408    /// An opaque nesting context behind the `HardwareNestable` handle, of the
409    /// kind an IOMMU backend crate would define.
410    #[derive(Debug, PartialEq)]
411    struct FakeNestingContext {
412        id: u32,
413    }
414
415    #[test]
416    fn with_nestable_iommu_exposes_downcastable_handle() {
417        let bus_range = AssignedBusRange::new();
418        bus_range.set_bus_range(5, 10);
419        let msi_conn = MsiConnection::new();
420
421        let iommu = RecordingIommu::new();
422        let target = DmaTarget::with_nestable_iommu(
423            bus_range.clone(),
424            0,
425            iommu,
426            Arc::new(FakeNestingContext { id: 0x1234 }),
427            &msi_conn,
428        );
429
430        // The base target accepts passthrough and hands back the context.
431        match target.passthrough() {
432            DmaPassthrough::HardwareNestable(handle) => {
433                let ctx = handle.downcast_ref::<FakeNestingContext>().unwrap();
434                assert_eq!(ctx, &FakeNestingContext { id: 0x1234 });
435            }
436            _ => panic!("expected HardwareNestable"),
437        }
438
439        // The nesting disposition (and handle) survives VF derivation.
440        let derived = target.with_rid_offset(0x18);
441        match derived.passthrough() {
442            DmaPassthrough::HardwareNestable(handle) => {
443                assert_eq!(
444                    handle.downcast_ref::<FakeNestingContext>().unwrap().id,
445                    0x1234
446                );
447            }
448            _ => panic!("expected HardwareNestable on derived target"),
449        }
450    }
451}