Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 36 additions & 10 deletions alioth/src/virtio/dev/vsock/vhost_vsock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,9 @@ impl VhostVsock {
vhost_dev.set_backend_features(&backend_feature)?;
}
let dev_feat = vhost_dev.get_features()? as u128;
let known_feat = VirtioFeature::from_bits_truncate(dev_feat).bits()
| VsockFeature::from_bits_truncate(dev_feat).bits();
let known_feat =
(VirtioFeature::from_bits_truncate(dev_feat) & !VirtioFeature::VHOST_PROTOCOL).bits()
| VsockFeature::from_bits_truncate(dev_feat).bits();
if !VirtioFeature::from_bits_retain(known_feat).contains(VirtioFeature::VERSION_1) {
return error::VhostMissingDeviceFeature {
feature: VirtioFeature::VERSION_1.bits(),
Expand Down Expand Up @@ -140,13 +141,25 @@ impl Virtio for VhostVsock {
}
}

// VirtIO spec 1.4, Sec 2.8.1: both driver and device ring wrap counters
// are initialized to 1. For packed rings, VHOST_SET_VRING_BASE encodes
// the avail wrap counter in bit 15 and the used wrap counter in bit 31.
fn vring_base_init(feature: u128) -> u32 {
if VirtioFeature::from_bits_retain(feature).contains(VirtioFeature::RING_PACKED) {
(1 << 15) | (1 << 31)
} else {
0
}
}

impl VirtioMio for VhostVsock {
fn activate<Q, S>(&mut self, feature: u128, active_mio: &mut ActiveMio<'_, Q, S>) -> Result<()>
where
Q: VirtQueue,
S: IrqSender,
{
self.vhost_dev.set_features(&(feature as u64))?;
let base = vring_base_init(feature);
for (index, notifier) in active_mio.notifiers.iter().take(2).enumerate() {
let kick = VirtqFile {
index: index as u32,
Expand All @@ -173,7 +186,7 @@ impl VirtioMio for VhostVsock {
val: addrs.size as _,
})?;
self.vhost_dev
.set_virtq_base(&VirtqState { index, val: 0 })?;
.set_virtq_base(&VirtqState { index, val: base })?;
let mem = queue.ram();
let virtq_addr = VirtqAddr {
index,
Expand Down Expand Up @@ -244,19 +257,28 @@ impl VirtioMio for VhostVsock {
Ok(())
}

fn handle_queue<Q, S>(
&mut self,
index: u16,
_active_mio: &mut ActiveMio<'_, Q, S>,
) -> Result<()>
fn handle_queue<Q, S>(&mut self, index: u16, active_mio: &mut ActiveMio<'_, Q, S>) -> Result<()>
where
Q: VirtQueue,
S: IrqSender,
{
match index {
0 | 1 => unreachable!("{}: queue 0 and 1 are offloaded to kernel", self.name),
0 | 1 => {
if let Some(notifier) = active_mio.notifiers.get(index as usize) {
log::warn!(
"{}: forwarding queue {index} notification to kernel",
self.name
);
notifier.notify()?;
} else {
log::error!(
"{}: unexpected queue {index} notification without kick notifier",
self.name
);
}
}
2 => log::info!("{}: event queue buffer available", self.name),
_ => unreachable!(),
_ => log::error!("{}: invalid queue index {index}", self.name),
}
Ok(())
}
Expand All @@ -270,3 +292,7 @@ impl Drop for VhostVsock {
}
}
}

#[cfg(test)]
#[path = "vhost_vsock_test.rs"]
mod tests;
105 changes: 105 additions & 0 deletions alioth/src/virtio/dev/vsock/vhost_vsock_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use std::os::fd::BorrowedFd;
use std::sync::Arc;
use std::time::Duration;

use assert_matches::assert_matches;
use mio::{Events, Interest, Poll, Token};
use rstest::rstest;

use crate::sync::notifier::Notifier;
use crate::virtio::dev::vsock::VsockConfig;
use crate::virtio::dev::vsock::vhost_vsock::{self, VhostVsock};
use crate::virtio::queue::Queue;
use crate::virtio::queue::split::SplitQueue;
use crate::virtio::vhost::{Error as VhostError, VhostDev};
use crate::virtio::worker::mio::{ActiveMio, VirtioMio};
use crate::virtio::{Error, IrqSender, Result, VirtioFeature};

#[derive(Debug)]
struct DummyIrqSender;

impl IrqSender for DummyIrqSender {
fn queue_irq(&self, _idx: u16) {}

fn config_irq(&self) {}

fn queue_irqfd<F, T>(&self, _idx: u16, _f: F) -> Result<T>
where
F: FnOnce(BorrowedFd) -> Result<T>,
{
unimplemented!()
}

fn config_irqfd<F, T>(&self, _f: F) -> Result<T>
where
F: FnOnce(BorrowedFd) -> Result<T>,
{
unimplemented!()
}
}

#[rstest]
#[case(VirtioFeature::VERSION_1, 0)]
#[case(VirtioFeature::VERSION_1 | VirtioFeature::RING_PACKED, (1 << 15) | (1 << 31))]
fn test_vring_base_init(#[case] feat: VirtioFeature, #[case] expected: u32) {
assert_eq!(vhost_vsock::vring_base_init(feat.bits()), expected);
}

#[test]
fn test_vhost_vsock_handle_event_and_queue() {
let mut dev = VhostVsock {
name: "vhost-vsock".into(),
vhost_dev: Arc::new(VhostDev::new("/dev/null").unwrap()),
config: VsockConfig::default(),
features: VirtioFeature::VERSION_1.bits() as u64,
error_fds: [None, None],
};

let mut poll = Poll::new().unwrap();
let mut kick_0 = Notifier::new().unwrap();
poll.registry()
.register(&mut kick_0, Token(1), Interest::READABLE)
.unwrap();
kick_0.notify().unwrap();

let mut events = Events::with_capacity(4);
poll.poll(&mut events, Some(Duration::from_secs(5)))
.unwrap();
let event = events.iter().next().unwrap();

let notifiers = [kick_0];
let mut queues: [Option<Queue<SplitQueue>>; 0] = [];
let irq_sender = DummyIrqSender;
let mut active_mio = ActiveMio {
queues: &mut queues,
irq_sender: &irq_sender,
notifiers: &notifiers,
poll: &mut poll,
};

let ret = dev.handle_event(event, &mut active_mio);
assert_matches!(
ret,
Err(Error::Vhost { source, .. })
if matches!(*source, VhostError::VhostQueueErr { dev: "vsock", index: 1, .. })
);

assert_matches!(dev.handle_queue(0, &mut active_mio), Ok(()));
assert_matches!(dev.handle_queue(1, &mut active_mio), Ok(()));
assert_matches!(dev.handle_queue(2, &mut active_mio), Ok(()));
assert_matches!(dev.handle_queue(3, &mut active_mio), Ok(()));
}
79 changes: 61 additions & 18 deletions alioth/src/virtio/vu/frontend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,14 @@
// See the License for the specific language governing permissions and
// limitations under the License.

use std::os::fd::{AsFd, AsRawFd, BorrowedFd, FromRawFd, OwnedFd};
use std::fs::File;
use std::io::Read;
use std::os::fd::{AsFd, AsRawFd, BorrowedFd, FromRawFd};
use std::path::Path;
use std::sync::Arc;
use std::thread::JoinHandle;

use libc::{EFD_CLOEXEC, EFD_NONBLOCK};
use mio::event::Event;
use mio::unix::SourceFd;
use mio::{Interest, Registry, Token};
Expand All @@ -42,6 +45,27 @@ bitflags! {
pub struct VuDevFeature(u128) { }
}

// VirtIO spec 1.4, Sec 2.2: bits 0..24 and 50..128 are device-specific;
// bits 24..50 are reserved for queue/transport extensions and future use.
const DEVICE_FEATURE_MASK: u64 = ((1 << 24) - 1) | (!0 << 50);

const SUPPORTED_VIRTIO_FEATURES: u64 = (VirtioFeature::INDIRECT_DESC.bits()
| VirtioFeature::EVENT_IDX.bits()
| VirtioFeature::VERSION_1.bits()
| VirtioFeature::ACCESS_PLATFORM.bits()
| VirtioFeature::RING_PACKED.bits()) as u64;

// VirtIO spec 1.4, Sec 2.8.1: both driver and device ring wrap counters
// are initialized to 1. For packed rings, vhost-user SET_VRING_BASE encodes
// the avail wrap counter in bit 15 and the used wrap counter in bit 31.
fn vring_base_init(feature: u128) -> u32 {
if VirtioFeature::from_bits_retain(feature).contains(VirtioFeature::RING_PACKED) {
(1 << 15) | (1 << 31)
} else {
0
}
}

#[derive(Debug)]
pub struct UpdateVuMem {
pub name: Arc<str>,
Expand Down Expand Up @@ -153,7 +177,7 @@ pub struct VuFrontend {
vu_feature: VuFeature,
device_feature: u64,
num_queues: u16,
err_fds: Box<[OwnedFd]>,
err_fds: Box<[File]>,
}

impl VuFrontend {
Expand Down Expand Up @@ -209,8 +233,8 @@ impl VuFrontend {

let mut err_fds = vec![];
for index in 0..num_queues {
let raw_fd = ffi!(unsafe { libc::eventfd(0, libc::EFD_CLOEXEC | libc::EFD_NONBLOCK) })?;
let fd = unsafe { OwnedFd::from_raw_fd(raw_fd) };
let raw_fd = ffi!(unsafe { libc::eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK) })?;
let fd = unsafe { File::from_raw_fd(raw_fd) };
session.set_virtq_err(&(index as u64), fd.as_fd())?;
log::trace!("{name}: queue-{index}: set error fd: {}", fd.as_raw_fd());
err_fds.push(fd);
Expand Down Expand Up @@ -264,7 +288,7 @@ impl Virtio for VuFrontend {
}

fn feature(&self) -> u128 {
self.device_feature as u128
(self.device_feature & (DEVICE_FEATURE_MASK | SUPPORTED_VIRTIO_FEATURES)) as u128
}

fn spawn_worker<S>(self, param: WorkerParam<S>) -> Result<(JoinHandle<()>, Arc<Notifier>)>
Expand Down Expand Up @@ -301,9 +325,10 @@ impl VirtioMio for VuFrontend {
S: IrqSender,
{
let name = &*self.name;
self.session
.set_features(&((feature | VirtioFeature::VHOST_PROTOCOL.bits()) as u64))?;
log::trace!("{name}: set driver feature: {feature:x?}");
let drv_feat =
feature as u64 | (self.device_feature & VirtioFeature::VHOST_PROTOCOL.bits() as u64);
self.session.set_features(&drv_feat)?;
log::trace!("{name}: set driver feature: {drv_feat:#x}");

for (index, notifier) in active_mio.notifiers.iter().enumerate() {
self.session
Expand All @@ -312,6 +337,7 @@ impl VirtioMio for VuFrontend {
log::trace!("{name}: queue-{index}: set kick fd: {raw_fd}");
}

let base = vring_base_init(feature);
for (index, queue) in active_mio.queues.iter().enumerate() {
let Some(queue) = queue else {
log::trace!("{name}: queue-{index} is disabled");
Expand All @@ -334,10 +360,10 @@ impl VirtioMio for VuFrontend {

let virtq_base = VirtqState {
index: index as _,
val: 0,
val: base,
};
self.session.set_virtq_base(&virtq_base)?;
log::trace!("{name}: queue-{index}: set base: {}", virtq_base.val);
log::trace!("{name}: queue-{index}: set base: {:#x}", virtq_base.val);

let mem = queue.ram();
let virtq_addr = VirtqAddr {
Expand Down Expand Up @@ -375,23 +401,39 @@ impl VirtioMio for VuFrontend {
Ok(())
}

fn handle_event<'a, Q, S>(&mut self, _: &Event, _: &mut ActiveMio<'_, Q, S>) -> Result<()>
fn handle_event<'a, Q, S>(&mut self, event: &Event, _: &mut ActiveMio<'_, Q, S>) -> Result<()>
where
Q: VirtQueue,
S: IrqSender,
{
unreachable!()
let q_index = event.token().0;
if let Some(mut fd) = self.err_fds.get(q_index) {
let _ = fd.read(&mut [0u8; 8]);
}
vu_error::QueueErr {
index: q_index as u16,
}
.fail()?
}

fn handle_queue<Q, S>(&mut self, index: u16, _: &mut ActiveMio<'_, Q, S>) -> Result<()>
fn handle_queue<Q, S>(&mut self, index: u16, active_mio: &mut ActiveMio<'_, Q, S>) -> Result<()>
where
Q: VirtQueue,
S: IrqSender,
{
unreachable!(
"{}: queue {index} notification should go to vhost-user backend",
self.name
)
if let Some(notifier) = active_mio.notifiers.get(index as usize) {
log::warn!(
"{}: forwarding queue {index} notification to vhost-user backend",
self.name
);
notifier.notify()?;
} else {
log::error!(
"{}: unexpected queue {index} notification without kick notifier",
self.name
);
}
Ok(())
}

fn reset(&mut self, registry: &Registry) {
Expand Down Expand Up @@ -421,7 +463,8 @@ impl VirtioMio for VuFrontend {
{
log::error!("{name}: failed to reset device status: {e:?}");
}
for (index, fd) in self.err_fds.iter().enumerate() {
for (index, mut fd) in self.err_fds.iter().enumerate() {
let _ = fd.read(&mut [0u8; 8]);
if let Err(e) = registry.deregister(&mut SourceFd(&fd.as_raw_fd())) {
log::error!("{name}: queue-{index}: failed to deregister error fd: {e:?}");
}
Expand Down
Loading
Loading