From 0350f6a5c13663ec6f11f2b9c1f65c72d1b096a5 Mon Sep 17 00:00:00 2001 From: Paul Kroeher Date: Tue, 5 May 2026 10:51:09 +0200 Subject: [PATCH 01/13] ci: implementation of flake bump auto approve and merge Add a GitHub workflow to automatically validate and merge pull requests that only bump flake.lock. This reduces manual review overhead for routine dependency updates while keeping the merge path constrained and auditable. The workflow uses a dedicated gitlint config and custom rule to ensure each eligible commit changes exactly flake.lock. Documentation is added for the required GitHub App, secrets, permissions, and branch-ruleset bypass setup. Signed-off-by: Paul Kroeher On-behalf-of: SAP paul.kroeher@sap.com --- .github/workflows/flake-bump.yaml | 56 +++++++++++++++++++ .gitlint_auto_approve | 15 +++++ ci/README.auto.approve.md | 43 +++++++++++++++ ci/gitlint/rules_auto_approve/only-flake.py | 61 +++++++++++++++++++++ 4 files changed, 175 insertions(+) create mode 100644 .github/workflows/flake-bump.yaml create mode 100644 .gitlint_auto_approve create mode 100644 ci/README.auto.approve.md create mode 100644 ci/gitlint/rules_auto_approve/only-flake.py diff --git a/.github/workflows/flake-bump.yaml b/.github/workflows/flake-bump.yaml new file mode 100644 index 0000000000..4e706be49e --- /dev/null +++ b/.github/workflows/flake-bump.yaml @@ -0,0 +1,56 @@ +name: Flake bump +on: + pull_request: + paths: + - 'flake.lock' + branches: + - gardenlinux + +jobs: + gitlint: + name: Check flake bump requirements + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + ref: ${{ github.event.pull_request.head.sha }} + fetch-depth: 0 + - name: Set up Python 3.11 + uses: actions/setup-python@v6 + with: + python-version: "3.11" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install --upgrade gitlint + - name: Lint git commit messages + run: | + gitlint --commits origin/$GITHUB_BASE_REF.. -C .gitlint_auto_approve + + gitmerge: + name: Merge flake bump + needs: gitlint # hard dependency on this check job + runs-on: ubuntu-latest + steps: + - name: Generate token + id: generate_token + uses: actions/create-github-app-token@v2 + with: + app-id: ${{ secrets.GH_AUTO_APPROVE_APP_ID }} + private-key: ${{ secrets.GH_AUTO_APPROVE_APP_PRIVATE_KEY }} + owner: daedalus-ca + repositories: test-auto-approve + - name: Merge Pull request + shell: bash + env: + GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }} + run: | + # GitHub CLI api + # https://cli.github.com/manual/gh_api + gh api \ + --method PUT \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2026-03-10" \ + /repos/${GITHUB_REPOSITORY}/pulls/${{ github.event.number }}/merge \ + -f 'merge_method=rebase' diff --git a/.gitlint_auto_approve b/.gitlint_auto_approve new file mode 100644 index 0000000000..d79e9b21f5 --- /dev/null +++ b/.gitlint_auto_approve @@ -0,0 +1,15 @@ +[general] +extra-path=ci/gitlint/rules_auto_approve +regex-style-search=true +ignore=body-is-missing,body-max-line-length + +# default 72 +[title-max-length] +line-length=72 + +# Empty bodies are fine +[body-min-length] +min-length=0 + +[UC-flake] +filepath=flake.lock diff --git a/ci/README.auto.approve.md b/ci/README.auto.approve.md new file mode 100644 index 0000000000..a38d3e6e7e --- /dev/null +++ b/ci/README.auto.approve.md @@ -0,0 +1,43 @@ +# Flake bump auto approve + +## Description + +We add a github workflow `Flake bump`. +First job of this workflow checks if a merge request contains only one commit which updates the `flake.lock` file. +If this condition is met the second job approve this merge request and automatically merge it. +The approval is done with a dedicated GitHubApp. + +## Install + +* Follow this guide: https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/registering-a-github-app +* Create a GitHub app `auto-approve-app` in your GH organization + * github.com/github-organization/ -> Settings -> Developer Settings -> GitHub Apps -> New GitHub App + * Add a name and Homepage URL + * Add Repository Permissions + * Actions: RO + * Contents: RW + * Metadata: RO + * Pull Requests: RW + * Workflows: RW + +* Install this app into your organization + * github.com/github-organization/ -> Settings -> Developer Settings -> GitHub Apps -> Select `auto-approve-app` -> Install App + * Only select repositories: + * repository-name + +* Find app_id + * github.com/github-organization/ -> Settings -> Developer Settings -> GitHub Apps -> Select `auto-approve-app` + * you find the app_id in the `General` section + +* Create app client secret + * github.com/github-organization/ -> Settings -> Developer Settings -> GitHub Apps -> Select `auto-approve-app` -> Client secrets + * The private key will be downloaded using your browser + * Save it in 1Password or vault + +* Create two organization secrets: + * GH_AUTO_APPROVE_APP_ID + * GH_AUTO_APPROVE_APP_PRIVATE_KEY + +* Add Github App `auto-approve-app` to your branch ruleset. + * github.com/github-organization/repository -> Settings -> Rules -> Rulesets -> rule name -> Bypass list -> Add bypass + * This allows the Github App `auto-approve-app` to merge the MRs even if other conditions of the ruleset are not met. diff --git a/ci/gitlint/rules_auto_approve/only-flake.py b/ci/gitlint/rules_auto_approve/only-flake.py new file mode 100644 index 0000000000..567323869f --- /dev/null +++ b/ci/gitlint/rules_auto_approve/only-flake.py @@ -0,0 +1,61 @@ +# Copyright © 2026 Cyberus Technology GmbH +# +# SPDX-License-Identifier: Apache-2.0 +# +from gitlint.options import ListOption, StrOption +from gitlint.rules import CommitRule, RuleViolation + +class SingleSpecificFile(CommitRule): + """Reject commits which modifies files other than those specified""" + id = "UC-flake" + name = "body-require-single-specific-file" + description = "Commit must change exactly one specific file" + target = None # Applies to entire commit + options_spec = [ + StrOption( + "filepath", + "flake.lock", + "The file path to check" + ) + ] + + def validate(self, commit): + changed_files = getattr(commit, "changed_files", None) + if changed_files is None: + # Newer gitlint commit objects expose the touched paths directly via + # `changed_files`. Older variants may only expose + # `changed_files_stats`, a mapping keyed by changed path, so we fall + # back to its keys when `changed_files` is unavailable. + changed_files_stats = getattr(commit, "changed_files_stats", {}) + changed_files = list(changed_files_stats.keys()) + + if len(changed_files) != 1: + return [RuleViolation("commit-changes-multiple-files-or-none", f"Commit changes {len(changed_files)} files, expected exactly 1: {', '.join(changed_files)}")] + + filepath = self.options["filepath"].value + if changed_files[0] != filepath: + return [RuleViolation("commit-wrong-file", f"Commit changes '{changed_files[0]}', expected only '{filepath}'")] + +#################### +# Usage of this rule +#################### +# +# .gitlint_auto_approve file +# [general] +# extra-path=ci/gitlint/rules_auto_approve +# regex-style-search=true +# ignore=body-is-missing,body-max-line-length + +# # default 72 +# [title-max-length] +# line-length=72 + +# # Empty bodies are fine +# [body-min-length] +# min-length=0 + +# [UC-flake] +# filepath=flake.lock + +## run with +# nix run nixpkgs#gitlint -- --commits origin/main.. -C .gitlint_auto_approve From 2a6fe8e8ddfb0293f12463a60497919bcf32d16c Mon Sep 17 00:00:00 2001 From: Peter Oskolkov Date: Thu, 12 Mar 2026 09:50:37 -0700 Subject: [PATCH 02/13] virtio-devices: introduce ActivationContext for device activation Signed-off-by: Peter Oskolkov (cherry picked from commit f77c6ef78bdca486eec0e6d81033af7082f9997d) On-behalf-of: SAP paul.kroeher@sap.com --- fuzz/fuzz_targets/balloon.rs | 10 +++--- fuzz/fuzz_targets/block.rs | 10 +++--- fuzz/fuzz_targets/console.rs | 10 +++--- fuzz/fuzz_targets/iommu.rs | 10 +++--- fuzz/fuzz_targets/mem.rs | 10 +++--- fuzz/fuzz_targets/net.rs | 10 +++--- fuzz/fuzz_targets/pmem.rs | 10 +++--- fuzz/fuzz_targets/rng.rs | 10 +++--- fuzz/fuzz_targets/vsock.rs | 10 +++--- fuzz/fuzz_targets/watchdog.rs | 10 +++--- virtio-devices/src/balloon.rs | 13 ++++---- virtio-devices/src/block.rs | 12 +++---- virtio-devices/src/console.rs | 13 ++++---- virtio-devices/src/device.rs | 13 ++++---- virtio-devices/src/iommu.rs | 13 ++++---- virtio-devices/src/lib.rs | 4 +-- virtio-devices/src/mem.rs | 13 ++++---- virtio-devices/src/net.rs | 12 +++---- virtio-devices/src/pmem.rs | 13 ++++---- virtio-devices/src/rng.rs | 13 ++++---- .../src/transport/pci_common_config.rs | 12 ++----- virtio-devices/src/transport/pci_device.rs | 13 ++++---- virtio-devices/src/vdpa.rs | 13 ++++---- virtio-devices/src/vhost_user/blk.rs | 14 ++++----- virtio-devices/src/vhost_user/fs.rs | 14 ++++----- virtio-devices/src/vhost_user/net.rs | 15 ++++----- virtio-devices/src/vsock/device.rs | 31 ++++++++++--------- virtio-devices/src/watchdog.rs | 13 ++++---- vmm/src/device_manager.rs | 2 +- 29 files changed, 176 insertions(+), 170 deletions(-) diff --git a/fuzz/fuzz_targets/balloon.rs b/fuzz/fuzz_targets/balloon.rs index b745cf6187..edb4f0cf1a 100644 --- a/fuzz/fuzz_targets/balloon.rs +++ b/fuzz/fuzz_targets/balloon.rs @@ -95,15 +95,15 @@ fuzz_target!(|bytes: &[u8]| -> Corpus { reporting_queue_evt.write(1).unwrap(); balloon - .activate( - guest_memory, - Arc::new(NoopVirtioInterrupt {}), - vec![ + .activate(virtio_devices::ActivationContext { + mem: guest_memory, + interrupt_cb: Arc::new(NoopVirtioInterrupt {}), + queues: vec![ (0, inflate_q, inflate_evt), (1, deflate_q, deflate_evt), (2, reporting_q, reporting_evt), ], - ) + }) .ok(); // Wait for the events to finish and balloon device worker thread to return diff --git a/fuzz/fuzz_targets/block.rs b/fuzz/fuzz_targets/block.rs index 7d1fbdf38f..108fb3fba3 100644 --- a/fuzz/fuzz_targets/block.rs +++ b/fuzz/fuzz_targets/block.rs @@ -89,11 +89,11 @@ fuzz_target!(|bytes: &[u8]| -> Corpus { queue_evt.write(1).unwrap(); block - .activate( - guest_memory, - Arc::new(NoopVirtioInterrupt {}), - vec![(0, q, evt)], - ) + .activate(virtio_devices::ActivationContext { + mem: guest_memory, + interrupt_cb: Arc::new(NoopVirtioInterrupt {}), + queues: vec![(0, q, evt)], + }) .ok(); // Wait for the events to finish and block device worker thread to return diff --git a/fuzz/fuzz_targets/console.rs b/fuzz/fuzz_targets/console.rs index 4b3a49df91..7f5fafaebc 100644 --- a/fuzz/fuzz_targets/console.rs +++ b/fuzz/fuzz_targets/console.rs @@ -128,11 +128,11 @@ fuzz_target!(|bytes: &[u8]| -> Corpus { pipe_tx.write_all(console_input_bytes).unwrap(); // To use fuzzed data; console - .activate( - guest_memory, - Arc::new(NoopVirtioInterrupt {}), - vec![(0, input_queue, input_evt), (1, output_queue, output_evt)], - ) + .activate(virtio_devices::ActivationContext { + mem: guest_memory, + interrupt_cb: Arc::new(NoopVirtioInterrupt {}), + queues: vec![(0, input_queue, input_evt), (1, output_queue, output_evt)], + }) .unwrap(); // Wait for the events to finish and console device worker thread to return diff --git a/fuzz/fuzz_targets/iommu.rs b/fuzz/fuzz_targets/iommu.rs index 8c9f26b262..791ab6b000 100644 --- a/fuzz/fuzz_targets/iommu.rs +++ b/fuzz/fuzz_targets/iommu.rs @@ -107,14 +107,14 @@ fuzz_target!(|bytes: &[u8]| -> Corpus { request_queue_evt.write(1).unwrap(); iommu - .activate( - guest_memory, - Arc::new(NoopVirtioInterrupt {}), - vec![ + .activate(virtio_devices::ActivationContext { + mem: guest_memory, + interrupt_cb: Arc::new(NoopVirtioInterrupt {}), + queues: vec![ (0, request_queue, request_evt), (0, _event_queue, _event_evt), ], - ) + }) .ok(); // Wait for the events to finish and vIOMMU device worker thread to return diff --git a/fuzz/fuzz_targets/mem.rs b/fuzz/fuzz_targets/mem.rs index 57fc9a91dd..46627e9315 100644 --- a/fuzz/fuzz_targets/mem.rs +++ b/fuzz/fuzz_targets/mem.rs @@ -105,11 +105,11 @@ fuzz_target!(|bytes: &[u8]| -> Corpus { queue_evt.write(1).unwrap(); virtio_mem - .activate( - guest_memory, - Arc::new(NoopVirtioInterrupt {}), - vec![(0, q, evt)], - ) + .activate(virtio_devices::ActivationContext { + mem: guest_memory, + interrupt_cb: Arc::new(NoopVirtioInterrupt {}), + queues: vec![(0, q, evt)], + }) .ok(); // Wait for the events to finish and virtio-mem device worker thread to return diff --git a/fuzz/fuzz_targets/net.rs b/fuzz/fuzz_targets/net.rs index 30968d2a47..0af835ac06 100644 --- a/fuzz/fuzz_targets/net.rs +++ b/fuzz/fuzz_targets/net.rs @@ -143,11 +143,11 @@ fuzz_target!(|bytes: &[u8]| -> Corpus { input_queue_evt.write(1).unwrap(); output_queue_evt.write(1).unwrap(); - net.activate( - guest_memory, - Arc::new(NoopVirtioInterrupt {}), - vec![(0, input_queue, input_evt), (1, output_queue, output_evt)], - ) + net.activate(virtio_devices::ActivationContext { + mem: guest_memory, + interrupt_cb: Arc::new(NoopVirtioInterrupt {}), + queues: vec![(0, input_queue, input_evt), (1, output_queue, output_evt)], + }) .unwrap(); // Wait for the events to finish and net device worker thread to return diff --git a/fuzz/fuzz_targets/pmem.rs b/fuzz/fuzz_targets/pmem.rs index a8fcb7a774..b42c20daea 100644 --- a/fuzz/fuzz_targets/pmem.rs +++ b/fuzz/fuzz_targets/pmem.rs @@ -61,11 +61,11 @@ fuzz_target!(|bytes: &[u8]| -> Corpus { // Kick the 'queue' event before activate the pmem device queue_evt.write(1).unwrap(); - pmem.activate( - guest_memory, - Arc::new(NoopVirtioInterrupt {}), - vec![(0, q, evt)], - ) + pmem.activate(virtio_devices::ActivationContext { + mem: guest_memory, + interrupt_cb: Arc::new(NoopVirtioInterrupt {}), + queues: vec![(0, q, evt)], + }) .ok(); // Wait for the events to finish and pmem device worker thread to return diff --git a/fuzz/fuzz_targets/rng.rs b/fuzz/fuzz_targets/rng.rs index 8d5ffe35b3..d9cd11f099 100644 --- a/fuzz/fuzz_targets/rng.rs +++ b/fuzz/fuzz_targets/rng.rs @@ -99,11 +99,11 @@ fuzz_target!(|bytes: &[u8]| -> Corpus { // Kick the 'queue' event before activate the rng device queue_evt.write(1).unwrap(); - rng.activate( - guest_memory, - Arc::new(NoopVirtioInterrupt {}), - vec![(0, q, evt)], - ) + rng.activate(virtio_devices::ActivationContext { + mem: guest_memory, + interrupt_cb: Arc::new(NoopVirtioInterrupt {}), + queues: vec![(0, q, evt)], + }) .ok(); // Wait for the events to finish and rng device worker thread to return diff --git a/fuzz/fuzz_targets/vsock.rs b/fuzz/fuzz_targets/vsock.rs index 144b8b4057..72bdeb4d63 100644 --- a/fuzz/fuzz_targets/vsock.rs +++ b/fuzz/fuzz_targets/vsock.rs @@ -108,11 +108,11 @@ fuzz_target!(|bytes: &[u8]| -> Corpus { .unwrap(); vsock - .activate( - guest_memory, - Arc::new(NoopVirtioInterrupt {}), - vec![(0, q, evt)], - ) + .activate(virtio_devices::ActivationContext { + mem: guest_memory, + interrupt_cb: Arc::new(NoopVirtioInterrupt {}), + queues: vec![(0, q, evt)], + }) .ok(); // Wait for the events to finish and vsock device worker thread to return diff --git a/fuzz/fuzz_targets/watchdog.rs b/fuzz/fuzz_targets/watchdog.rs index f203a228f9..8736f8af3f 100644 --- a/fuzz/fuzz_targets/watchdog.rs +++ b/fuzz/fuzz_targets/watchdog.rs @@ -64,11 +64,11 @@ fuzz_target!(|bytes: &[u8]| -> Corpus { queue_evt.write(1).unwrap(); watchdog - .activate( - guest_memory, - Arc::new(NoopVirtioInterrupt {}), - vec![(0, q, evt)], - ) + .activate(virtio_devices::ActivationContext { + mem: guest_memory, + interrupt_cb: Arc::new(NoopVirtioInterrupt {}), + queues: vec![(0, q, evt)], + }) .ok(); // Wait for the events to finish and watchdog device worker thread to return diff --git a/virtio-devices/src/balloon.rs b/virtio-devices/src/balloon.rs index 3db6832617..bb9c46cbc8 100644 --- a/virtio-devices/src/balloon.rs +++ b/virtio-devices/src/balloon.rs @@ -590,12 +590,13 @@ impl VirtioDevice for Balloon { } } - fn activate( - &mut self, - mem: GuestMemoryAtomic, - interrupt_cb: Arc, - mut queues: Vec<(usize, Queue, EventFd)>, - ) -> ActivateResult { + fn activate(&mut self, context: crate::device::ActivationContext) -> ActivateResult { + let crate::device::ActivationContext { + mem, + interrupt_cb, + mut queues, + .. + } = context; self.common.activate(&queues, interrupt_cb.clone())?; let (kill_evt, pause_evt) = self.common.dup_eventfds(); diff --git a/virtio-devices/src/block.rs b/virtio-devices/src/block.rs index 805a7c6155..0477ced070 100644 --- a/virtio-devices/src/block.rs +++ b/virtio-devices/src/block.rs @@ -1011,12 +1011,12 @@ impl VirtioDevice for Block { self.update_writeback(); } - fn activate( - &mut self, - mem: GuestMemoryAtomic, - interrupt_cb: Arc, - mut queues: Vec<(usize, Queue, EventFd)>, - ) -> ActivateResult { + fn activate(&mut self, context: crate::device::ActivationContext) -> ActivateResult { + let crate::device::ActivationContext { + mem, + interrupt_cb, + mut queues, + } = context; // See if the guest didn't ack the device being read-only. // If so, warn and pretend it did. let original_acked_features = self.common.acked_features; diff --git a/virtio-devices/src/console.rs b/virtio-devices/src/console.rs index c8a9f08a02..760864b3a5 100644 --- a/virtio-devices/src/console.rs +++ b/virtio-devices/src/console.rs @@ -703,12 +703,13 @@ impl VirtioDevice for Console { self.read_config_from_slice(self.config.lock().unwrap().as_slice(), offset, data); } - fn activate( - &mut self, - mem: GuestMemoryAtomic, - interrupt_cb: Arc, - mut queues: Vec<(usize, Queue, EventFd)>, - ) -> ActivateResult { + fn activate(&mut self, context: crate::device::ActivationContext) -> ActivateResult { + let crate::device::ActivationContext { + mem, + interrupt_cb, + mut queues, + .. + } = context; self.common.activate(&queues, interrupt_cb.clone())?; self.resizer .acked_features diff --git a/virtio-devices/src/device.rs b/virtio-devices/src/device.rs index 8289cdf116..05c15e0add 100644 --- a/virtio-devices/src/device.rs +++ b/virtio-devices/src/device.rs @@ -54,6 +54,12 @@ pub struct VirtioSharedMemoryList { pub region_list: Vec, } +pub struct ActivationContext { + pub mem: GuestMemoryAtomic, + pub interrupt_cb: Arc, + pub queues: Vec<(usize, Queue, EventFd)>, +} + /// Trait for virtio devices to be driven by a virtio transport. /// /// The lifecycle of a virtio device is to be moved to a virtio transport, which will then query the @@ -95,12 +101,7 @@ pub trait VirtioDevice: Send { } /// Activates this device for real usage. - fn activate( - &mut self, - mem: GuestMemoryAtomic, - interrupt_evt: Arc, - queues: Vec<(usize, Queue, EventFd)>, - ) -> ActivateResult; + fn activate(&mut self, context: ActivationContext) -> ActivateResult; /// Optionally deactivates this device and returns ownership of the guest memory map, interrupt /// event, and queue events. diff --git a/virtio-devices/src/iommu.rs b/virtio-devices/src/iommu.rs index f4812b04fb..87f8121696 100644 --- a/virtio-devices/src/iommu.rs +++ b/virtio-devices/src/iommu.rs @@ -1075,12 +1075,13 @@ impl VirtioDevice for Iommu { self.update_bypass(); } - fn activate( - &mut self, - mem: GuestMemoryAtomic, - interrupt_cb: Arc, - mut queues: Vec<(usize, Queue, EventFd)>, - ) -> ActivateResult { + fn activate(&mut self, context: crate::device::ActivationContext) -> ActivateResult { + let crate::device::ActivationContext { + mem, + interrupt_cb, + mut queues, + .. + } = context; self.common.activate(&queues, interrupt_cb.clone())?; let (kill_evt, pause_evt) = self.common.dup_eventfds(); diff --git a/virtio-devices/src/lib.rs b/virtio-devices/src/lib.rs index d2d428299d..aae1ece387 100644 --- a/virtio-devices/src/lib.rs +++ b/virtio-devices/src/lib.rs @@ -42,8 +42,8 @@ pub use self::balloon::Balloon; pub use self::block::{Block, BlockState}; pub use self::console::{Console, ConsoleResizer, Endpoint}; pub use self::device::{ - DmaRemapping, PostMigrationAnnouncer, VirtioCommon, VirtioDevice, VirtioInterrupt, - VirtioInterruptType, VirtioSharedMemoryList, + ActivationContext, DmaRemapping, PostMigrationAnnouncer, VirtioCommon, VirtioDevice, + VirtioInterrupt, VirtioInterruptType, VirtioSharedMemoryList, }; pub use self::epoll_helper::{ EPOLL_HELPER_EVENT_LAST, EpollHelper, EpollHelperError, EpollHelperHandler, diff --git a/virtio-devices/src/mem.rs b/virtio-devices/src/mem.rs index 936fdbe42a..aed8ed48d2 100644 --- a/virtio-devices/src/mem.rs +++ b/virtio-devices/src/mem.rs @@ -950,12 +950,13 @@ impl VirtioDevice for Mem { self.read_config_from_slice(self.config.lock().unwrap().as_slice(), offset, data); } - fn activate( - &mut self, - mem: GuestMemoryAtomic, - interrupt_cb: Arc, - mut queues: Vec<(usize, Queue, EventFd)>, - ) -> ActivateResult { + fn activate(&mut self, context: crate::device::ActivationContext) -> ActivateResult { + let crate::device::ActivationContext { + mem, + interrupt_cb, + mut queues, + .. + } = context; self.common.activate(&queues, interrupt_cb.clone())?; let (kill_evt, pause_evt) = self.common.dup_eventfds(); diff --git a/virtio-devices/src/net.rs b/virtio-devices/src/net.rs index 97e6349cec..a63b9978bc 100644 --- a/virtio-devices/src/net.rs +++ b/virtio-devices/src/net.rs @@ -827,12 +827,12 @@ impl VirtioDevice for Net { self.read_config_from_slice(config.as_slice(), offset, data); } - fn activate( - &mut self, - mem: GuestMemoryAtomic, - interrupt_cb: Arc, - mut queues: Vec<(usize, Queue, EventFd)>, - ) -> ActivateResult { + fn activate(&mut self, context: crate::device::ActivationContext) -> ActivateResult { + let crate::device::ActivationContext { + mem, + interrupt_cb, + mut queues, + } = context; self.common.activate(&queues, interrupt_cb.clone())?; let num_queues = queues.len(); diff --git a/virtio-devices/src/pmem.rs b/virtio-devices/src/pmem.rs index 549b62fd96..10cce76cb3 100644 --- a/virtio-devices/src/pmem.rs +++ b/virtio-devices/src/pmem.rs @@ -377,12 +377,13 @@ impl VirtioDevice for Pmem { self.read_config_from_slice(self.config.as_slice(), offset, data); } - fn activate( - &mut self, - mem: GuestMemoryAtomic, - interrupt_cb: Arc, - mut queues: Vec<(usize, Queue, EventFd)>, - ) -> ActivateResult { + fn activate(&mut self, context: crate::device::ActivationContext) -> ActivateResult { + let crate::device::ActivationContext { + mem, + interrupt_cb, + mut queues, + .. + } = context; self.common.activate(&queues, interrupt_cb.clone())?; let (kill_evt, pause_evt) = self.common.dup_eventfds(); if let Some(disk) = self.disk.as_ref() { diff --git a/virtio-devices/src/rng.rs b/virtio-devices/src/rng.rs index 2f980d4d8b..a3412a9e07 100644 --- a/virtio-devices/src/rng.rs +++ b/virtio-devices/src/rng.rs @@ -244,12 +244,13 @@ impl VirtioDevice for Rng { self.common.ack_features(value); } - fn activate( - &mut self, - mem: GuestMemoryAtomic, - interrupt_cb: Arc, - mut queues: Vec<(usize, Queue, EventFd)>, - ) -> ActivateResult { + fn activate(&mut self, context: crate::device::ActivationContext) -> ActivateResult { + let crate::device::ActivationContext { + mem, + interrupt_cb, + mut queues, + .. + } = context; self.common.activate(&queues, interrupt_cb.clone())?; let (kill_evt, pause_evt) = self.common.dup_eventfds(); diff --git a/virtio-devices/src/transport/pci_common_config.rs b/virtio-devices/src/transport/pci_common_config.rs index 07f5b4fc0f..d75e7c054e 100644 --- a/virtio-devices/src/transport/pci_common_config.rs +++ b/virtio-devices/src/transport/pci_common_config.rs @@ -404,11 +404,8 @@ impl Snapshottable for VirtioPciCommonConfig { #[cfg(test)] mod unit_tests { - use vm_memory::GuestMemoryAtomic; - use vmm_sys_util::eventfd::EventFd; - use super::*; - use crate::{ActivateResult, GuestMemoryMmap, VirtioInterrupt}; + use crate::{ActivateResult, ActivationContext}; struct DummyDevice(u32); const QUEUE_SIZE: u16 = 256; @@ -421,12 +418,7 @@ mod unit_tests { fn queue_max_sizes(&self) -> &[u16] { QUEUE_SIZES } - fn activate( - &mut self, - _mem: GuestMemoryAtomic, - _interrupt_evt: Arc, - _queues: Vec<(usize, Queue, EventFd)>, - ) -> ActivateResult { + fn activate(&mut self, _context: ActivationContext) -> ActivateResult { Ok(()) } diff --git a/virtio-devices/src/transport/pci_device.rs b/virtio-devices/src/transport/pci_device.rs index 408611e29a..a0d244bcb7 100644 --- a/virtio-devices/src/transport/pci_device.rs +++ b/virtio-devices/src/transport/pci_device.rs @@ -291,12 +291,13 @@ pub struct VirtioPciDeviceActivator { } impl VirtioPciDeviceActivator { - pub fn activate(&mut self) -> ActivateResult { - self.device.lock().unwrap().activate( - self.memory.take().unwrap(), - self.interrupt.take().unwrap(), - self.queues.take().unwrap(), - )?; + pub fn activate(mut self) -> ActivateResult { + let mut locked_device = self.device.lock().unwrap(); + locked_device.activate(crate::device::ActivationContext { + mem: self.memory.take().unwrap(), + interrupt_cb: self.interrupt.take().unwrap(), + queues: self.queues.take().unwrap(), + })?; self.device_activated.store(true, Ordering::SeqCst); if let Some(barrier) = self.barrier.take() { diff --git a/virtio-devices/src/vdpa.rs b/virtio-devices/src/vdpa.rs index 725f215c77..1996d94470 100644 --- a/virtio-devices/src/vdpa.rs +++ b/virtio-devices/src/vdpa.rs @@ -428,12 +428,13 @@ impl VirtioDevice for Vdpa { } } - fn activate( - &mut self, - mem: GuestMemoryAtomic, - virtio_interrupt: Arc, - queues: Vec<(usize, Queue, EventFd)>, - ) -> ActivateResult { + fn activate(&mut self, context: crate::device::ActivationContext) -> ActivateResult { + let crate::device::ActivationContext { + mem, + interrupt_cb: virtio_interrupt, + queues, + .. + } = context; self.activate_vdpa(&mem.memory(), virtio_interrupt.as_ref(), &queues) .map_err(ActivateError::ActivateVdpa)?; diff --git a/virtio-devices/src/vhost_user/blk.rs b/virtio-devices/src/vhost_user/blk.rs index d26350c91a..ea52872aa1 100644 --- a/virtio-devices/src/vhost_user/blk.rs +++ b/virtio-devices/src/vhost_user/blk.rs @@ -19,7 +19,6 @@ use virtio_bindings::virtio_blk::{ VIRTIO_BLK_F_GEOMETRY, VIRTIO_BLK_F_MQ, VIRTIO_BLK_F_RO, VIRTIO_BLK_F_SEG_MAX, VIRTIO_BLK_F_SIZE_MAX, VIRTIO_BLK_F_TOPOLOGY, VIRTIO_BLK_F_WRITE_ZEROES, }; -use virtio_queue::Queue; use vm_memory::{ByteValued, GuestMemoryAtomic}; use vm_migration::protocol::MemoryRangeTable; use vm_migration::{Migratable, MigratableError, Pausable, Snapshot, Snapshottable, Transportable}; @@ -279,12 +278,13 @@ impl VirtioDevice for Blk { } } - fn activate( - &mut self, - mem: GuestMemoryAtomic, - interrupt_cb: Arc, - queues: Vec<(usize, Queue, EventFd)>, - ) -> ActivateResult { + fn activate(&mut self, context: crate::device::ActivationContext) -> ActivateResult { + let crate::device::ActivationContext { + mem, + interrupt_cb, + queues, + .. + } = context; self.common.activate(&queues, interrupt_cb.clone())?; self.guest_memory = Some(mem.clone()); diff --git a/virtio-devices/src/vhost_user/fs.rs b/virtio-devices/src/vhost_user/fs.rs index d0005af90f..ca8c72539d 100644 --- a/virtio-devices/src/vhost_user/fs.rs +++ b/virtio-devices/src/vhost_user/fs.rs @@ -12,7 +12,6 @@ use serde::{Deserialize, Serialize}; use serde_with::{Bytes, serde_as}; use vhost::vhost_user::message::{VhostUserProtocolFeatures, VhostUserVirtioFeatures}; use vhost::vhost_user::{FrontendReqHandler, VhostUserFrontend, VhostUserFrontendReqHandler}; -use virtio_queue::Queue; use vm_device::UserspaceMapping; use vm_memory::{ByteValued, GuestMemoryAtomic}; use vm_migration::protocol::MemoryRangeTable; @@ -261,12 +260,13 @@ impl VirtioDevice for Fs { self.read_config_from_slice(self.config.as_slice(), offset, data); } - fn activate( - &mut self, - mem: GuestMemoryAtomic, - interrupt_cb: Arc, - queues: Vec<(usize, Queue, EventFd)>, - ) -> ActivateResult { + fn activate(&mut self, context: crate::device::ActivationContext) -> ActivateResult { + let crate::device::ActivationContext { + mem, + interrupt_cb, + queues, + .. + } = context; self.common.activate(&queues, interrupt_cb.clone())?; self.guest_memory = Some(mem.clone()); diff --git a/virtio-devices/src/vhost_user/net.rs b/virtio-devices/src/vhost_user/net.rs index 667c3747b2..4b77b4f91c 100644 --- a/virtio-devices/src/vhost_user/net.rs +++ b/virtio-devices/src/vhost_user/net.rs @@ -20,7 +20,7 @@ use virtio_bindings::virtio_net::{ VIRTIO_NET_F_STATUS, VIRTIO_NET_S_ANNOUNCE, VIRTIO_NET_S_LINK_UP, }; use virtio_bindings::virtio_ring::VIRTIO_RING_F_EVENT_IDX; -use virtio_queue::{Queue, QueueT}; +use virtio_queue::QueueT; use vm_memory::{ByteValued, GuestMemoryAtomic}; use vm_migration::protocol::MemoryRangeTable; use vm_migration::{Migratable, MigratableError, Pausable, Snapshot, Snapshottable, Transportable}; @@ -372,12 +372,13 @@ impl VirtioDevice for Net { self.read_config_from_slice(config.as_slice(), offset, data); } - fn activate( - &mut self, - mem: GuestMemoryAtomic, - interrupt_cb: Arc, - mut queues: Vec<(usize, Queue, EventFd)>, - ) -> ActivateResult { + fn activate(&mut self, context: crate::device::ActivationContext) -> ActivateResult { + let crate::device::ActivationContext { + mem, + interrupt_cb, + mut queues, + .. + } = context; self.common.activate(&queues, interrupt_cb.clone())?; self.guest_memory = Some(mem.clone()); diff --git a/virtio-devices/src/vsock/device.rs b/virtio-devices/src/vsock/device.rs index 27a0af1ff2..9acea8d707 100644 --- a/virtio-devices/src/vsock/device.rs +++ b/virtio-devices/src/vsock/device.rs @@ -435,12 +435,13 @@ where } } - fn activate( - &mut self, - mem: GuestMemoryAtomic, - interrupt_cb: Arc, - queues: Vec<(usize, Queue, EventFd)>, - ) -> ActivateResult { + fn activate(&mut self, context: crate::device::ActivationContext) -> ActivateResult { + let crate::device::ActivationContext { + mem, + interrupt_cb, + queues, + .. + } = context; self.common.activate(&queues, interrupt_cb.clone())?; let (kill_evt, pause_evt) = self.common.dup_eventfds(); @@ -593,9 +594,11 @@ mod unit_tests { let memory = GuestMemoryAtomic::new(ctx.mem.clone()); // Test a bad activation. - let bad_activate = - ctx.device - .activate(memory.clone(), Arc::new(NoopVirtioInterrupt {}), Vec::new()); + let bad_activate = ctx.device.activate(crate::device::ActivationContext { + mem: memory.clone(), + interrupt_cb: Arc::new(NoopVirtioInterrupt {}), + queues: Vec::new(), + }); match bad_activate { Err(ActivateError::BadActivate) => (), other => panic!("{other:?}"), @@ -603,10 +606,10 @@ mod unit_tests { // Test a correct activation. ctx.device - .activate( - memory, - Arc::new(NoopVirtioInterrupt {}), - vec![ + .activate(crate::device::ActivationContext { + mem: memory, + interrupt_cb: Arc::new(NoopVirtioInterrupt {}), + queues: vec![ ( 0, Queue::new(256).unwrap(), @@ -623,7 +626,7 @@ mod unit_tests { EventFd::new(EFD_NONBLOCK).unwrap(), ), ], - ) + }) .unwrap(); } diff --git a/virtio-devices/src/watchdog.rs b/virtio-devices/src/watchdog.rs index 6b9f7cc0ac..742a2e0241 100644 --- a/virtio-devices/src/watchdog.rs +++ b/virtio-devices/src/watchdog.rs @@ -326,12 +326,13 @@ impl VirtioDevice for Watchdog { self.common.ack_features(value); } - fn activate( - &mut self, - mem: GuestMemoryAtomic, - interrupt_cb: Arc, - mut queues: Vec<(usize, Queue, EventFd)>, - ) -> ActivateResult { + fn activate(&mut self, context: crate::device::ActivationContext) -> ActivateResult { + let crate::device::ActivationContext { + mem, + interrupt_cb, + mut queues, + .. + } = context; self.common.activate(&queues, interrupt_cb.clone())?; let (kill_evt, pause_evt) = self.common.dup_eventfds(); diff --git a/vmm/src/device_manager.rs b/vmm/src/device_manager.rs index 1e6293c350..8911e8e405 100644 --- a/vmm/src/device_manager.rs +++ b/vmm/src/device_manager.rs @@ -4566,7 +4566,7 @@ impl DeviceManager { } pub fn activate_virtio_devices(&self) -> DeviceManagerResult<()> { - for mut activator in self.pending_activations.lock().unwrap().drain(..) { + for activator in self.pending_activations.lock().unwrap().drain(..) { activator .activate() .map_err(DeviceManagerError::VirtioActivate)?; From 779ee4f3abf9f9b0f193959789cdc1bdb90256ea Mon Sep 17 00:00:00 2001 From: Peter Oskolkov Date: Thu, 12 Mar 2026 09:51:50 -0700 Subject: [PATCH 03/13] virtio-devices: switch driver_status to Arc Signed-off-by: Peter Oskolkov (cherry picked from commit 21bd3ae91661104a7c58662316b4538eac09fbc7) On-behalf-of: SAP paul.kroeher@sap.com --- virtio-devices/src/transport/pci_common_config.rs | 14 +++++++------- virtio-devices/src/transport/pci_device.rs | 10 ++++++---- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/virtio-devices/src/transport/pci_common_config.rs b/virtio-devices/src/transport/pci_common_config.rs index d75e7c054e..ec4542948f 100644 --- a/virtio-devices/src/transport/pci_common_config.rs +++ b/virtio-devices/src/transport/pci_common_config.rs @@ -6,7 +6,7 @@ // // SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause -use std::sync::atomic::{AtomicU16, Ordering}; +use std::sync::atomic::{AtomicU8, AtomicU16, Ordering}; use std::sync::{Arc, Mutex}; use byteorder::{ByteOrder, LittleEndian}; @@ -125,7 +125,7 @@ pub fn get_vring_size(t: VringType, queue_size: u16) -> u64 { /// le64 queue_used; // 0x30 // read-write pub struct VirtioPciCommonConfig { pub access_platform: Option>, - pub driver_status: u8, + pub driver_status: Arc, pub config_generation: u8, pub device_feature_select: u32, pub driver_feature_select: u32, @@ -141,7 +141,7 @@ impl VirtioPciCommonConfig { ) -> Self { VirtioPciCommonConfig { access_platform, - driver_status: state.driver_status, + driver_status: Arc::new(AtomicU8::new(state.driver_status)), config_generation: state.config_generation, device_feature_select: state.device_feature_select, driver_feature_select: state.driver_feature_select, @@ -153,7 +153,7 @@ impl VirtioPciCommonConfig { fn state(&self) -> VirtioPciCommonConfigState { VirtioPciCommonConfigState { - driver_status: self.driver_status, + driver_status: self.driver_status.load(Ordering::Acquire), config_generation: self.config_generation, device_feature_select: self.device_feature_select, driver_feature_select: self.driver_feature_select, @@ -223,7 +223,7 @@ impl VirtioPciCommonConfig { debug!("read_common_config_byte: offset 0x{offset:x}"); // The driver is only allowed to do aligned, properly sized access. match offset { - 0x14 => self.driver_status, + 0x14 => self.driver_status.load(Ordering::Acquire), 0x15 => self.config_generation, _ => { warn!("invalid virtio config byte read: 0x{offset:x}"); @@ -235,7 +235,7 @@ impl VirtioPciCommonConfig { fn write_common_config_byte(&mut self, offset: u64, value: u8) { debug!("write_common_config_byte: offset 0x{offset:x}"); match offset { - 0x14 => self.driver_status = value, + 0x14 => self.driver_status.store(value, Ordering::Release), _ => { warn!("invalid virtio config byte write: 0x{offset:x}"); } @@ -437,7 +437,7 @@ mod unit_tests { fn write_base_regs() { let mut regs = VirtioPciCommonConfig { access_platform: None, - driver_status: 0xaa, + driver_status: Arc::new(AtomicU8::new(0xaa)), config_generation: 0x55, device_feature_select: 0x0, driver_feature_select: 0x0, diff --git a/virtio-devices/src/transport/pci_device.rs b/virtio-devices/src/transport/pci_device.rs index a0d244bcb7..08b0506938 100644 --- a/virtio-devices/src/transport/pci_device.rs +++ b/virtio-devices/src/transport/pci_device.rs @@ -642,13 +642,13 @@ impl VirtioPciDevice { fn is_driver_ready(&self) -> bool { let ready_bits = (DEVICE_ACKNOWLEDGE | DEVICE_DRIVER | DEVICE_DRIVER_OK | DEVICE_FEATURES_OK) as u8; - self.common_config.driver_status == ready_bits - && self.common_config.driver_status & DEVICE_FAILED as u8 == 0 + let driver_status = self.common_config.driver_status.load(Ordering::SeqCst); + driver_status == ready_bits && (driver_status & DEVICE_FAILED as u8) == 0 } /// Determines if the driver has requested the device (re)init / reset itself fn is_driver_init(&self) -> bool { - self.common_config.driver_status == DEVICE_INIT as u8 + self.common_config.driver_status.load(Ordering::SeqCst) == DEVICE_INIT as u8 } pub fn config_bar_addr(&self) -> u64 { @@ -1220,7 +1220,9 @@ impl PciDevice for VirtioPciDevice { self.common_config.queue_select = 0; } else { error!("Attempt to reset device when not implemented in underlying device"); - self.common_config.driver_status = crate::DEVICE_FAILED as u8; + self.common_config + .driver_status + .store(crate::DEVICE_FAILED as u8, Ordering::SeqCst); } } From c2bbbcbb201082dc726db0fb9a2f6ba8044d6817 Mon Sep 17 00:00:00 2001 From: Peter Oskolkov Date: Thu, 12 Mar 2026 09:55:03 -0700 Subject: [PATCH 04/13] virtio-devices: wire driver_status to EpollHandler Signed-off-by: Peter Oskolkov (cherry picked from commit b5053ae4dededfda406bc36c8c1956c4fc0ed704) On-behalf-of: SAP paul.kroeher@sap.com --- fuzz/fuzz_targets/balloon.rs | 1 + fuzz/fuzz_targets/block.rs | 1 + fuzz/fuzz_targets/console.rs | 1 + fuzz/fuzz_targets/iommu.rs | 1 + fuzz/fuzz_targets/mem.rs | 1 + fuzz/fuzz_targets/net.rs | 1 + fuzz/fuzz_targets/pmem.rs | 1 + fuzz/fuzz_targets/rng.rs | 1 + fuzz/fuzz_targets/vsock.rs | 1 + fuzz/fuzz_targets/watchdog.rs | 1 + virtio-devices/src/block.rs | 9 ++++++++- virtio-devices/src/device.rs | 3 ++- virtio-devices/src/net.rs | 10 +++++++++- virtio-devices/src/transport/pci_device.rs | 5 ++++- virtio-devices/src/vsock/device.rs | 2 ++ 15 files changed, 35 insertions(+), 4 deletions(-) diff --git a/fuzz/fuzz_targets/balloon.rs b/fuzz/fuzz_targets/balloon.rs index edb4f0cf1a..58b9b30582 100644 --- a/fuzz/fuzz_targets/balloon.rs +++ b/fuzz/fuzz_targets/balloon.rs @@ -103,6 +103,7 @@ fuzz_target!(|bytes: &[u8]| -> Corpus { (1, deflate_q, deflate_evt), (2, reporting_q, reporting_evt), ], + device_status: Arc::new(std::sync::atomic::AtomicU8::new(0)), }) .ok(); diff --git a/fuzz/fuzz_targets/block.rs b/fuzz/fuzz_targets/block.rs index 108fb3fba3..12a4dcc726 100644 --- a/fuzz/fuzz_targets/block.rs +++ b/fuzz/fuzz_targets/block.rs @@ -93,6 +93,7 @@ fuzz_target!(|bytes: &[u8]| -> Corpus { mem: guest_memory, interrupt_cb: Arc::new(NoopVirtioInterrupt {}), queues: vec![(0, q, evt)], + device_status: Arc::new(std::sync::atomic::AtomicU8::new(0)), }) .ok(); diff --git a/fuzz/fuzz_targets/console.rs b/fuzz/fuzz_targets/console.rs index 7f5fafaebc..e27331ed01 100644 --- a/fuzz/fuzz_targets/console.rs +++ b/fuzz/fuzz_targets/console.rs @@ -132,6 +132,7 @@ fuzz_target!(|bytes: &[u8]| -> Corpus { mem: guest_memory, interrupt_cb: Arc::new(NoopVirtioInterrupt {}), queues: vec![(0, input_queue, input_evt), (1, output_queue, output_evt)], + device_status: Arc::new(std::sync::atomic::AtomicU8::new(0)), }) .unwrap(); diff --git a/fuzz/fuzz_targets/iommu.rs b/fuzz/fuzz_targets/iommu.rs index 791ab6b000..a10640487f 100644 --- a/fuzz/fuzz_targets/iommu.rs +++ b/fuzz/fuzz_targets/iommu.rs @@ -114,6 +114,7 @@ fuzz_target!(|bytes: &[u8]| -> Corpus { (0, request_queue, request_evt), (0, _event_queue, _event_evt), ], + device_status: Arc::new(std::sync::atomic::AtomicU8::new(0)), }) .ok(); diff --git a/fuzz/fuzz_targets/mem.rs b/fuzz/fuzz_targets/mem.rs index 46627e9315..73ec11b025 100644 --- a/fuzz/fuzz_targets/mem.rs +++ b/fuzz/fuzz_targets/mem.rs @@ -109,6 +109,7 @@ fuzz_target!(|bytes: &[u8]| -> Corpus { mem: guest_memory, interrupt_cb: Arc::new(NoopVirtioInterrupt {}), queues: vec![(0, q, evt)], + device_status: Arc::new(std::sync::atomic::AtomicU8::new(0)), }) .ok(); diff --git a/fuzz/fuzz_targets/net.rs b/fuzz/fuzz_targets/net.rs index 0af835ac06..df9a1dce5a 100644 --- a/fuzz/fuzz_targets/net.rs +++ b/fuzz/fuzz_targets/net.rs @@ -147,6 +147,7 @@ fuzz_target!(|bytes: &[u8]| -> Corpus { mem: guest_memory, interrupt_cb: Arc::new(NoopVirtioInterrupt {}), queues: vec![(0, input_queue, input_evt), (1, output_queue, output_evt)], + device_status: Arc::new(std::sync::atomic::AtomicU8::new(0)), }) .unwrap(); diff --git a/fuzz/fuzz_targets/pmem.rs b/fuzz/fuzz_targets/pmem.rs index b42c20daea..0bd083a1c2 100644 --- a/fuzz/fuzz_targets/pmem.rs +++ b/fuzz/fuzz_targets/pmem.rs @@ -65,6 +65,7 @@ fuzz_target!(|bytes: &[u8]| -> Corpus { mem: guest_memory, interrupt_cb: Arc::new(NoopVirtioInterrupt {}), queues: vec![(0, q, evt)], + device_status: Arc::new(std::sync::atomic::AtomicU8::new(0)), }) .ok(); diff --git a/fuzz/fuzz_targets/rng.rs b/fuzz/fuzz_targets/rng.rs index d9cd11f099..13548664a8 100644 --- a/fuzz/fuzz_targets/rng.rs +++ b/fuzz/fuzz_targets/rng.rs @@ -103,6 +103,7 @@ fuzz_target!(|bytes: &[u8]| -> Corpus { mem: guest_memory, interrupt_cb: Arc::new(NoopVirtioInterrupt {}), queues: vec![(0, q, evt)], + device_status: Arc::new(std::sync::atomic::AtomicU8::new(0)), }) .ok(); diff --git a/fuzz/fuzz_targets/vsock.rs b/fuzz/fuzz_targets/vsock.rs index 72bdeb4d63..33ebe78886 100644 --- a/fuzz/fuzz_targets/vsock.rs +++ b/fuzz/fuzz_targets/vsock.rs @@ -112,6 +112,7 @@ fuzz_target!(|bytes: &[u8]| -> Corpus { mem: guest_memory, interrupt_cb: Arc::new(NoopVirtioInterrupt {}), queues: vec![(0, q, evt)], + device_status: Arc::new(std::sync::atomic::AtomicU8::new(0)), }) .ok(); diff --git a/fuzz/fuzz_targets/watchdog.rs b/fuzz/fuzz_targets/watchdog.rs index 8736f8af3f..31361755df 100644 --- a/fuzz/fuzz_targets/watchdog.rs +++ b/fuzz/fuzz_targets/watchdog.rs @@ -68,6 +68,7 @@ fuzz_target!(|bytes: &[u8]| -> Corpus { mem: guest_memory, interrupt_cb: Arc::new(NoopVirtioInterrupt {}), queues: vec![(0, q, evt)], + device_status: Arc::new(std::sync::atomic::AtomicU8::new(0)), }) .ok(); diff --git a/virtio-devices/src/block.rs b/virtio-devices/src/block.rs index 0477ced070..d49ec49257 100644 --- a/virtio-devices/src/block.rs +++ b/virtio-devices/src/block.rs @@ -13,7 +13,7 @@ use std::num::Wrapping; use std::ops::Deref; use std::os::unix::io::AsRawFd; use std::path::PathBuf; -use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicU8, AtomicU64, Ordering}; use std::sync::{Arc, Barrier}; use std::{io, result}; @@ -161,6 +161,8 @@ struct BlockEpollHandler { host_cpus: Option>, acked_features: u64, disable_sector0_writes: bool, + #[allow(unused)] + device_status: Arc, } fn has_feature(features: u64, feature_flag: u64) -> bool { @@ -662,6 +664,7 @@ pub struct Block { serial: Vec, queue_affinity: BTreeMap>, disable_sector0_writes: bool, + device_status: Arc, } #[derive(Serialize, Deserialize)] @@ -820,6 +823,7 @@ impl Block { serial, queue_affinity, disable_sector0_writes, + device_status: Arc::new(AtomicU8::new(0)), }) } @@ -1016,7 +1020,9 @@ impl VirtioDevice for Block { mem, interrupt_cb, mut queues, + device_status, } = context; + self.device_status = device_status; // See if the guest didn't ack the device being read-only. // If so, warn and pretend it did. let original_acked_features = self.common.acked_features; @@ -1078,6 +1084,7 @@ impl VirtioDevice for Block { host_cpus: self.queue_affinity.get(&queue_idx).cloned(), acked_features: self.common.acked_features, disable_sector0_writes: self.disable_sector0_writes, + device_status: self.device_status.clone(), }; let paused = self.common.paused.clone(); diff --git a/virtio-devices/src/device.rs b/virtio-devices/src/device.rs index 05c15e0add..90958ecddc 100644 --- a/virtio-devices/src/device.rs +++ b/virtio-devices/src/device.rs @@ -9,7 +9,7 @@ use std::collections::HashMap; use std::io::Write; use std::num::Wrapping; -use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicU8, Ordering}; use std::sync::{Arc, Barrier}; use std::thread; @@ -58,6 +58,7 @@ pub struct ActivationContext { pub mem: GuestMemoryAtomic, pub interrupt_cb: Arc, pub queues: Vec<(usize, Queue, EventFd)>, + pub device_status: Arc, } /// Trait for virtio devices to be driven by a virtio transport. diff --git a/virtio-devices/src/net.rs b/virtio-devices/src/net.rs index a63b9978bc..e16d97765d 100644 --- a/virtio-devices/src/net.rs +++ b/virtio-devices/src/net.rs @@ -10,7 +10,7 @@ use std::net::IpAddr; use std::num::Wrapping; use std::ops::Deref; use std::os::unix::io::{AsRawFd, RawFd}; -use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicU8, AtomicU64, Ordering}; use std::sync::{Arc, Barrier}; use std::{result, thread}; @@ -176,6 +176,8 @@ struct NetEpollHandler { queue_index_base: u16, queue_pair: (Queue, Queue), queue_evt_pair: (EventFd, EventFd), + #[allow(unused)] + device_status: Arc, } impl NetEpollHandler { @@ -411,6 +413,7 @@ pub struct Net { seccomp_action: SeccompAction, rate_limiter_config: Option, exit_evt: EventFd, + device_status: Arc, } #[derive(Serialize, Deserialize)] @@ -577,6 +580,7 @@ impl Net { seccomp_action, rate_limiter_config, exit_evt, + device_status: Arc::new(AtomicU8::new(0)), }) } @@ -832,7 +836,9 @@ impl VirtioDevice for Net { mem, interrupt_cb, mut queues, + device_status, } = context; + self.device_status = device_status; self.common.activate(&queues, interrupt_cb.clone())?; let num_queues = queues.len(); @@ -946,6 +952,7 @@ impl VirtioDevice for Net { interrupt_cb: interrupt_cb.clone(), kill_evt, pause_evt, + device_status: self.device_status.clone(), }; let paused = self.common.paused.clone(); @@ -1170,6 +1177,7 @@ mod unit_tests { seccomp_action: SeccompAction::Allow, rate_limiter_config: None, exit_evt: EventFd::new(libc::EFD_NONBLOCK).unwrap(), + device_status: Arc::new(Default::default()), } } diff --git a/virtio-devices/src/transport/pci_device.rs b/virtio-devices/src/transport/pci_device.rs index 08b0506938..c4df68c999 100644 --- a/virtio-devices/src/transport/pci_device.rs +++ b/virtio-devices/src/transport/pci_device.rs @@ -10,7 +10,7 @@ use std::any::Any; use std::cmp; use std::io::Write; use std::ops::Deref; -use std::sync::atomic::{AtomicBool, AtomicU16, AtomicUsize, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicU8, AtomicU16, AtomicUsize, Ordering}; use std::sync::{Arc, Barrier, Mutex}; use anyhow::anyhow; @@ -288,6 +288,7 @@ pub struct VirtioPciDeviceActivator { queues: Option>, barrier: Option>, id: String, + status: Arc, } impl VirtioPciDeviceActivator { @@ -297,6 +298,7 @@ impl VirtioPciDeviceActivator { mem: self.memory.take().unwrap(), interrupt_cb: self.interrupt.take().unwrap(), queues: self.queues.take().unwrap(), + device_status: self.status, })?; self.device_activated.store(true, Ordering::SeqCst); @@ -802,6 +804,7 @@ impl VirtioPciDevice { device_activated: self.device_activated.clone(), barrier, id: self.id.clone(), + status: self.common_config.driver_status.clone(), } } diff --git a/virtio-devices/src/vsock/device.rs b/virtio-devices/src/vsock/device.rs index 9acea8d707..bfb3e0c142 100644 --- a/virtio-devices/src/vsock/device.rs +++ b/virtio-devices/src/vsock/device.rs @@ -598,6 +598,7 @@ mod unit_tests { mem: memory.clone(), interrupt_cb: Arc::new(NoopVirtioInterrupt {}), queues: Vec::new(), + device_status: Arc::new(std::sync::atomic::AtomicU8::new(0)), }); match bad_activate { Err(ActivateError::BadActivate) => (), @@ -626,6 +627,7 @@ mod unit_tests { EventFd::new(EFD_NONBLOCK).unwrap(), ), ], + device_status: Arc::new(std::sync::atomic::AtomicU8::new(0)), }) .unwrap(); } From 4bcb6a1ef73cc603170af89555612343b8de828b Mon Sep 17 00:00:00 2001 From: Peter Oskolkov Date: Thu, 12 Mar 2026 09:56:49 -0700 Subject: [PATCH 05/13] virtio-devices: net: handle corrupted requests with NEEDS_RESET A buggy or malicious guest may write an inappropriate value into virtqueue's next_avail field. This will result in an error when iterating over the queue: https://github.com/rust-vmm/vm-virtio/blob/863837ef863f6880bb8357e60bbac49e72c0844c/virtio-queue/src/queue.rs#L708 but this error is (logged and) ignored if pop_descriptor_chain() is used: https://github.com/rust-vmm/vm-virtio/blob/863837ef863f6880bb8357e60bbac49e72c0844c/virtio-queue/src/queue.rs#L583 A reasonable approach, implemented here, is to mark the device as NEEDS_RESET and ignore further queue events until the guest reinitializes the device. How this patch was tested: Linux kernel was patched to trigger a bad next_avail when the virtqueue queue counter reaches 5000: --------------- START OF LINUX KERNEL PATCH ---------- $ git diff diff --git a/drivers/virtio/virtio_ring.c b/drivers/virtio/virtio_ring.c index b784aab668670..989f2a0c64a77 100644 --- a/drivers/virtio/virtio_ring.c +++ b/drivers/virtio/virtio_ring.c @@ -15,6 +15,9 @@ #include #include + +void virtqueue_kick_always(struct virtqueue *vq); + #ifdef DEBUG /* For development, we want to crash whenever the ring is screwed. */ #define BAD_RING(_vq, fmt, args...) \ @@ -677,6 +680,12 @@ static inline int virtqueue_add_split( struct virtqueue *_vq, * new available array entries. */ virtio_wmb(vq->weak_barriers); vq->split.avail_idx_shadow++; + { + if ((vq->split.avail_idx_shadow % 100) == 0) + printk(KERN_ERR "avail idx: %d", + (int)vq->split.avail_idx_shadow); + if (vq->split.avail_idx_shadow == 5000) + vq->split.avail_idx_shadow = 0; + } vq->split.vring.avail->idx = cpu_to_virtio16(_vq->vdev, vq->split.avail_idx_shadow); vq->num_added++; @@ -689,6 +698,11 @@ static inline int virtqueue_add_split( struct virtqueue *_vq, if (unlikely(vq->num_added == (1 << 16) - 1)) virtqueue_kick(_vq); + { + if (unlikely(vq->split.avail_idx_shadow == 0)) + virtqueue_kick_always(_vq); + } + return 0; unmap_release: @@ -2515,6 +2529,11 @@ bool virtqueue_kick(struct virtqueue *vq) } EXPORT_SYMBOL_GPL(virtqueue_kick); +void virtqueue_kick_always(struct virtqueue *vq) +{ + virtqueue_kick_prepare(vq); + virtqueue_notify(vq); +} /** * virtqueue_get_buf_ctx - get the next used buffer * @_vq: the struct virtqueue we're talking about. --------------- END OF LINUX KERNEL PATCH ---------- Then the kernel was booted, and the host pinged until the nic became unresponsive: ping -i 0.002 192.168.4.1 Device status was confirmed using cat /sys/class/net/eth0/device/status (it was 0x4f). Then the device was re-initialized: DEV_NAME=$(basename $(readlink -f /sys/class/net/eth0/device)) echo $DEV_NAME | tee /sys/bus/virtio/drivers/virtio_net/unbind echo $DEV_NAME | tee /sys/bus/virtio/drivers/virtio_net/bind ip link set eth0 up At this point networking became healthly again. Signed-off-by: Peter Oskolkov (cherry picked from commit 563303b50a6d4a06b0660840887684d6167da3c2) On-behalf-of: SAP paul.kroeher@sap.com --- net_util/src/queue_pair.rs | 16 ++++++++-- virtio-devices/src/lib.rs | 1 + virtio-devices/src/net.rs | 63 ++++++++++++++++++++++++++++++++------ 3 files changed, 69 insertions(+), 11 deletions(-) diff --git a/net_util/src/queue_pair.rs b/net_util/src/queue_pair.rs index 86a1c758dc..c0b8825e71 100644 --- a/net_util/src/queue_pair.rs +++ b/net_util/src/queue_pair.rs @@ -51,7 +51,13 @@ impl TxVirtio { let mut retry_write = false; let mut rate_limit_reached = false; - while let Some(mut desc_chain) = queue.pop_descriptor_chain(mem) { + loop { + let mut iter = queue + .iter(mem) + .map_err(NetQueuePairError::QueueIteratorFailed)?; + let Some(mut desc_chain) = iter.next() else { + break; + }; if rate_limit_reached { queue.go_to_previous_position(); break; @@ -180,7 +186,13 @@ impl RxVirtio { let mut exhausted_descs = true; let mut rate_limit_reached = false; - while let Some(mut desc_chain) = queue.pop_descriptor_chain(mem) { + loop { + let mut iter = queue + .iter(mem) + .map_err(NetQueuePairError::QueueIteratorFailed)?; + let Some(mut desc_chain) = iter.next() else { + break; + }; if rate_limit_reached { exhausted_descs = false; queue.go_to_previous_position(); diff --git a/virtio-devices/src/lib.rs b/virtio-devices/src/lib.rs index aae1ece387..7084879a06 100644 --- a/virtio-devices/src/lib.rs +++ b/virtio-devices/src/lib.rs @@ -66,6 +66,7 @@ const DEVICE_ACKNOWLEDGE: u32 = 0x01; const DEVICE_DRIVER: u32 = 0x02; const DEVICE_DRIVER_OK: u32 = 0x04; const DEVICE_FEATURES_OK: u32 = 0x08; +const DEVICE_NEEDS_RESET: u32 = 0x40; const DEVICE_FAILED: u32 = 0x80; const VIRTIO_F_RING_INDIRECT_DESC: u32 = 28; diff --git a/virtio-devices/src/net.rs b/virtio-devices/src/net.rs index e16d97765d..9d5c2f7799 100644 --- a/virtio-devices/src/net.rs +++ b/virtio-devices/src/net.rs @@ -176,7 +176,6 @@ struct NetEpollHandler { queue_index_base: u16, queue_pair: (Queue, Queue), queue_evt_pair: (EventFd, EventFd), - #[allow(unused)] device_status: Arc, } @@ -191,6 +190,9 @@ impl NetEpollHandler { } fn handle_rx_event(&mut self) -> result::Result<(), DeviceError> { + if self.needs_reset() { + return Ok(()); + } let queue_evt = &self.queue_evt_pair.0; if let Err(e) = queue_evt.read() { error!("Failed to get rx queue event: {e:?}"); @@ -219,12 +221,43 @@ impl NetEpollHandler { Ok(()) } + fn handle_queue_iterator_error(&mut self, err: &virtio_queue::Error) { + // The guest submitted a corrupted VirtQ request, and the error + // was logged during queue processing. We cannot just ignore the + // error, as the guest could continue spamming the VMM with bad + // requests, triggering excessive error logging. So we mark + // the device "NEEDS_RESET", effectively stopping all request + // processing (see self.needs_reset() usage) until the guest + // resets and reactivates the device. + + warn!( + "Corrupted request detected (virtqueue error: {err:?}). \ +Setting device status to 'NEEDS_RESET' and stopping processing queues until reset." + ); + + self.device_status + .fetch_or(crate::DEVICE_NEEDS_RESET as u8, Ordering::SeqCst); + + // Let the guest know that the device status has changed. + if let Err(e) = self.interrupt_cb.trigger(VirtioInterruptType::Config) { + error!("Failed to signal config interrupt: {e:?}"); + } + } + fn process_tx(&mut self) -> result::Result<(), DeviceError> { - if self + if self.needs_reset() { + return Ok(()); + } + let res = self .net - .process_tx(&self.mem.memory(), &mut self.queue_pair.1) - .map_err(DeviceError::NetQueuePair)? - { + .process_tx(&self.mem.memory(), &mut self.queue_pair.1); + + if let Err(net_util::NetQueuePairError::QueueIteratorFailed(err)) = res { + self.handle_queue_iterator_error(&err); + return Ok(()); + } + + if res.map_err(DeviceError::NetQueuePair)? { self.signal_used_queue(self.queue_index_base + 1)?; debug!("Signalling TX queue"); } else { @@ -248,11 +281,19 @@ impl NetEpollHandler { } fn handle_rx_tap_event(&mut self) -> result::Result<(), DeviceError> { - if self + if self.needs_reset() { + return Ok(()); + } + let res = self .net - .process_rx(&self.mem.memory(), &mut self.queue_pair.0) - .map_err(DeviceError::NetQueuePair)? - { + .process_rx(&self.mem.memory(), &mut self.queue_pair.0); + + if let Err(net_util::NetQueuePairError::QueueIteratorFailed(err)) = res { + self.handle_queue_iterator_error(&err); + return Ok(()); + } + + if res.map_err(DeviceError::NetQueuePair)? { self.signal_used_queue(self.queue_index_base)?; trace!("Signalling RX queue"); } else { @@ -302,6 +343,10 @@ impl NetEpollHandler { Ok(()) } + + fn needs_reset(&self) -> bool { + (self.device_status.load(Ordering::Acquire) & crate::DEVICE_NEEDS_RESET as u8) != 0 + } } impl EpollHelperHandler for NetEpollHandler { From 2a11aba488033cce32dfd2325af201259c6b5b22 Mon Sep 17 00:00:00 2001 From: Peter Oskolkov Date: Thu, 12 Mar 2026 09:58:15 -0700 Subject: [PATCH 06/13] virtio-devices: block: handle corrupted requests with NEEDS_RESET Signed-off-by: Peter Oskolkov (cherry picked from commit 8b60b38281f95a519802e21358080582c28c46ef) On-behalf-of: SAP paul.kroeher@sap.com --- virtio-devices/src/block.rs | 46 +++++++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/virtio-devices/src/block.rs b/virtio-devices/src/block.rs index d49ec49257..95304bb3d7 100644 --- a/virtio-devices/src/block.rs +++ b/virtio-devices/src/block.rs @@ -161,7 +161,6 @@ struct BlockEpollHandler { host_cpus: Option>, acked_features: u64, disable_sector0_writes: bool, - #[allow(unused)] device_status: Arc, } @@ -170,6 +169,10 @@ fn has_feature(features: u64, feature_flag: u64) -> bool { } impl BlockEpollHandler { + fn needs_reset(&self) -> bool { + (self.device_status.load(Ordering::Acquire) & crate::DEVICE_NEEDS_RESET as u8) != 0 + } + fn check_request( features: u64, request: &Request, @@ -194,12 +197,48 @@ impl BlockEpollHandler { Ok(()) } + fn handle_queue_iterator_error(&mut self, err: &virtio_queue::Error) { + // The guest submitted a corrupted VirtQ request, and the error + // was logged during queue processing. We cannot just ignore the + // error, as the guest could continue spamming the VMM with bad + // requests, triggering excessive error logging. So we mark + // the device "NEEDS_RESET", effectively stopping all request + // processing (see self.needs_reset() usage) until the guest + // resets and reactivates the device. + + warn!( + "Corrupted request detected (virtqueue error: {err:?}). \ +Setting device status to 'NEEDS_RESET' and stopping processing queues until reset." + ); + + self.device_status + .fetch_or(crate::DEVICE_NEEDS_RESET as u8, Ordering::SeqCst); + + // Let the guest know that the device status has changed. + if let Err(e) = self.interrupt_cb.trigger(VirtioInterruptType::Config) { + error!("Failed to signal config interrupt: {e:?}"); + } + } + fn process_queue_submit(&mut self) -> Result<()> { + if self.needs_reset() { + return Ok(()); + } let queue = &mut self.queue; let mut batch_requests = Vec::new(); let mut batch_inflight_requests = Vec::new(); - while let Some(mut desc_chain) = queue.pop_descriptor_chain(self.mem.memory()) { + loop { + let mut desc_chain = match queue.iter(self.mem.memory()) { + Ok(mut iter) => match iter.next() { + Some(c) => c, + None => break, + }, + Err(err) => { + self.handle_queue_iterator_error(&err); + return Ok(()); + } + }; let mut request = Request::parse(&mut desc_chain, self.access_platform.as_deref()) .map_err(Error::RequestParsing)?; @@ -382,6 +421,9 @@ impl BlockEpollHandler { } fn process_queue_complete(&mut self) -> Result<()> { + if self.needs_reset() { + return Ok(()); + } let mem = self.mem.memory(); let mut read_bytes = Wrapping(0); let mut write_bytes = Wrapping(0); From 44f3360da99abba2e035a5b4cffffd4f41e782de Mon Sep 17 00:00:00 2001 From: Dylan Reid Date: Fri, 24 Apr 2026 16:10:15 -0700 Subject: [PATCH 07/13] virtio-devices: block: reject duplicate in-flight head_index A malicious or buggy guest can violate virtio by making the same descriptor head available twice before the first chain has been placed on the used ring. The submit path pushed both chains onto the VecDeque-backed inflight_requests keyed by head_index, and on completion find_inflight_request() returned the first linear match. That Request's complete_async() freed its bounce buffer while the other chain's io_uring op was still targeting it, producing a use-after-free the kernel could then scribble into. Signed-off-by: Dylan Reid (cherry picked from commit 544fa4aa764abae9d7cbe53c69598521570360a8) On-behalf-of: SAP paul.kroeher@sap.com --- virtio-devices/src/block.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/virtio-devices/src/block.rs b/virtio-devices/src/block.rs index 95304bb3d7..92bef5ba57 100644 --- a/virtio-devices/src/block.rs +++ b/virtio-devices/src/block.rs @@ -211,6 +211,10 @@ impl BlockEpollHandler { Setting device status to 'NEEDS_RESET' and stopping processing queues until reset." ); + self.set_needs_reset(); + } + + fn set_needs_reset(&mut self) { self.device_status .fetch_or(crate::DEVICE_NEEDS_RESET as u8, Ordering::SeqCst); @@ -220,6 +224,17 @@ Setting device status to 'NEEDS_RESET' and stopping processing queues until rese } } + // A spec-compliant driver never reuses a virtqueue head_index while the + // corresponding chain is still available (virtio 1.x §2.7.13.4). + // Double check the guest driver is behaving. + fn is_head_in_flight( + inflight: &VecDeque<(u16, Request)>, + batch: &[(u16, Request)], + head: u16, + ) -> bool { + batch.iter().any(|(h, _)| *h == head) || inflight.iter().any(|(h, _)| *h == head) + } + fn process_queue_submit(&mut self) -> Result<()> { if self.needs_reset() { return Ok(()); @@ -239,6 +254,14 @@ Setting device status to 'NEEDS_RESET' and stopping processing queues until rese return Ok(()); } }; + + let head = desc_chain.head_index(); + if Self::is_head_in_flight(&self.inflight_requests, &batch_inflight_requests, head) { + warn!("Guest reused virtio-blk head_index {head} while the chain was used"); + self.set_needs_reset(); + return Ok(()); + } + let mut request = Request::parse(&mut desc_chain, self.access_platform.as_deref()) .map_err(Error::RequestParsing)?; From a2ca250d289dd7bf1a5694ab10a32c59c80ea21c Mon Sep 17 00:00:00 2001 From: Dylan Reid Date: Fri, 24 Apr 2026 17:09:06 -0700 Subject: [PATCH 08/13] virtio-devices: block: track non-batch inflight reqs immediately For non-batch backends execute_async submits the kernel I/O inline before returning. An early return while processing before inserting in inflight_requests, meant the request went untracked, the local batch list was never appended to inflight_requests, even though the request is pending in the kernel. To track it, insert into self.inflight_requests as soon as execute_async returns Ok. The completion path's find_inflight_request now matches the orphan and the bounce buffer is freed only after the kernel signals it is done. Signed-off-by: Dylan Reid (cherry picked from commit fa8acbd712ebf6574938191aaf846001e3be8d1e) On-behalf-of: SAP paul.kroeher@sap.com --- virtio-devices/src/block.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/virtio-devices/src/block.rs b/virtio-devices/src/block.rs index 92bef5ba57..328df0e6dd 100644 --- a/virtio-devices/src/block.rs +++ b/virtio-devices/src/block.rs @@ -346,8 +346,11 @@ Setting device status to 'NEEDS_RESET' and stopping processing queues until rese ) } } + batch_inflight_requests.push((desc_chain.head_index(), request)); + } else { + self.inflight_requests + .push_back((desc_chain.head_index(), request)); } - batch_inflight_requests.push((desc_chain.head_index(), request)); } else { let status = match result { Ok(_) => VIRTIO_BLK_S_OK, From 1f9cc99fffdb867fc3094d7456c37915e3b6570d Mon Sep 17 00:00:00 2001 From: Dylan Reid Date: Fri, 8 May 2026 10:36:14 +0200 Subject: [PATCH 09/13] block: AlignedOperation owns its bounce buffer via Drop The bounce buffer for an unaligned descriptor was allocated in execute_async and leaked on error paths, even though, for the sync case the kernel already had a pointer to the buffer. Clean this up by moving ownership of the buffer to the AlignedOperation type. To make it actually safe, stop stashing a guest memory pointer for the duration of the op. Instead, save the guest address and pass guest memory back to the complete function. Signed-off-by: Dylan Reid (cherry picked from commit 1b8c92dd5e3c0c58316826486ce5ee30eeb71407)) [backport: adapted to stable/v51.x; v51.x has no block/src/request.rs split, so the new aligned_operation module is added next to block/src/lib.rs and the in tree struct, alloc, free path is replaced in place.] Signed-off-by: Anatol Belski On-behalf-of: SAP paul.kroeher@sap.com --- block/src/aligned_operation.rs | 90 ++++++++++++++++++++++++++++++++++ block/src/lib.rs | 66 +++++++------------------ virtio-devices/src/block.rs | 4 +- 3 files changed, 110 insertions(+), 50 deletions(-) create mode 100644 block/src/aligned_operation.rs diff --git a/block/src/aligned_operation.rs b/block/src/aligned_operation.rs new file mode 100644 index 0000000000..3081b7b705 --- /dev/null +++ b/block/src/aligned_operation.rs @@ -0,0 +1,90 @@ +// Copyright (c) 2026 Meta Platforms, Inc. and affiliates. +// +// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause + +use std::alloc::{Layout, alloc_zeroed, dealloc}; +use std::io; + +use vm_memory::GuestAddress; + +/// Owns an aligned bounce buffer used when a guest descriptor's host VA +/// does not meet the disk backend's alignment requirement. +#[derive(Debug)] +pub struct AlignedOperation { + data_addr: GuestAddress, + aligned_ptr: *mut u8, + size: usize, + layout: Layout, +} + +impl AlignedOperation { + /// Allocate a zero-initialized buffer of `size` bytes aligned to + /// `alignment`. Returns `InvalidInput` if `size` is zero; + /// `alignment` must be a power of two and not exceed `isize::MAX` + /// after rounding up. + pub fn new(data_addr: GuestAddress, size: usize, alignment: usize) -> io::Result { + if size == 0 { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "AlignedOperation requires a non-zero size", + )); + } + let layout = Layout::from_size_align(size, alignment) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?; + // SAFETY: size is non-zero (checked above) and Layout::from_size_align + // rejects alignments that are not a power of two or that overflow. + let aligned_ptr = unsafe { alloc_zeroed(layout) }; + if aligned_ptr.is_null() { + return Err(io::Error::last_os_error()); + } + Ok(Self { + data_addr, + aligned_ptr, + size, + layout, + }) + } + + /// Gets the raw pointer to the aligned buffer. + pub fn as_mut_ptr(&mut self) -> *mut u8 { + self.aligned_ptr + } + + /// Returns the aligned buffer as a slice. + pub fn as_bytes(&self) -> &[u8] { + // SAFETY: `new` allocates `size` bytes via alloc_zeroed (so they + // are initialized) and AlignedOperation owns the buffer + // exclusively. + unsafe { std::slice::from_raw_parts(self.aligned_ptr, self.size) } + } + + /// Returns the aligned buffer as a mutable slice. + pub fn as_bytes_mut(&mut self) -> &mut [u8] { + // SAFETY: same invariant as as_bytes; &mut self rules out other + // simultaneous borrows. + unsafe { std::slice::from_raw_parts_mut(self.aligned_ptr, self.size) } + } + + /// Returns the guest address for this op. + pub fn data_addr(&self) -> GuestAddress { + self.data_addr + } +} + +impl Drop for AlignedOperation { + fn drop(&mut self) { + // SAFETY: `new` is the only constructor, and it stores a pointer + // returned by `alloc_zeroed` paired with the exact `layout` used + // for that allocation. Ownership has not escaped (the type is + // neither `Clone` nor `Copy`). + unsafe { + dealloc(self.aligned_ptr, self.layout); + } + } +} + +// SAFETY: AlignedOperation owns its heap allocation exclusively (no Clone/ +// Copy, no shared aliases) and the allocation's lifetime is tied to the +// value's. Moving an AlignedOperation between threads transfers that +// ownership; the same rationale Box uses for its Send impl. +unsafe impl Send for AlignedOperation {} diff --git a/block/src/lib.rs b/block/src/lib.rs index 9f78cefd9e..504f4cc3c2 100644 --- a/block/src/lib.rs +++ b/block/src/lib.rs @@ -8,6 +8,7 @@ // // SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause +mod aligned_operation; pub mod async_io; pub mod fcntl; pub mod fixed_vhd; @@ -28,7 +29,7 @@ pub mod vhd; pub mod vhdx; pub mod vhdx_sync; -use std::alloc::{Layout, alloc_zeroed, dealloc}; +use std::alloc::{Layout, alloc_zeroed}; use std::collections::VecDeque; use std::fmt::{self, Debug}; use std::fs::File; @@ -40,6 +41,7 @@ use std::str::FromStr; use std::time::Instant; use std::{cmp, result}; +pub use aligned_operation::AlignedOperation; #[cfg(feature = "io_uring")] use io_uring::{IoUring, Probe, opcode}; use libc::{S_IFBLK, S_IFMT, ioctl}; @@ -232,14 +234,6 @@ fn sector( const DEFAULT_DESCRIPTOR_VEC_SIZE: usize = 32; -#[derive(Debug)] -pub struct AlignedOperation { - origin_ptr: u64, - aligned_ptr: u64, - size: usize, - layout: Layout, -} - pub struct BatchRequest { pub offset: libc::off_t, pub iovecs: SmallVec<[libc::iovec; DEFAULT_DESCRIPTOR_VEC_SIZE]>, @@ -473,31 +467,19 @@ impl Request { let iov_base = if (origin_ptr.as_ptr() as u64).is_multiple_of(SECTOR_SIZE) { origin_ptr.as_ptr() as *mut libc::c_void } else { - let layout = Layout::from_size_align(data_len, SECTOR_SIZE as usize).unwrap(); - // SAFETY: layout has non-zero size - let aligned_ptr = unsafe { alloc_zeroed(layout) }; - if aligned_ptr.is_null() { - return Err(ExecuteError::TemporaryBufferAllocation( - io::Error::last_os_error(), - )); - } + let mut aligned_op = + AlignedOperation::new(data_addr, data_len, SECTOR_SIZE as usize) + .map_err(ExecuteError::TemporaryBufferAllocation)?; // We need to perform the copy beforehand in case we're writing // data out. if request_type == RequestType::Out { - // SAFETY: destination buffer has been allocated with - // the proper size. - unsafe { std::ptr::copy(origin_ptr.as_ptr(), aligned_ptr, data_len) }; + mem.read_slice(aligned_op.as_bytes_mut(), data_addr) + .map_err(ExecuteError::Read)?; } - // Store both origin and aligned pointers for complete_async() - // to process them. - self.aligned_operations.push(AlignedOperation { - origin_ptr: origin_ptr.as_ptr() as u64, - aligned_ptr: aligned_ptr as u64, - size: data_len, - layout, - }); + let aligned_ptr = aligned_op.as_mut_ptr(); + self.aligned_operations.push(aligned_op); aligned_ptr as *mut libc::c_void }; @@ -639,31 +621,17 @@ impl Request { Ok(ret) } - pub fn complete_async(&mut self) -> result::Result<(), Error> { - for aligned_operation in self.aligned_operations.drain(..) { + pub fn complete_async( + &mut self, + mem: &vm_memory::GuestMemoryMmap, + ) -> result::Result<(), Error> { + for aligned_op in self.aligned_operations.drain(..) { // We need to perform the copy after the data has been read inside // the aligned buffer in case we're reading data in. if self.request_type == RequestType::In { - // SAFETY: origin buffer has been allocated with the - // proper size. - unsafe { - std::ptr::copy( - aligned_operation.aligned_ptr as *const u8, - aligned_operation.origin_ptr as *mut u8, - aligned_operation.size, - ); - }; + mem.write_slice(aligned_op.as_bytes(), aligned_op.data_addr()) + .map_err(Error::GuestMemory)?; } - - // Free the temporary aligned buffer. - // SAFETY: aligned_ptr was allocated by alloc_zeroed with the same - // layout - unsafe { - dealloc( - aligned_operation.aligned_ptr as *mut u8, - aligned_operation.layout, - ); - }; } Ok(()) diff --git a/virtio-devices/src/block.rs b/virtio-devices/src/block.rs index 328df0e6dd..c035d571f8 100644 --- a/virtio-devices/src/block.rs +++ b/virtio-devices/src/block.rs @@ -461,7 +461,9 @@ Setting device status to 'NEEDS_RESET' and stopping processing queues until rese let mut request = self.find_inflight_request(desc_index)?; - request.complete_async().map_err(Error::RequestCompleting)?; + request + .complete_async(&mem) + .map_err(Error::RequestCompleting)?; let latency = request.start.elapsed().as_micros() as u64; let read_ops_last = self.counters.read_ops.load(Ordering::Relaxed); From b2e64bc12cd638b6f12cc29cf5aa2f3644577782 Mon Sep 17 00:00:00 2001 From: Dylan Reid Date: Fri, 24 Apr 2026 17:10:44 -0700 Subject: [PATCH 10/13] block: raw_async: reject batch atomically when SQ lacks capacity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit submit_batch_requests pushed each BatchRequest into the io_uring SQ in turn and used `?` to bail on the first push failure. Leaving the initial SQEs visible to the kernel — but submitter.submit() was never called, and every other call site in this file gates submit() behind a preceding sq.push() that now also fails on the full ring. This could allow a guest to DoS it's own queue or worse if the buffer is freed early. Signed-off-by: Dylan Reid (cherry picked from commit ee315d2e7c98b65a51573b3be48df9cc66115179) On-behalf-of: SAP paul.kroeher@sap.com --- block/src/raw_async.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/block/src/raw_async.rs b/block/src/raw_async.rs index 539aaa9095..45fb86d1a1 100644 --- a/block/src/raw_async.rs +++ b/block/src/raw_async.rs @@ -197,6 +197,14 @@ impl AsyncIo for RawFileAsync { let (submitter, mut sq, _) = self.io_uring.split(); let mut submitted = false; + // Refuse the whole batch if it can't fit in the SQ to avoid having to unroll a partially + // successful push. + if batch_request.len() > sq.capacity() - sq.len() { + return Err(AsyncIoError::SubmitBatchRequests(Error::other( + "io_uring submission queue is full", + ))); + } + for req in batch_request { match req.request_type { RequestType::In => { From ff770cc33afab3100c298679f7c232269b1aa357 Mon Sep 17 00:00:00 2001 From: Bo Chen Date: Thu, 14 May 2026 19:51:48 +0000 Subject: [PATCH 11/13] build: Release v51.2 Signed-off-by: Bo Chen On-behalf-of: SAP paul.kroeher@sap.com --- Cargo.lock | 2 +- cloud-hypervisor/Cargo.toml | 2 +- release-notes.md | 7 +++++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dd3501a2ac..2aa24483b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -512,7 +512,7 @@ checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" [[package]] name = "cloud-hypervisor" -version = "51.1.0" +version = "51.2.0" dependencies = [ "anyhow", "api_client", diff --git a/cloud-hypervisor/Cargo.toml b/cloud-hypervisor/Cargo.toml index ebb7c8d95d..39b4d1b871 100644 --- a/cloud-hypervisor/Cargo.toml +++ b/cloud-hypervisor/Cargo.toml @@ -7,7 +7,7 @@ edition = "2024" homepage = "https://github.com/cloud-hypervisor/cloud-hypervisor" license = "Apache-2.0 AND BSD-3-Clause" name = "cloud-hypervisor" -version = "51.1.0" +version = "51.2.0" # Minimum buildable version: # Keep in sync with version in .github/workflows/build.yaml # Policy on MSRV (see #4318): diff --git a/release-notes.md b/release-notes.md index 1f2ff74152..225039cbcf 100644 --- a/release-notes.md +++ b/release-notes.md @@ -1,3 +1,4 @@ +- [v51.2](#v512) - [v51.1](#v511) - [v51.0](#v510) - [Security Fixes](#security-fixes) @@ -419,6 +420,12 @@ - [Unit testing](#unit-testing) - [Integration tests parallelization](#integration-tests-parallelization) +# v51.2 + +This is a point release containing security fixes to a use-after-free +vulnerability in the `virtio-block` async I/O completion path +(#8220). Details can be found in GHSA-f47p-p25q-83rh (CVE-2026-45782). + # v51.1 This is a bug fix release. The following issues have been addressed: From 738e1533d220c9e5286e2ebb128f70e7537bb24c Mon Sep 17 00:00:00 2001 From: Philipp Schuster Date: Tue, 19 May 2026 10:22:27 +0200 Subject: [PATCH 12/13] vmm: clear restore snapshot after device creation A restore snapshot is only construction input. The restore path should use it while rebuilding the VM from saved state, then discard it before later VM lifecycle operations run. Keeping it around is observable after a restored VM changes its device set. For example, a VM can be restored from a snapshot, live-migrated, and then hot-remove a device on the destination. The restored device tree no longer contains that device, but DeviceManager still carries the original device snapshot. That stale snapshot can affect later hotplug. Every added device eventually reaches a constructor that calls state_from_id(self.snapshot.as_ref(), id) or an equivalent snapshot lookup. If the stale snapshot still contains an entry with the newly added device name, the new device is created with old saved state even though the matching device was already removed from the live device tree. Clear the DeviceManager snapshot after hypervisor-specific initialization has rebuilt the interrupt controller and devices. From that point onward, device operations should behave as normal runtime operations, not as restore operations. Co-developed-by: Thomas Prescher Co-developed-by: Leander Kohler On-behalf-of: SAP philipp.schuster@sap.com Signed-off-by: Philipp Schuster --- vmm/src/device_manager.rs | 5 +++++ vmm/src/vm.rs | 3 +++ 2 files changed, 8 insertions(+) diff --git a/vmm/src/device_manager.rs b/vmm/src/device_manager.rs index 8911e8e405..ce224272b8 100644 --- a/vmm/src/device_manager.rs +++ b/vmm/src/device_manager.rs @@ -1524,6 +1524,11 @@ impl DeviceManager { Ok(()) } + /// Drop restore-only state once all devices have consumed it. + pub(crate) fn clear_restore_snapshot(&mut self) { + self.snapshot = None; + } + #[cfg(feature = "fw_cfg")] pub fn create_fw_cfg_device(&mut self) -> Result<(), DeviceManagerError> { let fw_cfg = Arc::new(Mutex::new(devices::legacy::FwCfg::new( diff --git a/vmm/src/vm.rs b/vmm/src/vm.rs index 96928b4c1f..9daf5ff8dd 100644 --- a/vmm/src/vm.rs +++ b/vmm/src/vm.rs @@ -639,6 +639,9 @@ impl Vm { snapshot, )?; + // Remove any snapshot artifacts after the hypervisor-specific init. + device_manager.lock().unwrap().clear_restore_snapshot(); + // Load kernel and initramfs files #[cfg(feature = "tdx")] let kernel = config From 91666d466b4752d969cb97d8afc9f8d02c6876cb Mon Sep 17 00:00:00 2001 From: Paul Kroeher Date: Tue, 5 May 2026 10:51:09 +0200 Subject: [PATCH 13/13] ci: implementation of flake bump auto approve and merge Add a GitHub workflow to automatically validate and merge pull requests that only bump flake.lock. This reduces manual review overhead for routine dependency updates while keeping the merge path constrained and auditable. The workflow uses a dedicated gitlint config and custom rule to ensure each eligible commit changes exactly flake.lock. Documentation is added for the required GitHub App, secrets, permissions, and branch-ruleset bypass setup. Signed-off-by: Paul Kroeher On-behalf-of: SAP paul.kroeher@sap.com --- ...bump.yaml => flake-bump-auto-approve.yaml} | 36 +++++++++++++------ 1 file changed, 26 insertions(+), 10 deletions(-) rename .github/workflows/{flake-bump.yaml => flake-bump-auto-approve.yaml} (53%) diff --git a/.github/workflows/flake-bump.yaml b/.github/workflows/flake-bump-auto-approve.yaml similarity index 53% rename from .github/workflows/flake-bump.yaml rename to .github/workflows/flake-bump-auto-approve.yaml index 4e706be49e..4bc796980c 100644 --- a/.github/workflows/flake-bump.yaml +++ b/.github/workflows/flake-bump-auto-approve.yaml @@ -1,4 +1,4 @@ -name: Flake bump +name: Flake bump auto approve on: pull_request: paths: @@ -8,7 +8,7 @@ on: jobs: gitlint: - name: Check flake bump requirements + name: Flake bump auto approve runs-on: ubuntu-latest steps: - name: Checkout repository @@ -24,24 +24,40 @@ jobs: run: | python -m pip install --upgrade pip pip install --upgrade gitlint + # this rule checks the prerequisits and write the exit code in its output - name: Lint git commit messages + id: gitlint run: | + set +e gitlint --commits origin/$GITHUB_BASE_REF.. -C .gitlint_auto_approve - - gitmerge: - name: Merge flake bump - needs: gitlint # hard dependency on this check job - runs-on: ubuntu-latest - steps: + code=$? + if [ $code -eq 0 ]; then + echo "this merge request is eligible for a flake bump auto approve and merge" + else + echo "this merge request will not be automatically approved." + fi + echo "exit_code=$code" >> "$GITHUB_OUTPUT" + exit 0 + # the following steps only run if gitlint run successful + - name: Create variables + if: steps.gitlint.outputs.exit_code == '0' + id: create_variable + run: | + REPO=$(echo ${GITHUB_REPOSITORY} | cut -f 2 -d '/') + OWNER=$(echo ${GITHUB_REPOSITORY} | cut -f 1 -d '/') + echo "repo=$REPO" >> "$GITHUB_OUTPUT" + echo "owner=$OWNER" >> "$GITHUB_OUTPUT" - name: Generate token + if: steps.gitlint.outputs.exit_code == '0' && steps.create_variable.outputs.repo != '' id: generate_token uses: actions/create-github-app-token@v2 with: app-id: ${{ secrets.GH_AUTO_APPROVE_APP_ID }} private-key: ${{ secrets.GH_AUTO_APPROVE_APP_PRIVATE_KEY }} - owner: daedalus-ca - repositories: test-auto-approve + owner: ${{ steps.create_variable.outputs.owner }} + repositories: ${{ steps.create_variable.outputs.repo }} - name: Merge Pull request + if: steps.gitlint.outputs.exit_code == '0' && steps.generate_token.outputs.token != '' shell: bash env: GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }}