Skip to content

feat: Impl virtio-gpu 2D - #186

Merged
junyu0312 merged 2 commits into
mainfrom
dev
Jul 14, 2026
Merged

junyu0312 merged 2 commits into
mainfrom
dev

Conversation

@junyu0312

@junyu0312 junyu0312 commented Jul 13, 2026 •

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features
    • Added GPU emulation through Virtio-GPU devices.
    • Added support for MMIO and PCI GPU transports.
    • Added display scanout, 2D resource management, memory backing, and host transfer operations.
    • Added GPU configuration, capability, and error handling support.
  • Bug Fixes
    • Improved safe access to guest memory and virtqueue descriptor data.

@coderabbitai

coderabbitai Bot commented Jul 13, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds virtio-gpu protocol definitions, guest-memory and descriptor helpers, control/cursor queue handlers, GPU resource and scanout state, and MMIO/PCI device registration.

Changes

Virtio GPU support

Layer / File(s) Summary
GPU protocol contracts
crates/vm-virtio/src/types/device/gpu/*, crates/vm-virtio/src/types/device_id.rs
Defines GPU configuration, features, commands, request layouts, formats, errors, and scanout structures.
Guest memory and descriptor access
crates/vm-mm/src/manager.rs, crates/vm-virtio/src/result.rs, crates/vm-virtio/src/virtqueue/virtq_desc_table.rs
Adds guest-to-slice copying, typed descriptor access, descriptor-chain traversal, and GPU-related virtio errors.
GPU device and queue handlers
crates/vm-device/src/device/virtio/*
Implements GPU configuration handling, resource and scanout state, control-queue commands, and the cursor-queue interface.
Device selection and VM registration
crates/vm-cli/src/cmd/device.rs, crates/vm-device/src/device/mod.rs, crates/vm-vmm/src/vm/device_builder.rs
Adds MMIO and PCI GPU variants and wires the resulting device into VM initialization.
Estimated code review effort: 4 (Complex) ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant VM as DeviceManagerBuilder
  participant GPU as VirtioGpu
  participant Queue as ControlqHandler
  participant Memory as MemoryAddressSpace
  VM->>GPU: Create with shared memory
  VM->>GPU: Attach via MMIO or register via PCI
  GPU->>Queue: Handle control queue descriptor
  Queue->>Memory: Read guest request and backing data
  Queue-->>GPU: Return response length
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and matches the main change: adding virtio-gpu 2D support and related device wiring.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev

Warning

Review ran into problems

🔥 Problems

Git: Failed to clone repository. Please run the @coderabbitai full review command to re-trigger a full review. If the issue persists, set path_filters to include or exclude specific files.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (3)
crates/vm-device/src/device/virtio/virtio_gpu/controlq_handler.rs (1)

295-297: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Unimplemented commands panic via todo!().

handle_get_edid and many handle_command branches (GET_CAPSET*, CTX_*, *_3D, *_BLOB) call todo!(). Most currently correspond to feature bits (VIRTIO_GPU_F_VIRGL, VIRTIO_GPU_F_EDID, blob features) not advertised in VirtioGpu::DEVICE_FEATURES, so a spec-compliant driver shouldn't reach them today — but as a defense-in-depth measure and to avoid surprises as features are added later, consider returning VIRTIO_GPU_RESP_ERR_UNSPEC instead of panicking.

Also applies to: 409-437

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/vm-device/src/device/virtio/virtio_gpu/controlq_handler.rs` around
lines 295 - 297, Replace the todo!() calls in handle_get_edid and the
unsupported handle_command branches covering GET_CAPSET*, CTX_*, *_3D, and
*_BLOB with graceful unsupported-command responses using
VIRTIO_GPU_RESP_ERR_UNSPEC. Preserve normal handling for supported commands and
ensure none of these currently unadvertised or future feature paths can panic.
crates/vm-virtio/src/types/device/gpu.rs (1)

47-54: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add #[repr(u32)] to VirtioGpuConfigOffset for consistency and correct FromRepr signature.

VirtioGpuCtrlType in request.rs uses #[repr(u32)], but VirtioGpuConfigOffset omits it. Without #[repr(...)], strum's FromRepr generates from_repr(usize) rather than from_repr(u32), forcing callers to cast from u32 MMIO offsets unnecessarily.

♻️ Proposed fix
 #[derive(FromRepr)]
+#[repr(u32)]
 pub enum VirtioGpuConfigOffset {
     EventsRead = 0x00,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/vm-virtio/src/types/device/gpu.rs` around lines 47 - 54, Add
#[repr(u32)] to the VirtioGpuConfigOffset enum alongside its existing FromRepr
derive, ensuring FromRepr exposes a u32-based from_repr signature consistent
with VirtioGpuCtrlType and allowing direct use of u32 MMIO offsets.
crates/vm-mm/src/manager.rs (1)

113-151: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the GPA range validation loop to reduce duplication.

The validation loop (lines 117–126) is identical to the one in copy_from_slice (lines 74–86) and memset (lines 39–51). A shared helper would eliminate three copies of the same logic and prevent them from diverging.

♻️ Proposed helper extraction
+ fn validate_gpa_range(&self, gpa: u64, len: usize) -> Result<(), Error> {
+     let mut remaining = len;
+     let mut check_gpa = gpa;
+     while remaining > 0 {
+         let region = self.try_get_region_by_gpa(check_gpa)?;
+         let offset = check_gpa - region.gpa;
+         let avail = region.len() - offset as usize;
+         let step = remaining.min(avail);
+         remaining -= step;
+         check_gpa += step as u64;
+     }
+     Ok(())
+ }

  pub fn copy_to_slice(&self, mut gpa: u64, buf: &mut [u8]) -> Result<(), Error> {
-     let mut remaining = buf.len();
-     let mut check_gpa = gpa;
-
-     while remaining > 0 {
-         let region = self.try_get_region_by_gpa(check_gpa)?;
-         let offset = check_gpa - region.gpa;
-         let avail = region.len() - offset as usize;
-         let step = remaining.min(avail);
-         remaining -= step;
-         check_gpa += step as u64;
-     }
+     self.validate_gpa_range(gpa, buf.len())?;
      remaining = buf.len();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/vm-mm/src/manager.rs` around lines 113 - 151, Extract the duplicated
GPA range validation loop from copy_to_slice into a shared helper on the
manager, then replace the corresponding validation loops in copy_to_slice,
copy_from_slice, and memset with calls to that helper. Preserve the existing
region lookup and error behavior while keeping each method’s actual memory
operation unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/vm-device/src/device/virtio/virtio_gpu/controlq_handler.rs`:
- Around line 32-76: Replace the panic paths in copy_from_iov and
copy_from_framebuffer_to_resource with propagated VirtioError/VirtioGpuError
results: convert memory.copy_to_slice failures and incomplete backing copies
into errors instead of unwrap/assert. Update handle_transfer_to_host_2d and its
callers to propagate the failure and return
VIRTIO_GPU_RESP_ERR_INVALID_PARAMETER for invalid guest addresses or offsets.
- Around line 214-224: Update the transfer bounds validation in the handler
containing the shown VirtioGpuCtrlType response to avoid unchecked u32 additions
for x + width and y + height. Use checked arithmetic so overflow is treated as
an invalid parameter, matching the existing handle_resource_create_2d approach,
while preserving the current rejection of out-of-bounds rectangles.
- Around line 299-442: Replace every descriptor-chain assert in handle_command
with non-panicking validation that returns the appropriate VirtioError when
chain.len() differs from the command’s required descriptor count. Apply this
consistently across all handled command branches, preserving each branch’s
existing processing for valid chains and response behavior.
- Around line 101-142: Update handle_resource_create_2d to reject an
already-registered cmd.resource_id before constructing or inserting the new
VirtioGpuResource. Set resp.r#type to the appropriate invalid-parameter response
and return, preserving the existing resource and scanout binding; only insert
the resource when the id is unused.

In `@crates/vm-virtio/src/types/device/gpu.rs`:
- Line 43: Update the doc comment for blob_alignment to state the representable
u32 maximum, 4294967295, instead of 4294967296. Preserve the existing
power-of-two and minimum-value documentation.

In `@crates/vm-virtio/src/virtqueue/virtq_desc_table.rs`:
- Around line 42-52: Both VirtqDescTable::as_ref and VirtqDescTable::as_mut must
validate the descriptor length before constructing typed slices. In
crates/vm-virtio/src/virtqueue/virtq_desc_table.rs lines 42-52, add the self.len
versus size_of::<T>() check before the immutable slice creation; apply the
identical check in lines 56-66 before mutable slice creation, returning
VirtioError::TransmuteDesc when the descriptor is too short.
- Around line 138-149: Update get_chain_mut to prevent infinite traversal by
tracking visited descriptor indices and terminating or rejecting the chain when
an index repeats; apply the same cycle protection to get_chain so both traversal
methods are safe. Resolve the misleading mut API in get_chain_mut by either
removing it and reusing get_chain, or changing it to take &mut self, call
get_mut, and return mutable descriptor references if callers require mutation.

---

Nitpick comments:
In `@crates/vm-device/src/device/virtio/virtio_gpu/controlq_handler.rs`:
- Around line 295-297: Replace the todo!() calls in handle_get_edid and the
unsupported handle_command branches covering GET_CAPSET*, CTX_*, *_3D, and
*_BLOB with graceful unsupported-command responses using
VIRTIO_GPU_RESP_ERR_UNSPEC. Preserve normal handling for supported commands and
ensure none of these currently unadvertised or future feature paths can panic.

In `@crates/vm-mm/src/manager.rs`:
- Around line 113-151: Extract the duplicated GPA range validation loop from
copy_to_slice into a shared helper on the manager, then replace the
corresponding validation loops in copy_to_slice, copy_from_slice, and memset
with calls to that helper. Preserve the existing region lookup and error
behavior while keeping each method’s actual memory operation unchanged.

In `@crates/vm-virtio/src/types/device/gpu.rs`:
- Around line 47-54: Add #[repr(u32)] to the VirtioGpuConfigOffset enum
alongside its existing FromRepr derive, ensuring FromRepr exposes a u32-based
from_repr signature consistent with VirtioGpuCtrlType and allowing direct use of
u32 MMIO offsets.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cfc1dfd0-c4de-40e3-aa7f-596930ec359a

📥 Commits

Reviewing files that changed from the base of the PR and between bef7270 and 170cde1.

📒 Files selected for processing (27)
  • crates/vm-cli/src/cmd/device.rs
  • crates/vm-device/src/device/mod.rs
  • crates/vm-device/src/device/virtio.rs
  • crates/vm-device/src/device/virtio/virtio_gpu.rs
  • crates/vm-device/src/device/virtio/virtio_gpu/controlq_handler.rs
  • crates/vm-device/src/device/virtio/virtio_gpu/cursorq_handler.rs
  • crates/vm-device/src/device/virtio/virtio_gpu/resource.rs
  • crates/vm-device/src/device/virtio/virtio_gpu/scanout.rs
  • crates/vm-mm/src/manager.rs
  • crates/vm-virtio/src/result.rs
  • crates/vm-virtio/src/types/device.rs
  • crates/vm-virtio/src/types/device/gpu.rs
  • crates/vm-virtio/src/types/device/gpu/error.rs
  • crates/vm-virtio/src/types/device/gpu/request.rs
  • crates/vm-virtio/src/types/device/gpu/request/cmd_get_display_info.rs
  • crates/vm-virtio/src/types/device/gpu/request/cmd_get_edid.rs
  • crates/vm-virtio/src/types/device/gpu/request/cmd_resource_attach_backing.rs
  • crates/vm-virtio/src/types/device/gpu/request/cmd_resource_create_2d.rs
  • crates/vm-virtio/src/types/device/gpu/request/cmd_resource_detach_baking.rs
  • crates/vm-virtio/src/types/device/gpu/request/cmd_resource_flush.rs
  • crates/vm-virtio/src/types/device/gpu/request/cmd_resource_unref.rs
  • crates/vm-virtio/src/types/device/gpu/request/cmd_set_scanout.rs
  • crates/vm-virtio/src/types/device/gpu/request/cmd_transfer_to_host_2d.rs
  • crates/vm-virtio/src/types/device/gpu/request/virtio_gpu_scanout.rs
  • crates/vm-virtio/src/types/device_id.rs
  • crates/vm-virtio/src/virtqueue/virtq_desc_table.rs
  • crates/vm-vmm/src/vm/device_builder.rs

Comment on lines +32 to +76
fn copy_from_iov(
memory: &MemoryAddressSpace,
iovs: &[VirtioGpuMemBacking],
src_offset: u64,
mut dst: &mut [u8],
) -> bool {
let mut offset = src_offset;

for iov in iovs {
if offset >= iov.length as u64 {
offset -= iov.length as u64;
continue;
}

let len = (iov.length as usize - offset as usize).min(dst.len());

memory
.copy_to_slice(iov.addr + offset, &mut dst[..len])
.unwrap();

dst = &mut dst[len..];

if dst.is_empty() {
return true;
}

offset = 0;
}

false
}

fn copy_from_framebuffer_to_resource(
memory: &MemoryAddressSpace,
resource: &mut VirtioGpuResource,
src_offset: u64,
dst_offset: u32,
len: usize,
) {
let dst = &mut resource.buf[dst_offset as usize..dst_offset as usize + len];

let is_empty = copy_from_iov(memory, &resource.backing, src_offset, dst);

assert!(is_empty);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Guest-controlled memory addresses/offsets can panic the process.

copy_from_iov calls .unwrap() on memory.copy_to_slice(iov.addr + offset, ...), where iov.addr comes straight from the guest via RESOURCE_ATTACH_BACKING (Line 269-273). An invalid/unmapped address supplied by the guest will panic here instead of returning an error. Separately, copy_from_framebuffer_to_resource's assert!(is_empty) (Line 75) fires whenever the guest-controlled cmd.offset in TRANSFER_TO_HOST_2D runs past the total attached backing length — also guest-triggerable.

Propagate a VirtioError/VirtioGpuError instead of panicking so a malicious or buggy driver can only get an error response, not crash the VMM.

Sketch
-fn copy_from_iov(
+fn copy_from_iov(
     memory: &MemoryAddressSpace,
     iovs: &[VirtioGpuMemBacking],
     src_offset: u64,
     mut dst: &mut [u8],
-) -> bool {
+) -> Result<bool, VirtioError> {
     ...
-        memory
-            .copy_to_slice(iov.addr + offset, &mut dst[..len])
-            .unwrap();
+        memory.copy_to_slice(iov.addr + offset, &mut dst[..len])?;
     ...
-    false
+    Ok(false)
 }

copy_from_framebuffer_to_resource and handle_transfer_to_host_2d would then propagate the Result and set VIRTIO_GPU_RESP_ERR_INVALID_PARAMETER on failure instead of asserting.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/vm-device/src/device/virtio/virtio_gpu/controlq_handler.rs` around
lines 32 - 76, Replace the panic paths in copy_from_iov and
copy_from_framebuffer_to_resource with propagated VirtioError/VirtioGpuError
results: convert memory.copy_to_slice failures and incomplete backing copies
into errors instead of unwrap/assert. Update handle_transfer_to_host_2d and its
callers to propagate the failure and return
VIRTIO_GPU_RESP_ERR_INVALID_PARAMETER for invalid guest addresses or offsets.

Comment on lines +101 to +142
async fn handle_resource_create_2d(
&self,
cmd: &VirtioGpuResourceCreate2D,
resp: &mut VirtioGpuCtrlHdr,
) {
let mut resources = self.resource.lock().await;

let Some(format) = VirtioGpuFormats::from_repr(cmd.format) else {
resp.r#type = VirtioGpuCtrlType::VIRTIO_GPU_RESP_ERR_INVALID_PARAMETER as u32;

return;
};

let Some(stride) = (cmd.width as usize).checked_mul(format.bpp()) else {
resp.r#type = VirtioGpuCtrlType::VIRTIO_GPU_RESP_ERR_INVALID_PARAMETER as u32;

return;
};

let Some(len) = (cmd.height as usize).checked_mul(stride) else {
resp.r#type = VirtioGpuCtrlType::VIRTIO_GPU_RESP_ERR_INVALID_PARAMETER as u32;

return;
};

let buf = vec![0; len];

resources.insert(
cmd.resource_id,
VirtioGpuResource {
id: cmd.resource_id,
format,
width: cmd.width,
height: cmd.height,
stride,
buf,
backing: Default::default(),
},
);

resp.r#type = VirtioGpuCtrlType::VIRTIO_GPU_RESP_OK_NODATA as u32;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Duplicate resource_id silently overwrites the existing resource.

resources.insert(cmd.resource_id, ...) overwrites any resource already registered under that id without checking. A guest re-creating an id currently bound to a scanout (via SET_SCANOUT) could swap the scanout's backing content without an explicit SET_SCANOUT, which deviates from expected resource-id semantics.

Suggested fix
+        if resources.contains_key(&cmd.resource_id) {
+            resp.r#type = VirtioGpuCtrlType::VIRTIO_GPU_RESP_ERR_INVALID_RESOURCE_ID as u32;
+            return;
+        }
+
         let Some(format) = VirtioGpuFormats::from_repr(cmd.format) else {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async fn handle_resource_create_2d(
&self,
cmd: &VirtioGpuResourceCreate2D,
resp: &mut VirtioGpuCtrlHdr,
) {
let mut resources = self.resource.lock().await;
let Some(format) = VirtioGpuFormats::from_repr(cmd.format) else {
resp.r#type = VirtioGpuCtrlType::VIRTIO_GPU_RESP_ERR_INVALID_PARAMETER as u32;
return;
};
let Some(stride) = (cmd.width as usize).checked_mul(format.bpp()) else {
resp.r#type = VirtioGpuCtrlType::VIRTIO_GPU_RESP_ERR_INVALID_PARAMETER as u32;
return;
};
let Some(len) = (cmd.height as usize).checked_mul(stride) else {
resp.r#type = VirtioGpuCtrlType::VIRTIO_GPU_RESP_ERR_INVALID_PARAMETER as u32;
return;
};
let buf = vec![0; len];
resources.insert(
cmd.resource_id,
VirtioGpuResource {
id: cmd.resource_id,
format,
width: cmd.width,
height: cmd.height,
stride,
buf,
backing: Default::default(),
},
);
resp.r#type = VirtioGpuCtrlType::VIRTIO_GPU_RESP_OK_NODATA as u32;
}
async fn handle_resource_create_2d(
&self,
cmd: &VirtioGpuResourceCreate2D,
resp: &mut VirtioGpuCtrlHdr,
) {
let mut resources = self.resource.lock().await;
if resources.contains_key(&cmd.resource_id) {
resp.r#type = VirtioGpuCtrlType::VIRTIO_GPU_RESP_ERR_INVALID_RESOURCE_ID as u32;
return;
}
let Some(format) = VirtioGpuFormats::from_repr(cmd.format) else {
resp.r#type = VirtioGpuCtrlType::VIRTIO_GPU_RESP_ERR_INVALID_PARAMETER as u32;
return;
};
let Some(stride) = (cmd.width as usize).checked_mul(format.bpp()) else {
resp.r#type = VirtioGpuCtrlType::VIRTIO_GPU_RESP_ERR_INVALID_PARAMETER as u32;
return;
};
let Some(len) = (cmd.height as usize).checked_mul(stride) else {
resp.r#type = VirtioGpuCtrlType::VIRTIO_GPU_RESP_ERR_INVALID_PARAMETER as u32;
return;
};
let buf = vec![0; len];
resources.insert(
cmd.resource_id,
VirtioGpuResource {
id: cmd.resource_id,
format,
width: cmd.width,
height: cmd.height,
stride,
buf,
backing: Default::default(),
},
);
resp.r#type = VirtioGpuCtrlType::VIRTIO_GPU_RESP_OK_NODATA as u32;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/vm-device/src/device/virtio/virtio_gpu/controlq_handler.rs` around
lines 101 - 142, Update handle_resource_create_2d to reject an
already-registered cmd.resource_id before constructing or inserting the new
VirtioGpuResource. Set resp.r#type to the appropriate invalid-parameter response
and return, preserving the existing resource and scanout binding; only insert
the resource when the id is unused.

Comment on lines +214 to +224
if cmd.r.x > resource.width
|| cmd.r.width > resource.width
|| cmd.r.x + cmd.r.width > resource.width
|| cmd.r.y > resource.height
|| cmd.r.height > resource.height
|| cmd.r.y + cmd.r.height > resource.height
{
resp.r#type = VirtioGpuCtrlType::VIRTIO_GPU_RESP_ERR_INVALID_PARAMETER as u32;

return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Unchecked u32 addition in the transfer bounds check can overflow.

cmd.r.x + cmd.r.width > resource.width and cmd.r.y + cmd.r.height > resource.height can overflow u32 and wrap, bypassing the intended bound. handle_resource_create_2d already uses checked_mul for similar arithmetic (Lines 114-124); do the same here.

Suggested fix
-        if cmd.r.x > resource.width
-            || cmd.r.width > resource.width
-            || cmd.r.x + cmd.r.width > resource.width
-            || cmd.r.y > resource.height
-            || cmd.r.height > resource.height
-            || cmd.r.y + cmd.r.height > resource.height
-        {
+        let x_end = cmd.r.x.checked_add(cmd.r.width);
+        let y_end = cmd.r.y.checked_add(cmd.r.height);
+        if x_end.is_none_or(|v| v > resource.width) || y_end.is_none_or(|v| v > resource.height) {
             resp.r#type = VirtioGpuCtrlType::VIRTIO_GPU_RESP_ERR_INVALID_PARAMETER as u32;
             return;
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if cmd.r.x > resource.width
|| cmd.r.width > resource.width
|| cmd.r.x + cmd.r.width > resource.width
|| cmd.r.y > resource.height
|| cmd.r.height > resource.height
|| cmd.r.y + cmd.r.height > resource.height
{
resp.r#type = VirtioGpuCtrlType::VIRTIO_GPU_RESP_ERR_INVALID_PARAMETER as u32;
return;
}
let x_end = cmd.r.x.checked_add(cmd.r.width);
let y_end = cmd.r.y.checked_add(cmd.r.height);
if x_end.is_none_or(|v| v > resource.width) || y_end.is_none_or(|v| v > resource.height) {
resp.r#type = VirtioGpuCtrlType::VIRTIO_GPU_RESP_ERR_INVALID_PARAMETER as u32;
return;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/vm-device/src/device/virtio/virtio_gpu/controlq_handler.rs` around
lines 214 - 224, Update the transfer bounds validation in the handler containing
the shown VirtioGpuCtrlType response to avoid unchecked u32 additions for x +
width and y + height. Use checked arithmetic so overflow is treated as an
invalid parameter, matching the existing handle_resource_create_2d approach,
while preserving the current rejection of out-of-bounds rectangles.

Comment on lines +299 to +442
async fn handle_command(
&self,
desc_ring: &VirtqDescTableRef,
desc_id: u16,
) -> Result<u32, VirtioError> {
let desc_entry = desc_ring.get(desc_id);
let command = desc_entry.as_ref::<VirtioGpuCtrlHdr>(&self.memory)?;

let ctrl_type = VirtioGpuCtrlType::from_repr(command.r#type)
.ok_or(VirtioGpuError::UnknownCtrlType(command.r#type))?;

match ctrl_type {
VirtioGpuCtrlType::VIRTIO_GPU_CMD_GET_DISPLAY_INFO => {
let chain = desc_ring.get_chain(desc_id);
assert_eq!(chain.len(), 2);

let response = chain[1].as_mut::<VirtioGpuRespDisplayInfo>(&self.memory)?;
response.as_mut_bytes().fill(0);

self.handle_get_display_info(response).await;

Ok(size_of::<VirtioGpuRespDisplayInfo>().try_into().unwrap())
}
VirtioGpuCtrlType::VIRTIO_GPU_CMD_RESOURCE_CREATE_2D => {
let chain = desc_ring.get_chain(desc_id);
assert_eq!(chain.len(), 2);

let cmd = chain[0].as_ref::<VirtioGpuResourceCreate2D>(&self.memory)?;
let response = chain[1].as_mut::<VirtioGpuCtrlHdr>(&self.memory)?;
response.as_mut_bytes().fill(0);

self.handle_resource_create_2d(cmd, response).await;

Ok(size_of::<VirtioGpuCtrlHdr>().try_into().unwrap())
}
VirtioGpuCtrlType::VIRTIO_GPU_CMD_RESOURCE_UNREF => {
let chain = desc_ring.get_chain(desc_id);
assert_eq!(chain.len(), 2);

let cmd = chain[0].as_ref::<VirtioGpuResourceUnref>(&self.memory)?;
let response = chain[1].as_mut::<VirtioGpuCtrlHdr>(&self.memory)?;
response.as_mut_bytes().fill(0);

self.handle_resource_unref(cmd, response).await;

Ok(size_of::<VirtioGpuCtrlHdr>().try_into().unwrap())
}
VirtioGpuCtrlType::VIRTIO_GPU_CMD_SET_SCANOUT => {
let chain = desc_ring.get_chain(desc_id);
assert_eq!(chain.len(), 2);

let cmd = chain[0].as_ref::<VirtioGpuSetScanout>(&self.memory)?;
let response = chain[1].as_mut::<VirtioGpuCtrlHdr>(&self.memory)?;
response.as_mut_bytes().fill(0);

self.handle_set_scanout(cmd, response).await;

Ok(size_of::<VirtioGpuCtrlHdr>().try_into().unwrap())
}
VirtioGpuCtrlType::VIRTIO_GPU_CMD_RESOURCE_FLUSH => {
let chain = desc_ring.get_chain(desc_id);
assert_eq!(chain.len(), 2);

let cmd = chain[0].as_ref::<VirtioGpuCmdResourceFlush>(&self.memory)?;
let response = chain[1].as_mut::<VirtioGpuCtrlHdr>(&self.memory)?;
response.as_mut_bytes().fill(0);

self.handle_resource_flush(cmd, response).await;

Ok(size_of::<VirtioGpuCtrlHdr>().try_into().unwrap())
}
VirtioGpuCtrlType::VIRTIO_GPU_CMD_TRANSFER_TO_HOST_2D => {
let chain = desc_ring.get_chain(desc_id);
assert_eq!(chain.len(), 2);

let cmd = chain[0].as_ref::<VirtioGpuTransferToHost2D>(&self.memory)?;
let response = chain[1].as_mut::<VirtioGpuCtrlHdr>(&self.memory)?;
response.as_mut_bytes().fill(0);

self.handle_transfer_to_host_2d(cmd, response).await;

Ok(size_of::<VirtioGpuCtrlHdr>().try_into().unwrap())
}
VirtioGpuCtrlType::VIRTIO_GPU_CMD_RESOURCE_ATTACH_BACKING => {
let chain = desc_ring.get_chain(desc_id);
assert_eq!(chain.len(), 3);

let cmd = chain[0].as_ref::<VirtioGpuResourceAttachBacking>(&self.memory)?;
let entries = chain[1]
.as_slice::<VirtioGpuMemEntry>(&self.memory, cmd.nr_entries as usize)?;
let response = chain[2].as_mut::<VirtioGpuCtrlHdr>(&self.memory)?;
response.as_mut_bytes().fill(0);

self.handle_resource_attach_backing(cmd, entries, response)
.await;

Ok(size_of::<VirtioGpuCtrlHdr>().try_into().unwrap())
}
VirtioGpuCtrlType::VIRTIO_GPU_CMD_RESOURCE_DETACH_BACKING => {
let chain = desc_ring.get_chain(desc_id);
assert_eq!(chain.len(), 2);

let cmd = chain[0].as_ref::<VirtioGpuResourceDetachBacking>(&self.memory)?;
let response = chain[1].as_mut::<VirtioGpuCtrlHdr>(&self.memory)?;
response.as_mut_bytes().fill(0);

self.handle_resource_detach_backing(cmd, response).await;

Ok(size_of::<VirtioGpuCtrlHdr>().try_into().unwrap())
}
VirtioGpuCtrlType::VIRTIO_GPU_CMD_GET_CAPSET_INFO => todo!(),
VirtioGpuCtrlType::VIRTIO_GPU_CMD_GET_CAPSET => todo!(),
VirtioGpuCtrlType::VIRTIO_GPU_CMD_GET_EDID => {
let chain = desc_ring.get_chain(desc_id);
assert_eq!(chain.len(), 2);

let cmd = chain[0].as_ref::<VirtioGpuGetEdid>(&self.memory)?;
let response = chain[1].as_mut::<VirtioGpuRespEdid>(&self.memory)?;
response.as_mut_bytes().fill(0);

self.handle_get_edid(cmd, response);

Ok(size_of::<VirtioGpuRespEdid>().try_into().unwrap())
}
VirtioGpuCtrlType::VIRTIO_GPU_CMD_RESOURCE_ASSIGN_UUID => todo!(),
VirtioGpuCtrlType::VIRTIO_GPU_CMD_RESOURCE_CREATE_BLOB => todo!(),
VirtioGpuCtrlType::VIRTIO_GPU_CMD_SET_SCANOUT_BLOB => todo!(),
VirtioGpuCtrlType::VIRTIO_GPU_CMD_CTX_CREATE => todo!(),
VirtioGpuCtrlType::VIRTIO_GPU_CMD_CTX_DESTROY => todo!(),
VirtioGpuCtrlType::VIRTIO_GPU_CMD_CTX_ATTACH_RESOURCE => todo!(),
VirtioGpuCtrlType::VIRTIO_GPU_CMD_CTX_DETACH_RESOURCE => todo!(),
VirtioGpuCtrlType::VIRTIO_GPU_CMD_RESOURCE_CREATE_3D => todo!(),
VirtioGpuCtrlType::VIRTIO_GPU_CMD_TRANSFER_TO_HOST_3D => todo!(),
VirtioGpuCtrlType::VIRTIO_GPU_CMD_TRANSFER_FROM_HOST_3D => todo!(),
VirtioGpuCtrlType::VIRTIO_GPU_CMD_SUBMIT_3D => todo!(),
VirtioGpuCtrlType::VIRTIO_GPU_CMD_RESOURCE_MAP_BLOB => todo!(),
VirtioGpuCtrlType::VIRTIO_GPU_CMD_RESOURCE_UNMAP_BLOB => todo!(),
VirtioGpuCtrlType::VIRTIO_GPU_CMD_UPDATE_CURSOR => todo!(),
VirtioGpuCtrlType::VIRTIO_GPU_CMD_MOVE_CURSOR => todo!(),
_ => Err(VirtioError::VirtioGpu(VirtioGpuError::InvalidCommand(
ctrl_type,
))),
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Malformed descriptor chains panic instead of returning an error.

Every branch in handle_command (e.g. Lines 313, 324, 336, 348, 360, 372, 384, 399, 413) asserts the exact descriptor-chain length with assert_eq!. A guest that sends a control command with the wrong number of chained buffers crashes the whole process instead of getting an error response.

Sketch
-                let chain = desc_ring.get_chain(desc_id);
-                assert_eq!(chain.len(), 2);
+                let chain = desc_ring.get_chain(desc_id);
+                if chain.len() != 2 {
+                    return Err(VirtioError::VirtioGpu(VirtioGpuError::InvalidCommand(ctrl_type)));
+                }

Apply the same pattern to every assert_eq!(chain.len(), N) site in this function.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/vm-device/src/device/virtio/virtio_gpu/controlq_handler.rs` around
lines 299 - 442, Replace every descriptor-chain assert in handle_command with
non-panicking validation that returns the appropriate VirtioError when
chain.len() differs from the command’s required descriptor count. Apply this
consistently across all handled command branches, preserving each branch’s
existing processing for valid chains and response behavior.

/// value is zero.
pub num_capsets: u32,
/// specifies the minimal alignment, in bytes, required by the device for resource blobs. The
/// value is a power of two. Minimum value is 1, maximum value is 4294967296.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Doc comment says "maximum value is 4294967296" but that overflows u32.

blob_alignment is u32 whose max is 4294967295. The value 4294967296 (2³²) cannot be represented. Clarify the comment to avoid confusion.

📝 Proposed fix
-    /// specifies the minimal alignment, in bytes, required by the device for resource blobs. The
-    /// value is a power of two. Minimum value is 1, maximum value is 4294967296.
+    /// specifies the minimal alignment, in bytes, required by the device for resource blobs. The
+    /// value is a power of two. Minimum value is 1, maximum value is 4294967295.
     pub blob_alignment: u32,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// value is a power of two. Minimum value is 1, maximum value is 4294967296.
/// specifies the minimal alignment, in bytes, required by the device for resource blobs. The
/// value is a power of two. Minimum value is 1, maximum value is 4294967295.
pub blob_alignment: u32,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/vm-virtio/src/types/device/gpu.rs` at line 43, Update the doc comment
for blob_alignment to state the representable u32 maximum, 4294967295, instead
of 4294967296. Preserve the existing power-of-two and minimum-value
documentation.

Comment on lines +42 to +52
pub fn as_ref<T>(&self, memory: &MemoryAddressSpace) -> Result<&T>
where
T: FromBytes + KnownLayout + Immutable,
{
let req: NonNull<u8> = self.addr(memory)?;

let bytes = unsafe { slice::from_raw_parts(req.as_ptr(), size_of::<T>()) };
let t = T::ref_from_bytes(bytes).map_err(|_| VirtioError::TransmuteDesc)?;

Ok(t)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

Missing self.len bounds check in as_ref and as_mut. Both methods create typed slices from guest memory using size_of::<T>() without validating that the descriptor's len is large enough, unlike as_slice which performs this check at Line 79. A descriptor with a smaller len causes out-of-bounds reads (in as_ref) or writes (in as_mut), leading to incorrect data processing or guest memory corruption.

  • crates/vm-virtio/src/virtqueue/virtq_desc_table.rs#L42-L52: Add if size_of::<T>() > self.len as usize { return Err(VirtioError::TransmuteDesc); } before creating the slice in as_ref.
  • crates/vm-virtio/src/virtqueue/virtq_desc_table.rs#L56-L66: Add the same bounds check in as_mut before creating the mutable slice.
🛡️ Proposed fix for both methods
     pub fn as_ref<T>(&self, memory: &MemoryAddressSpace) -> Result<&T>
     where
         T: FromBytes + KnownLayout + Immutable,
     {
+        if size_of::<T>() > self.len as usize {
+            return Err(VirtioError::TransmuteDesc);
+        }
         let req: NonNull<u8> = self.addr(memory)?;

         let bytes = unsafe { slice::from_raw_parts(req.as_ptr(), size_of::<T>()) };
         let t = T::ref_from_bytes(bytes).map_err(|_| VirtioError::TransmuteDesc)?;

         Ok(t)
     }
     #[allow(clippy::mut_from_ref)]
     pub fn as_mut<T>(&self, memory: &MemoryAddressSpace) -> Result<&mut T>
     where
         T: FromBytes + IntoBytes + KnownLayout,
     {
+        if size_of::<T>() > self.len as usize {
+            return Err(VirtioError::TransmuteDesc);
+        }
         let req: NonNull<u8> = self.addr(memory)?;

         let bytes = unsafe { slice::from_raw_parts_mut(req.as_ptr(), size_of::<T>()) };
         let t = T::mut_from_bytes(bytes).map_err(|_| VirtioError::TransmuteDesc)?;

         Ok(t)
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pub fn as_ref<T>(&self, memory: &MemoryAddressSpace) -> Result<&T>
where
T: FromBytes + KnownLayout + Immutable,
{
let req: NonNull<u8> = self.addr(memory)?;
let bytes = unsafe { slice::from_raw_parts(req.as_ptr(), size_of::<T>()) };
let t = T::ref_from_bytes(bytes).map_err(|_| VirtioError::TransmuteDesc)?;
Ok(t)
}
pub fn as_ref<T>(&self, memory: &MemoryAddressSpace) -> Result<&T>
where
T: FromBytes + KnownLayout + Immutable,
{
if size_of::<T>() > self.len as usize {
return Err(VirtioError::TransmuteDesc);
}
let req: NonNull<u8> = self.addr(memory)?;
let bytes = unsafe { slice::from_raw_parts(req.as_ptr(), size_of::<T>()) };
let t = T::ref_from_bytes(bytes).map_err(|_| VirtioError::TransmuteDesc)?;
Ok(t)
}
📍 Affects 1 file
  • crates/vm-virtio/src/virtqueue/virtq_desc_table.rs#L42-L52 (this comment)
  • crates/vm-virtio/src/virtqueue/virtq_desc_table.rs#L56-L66
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/vm-virtio/src/virtqueue/virtq_desc_table.rs` around lines 42 - 52,
Both VirtqDescTable::as_ref and VirtqDescTable::as_mut must validate the
descriptor length before constructing typed slices. In
crates/vm-virtio/src/virtqueue/virtq_desc_table.rs lines 42-52, add the self.len
versus size_of::<T>() check before the immutable slice creation; apply the
identical check in lines 56-66 before mutable slice creation, returning
VirtioError::TransmuteDesc when the descriptor is too short.

Comment on lines +138 to +149
pub fn get_chain_mut(&self, first_idx: u16) -> Vec<&VirtqDesc> {
let mut descs = vec![];

let mut curr = self.get(first_idx);
descs.push(curr);
while curr.flags & VIRTQ_DESC_F_NEXT != 0 {
curr = self.get(curr.next);
descs.push(curr);
}

descs
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

get_chain_mut lacks cycle detection and duplicates get_chain.

The method follows NEXT-linked descriptors without cycle detection — a malicious guest could create a circular chain (e.g., A→B→A) to hang the VMM indefinitely. The pre-existing get_chain (Lines 125-136) has the same vulnerability, but this is new code that perpetuates it.

Additionally, get_chain_mut is identical to get_chain despite the "mut" name: it takes &self and returns Vec<&VirtqDesc> (shared references). If mutable access is intended, it should take &mut self, use get_mut, and return Vec<&mut VirtqDesc>. If not, remove it and use get_chain directly.

🔒 Proposed fix: add cycle detection
 pub fn get_chain_mut(&self, first_idx: u16) -> Vec<&VirtqDesc> {
     let mut descs = vec![];

     let mut curr = self.get(first_idx);
     descs.push(curr);
     while curr.flags & VIRTQ_DESC_F_NEXT != 0 {
         curr = self.get(curr.next);
         descs.push(curr);
+        if descs.len() >= self.queue_size as usize {
+            break;
+        }
     }

     descs
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pub fn get_chain_mut(&self, first_idx: u16) -> Vec<&VirtqDesc> {
let mut descs = vec![];
let mut curr = self.get(first_idx);
descs.push(curr);
while curr.flags & VIRTQ_DESC_F_NEXT != 0 {
curr = self.get(curr.next);
descs.push(curr);
}
descs
}
pub fn get_chain_mut(&self, first_idx: u16) -> Vec<&VirtqDesc> {
let mut descs = vec![];
let mut curr = self.get(first_idx);
descs.push(curr);
while curr.flags & VIRTQ_DESC_F_NEXT != 0 {
curr = self.get(curr.next);
descs.push(curr);
if descs.len() >= self.queue_size as usize {
break;
}
}
descs
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/vm-virtio/src/virtqueue/virtq_desc_table.rs` around lines 138 - 149,
Update get_chain_mut to prevent infinite traversal by tracking visited
descriptor indices and terminating or rejecting the chain when an index repeats;
apply the same cycle protection to get_chain so both traversal methods are safe.
Resolve the misleading mut API in get_chain_mut by either removing it and
reusing get_chain, or changing it to take &mut self, call get_mut, and return
mutable descriptor references if callers require mutation.

@junyu0312
junyu0312 merged commit 11ed545 into main Jul 14, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant