22//
33// SPDX-License-Identifier: Apache-2.0
44
5+ //! GPU sanitization before QEMU attach.
6+ //!
7+ //! Terminating QEMU while GSP/SPDM initialization is in progress can leave
8+ //! stale FSP/SPDM session state in a GPU. VFIO's attach-time FLR does not
9+ //! clear that state; a PCIe Secondary Bus Reset does. The next guest
10+ //! otherwise fails with an SPDM timeout followed by GSP/RmInitAdapter errors.
11+ //!
12+ //! The VMM runs as an unprivileged user, so it cannot issue the SBR by
13+ //! writing Bridge Control in the upstream bridge's sysfs config space: that
14+ //! file is writable by root only, as is /sys/bus/pci/drivers_probe. Instead
15+ //! the VFIO_DEVICE_PCI_HOT_RESET ioctl asks the kernel to perform the same
16+ //! bus reset. The ioctl is authorized by device ownership rather than
17+ //! privilege: the caller must present an fd for every VFIO group affected by
18+ //! the reset. The group nodes under /dev/vfio are the same ones QEMU opens
19+ //! to attach the GPU, so the VMM user already has access to them.
20+ //!
21+ //! A single group fd suffices only because every sanitized GPU sits alone
22+ //! behind a dedicated PCIe bridge and alone in its IOMMU group. The bridge
23+ //! topology is validated first, and the kernel-reported set of devices
24+ //! affected by the reset must all belong to the GPU's own group; anything
25+ //! else aborts the launch rather than risking disruption to other devices.
26+ //!
27+ //! A VFIO group can be opened by only one process at a time, so every fd is
28+ //! closed again before QEMU is spawned.
29+
530use std:: {
631 collections:: BTreeSet ,
7- fs:: { File , OpenOptions } ,
32+ fs:: File ,
833 os:: unix:: fs:: FileExt ,
934 path:: { Path , PathBuf } ,
35+ sync:: Arc ,
1036 thread,
1137 time:: { Duration , Instant } ,
1238} ;
1339
1440use anyhow:: { bail, Context , Result } ;
1541use tracing:: info;
42+ use vfio_ioctls:: { PciHotResetDevice , VfioContainer , VfioDevice } ;
1643
1744use crate :: { app:: GpuConfig , config:: GpuConfig as HostGpuConfig } ;
1845
1946const PCI_SYSFS_DEVICES : & str = "/sys/bus/pci/devices" ;
2047const PCI_BRIDGE_CLASS : u32 = 0x0604 ;
21- const PCI_BRIDGE_CONTROL : u64 = 0x3e ;
22- const PCI_BRIDGE_CTL_BUS_RESET : u16 = 1 << 6 ;
23- const SBR_ASSERT_TIME : Duration = Duration :: from_millis ( 100 ) ;
2448const SBR_POLL_INTERVAL : Duration = Duration :: from_millis ( 100 ) ;
2549const SBR_STABLE_TIME : Duration = Duration :: from_millis ( 500 ) ;
2650
@@ -33,63 +57,123 @@ pub fn sanitize_on_attach(host: &HostGpuConfig, devices: &GpuConfig) -> Result<(
3357 if !host. enabled || !host. sanitize_on_attach || devices. gpus . is_empty ( ) {
3458 return Ok ( ( ) ) ;
3559 }
36- sanitize_at (
37- Path :: new ( PCI_SYSFS_DEVICES ) ,
38- devices,
39- Duration :: from_millis ( host. sbr_timeout_ms ) ,
40- )
60+ let slots = devices
61+ . gpus
62+ . iter ( )
63+ . map ( |gpu| gpu. slot . clone ( ) )
64+ . collect :: < Vec < _ > > ( ) ;
65+ sanitize_slots ( & slots, Duration :: from_millis ( host. sbr_timeout_ms ) )
4166}
4267
43- fn sanitize_at ( sysfs_devices : & Path , devices : & GpuConfig , timeout : Duration ) -> Result < ( ) > {
44- let selected = devices
45- . gpus
68+ /// Sanitizes the given GPU slots. Entry point for the `sanitize-gpu`
69+ /// subcommand; `sanitize_on_attach` funnels here as well.
70+ pub fn sanitize_slots ( slots : & [ String ] , timeout : Duration ) -> Result < ( ) > {
71+ if slots. is_empty ( ) {
72+ bail ! ( "no GPU slots specified" ) ;
73+ }
74+ let selected = slots
4675 . iter ( )
47- . map ( |gpu | normalize_slot ( & gpu . slot ) )
76+ . map ( |slot | normalize_slot ( slot) )
4877 . collect :: < BTreeSet < _ > > ( ) ;
49- let mut bridges = BTreeSet :: new ( ) ;
78+ sanitize_at ( Path :: new ( PCI_SYSFS_DEVICES ) , & selected, timeout)
79+ }
5080
51- for gpu in & selected {
81+ fn sanitize_at ( sysfs_devices : & Path , selected : & BTreeSet < String > , timeout : Duration ) -> Result < ( ) > {
82+ for gpu in selected {
5283 let bridge = upstream_bridge ( sysfs_devices, gpu) ?;
5384 ensure_dedicated_bridge ( & bridge, gpu) ?;
54- bridges. insert ( bridge) ;
5585 }
5686
57- for bridge in bridges {
58- info ! ( bridge = %bridge. display( ) , "sanitizing GPU with PCIe Secondary Bus Reset" ) ;
59- secondary_bus_reset ( & bridge)
60- . with_context ( || format ! ( "failed to sanitize GPU using bridge {}" , bridge. display( ) ) ) ?;
87+ for gpu in selected {
88+ info ! ( gpu = %gpu, "sanitizing GPU with VFIO PCI hot reset" ) ;
89+ hot_reset ( sysfs_devices, gpu)
90+ . with_context ( || format ! ( "failed to sanitize GPU {gpu} with VFIO hot reset" ) ) ?;
91+ }
92+ wait_for_vfio_ready ( sysfs_devices, selected, timeout) ?;
93+ Ok ( ( ) )
94+ }
95+
96+ /// Issues a Secondary Bus Reset on the GPU's upstream bridge through VFIO.
97+ ///
98+ /// All fds are dropped on return so that QEMU can open the group afterwards.
99+ fn hot_reset ( sysfs_devices : & Path , slot : & str ) -> Result < ( ) > {
100+ let group_id = iommu_group_id ( sysfs_devices, slot) ?;
101+ let container = Arc :: new ( VfioContainer :: new ( None ) . context ( "failed to open VFIO container" ) ?) ;
102+ let device = VfioDevice :: new ( & sysfs_devices. join ( slot) , container)
103+ . with_context ( || format ! ( "failed to open VFIO device {slot}" ) ) ?;
104+
105+ let dependents = device
106+ . pci_hot_reset_info ( )
107+ . context ( "failed to query PCI hot reset scope" ) ?;
108+ let foreign = dependents
109+ . iter ( )
110+ . filter ( |dep| dep. group_id != group_id)
111+ . map ( format_dependent)
112+ . collect :: < Vec < _ > > ( ) ;
113+ if !foreign. is_empty ( ) {
114+ bail ! (
115+ "refusing hot reset of GPU {slot}: it would also reset devices outside \
116+ IOMMU group {group_id}: {}",
117+ foreign. join( ", " )
118+ ) ;
61119 }
62- wait_for_vfio_ready ( sysfs_devices, & selected, timeout) ?;
120+ info ! (
121+ gpu = %slot,
122+ affected = %dependents. iter( ) . map( format_dependent) . collect:: <Vec <_>>( ) . join( ", " ) ,
123+ "issuing VFIO PCI hot reset"
124+ ) ;
125+
126+ device
127+ . pci_hot_reset_own_group ( )
128+ . context ( "failed to perform PCI hot reset" ) ?;
63129 Ok ( ( ) )
64130}
65131
132+ fn format_dependent ( dep : & PciHotResetDevice ) -> String {
133+ format ! (
134+ "{:04x}:{:02x}:{:02x}.{:x} (group {})" ,
135+ dep. segment,
136+ dep. bus,
137+ dep. devfn >> 3 ,
138+ dep. devfn & 0x7 ,
139+ dep. group_id
140+ )
141+ }
142+
143+ fn iommu_group_id ( sysfs_devices : & Path , slot : & str ) -> Result < u32 > {
144+ let link = sysfs_devices. join ( slot) . join ( "iommu_group" ) ;
145+ let group = link
146+ . canonicalize ( )
147+ . with_context ( || format ! ( "failed to resolve IOMMU group of {slot}" ) ) ?;
148+ group
149+ . file_name ( )
150+ . and_then ( |name| name. to_str ( ) )
151+ . and_then ( |name| name. parse ( ) . ok ( ) )
152+ . with_context ( || format ! ( "invalid IOMMU group path {}" , group. display( ) ) )
153+ }
154+
66155fn wait_for_vfio_ready (
67156 sysfs_devices : & Path ,
68157 devices : & BTreeSet < String > ,
69158 timeout : Duration ,
70159) -> Result < ( ) > {
71- let drivers_probe = sysfs_devices
72- . parent ( )
73- . context ( "PCI sysfs devices directory has no parent" ) ?
74- . join ( "drivers_probe" ) ;
75- let deadline = Instant :: now ( ) + timeout;
160+ let started = Instant :: now ( ) ;
161+ let deadline = started + timeout;
76162 let mut stable_since = None ;
77163 loop {
78164 let not_ready = devices
79165 . iter ( )
80- . filter ( |slot| {
81- let device = sysfs_devices. join ( slot) ;
82- if !driver_is_vfio ( & device) {
83- let _ = fs_err:: write ( & drivers_probe, format ! ( "{slot}\n " ) ) ;
84- }
85- !is_vfio_ready ( & device)
86- } )
166+ . filter ( |slot| !is_vfio_ready ( & sysfs_devices. join ( slot) ) )
87167 . cloned ( )
88168 . collect :: < Vec < _ > > ( ) ;
89169 if not_ready. is_empty ( ) {
90170 let since = * stable_since. get_or_insert_with ( Instant :: now) ;
91171 if since. elapsed ( ) >= SBR_STABLE_TIME {
92- info ! ( count = devices. len( ) , "all sanitized GPUs are VFIO-ready" ) ;
172+ info ! (
173+ count = devices. len( ) ,
174+ elapsed_ms = started. elapsed( ) . as_millis( ) as u64 ,
175+ "all sanitized GPUs are VFIO-ready"
176+ ) ;
93177 return Ok ( ( ) ) ;
94178 }
95179 } else {
@@ -194,39 +278,12 @@ fn is_pci_slot(name: &str) -> bool {
194278 . all ( |( i, c) | matches ! ( i, 4 | 7 | 10 ) || c. is_ascii_hexdigit ( ) )
195279}
196280
197- fn secondary_bus_reset ( bridge : & Path ) -> Result < ( ) > {
198- let config_path = bridge. join ( "config" ) ;
199- let config = OpenOptions :: new ( )
200- . read ( true )
201- . write ( true )
202- . open ( & config_path)
203- . with_context ( || format ! ( "failed to open {}" , config_path. display( ) ) ) ?;
204- let original = read_u16 ( & config, PCI_BRIDGE_CONTROL ) ?;
205- write_u16 (
206- & config,
207- PCI_BRIDGE_CONTROL ,
208- original | PCI_BRIDGE_CTL_BUS_RESET ,
209- ) ?;
210- thread:: sleep ( SBR_ASSERT_TIME ) ;
211- write_u16 (
212- & config,
213- PCI_BRIDGE_CONTROL ,
214- original & !PCI_BRIDGE_CTL_BUS_RESET ,
215- ) ?;
216- Ok ( ( ) )
217- }
218-
219281fn read_u16 ( file : & File , offset : u64 ) -> Result < u16 > {
220282 let mut value = [ 0_u8 ; 2 ] ;
221283 file. read_exact_at ( & mut value, offset) ?;
222284 Ok ( u16:: from_le_bytes ( value) )
223285}
224286
225- fn write_u16 ( file : & File , offset : u64 , value : u16 ) -> Result < ( ) > {
226- file. write_all_at ( & value. to_le_bytes ( ) , offset) ?;
227- Ok ( ( ) )
228- }
229-
230287#[ cfg( test) ]
231288mod tests {
232289 use super :: * ;
@@ -247,6 +304,17 @@ mod tests {
247304 assert ! ( !is_pci_slot( "0000:0g:00.0" ) ) ;
248305 }
249306
307+ #[ test]
308+ fn formats_dependent_devices_with_pci_slot_and_function ( ) {
309+ let dep = PciHotResetDevice {
310+ group_id : 46 ,
311+ segment : 0 ,
312+ bus : 0x0f ,
313+ devfn : 0x1 ,
314+ } ;
315+ assert_eq ! ( format_dependent( & dep) , "0000:0f:00.1 (group 46)" ) ;
316+ }
317+
250318 #[ test]
251319 fn skips_sanitization_when_gpu_passthrough_is_disabled ( ) {
252320 let host = HostGpuConfig {
0 commit comments