Skip to content

Commit 35d1abe

Browse files
dianpopalikebreath
andcommitted
virtio-queue: panic on invalid avail ring index
The number of descriptor chain heads to process should always be smaller or equal to the queue size, as the driver should never ask the VMM to process a available ring entry more than once. Checking and reporting such incorrect driver behavior can prevent potential hanging and Denial-of-Service from happening on the VMM side. Issue reported in rust-vmm/vm-virtio and fixed in rust-vmm/vm-virtio#196. Signed-off-by: Diana Popa <dpopa@amazon.com> Co-authored-by: Bo Chen <chen.bo@intel.com>
1 parent 081825e commit 35d1abe

File tree

2 files changed

+117
-8
lines changed

2 files changed

+117
-8
lines changed

src/devices/src/virtio/block/device.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1477,7 +1477,6 @@ pub(crate) mod tests {
14771477
add_flush_requests_batch(&mut block, &mem, &vq, 1);
14781478
simulate_queue_event(&mut block, Some(false));
14791479
assert_eq!(block.is_io_engine_throttled, true);
1480-
assert_eq!(block.queues[0].len(&mem), 1);
14811480

14821481
simulate_async_completion_event(&mut block, true);
14831482
assert_eq!(block.is_io_engine_throttled, false);

src/devices/src/virtio/queue.rs

Lines changed: 117 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ impl fmt::Display for QueueError {
4949
}
5050
}
5151

52-
/// A virtio descriptor constraints with C representive.
52+
/// A virtio descriptor constraints with C representative.
5353
#[repr(C)]
5454
#[derive(Default, Clone, Copy)]
5555
struct Descriptor {
@@ -104,9 +104,9 @@ impl<'a> DescriptorChain<'a> {
104104
// These reads can't fail unless Guest memory is hopelessly broken.
105105
let desc = match mem.read_obj::<Descriptor>(desc_head) {
106106
Ok(ret) => ret,
107-
Err(_) => {
107+
Err(err) => {
108108
// TODO log address
109-
error!("Failed to read from memory");
109+
error!("Failed to read virtio descriptor from memory: {}", err);
110110
return None;
111111
}
112112
};
@@ -289,7 +289,7 @@ impl Queue {
289289
}
290290

291291
/// Returns the number of yet-to-be-popped descriptor chains in the avail ring.
292-
pub fn len(&self, mem: &GuestMemoryMmap) -> u16 {
292+
fn len(&self, mem: &GuestMemoryMmap) -> u16 {
293293
(self.avail_idx(mem) - self.next_avail).0
294294
}
295295

@@ -300,7 +300,21 @@ impl Queue {
300300

301301
/// Pop the first available descriptor chain from the avail ring.
302302
pub fn pop<'a, 'b>(&'a mut self, mem: &'b GuestMemoryMmap) -> Option<DescriptorChain<'b>> {
303-
if self.len(mem) == 0 {
303+
let len = self.len(mem);
304+
// The number of descriptor chain heads to process should always
305+
// be smaller or equal to the queue size, as the driver should
306+
// never ask the VMM to process a available ring entry more than
307+
// once. Checking and reporting such incorrect driver behavior
308+
// can prevent potential hanging and Denial-of-Service from
309+
// happening on the VMM side.
310+
if len > self.actual_size() {
311+
// We are choosing to interrupt execution since this could be a potential malicious
312+
// driver scenario. This way we also eliminate the risk of repeatedly
313+
// logging and potentially clogging the microVM through the log system.
314+
panic!("The number of available virtio descriptors is greater than queue size!");
315+
}
316+
317+
if len == 0 {
304318
return None;
305319
}
306320

@@ -425,7 +439,7 @@ impl Queue {
425439
// via `self.is_valid()`, so it's safe to unwrap and use unchecked offsets here.
426440
// Note: the `MmioTransport` code ensures that queue addresses cannot be changed by the
427441
// guest after device activation, so we can be certain that no change has
428-
// occured since the last `self.is_valid()` check.
442+
// occurred since the last `self.is_valid()` check.
429443
let addr = self.avail_ring.unchecked_add(2);
430444
Wrapping(mem.read_obj::<u16>(addr).unwrap())
431445
}
@@ -461,7 +475,17 @@ impl Queue {
461475
return true;
462476
}
463477

464-
if self.len(mem) != 0 {
478+
let len = self.len(mem);
479+
if len != 0 {
480+
// The number of descriptor chain heads to process should always
481+
// be smaller or equal to the queue size.
482+
if len > self.actual_size() {
483+
// We are choosing to interrupt execution since this could be a potential malicious
484+
// driver scenario. This way we also eliminate the risk of
485+
// repeatedly logging and potentially clogging the microVM through
486+
// the log system.
487+
panic!("The number of available virtio descriptors is greater than queue size!");
488+
}
465489
return false;
466490
}
467491

@@ -741,6 +765,92 @@ pub(crate) mod tests {
741765
assert_eq!(q.avail_event(m), 2);
742766
}
743767

768+
#[test]
769+
#[should_panic(
770+
expected = "The number of available virtio descriptors is greater than queue size!"
771+
)]
772+
fn test_invalid_avail_idx_no_notification() {
773+
// This test ensures constructing a descriptor chain succeeds
774+
// with valid available ring indexes while it produces an error with invalid
775+
// indexes.
776+
// No notification suppression enabled.
777+
let m = &create_anon_guest_memory(&[(GuestAddress(0), 0x6000)], false).unwrap();
778+
779+
// We set up a queue of size 4.
780+
let vq = VirtQueue::new(GuestAddress(0), m, 4);
781+
let mut q = vq.create_queue();
782+
783+
for j in 0..4 {
784+
vq.dtable[j].set(
785+
0x1000 * (j + 1) as u64,
786+
0x1000,
787+
VIRTQ_DESC_F_NEXT,
788+
(j + 1) as u16,
789+
);
790+
}
791+
792+
// Create 2 descriptor chains.
793+
// the chains are (0, 1) and (2, 3)
794+
vq.dtable[1].flags.set(0);
795+
vq.dtable[3].flags.set(0);
796+
vq.avail.ring[0].set(0);
797+
vq.avail.ring[1].set(2);
798+
vq.avail.idx.set(2);
799+
800+
// We've just set up two chains.
801+
assert_eq!(q.len(m), 2);
802+
803+
// We process the first descriptor.
804+
let d = q.pop(m).unwrap().next_descriptor();
805+
assert!(matches!(d, Some(x) if !x.has_next()));
806+
// We confuse the device and set the available index as being 6.
807+
vq.avail.idx.set(6);
808+
809+
// We've actually just popped a descriptor so 6 - 1 = 5.
810+
assert_eq!(q.len(m), 5);
811+
812+
// However, since the apparent length set by the driver is more than the queue size,
813+
// we would be running the risk of going through some descriptors more than once.
814+
// As such, we expect to panic.
815+
q.pop(m);
816+
}
817+
818+
#[test]
819+
#[should_panic(
820+
expected = "The number of available virtio descriptors is greater than queue size!"
821+
)]
822+
fn test_invalid_avail_idx_with_notification() {
823+
// This test ensures constructing a descriptor chain succeeds
824+
// with valid available ring indexes while it produces an error with invalid
825+
// indexes.
826+
// Notification suppression is enabled.
827+
let m = &create_anon_guest_memory(&[(GuestAddress(0), 0x6000)], false).unwrap();
828+
829+
// We set up a queue of size 4.
830+
let vq = VirtQueue::new(GuestAddress(0), m, 4);
831+
let mut q = vq.create_queue();
832+
833+
q.uses_notif_suppression = true;
834+
835+
// Create 1 descriptor chain of 4.
836+
for j in 0..4 {
837+
vq.dtable[j].set(
838+
0x1000 * (j + 1) as u64,
839+
0x1000,
840+
VIRTQ_DESC_F_NEXT,
841+
(j + 1) as u16,
842+
);
843+
}
844+
// We need to clear the VIRTQ_DESC_F_NEXT for the last descriptor.
845+
vq.dtable[3].flags.set(0);
846+
vq.avail.ring[0].set(0);
847+
848+
// driver sets available index to suspicious value.
849+
vq.avail.idx.set(6);
850+
851+
q.pop_or_enable_notification(m);
852+
}
853+
744854
#[test]
745855
fn test_add_used() {
746856
let m = &create_anon_guest_memory(&[(GuestAddress(0), 0x10000)], false).unwrap();

0 commit comments

Comments
 (0)