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}