Skip to content

Commit 7237e16

Browse files
authored
virtio-nic: in-repo tests and a better device state FSM (#1064)
VirtIO 1.2 states clearly that a driver MUST NOT clear bits from device_status (except, implicitly, by writing RESET aka 0 to device_status to.. do a device reset). Since this is the only way to clear a bit, bits that *are* set can be set at most once. Our old FSM accepted whatever status a guest happened to provide and stored that; a guest could intentionally or unintentionally clear NEEDS_RESET even, though it would be very much in error at that point. Instead of this very lax treatment of device_status, be more clear that FEATURES_OK can be toggled to set only once (which fixes a bug on its own), and if a guest tries clearing a status bit by any means other than reset, set NEEDS_RESET and demand a reset outright. Along with this, `viona.rs` gets a very simple driver to exercise some usage patterns we've seen in starting up virtio NIC devices. `basic_operation_multiqueue` fails without the device state FSM improvements, a reflection of the bug mentioned above; since FEATURES_OK is set before DEVICE_OK, the final status write including DEVICE_OK would cause a second attempt to apply the negotiated features. We would then try to set up all the virtqueues again, which fails because the virtqueues were just taken out of the RESET state when the driver initialized them, and we come to rest with the NIC in NEEDS_RESET. Linux (and BSDs, and illumos) clearly do not care about this, as such instances proceed to network seemingly without issue. Windows seems to have a periodic task that notices the NEEDS_RESET bit and obliges. It resets the NIC, which fails to ever come up correctly, and dutifully stops networking (#1048). Commenting out parts of prior patches (957f5c4, 1df49a4, 45af0f7) also cause these new tests to fail; they are collectively as necessary as it seemed for correct operation in the face of resets. Finally, one of the new tests verifies that a device in NEEDS_RESET cannot have the bit inappropriately cleared without reset. This tripped over a deadlock if a virtqueue is set with an inappropriate size. That is fixed in this patch as well.
1 parent 5372135 commit 7237e16

5 files changed

Lines changed: 786 additions & 12 deletions

File tree

lib/propolis/src/hw/virtio/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,4 +263,6 @@ mod probes {
263263
fn virtio_viona_mq_set_use_pairs(cause: u8, npairs: u16) {}
264264

265265
fn virtio_device_needs_reset() {}
266+
fn virtio_set_status(value: u8) {}
267+
fn virtio_state_reset() {}
266268
}

lib/propolis/src/hw/virtio/pci.rs

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -658,11 +658,11 @@ impl PciVirtioState {
658658
state.queue_select = wo.read_u16();
659659
}
660660
CommonConfigReg::QueueSize => {
661-
let state = self.state.lock().unwrap();
661+
let mut state = self.state.lock().unwrap();
662662
match VqSize::try_from(wo.read_u16()) {
663663
Err(_) => {
664664
// Bad queue size.
665-
self.set_needs_reset(dev);
665+
self.needs_reset_locked(dev, &mut state);
666666
}
667667
Ok(offered) => {
668668
let qs = state.queue_select;
@@ -1051,19 +1051,65 @@ impl PciVirtioState {
10511051
}
10521052

10531053
fn set_status(&self, dev: &dyn VirtioDevice, value: u8) {
1054+
probes::virtio_set_status!(|| value);
10541055
let mut state = self.state.lock().unwrap();
10551056
let status = Status::from_bits_truncate(value);
10561057
if status == Status::RESET && state.status != Status::RESET {
10571058
self.virtio_reset(dev, state);
10581059
} else {
1059-
// XXX: better device status FSM
1060-
state.status = status;
1061-
if status.contains(Status::FEATURES_OK) {
1062-
if dev.set_features(state.negotiated_features).is_err() {
1063-
self.needs_reset_locked(dev, &mut state);
1064-
}
1060+
self.apply_status(dev, &mut state, status);
1061+
}
1062+
}
1063+
1064+
fn apply_status(
1065+
&self,
1066+
dev: &dyn VirtioDevice,
1067+
state: &mut MutexGuard<VirtioState>,
1068+
status: Status,
1069+
) {
1070+
if status == state.status {
1071+
// No actual difference, bail out early.
1072+
return;
1073+
}
1074+
1075+
if !status.contains(state.status) {
1076+
// The driver has disregarded VirtIO 1.2 section 2.1.2:
1077+
//
1078+
// > The driver MUST NOT clear a device status bit.
1079+
//
1080+
// If we allowed such a thing then the guest might toggle
1081+
// FEATURES_OK and violate the expectation that `set_features`
1082+
// is called only once when setting up a device. Instead, the
1083+
// guest driver is in the wrong and we'll set NEEDS_RESET.
1084+
self.needs_reset_locked(dev, state);
1085+
return;
1086+
}
1087+
1088+
// Any bits here are being set at most once since the last device reset.
1089+
let new_bits = status.difference(state.status);
1090+
1091+
if new_bits.contains(Status::FEATURES_OK) {
1092+
// From VirtIO 1.2 section 2.1:
1093+
//
1094+
// > FEATURES_OK (8) Indicates that the driver has acknowledged
1095+
// > all the features it understands, and feature negotiation
1096+
// > is complete.
1097+
//
1098+
// So, at this point if the guest sets additional features, we don't
1099+
// have to care about them; renegotiation requires a device reset
1100+
// ("The only way to renegotiate is to reset the device."). The
1101+
// features provided are the ones we should enable.
1102+
if dev.set_features(state.negotiated_features) == Err(()) {
1103+
// Those requested features were not tolerable. We *must not*
1104+
// reflect FEATURES_OK in status. Additionally, set NEEDS_RESET
1105+
// in the hopes that the guset might see the issue and attempt
1106+
// operating in a less-featureful mode.
1107+
self.needs_reset_locked(dev, state);
1108+
return;
10651109
}
10661110
}
1111+
1112+
state.status = status;
10671113
}
10681114

10691115
/// Set the "Needs Reset" state on the VirtIO device
@@ -1130,6 +1176,7 @@ impl PciVirtioState {
11301176
dev: &dyn VirtioDevice,
11311177
mut state: MutexGuard<VirtioState>,
11321178
) {
1179+
probes::virtio_state_reset!(|| ());
11331180
self.reset_queues_locked(dev, &mut state);
11341181
state.reset();
11351182
let _ = self.isr_state.read_clear();
@@ -1514,6 +1561,11 @@ const LEGACY_REG_QUEUE_NOTIFY_OFFSET: usize = 0x10;
15141561
const COMMON_REG_OFFSET: usize = 0;
15151562
const COMMON_REG_SIZE: usize =
15161563
4 + 4 + 4 + 4 + 2 + 2 + 1 + 1 + 2 + 2 + 2 + 2 + 2 + 8 + 8 + 8 + 2 + 2;
1564+
// Some tests want to poke at the common config registers, but before doing so use the total
1565+
// common_cfg struct size to spot-check that the layout is correct. Ideally the register map we
1566+
// build here could go both ways and either be public, or public for tests.
1567+
#[cfg(test)]
1568+
pub const COMMON_REG_SIZE_TEST: usize = COMMON_REG_SIZE;
15171569
const DEVICE_REG_OFFSET: usize = PAGE_SIZE;
15181570
const NOTIFY_REG_OFFSET: usize = 2 * PAGE_SIZE;
15191571
pub const NOTIFY_REG_SIZE: usize = 4;

lib/propolis/src/hw/virtio/queue.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1015,10 +1015,11 @@ impl VirtQueues {
10151015
pub fn get(&self, qid: u16) -> Option<&Arc<VirtQueue>> {
10161016
let len = self.len();
10171017
let qid = usize::from(qid);
1018-
// XXX: This special case is for viona, which always puts the
1019-
// control queue at the end of queue vector. None of the other
1020-
// devices currently handle queues specially in this way, but we
1021-
// should come up with some better mechanism here.
1018+
// XXX: This special case is for the virtio network device, which always
1019+
// puts the control queue at the end of queue vector (see VirtIO 1.2
1020+
// section 5.1.2). None of the other devices currently handle queues
1021+
// specially in this way, but we should come up with some better
1022+
// mechanism here.
10221023
if qid + 1 == len {
10231024
Some(self.get_control())
10241025
} else {

0 commit comments

Comments
 (0)