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
- Verify the desc_table GuestMemorySlice passed at activation covers exactly queue_size * size_of::<Descriptor>() bytes from the guest-configured address.
- 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.
- Ensure the same queue_size is used for slice construction and DescriptorChain popping; never let pop paths use a stale or independently computed size.
- 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
- Validate desc_table length >= queue_size * size_of::<Descriptor>() at activation and fail activation with a typed error.
- Share one queue_size constant between ring construction and pop paths.
- Re-run validation whenever guest memory or queue configuration changes.
- Convert hot-path expects into typed errors so bugs surface as controlled failures.
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
- Device is not initialized
- vsock: Could not trigger device interrupt
- Error converting cpu id to Tid
- Error getting initial gdb event
- error unlocking vmm
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)