firecracker-microvm/firecracker · critical

virtio ring access outside the range validated at activation

Error message

virtio ring access outside the range validated at activation

What it means

DescriptorChain::checked_new reads a Descriptor from the guest descriptor table via an unsafe GuestMemorySlice::read_obj whose Result is unwrapped with expect(). The slice's valid range was validated once at device activation, so this expect only fires if the code accesses outside that pre-validated range - an internal invariant violation, not a recoverable guest error. Because the panic aborts the VMM process, it signals a bug in index/offset bookkeeping rather than something callers should catch.

Solutions

  1. Verify the desc_table GuestMemorySlice passed at activation covers exactly queue_size * size_of::<Descriptor>() bytes from the guest-configured address.
  2. Re-run the activation-time range/alignment validation (length >= RING_HEADER_SIZE + queue_size*stride) after any change to queue size or guest memory layout.
  3. Ensure the same queue_size is used for slice construction and DescriptorChain popping; never let pop paths use a stale or independently computed size.
  4. If you need recoverable behavior, replace the expect with error propagation (e.g. QueueError::DescIndexOutOfBounds) instead of relying on the invariant.

Example fix

// before
let desc = unsafe { desc_table.read_obj::<Descriptor>(idx * SIZE) }
    .expect("virtio ring access outside the range validated at activation");
// after
let desc = unsafe { desc_table.read_obj::<Descriptor>(idx * SIZE) }
    .map_err(|_| QueueError::DescIndexOutOfBounds(index))?;
Defensive patterns

Strategy: validation

Validate before calling

fn desc_table_len_ok(len: usize, queue_size: u16) -> bool {
    len >= queue_size as usize * std::mem::size_of::<Descriptor>()
}

Type guard

fn chain_index_valid(index: u16, queue_size: u16, table_len: usize) -> bool {
    (index as usize) < queue_size as usize
        && table_len >= queue_size as usize * std::mem::size_of::<Descriptor>()
}

Try / catch

// expect() panics; guard before calling or propagate instead:
let chain = DescriptorChain::try_new(desc_table, queue_size, index)
    .ok_or_else(|| DeviceError::InvalidDescriptor(index))?;

Prevention

When it happens

Trigger: Calling DescriptorChain::checked_new with an index >= queue_size would return None before the read, so the expect fires only if the desc_table slice is smaller than queue_size * size_of::<Descriptor>() - e.g. the slice retained from activation no longer matches the queue size used at pop time, or the table base/length validation at activation was bypassed or mismatched.

Common situations: Refactors that change queue size or rebuild GuestMemorySlice ranges without re-validating; device activation code that computes the desc-table length incorrectly; tests/property checkers feeding hand-crafted slices shorter than the declared queue_size.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


AI-assisted analysis of firecracker-microvm/firecracker@23b09b943f (2026-09-19). Data as JSON: /api/errors/1a395df59ed76e3d. Report an issue: GitHub.

Appendix: source

Thrown at src/vmm/src/devices/virtio/queue.rs:126

    pub next: u16,
}

impl DescriptorChain {
    /// Creates a new `DescriptorChain` from the given descriptor table.
    ///
    /// Note that the desc_table and queue_size are assumed to be validated by the caller.
    fn checked_new(desc_table: GuestMemorySlice, queue_size: u16, index: u16) -> Option<Self> {
        if queue_size <= index {
            return None;
        }

        // SAFETY: initialization aligns the table, and descriptor-sized offsets preserve
        // alignment. The caller keeps the mapping alive.
        let desc = unsafe {
            desc_table
                .read_obj::<Descriptor>(usize::from(index) * std::mem::size_of::<Descriptor>())
        }
        .expect("virtio ring access outside the range validated at activation");
        let chain = DescriptorChain {
            desc_table,
            queue_size,
            ttl: queue_size,
            index,
            addr: GuestAddress(desc.addr),
            len: desc.len,
            flags: desc.flags,
            next: desc.next,
        };

        if chain.is_valid() { Some(chain) } else { None }
    }

    fn is_valid(&self) -> bool {
        !self.has_next() || self.next < self.queue_size
    }

View on GitHub (pinned to 23b09b943f)