Skip to content

Commit 2e922c7

Browse files
committed
refactor: eliminate Queue::is_valid
Vring validation was a bit awkwardly split across two functions which did overlapping sets of checks: Queue::initialize verified alignment and memory accesses, while Queue::is_valid additionally checked Queue::ready and Queue::size. However, on the activation path, both were called, meanign we checked alignment twice (.initialize() is called in .activate(), but we only call .activate() if .is_valid() returned true). This is confusing at best, and at worst made us potentially virtio spec incompliant: If the quest tried to activate a virtio device, but this failed because some vring was not valid (in terms of Queue::is_valid), then Firecracker would silently ignore the activation request. Now, it instead marks the device as needing reset, and notifies the guest of its failure to properly configure the vrings. While we're at it, also remove some duplicated checks from the vring restoration code: .initialize() is called for activated devices, so there's no need to later validate the size specifically again, and also no need for the additional call to is_valid(). Signed-off-by: Patrick Roy <[email protected]>
1 parent 3f04036 commit 2e922c7

File tree

4 files changed

+73
-108
lines changed

4 files changed

+73
-108
lines changed

src/vmm/src/devices/virtio/mmio.rs

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -95,13 +95,6 @@ impl MmioTransport {
9595
self.device_status & (set | clr) == set
9696
}
9797

98-
fn are_queues_valid(&self) -> bool {
99-
self.locked_device()
100-
.queues()
101-
.iter()
102-
.all(|q| q.is_valid(&self.mem))
103-
}
104-
10598
fn with_queue<U, F>(&self, d: U, f: F) -> U
10699
where
107100
F: FnOnce(&Queue) -> U,
@@ -185,7 +178,7 @@ impl MmioTransport {
185178
DRIVER_OK if self.device_status == (ACKNOWLEDGE | DRIVER | FEATURES_OK) => {
186179
self.device_status = status;
187180
let device_activated = self.locked_device().is_activated();
188-
if !device_activated && self.are_queues_valid() {
181+
if !device_activated {
189182
// temporary variable needed for borrow checker
190183
let activate_result = self.locked_device().activate(self.mem.clone());
191184
if let Err(err) = activate_result {
@@ -486,8 +479,6 @@ pub(crate) mod tests {
486479

487480
assert_eq!(d.locked_device().queue_events().len(), 2);
488481

489-
assert!(!d.are_queues_valid());
490-
491482
d.queue_select = 0;
492483
assert_eq!(d.with_queue(0, |q| q.max_size), 16);
493484
assert!(d.with_queue_mut(|q| q.size = 16));
@@ -501,8 +492,6 @@ pub(crate) mod tests {
501492
d.queue_select = 2;
502493
assert_eq!(d.with_queue(0, |q| q.max_size), 0);
503494
assert!(!d.with_queue_mut(|q| q.size = 16));
504-
505-
assert!(!d.are_queues_valid());
506495
}
507496

508497
#[test]
@@ -761,7 +750,6 @@ pub(crate) mod tests {
761750
let m = single_region_mem(0x1000);
762751
let mut d = MmioTransport::new(m, Arc::new(Mutex::new(DummyDevice::new())), false);
763752

764-
assert!(!d.are_queues_valid());
765753
assert!(!d.locked_device().is_activated());
766754
assert_eq!(d.device_status, device_status::INIT);
767755

@@ -800,7 +788,6 @@ pub(crate) mod tests {
800788
write_le_u32(&mut buf[..], 1);
801789
d.bus_write(0x44, &buf[..]);
802790
}
803-
assert!(d.are_queues_valid());
804791
assert!(!d.locked_device().is_activated());
805792

806793
// Device should be ready for activation now.
@@ -860,7 +847,6 @@ pub(crate) mod tests {
860847
write_le_u32(&mut buf[..], 1);
861848
d.bus_write(0x44, &buf[..]);
862849
}
863-
assert!(d.are_queues_valid());
864850
assert_eq!(
865851
d.locked_device().interrupt_status().load(Ordering::SeqCst),
866852
0
@@ -910,7 +896,6 @@ pub(crate) mod tests {
910896
write_le_u32(&mut buf[..], 1);
911897
d.bus_write(0x44, &buf[..]);
912898
}
913-
assert!(d.are_queues_valid());
914899
assert!(!d.locked_device().is_activated());
915900

916901
// Device should be ready for activation now.
@@ -937,7 +922,6 @@ pub(crate) mod tests {
937922
let mut d = MmioTransport::new(m, Arc::new(Mutex::new(DummyDevice::new())), false);
938923
let mut buf = [0; 4];
939924

940-
assert!(!d.are_queues_valid());
941925
assert!(!d.locked_device().is_activated());
942926
assert_eq!(d.device_status, 0);
943927
activate_device(&mut d);

src/vmm/src/devices/virtio/persist.rs

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -184,14 +184,7 @@ impl VirtioDeviceState {
184184

185185
for q in &queues {
186186
// Sanity check queue size and queue max size.
187-
if q.max_size != expected_queue_max_size || q.size > expected_queue_max_size {
188-
return Err(PersistError::InvalidInput);
189-
}
190-
// Snapshot can happen at any time, including during device configuration/activation
191-
// when fields are only partially configured.
192-
//
193-
// Only if the device was activated, check `q.is_valid()`.
194-
if self.activated && !q.is_valid(mem) {
187+
if q.max_size != expected_queue_max_size {
195188
return Err(PersistError::InvalidInput);
196189
}
197190
}

src/vmm/src/devices/virtio/queue.rs

Lines changed: 70 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use std::sync::atomic::{Ordering, fence};
1111
use crate::arch::host_page_size;
1212
use crate::logger::error;
1313
use crate::utils::u64_to_usize;
14-
use crate::vstate::memory::{Address, Bitmap, ByteValued, GuestAddress, GuestMemory};
14+
use crate::vstate::memory::{Bitmap, ByteValued, GuestAddress, GuestMemory};
1515

1616
pub const VIRTQ_DESC_F_NEXT: u16 = 0x1;
1717
pub const VIRTQ_DESC_F_WRITE: u16 = 0x2;
@@ -35,6 +35,10 @@ pub enum QueueError {
3535
MemoryError(#[from] vm_memory::GuestMemoryError),
3636
/// Pointer is not aligned properly: {0:#x} not {1}-byte aligned.
3737
PointerNotAligned(usize, usize),
38+
/// Attempt to use virtio queue that is not marked ready
39+
NotReady,
40+
/// Virtio queue with invalid size: {0}
41+
InvalidSize(u16),
3842
}
3943

4044
/// Error type indicating the guest configured a virtio queue such that the avail_idx field would
@@ -339,6 +343,14 @@ impl Queue {
339343
/// Set up pointers to the queue objects in the guest memory
340344
/// and mark memory dirty for those objects
341345
pub fn initialize<M: GuestMemory>(&mut self, mem: &M) -> Result<(), QueueError> {
346+
if !self.ready {
347+
return Err(QueueError::NotReady);
348+
}
349+
350+
if self.size > self.max_size || self.size == 0 || (self.size & (self.size - 1)) != 0 {
351+
return Err(QueueError::InvalidSize(self.size));
352+
}
353+
342354
// All the below pointers are verified to be aligned properly; otherwise some methods (e.g.
343355
// `read_volatile()`) will panic. Such an unalignment is possible when restored from a
344356
// broken/fuzzed snapshot.
@@ -446,58 +458,6 @@ impl Queue {
446458
}
447459
}
448460

449-
/// Validates the queue's in-memory layout is correct.
450-
pub fn is_valid<M: GuestMemory>(&self, mem: &M) -> bool {
451-
let desc_table = self.desc_table_address;
452-
let desc_table_size = self.desc_table_size();
453-
let avail_ring = self.avail_ring_address;
454-
let avail_ring_size = self.avail_ring_size();
455-
let used_ring = self.used_ring_address;
456-
let used_ring_size = self.used_ring_size();
457-
458-
if !self.ready {
459-
error!("attempt to use virtio queue that is not marked ready");
460-
false
461-
} else if self.size > self.max_size || self.size == 0 || (self.size & (self.size - 1)) != 0
462-
{
463-
error!("virtio queue with invalid size: {}", self.size);
464-
false
465-
} else if desc_table.raw_value() & 0xf != 0 {
466-
error!("virtio queue descriptor table breaks alignment constraints");
467-
false
468-
} else if avail_ring.raw_value() & 0x1 != 0 {
469-
error!("virtio queue available ring breaks alignment constraints");
470-
false
471-
} else if used_ring.raw_value() & 0x3 != 0 {
472-
error!("virtio queue used ring breaks alignment constraints");
473-
false
474-
// range check entire descriptor table to be assigned valid guest physical addresses
475-
} else if mem.get_slice(desc_table, desc_table_size).is_err() {
476-
error!(
477-
"virtio queue descriptor table goes out of bounds: start:0x{:08x} size:0x{:08x}",
478-
desc_table.raw_value(),
479-
desc_table_size
480-
);
481-
false
482-
} else if mem.get_slice(avail_ring, avail_ring_size).is_err() {
483-
error!(
484-
"virtio queue available ring goes out of bounds: start:0x{:08x} size:0x{:08x}",
485-
avail_ring.raw_value(),
486-
avail_ring_size
487-
);
488-
false
489-
} else if mem.get_slice(used_ring, used_ring_size).is_err() {
490-
error!(
491-
"virtio queue used ring goes out of bounds: start:0x{:08x} size:0x{:08x}",
492-
used_ring.raw_value(),
493-
used_ring_size
494-
);
495-
false
496-
} else {
497-
true
498-
}
499-
}
500-
501461
/// Returns the number of yet-to-be-popped descriptor chains in the avail ring.
502462
pub fn len(&self) -> u16 {
503463
(Wrapping(self.avail_ring_idx_get()) - self.next_avail).0
@@ -901,8 +861,6 @@ mod verification {
901861
let mut queue = less_arbitrary_queue();
902862
queue.initialize(&mem).unwrap();
903863

904-
assert!(queue.is_valid(&mem));
905-
906864
ProofContext(queue, mem)
907865
}
908866
}
@@ -912,8 +870,7 @@ mod verification {
912870
let mem = setup_kani_guest_memory();
913871
let mut queue: Queue = kani::any();
914872

915-
kani::assume(queue.is_valid(&mem));
916-
queue.initialize(&mem).unwrap();
873+
kani::assume(queue.initialize(&mem).is_ok());
917874

918875
ProofContext(queue, mem)
919876
}
@@ -1080,10 +1037,10 @@ mod verification {
10801037
#[kani::proof]
10811038
#[kani::unwind(0)]
10821039
#[kani::solver(cadical)]
1083-
fn verify_is_valid() {
1040+
fn verify_initialize() {
10841041
let ProofContext(queue, mem) = kani::any();
10851042

1086-
if queue.is_valid(&mem) {
1043+
if queue.initialize(&mem).is_ok() {
10871044
// Section 2.6: Alignment of descriptor table, available ring and used ring; size of
10881045
// queue
10891046
fn alignment_of(val: u64) -> u64 {
@@ -1167,7 +1124,7 @@ mod verification {
11671124

11681125
// This is an assertion in pop which we use to abort firecracker in a ddos scenario
11691126
// This condition being false means that the guest is asking us to process every element
1170-
// in the queue multiple times. It cannot be checked by is_valid, as that function
1127+
// in the queue multiple times. It cannot be checked by initialize, as that function
11711128
// is called when the queue is being initialized, e.g. empty. We compute it using
11721129
// local variables here to make things easier on kani: One less roundtrip through vm-memory.
11731130
let queue_len = queue.len();
@@ -1252,7 +1209,7 @@ mod verification {
12521209

12531210
#[cfg(test)]
12541211
mod tests {
1255-
use vm_memory::Bytes;
1212+
use vm_memory::{Address, Bytes};
12561213

12571214
pub use super::*;
12581215
use crate::devices::virtio::queue::QueueError::DescIndexOutOfBounds;
@@ -1312,26 +1269,35 @@ mod tests {
13121269
let mut q = vq.create_queue();
13131270

13141271
// q is currently valid
1315-
assert!(q.is_valid(m));
1272+
q.initialize(m).unwrap();
13161273

13171274
// shouldn't be valid when not marked as ready
13181275
q.ready = false;
1319-
assert!(!q.is_valid(m));
1276+
assert!(matches!(q.initialize(m).unwrap_err(), QueueError::NotReady));
13201277
q.ready = true;
13211278

13221279
// or when size > max_size
13231280
q.size = q.max_size << 1;
1324-
assert!(!q.is_valid(m));
1281+
assert!(matches!(
1282+
q.initialize(m).unwrap_err(),
1283+
QueueError::InvalidSize(_)
1284+
));
13251285
q.size = q.max_size;
13261286

13271287
// or when size is 0
13281288
q.size = 0;
1329-
assert!(!q.is_valid(m));
1289+
assert!(matches!(
1290+
q.initialize(m).unwrap_err(),
1291+
QueueError::InvalidSize(_)
1292+
));
13301293
q.size = q.max_size;
13311294

13321295
// or when size is not a power of 2
13331296
q.size = 11;
1334-
assert!(!q.is_valid(m));
1297+
assert!(matches!(
1298+
q.initialize(m).unwrap_err(),
1299+
QueueError::InvalidSize(_)
1300+
));
13351301
q.size = q.max_size;
13361302

13371303
// reset dirtied values
@@ -1342,22 +1308,40 @@ mod tests {
13421308

13431309
// or if the various addresses are off
13441310

1345-
q.desc_table_address = GuestAddress(0xffff_ffff);
1346-
assert!(!q.is_valid(m));
1311+
q.desc_table_address = GuestAddress(0xffff_ff00);
1312+
assert!(matches!(
1313+
q.initialize(m).unwrap_err(),
1314+
QueueError::MemoryError(_)
1315+
));
13471316
q.desc_table_address = GuestAddress(0x1001);
1348-
assert!(!q.is_valid(m));
1317+
assert!(matches!(
1318+
q.initialize(m).unwrap_err(),
1319+
QueueError::PointerNotAligned(_, _)
1320+
));
13491321
q.desc_table_address = vq.dtable_start();
13501322

1351-
q.avail_ring_address = GuestAddress(0xffff_ffff);
1352-
assert!(!q.is_valid(m));
1323+
q.avail_ring_address = GuestAddress(0xffff_ff00);
1324+
assert!(matches!(
1325+
q.initialize(m).unwrap_err(),
1326+
QueueError::MemoryError(_)
1327+
));
13531328
q.avail_ring_address = GuestAddress(0x1001);
1354-
assert!(!q.is_valid(m));
1329+
assert!(matches!(
1330+
q.initialize(m).unwrap_err(),
1331+
QueueError::PointerNotAligned(_, _)
1332+
));
13551333
q.avail_ring_address = vq.avail_start();
13561334

1357-
q.used_ring_address = GuestAddress(0xffff_ffff);
1358-
assert!(!q.is_valid(m));
1335+
q.used_ring_address = GuestAddress(0xffff_ff00);
1336+
assert!(matches!(
1337+
q.initialize(m).unwrap_err(),
1338+
QueueError::MemoryError(_)
1339+
));
13591340
q.used_ring_address = GuestAddress(0x1001);
1360-
assert!(!q.is_valid(m));
1341+
assert!(matches!(
1342+
q.initialize(m).unwrap_err(),
1343+
QueueError::PointerNotAligned(_, _)
1344+
));
13611345
q.used_ring_address = vq.used_start();
13621346
}
13631347

@@ -1673,23 +1657,27 @@ mod tests {
16731657

16741658
#[test]
16751659
fn test_initialize_with_aligned_pointer() {
1676-
let mut q = Queue::new(0);
1660+
let mut q = Queue::new(FIRECRACKER_MAX_QUEUE_SIZE);
1661+
1662+
q.ready = true;
1663+
q.size = q.max_size;
16771664

1678-
let random_addr = 0x321;
16791665
// Descriptor table must be 16-byte aligned.
1680-
q.desc_table_address = GuestAddress(random_addr / 16 * 16);
1666+
q.desc_table_address = GuestAddress(16);
16811667
// Available ring must be 2-byte aligned.
1682-
q.avail_ring_address = GuestAddress(random_addr / 2 * 2);
1668+
q.avail_ring_address = GuestAddress(2);
16831669
// Used ring must be 4-byte aligned.
1684-
q.avail_ring_address = GuestAddress(random_addr / 4 * 4);
1670+
q.avail_ring_address = GuestAddress(4);
16851671

1686-
let mem = single_region_mem(0x1000);
1672+
let mem = single_region_mem(0x10000);
16871673
q.initialize(&mem).unwrap();
16881674
}
16891675

16901676
#[test]
16911677
fn test_initialize_with_misaligned_pointer() {
1692-
let mut q = Queue::new(0);
1678+
let mut q = Queue::new(FIRECRACKER_MAX_QUEUE_SIZE);
1679+
q.ready = true;
1680+
q.size = q.max_size;
16931681
let mem = single_region_mem(0x1000);
16941682

16951683
// Descriptor table must be 16-byte aligned.

src/vmm/src/persist.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ pub fn create_snapshot(
166166
.snapshot_memory_to_file(&params.mem_file_path, params.snapshot_type)?;
167167

168168
// We need to mark queues as dirty again for all activated devices. The reason we
169-
// do it here is because we don't mark pages as dirty during runtime
169+
// do it here is that we don't mark pages as dirty during runtime
170170
// for queue objects.
171171
// SAFETY:
172172
// This should never fail as we only mark pages only if device has already been activated,

0 commit comments

Comments
 (0)