Skip to content

Commit a112488

Browse files
committed
fix(vfio): centralize vfio ID refcounting
1 parent cc22198 commit a112488

1 file changed

Lines changed: 129 additions & 48 deletions

File tree

crates/openshell-vfio/src/bind.rs

Lines changed: 129 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,9 @@ use std::time::Duration;
1010
pub(crate) const VFIO_BIND_POLL_INTERVAL: Duration = Duration::from_millis(100);
1111
pub(crate) const VFIO_BIND_MAX_POLL_ATTEMPTS: u32 = 20;
1212

13-
/// Reference counter for vendor:device ID registrations in the vfio-pci
14-
/// match table. Multiple devices may share the same vendor:device pair. We
15-
/// only write to the kernel's `new_id`/`remove_id` sysfs files when the first
16-
/// device registers or the last device deregisters an ID.
17-
static VFIO_ID_REFCOUNTS: LazyLock<Mutex<HashMap<String, usize>>> =
18-
LazyLock::new(|| Mutex::new(HashMap::new()));
13+
// Process-local registry shared across bind guards to coordinate vfio-pci ID writes.
14+
static VFIO_ID_REGISTRY: LazyLock<Mutex<VfioIdRegistry>> =
15+
LazyLock::new(|| Mutex::new(VfioIdRegistry::default()));
1916

2017
pub(crate) fn current_driver_name(sysfs: &SysfsRoot, bdf: &str) -> Option<String> {
2118
sysfs.pci_device_ref(bdf).driver_name()
@@ -37,14 +34,9 @@ pub(crate) fn register_vfio_new_id(sysfs: &SysfsRoot, bdf: &str) {
3734
return;
3835
};
3936

40-
let should_write = {
41-
let mut map = VFIO_ID_REFCOUNTS.lock().unwrap();
42-
let count = map.entry(id_str.clone()).or_insert(0);
43-
*count += 1;
44-
*count == 1
45-
};
37+
let registration = VFIO_ID_REGISTRY.lock().unwrap().register(&id_str);
4638

47-
if !should_write {
39+
if registration != VfioIdRegistration::FirstUser {
4840
tracing::debug!(
4941
bdf, id = %id_str,
5042
"vfio-pci new_id already registered by another device, refcount incremented"
@@ -80,37 +72,29 @@ pub(crate) fn deregister_vfio_new_id(sysfs: &SysfsRoot, bdf: &str) {
8072
return;
8173
};
8274

83-
let should_write = {
84-
let mut map = VFIO_ID_REFCOUNTS.lock().unwrap();
85-
match map.get_mut(&id_str) {
86-
Some(count) if *count > 1 => {
87-
*count -= 1;
88-
false
89-
}
90-
Some(_) => {
91-
map.remove(&id_str);
92-
true
93-
}
94-
None => true,
95-
}
96-
};
75+
deregister_vfio_id(sysfs, &id_str, Some(bdf));
76+
}
77+
78+
fn deregister_vfio_id(sysfs: &SysfsRoot, id_str: &str, bdf: Option<&str>) {
79+
let deregistration = VFIO_ID_REGISTRY.lock().unwrap().deregister(id_str);
9780

98-
if !should_write {
81+
if deregistration == VfioIdDeregistration::StillInUse {
9982
tracing::debug!(
100-
bdf, id = %id_str,
83+
bdf = ?bdf, id = %id_str,
10184
"vfio-pci remove_id skipped (other devices still using this ID)"
10285
);
10386
return;
10487
}
10588

89+
// LastUser and NotTracked both require a best-effort remove_id write.
10690
let remove_id_path = sysfs.vfio_pci_remove_id();
107-
match write_sysfs(&remove_id_path, &id_str) {
91+
match write_sysfs(&remove_id_path, id_str) {
10892
Ok(()) => {
109-
tracing::debug!(bdf, id = %id_str, "deregistered vfio-pci new_id");
93+
tracing::debug!(bdf = ?bdf, id = %id_str, "deregistered vfio-pci new_id");
11094
}
11195
Err(_) => {
11296
tracing::debug!(
113-
bdf, id = %id_str,
97+
bdf = ?bdf, id = %id_str,
11498
"vfio-pci remove_id write skipped (not registered or already removed)"
11599
);
116100
}
@@ -123,23 +107,12 @@ pub(crate) fn deregister_vfio_new_id(sysfs: &SysfsRoot, bdf: &str) {
123107
/// sysfs at call time, making it reliable even when the device has been
124108
/// physically removed or sysfs is otherwise inaccessible.
125109
pub(crate) fn deregister_vfio_id_by_value(sysfs: &SysfsRoot, id_str: &str) {
126-
let remove_id_path = sysfs.vfio_pci_remove_id();
127-
match write_sysfs(&remove_id_path, id_str) {
128-
Ok(()) => {
129-
tracing::debug!(id = %id_str, "deregistered vfio-pci new_id (by cached value)");
130-
}
131-
Err(_) => {
132-
tracing::debug!(
133-
id = %id_str,
134-
"vfio-pci remove_id write skipped (not registered or already removed)"
135-
);
136-
}
137-
}
110+
deregister_vfio_id(sysfs, id_str, None);
138111
}
139112

140113
pub(crate) fn clear_vfio_id_refcounts() {
141-
if let Ok(mut map) = VFIO_ID_REFCOUNTS.lock() {
142-
map.clear();
114+
if let Ok(mut registry) = VFIO_ID_REGISTRY.lock() {
115+
registry.clear();
143116
}
144117
}
145118

@@ -280,9 +253,72 @@ pub(crate) fn restore_to_host_driver_ex(
280253
Ok(())
281254
}
282255

256+
/// Process-local reference counter for vendor:device ID registrations in the
257+
/// vfio-pci match table. Multiple devices may share the same vendor:device
258+
/// pair. We only write to the kernel's `new_id`/`remove_id` sysfs files when
259+
/// the first device registers or the last device deregisters an ID.
260+
#[derive(Debug, Default)]
261+
struct VfioIdRegistry {
262+
refcounts: HashMap<String, usize>,
263+
}
264+
265+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
266+
enum VfioIdRegistration {
267+
FirstUser,
268+
AlreadyTracked,
269+
}
270+
271+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
272+
enum VfioIdDeregistration {
273+
LastUser,
274+
StillInUse,
275+
NotTracked,
276+
}
277+
278+
impl VfioIdRegistry {
279+
/// Record one active user of `id`.
280+
///
281+
/// Returns the resulting process-local registration state.
282+
fn register(&mut self, id: &str) -> VfioIdRegistration {
283+
let count = self.refcounts.entry(id.to_string()).or_insert(0);
284+
*count += 1;
285+
if *count == 1 {
286+
VfioIdRegistration::FirstUser
287+
} else {
288+
VfioIdRegistration::AlreadyTracked
289+
}
290+
}
291+
292+
/// Record one fewer active user of `id`.
293+
///
294+
/// Returns the resulting process-local deregistration state.
295+
fn deregister(&mut self, id: &str) -> VfioIdDeregistration {
296+
match self.refcounts.get_mut(id) {
297+
Some(count) if *count > 1 => {
298+
*count -= 1;
299+
VfioIdDeregistration::StillInUse
300+
}
301+
Some(_) => {
302+
self.refcounts.remove(id);
303+
VfioIdDeregistration::LastUser
304+
}
305+
None => VfioIdDeregistration::NotTracked,
306+
}
307+
}
308+
309+
fn clear(&mut self) {
310+
self.refcounts.clear();
311+
}
312+
313+
#[cfg(test)]
314+
fn remove(&mut self, id: &str) {
315+
self.refcounts.remove(id);
316+
}
317+
}
318+
283319
#[cfg(test)]
284320
pub(crate) mod test_refcounts {
285-
use super::VFIO_ID_REFCOUNTS;
321+
use super::VFIO_ID_REGISTRY;
286322
use std::sync::{Mutex, MutexGuard, PoisonError};
287323

288324
static VFIO_ID_REFCOUNT_TEST_LOCK: Mutex<()> = Mutex::new(());
@@ -297,7 +333,7 @@ pub(crate) mod test_refcounts {
297333
/// Used by tests to clean up their own entries without disturbing
298334
/// parallel tests that hold refcounts for different device IDs.
299335
pub fn clear(id: &str) {
300-
VFIO_ID_REFCOUNTS.lock().unwrap().remove(id);
336+
VFIO_ID_REGISTRY.lock().unwrap().remove(id);
301337
}
302338
}
303339

@@ -447,6 +483,51 @@ mod tests {
447483
);
448484
}
449485

486+
#[test]
487+
fn test_deregister_by_cached_value_updates_refcount() {
488+
let _refcount_guard = test_refcounts::guard();
489+
test_refcounts::clear("10de 26ba");
490+
let (tmp, sysfs) = setup_mock_sysfs();
491+
492+
create_pci_device(
493+
&sysfs,
494+
tmp.path(),
495+
"0000:2d:00.0",
496+
"0x10de",
497+
"0x26ba",
498+
"0x030000",
499+
42,
500+
);
501+
create_pci_device(
502+
&sysfs,
503+
tmp.path(),
504+
"0000:3b:00.0",
505+
"0x10de",
506+
"0x26ba",
507+
"0x030200",
508+
43,
509+
);
510+
create_new_id_file(&sysfs);
511+
create_remove_id_file(&sysfs);
512+
513+
register_vfio_new_id(&sysfs, "0000:2d:00.0");
514+
register_vfio_new_id(&sysfs, "0000:3b:00.0");
515+
516+
deregister_vfio_id_by_value(&sysfs, "10de 26ba");
517+
let written = fs::read_to_string(sysfs.vfio_pci_remove_id()).unwrap();
518+
assert_eq!(
519+
written, "",
520+
"cached deregister should respect remaining refcount users"
521+
);
522+
523+
deregister_vfio_new_id(&sysfs, "0000:3b:00.0");
524+
let written = fs::read_to_string(sysfs.vfio_pci_remove_id()).unwrap();
525+
assert_eq!(
526+
written, "10de 26ba",
527+
"final deregister should write remove_id after cached release decremented refcount"
528+
);
529+
}
530+
450531
#[test]
451532
fn test_bind_device_to_vfio_already_bound() {
452533
let (tmp, sysfs) = setup_mock_sysfs();

0 commit comments

Comments
 (0)