diff --git a/alioth/src/fuse/fuse.rs b/alioth/src/fuse/fuse.rs index 341c9347..c9c489d2 100644 --- a/alioth/src/fuse/fuse.rs +++ b/alioth/src/fuse/fuse.rs @@ -57,6 +57,8 @@ pub enum Error { DaxMapping { source: Box, }, + #[snafu(display("Invalid DAX range: offset {offset:#x}, size {len:#x}"))] + InvalidDaxRange { offset: u64, len: u64 }, } impl From<&IoError> for Error { @@ -78,6 +80,7 @@ impl Error { Error::FileNotOpened { .. } => libc::EBADF, Error::InvalidFileHandle { .. } => libc::EBADF, Error::DaxMapping { .. } => libc::EINVAL, + Error::InvalidDaxRange { .. } => libc::EINVAL, } } } diff --git a/alioth/src/virtio/dev/fs/fs.rs b/alioth/src/virtio/dev/fs/fs.rs index d964dbc5..5f2ab511 100644 --- a/alioth/src/virtio/dev/fs/fs.rs +++ b/alioth/src/virtio/dev/fs/fs.rs @@ -44,6 +44,17 @@ use crate::{bitflags, ffi, impl_mmio_for_zerocopy}; pub const DAX_SHMEM_ID: u8 = 0; +/// Returns the address of `len` bytes at `offset` of a DAX window. +/// +/// The bytes must be within the window, so that mapping them does not +/// replace other mappings of the process. +fn dax_addr(window: &ArcMemPages, offset: u64, len: u64) -> fuse::Result { + match offset.checked_add(len) { + Some(end) if end <= window.size() => Ok(window.addr() + offset as usize), + _ => fuse::error::InvalidDaxRange { offset, len }.fail(), + } +} + impl DaxRegion for ArcMemPages { fn map( &self, @@ -55,7 +66,7 @@ impl DaxRegion for ArcMemPages { ) -> fuse::Result<()> { let fd = fd.as_raw_fd(); - let map_addr = self.addr() + m_offset as usize; + let map_addr = dax_addr(self, m_offset, len)?; let mut prot = 0; if flag.contains(FuseSetupmappingFlag::READ) { @@ -83,7 +94,7 @@ impl DaxRegion for ArcMemPages { } fn unmap(&self, m_offset: u64, len: u64) -> fuse::Result<()> { - let map_addr = self.addr() + m_offset as usize; + let map_addr = dax_addr(self, m_offset, len)?; let flags = libc::MAP_ANONYMOUS | libc::MAP_PRIVATE | libc::MAP_FIXED; ffi!( unsafe { libc::mmap(map_addr as _, len as _, libc::PROT_NONE, flags, -1, 0) }, @@ -431,3 +442,7 @@ where self.fuse.set_dax_region(Box::new(vu_dax_region)); } } + +#[cfg(test)] +#[path = "fs_test.rs"] +mod tests; diff --git a/alioth/src/virtio/dev/fs/fs_test.rs b/alioth/src/virtio/dev/fs/fs_test.rs new file mode 100644 index 00000000..ba06874d --- /dev/null +++ b/alioth/src/virtio/dev/fs/fs_test.rs @@ -0,0 +1,68 @@ +// 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::io::Write; + +use assert_matches::assert_matches; +use libc::{_SC_PAGESIZE, PROT_NONE, sysconf}; +use rstest::rstest; + +use crate::fuse::bindings::FuseSetupmappingFlag; +use crate::fuse::{DaxRegion, Error}; +use crate::mem::mapped::ArcMemPages; + +fn page_size() -> u64 { + unsafe { sysconf(_SC_PAGESIZE) as u64 } +} + +#[rstest] +#[case::first_page(0, 1, true)] +#[case::last_page(1, 1, true)] +#[case::whole_window(0, 2, true)] +#[case::after_window(2, 1, false)] +#[case::across_end(1, 2, false)] +fn dax_range(#[case] offset: u64, #[case] len: u64, #[case] valid: bool) { + // A DAX window of 2 pages, with `len` pages at page `offset` to map. + let page = page_size(); + let window = ArcMemPages::from_anonymous(2 * page as usize, Some(PROT_NONE), None).unwrap(); + let (offset, len) = (offset * page, len * page); + + let mut file = tempfile::tempfile().unwrap(); + file.write_all(b"alioth").unwrap(); + file.set_len(2 * page).unwrap(); + + let ret = window.map(offset, &file, 0, len, FuseSetupmappingFlag::READ); + if valid { + assert_matches!(ret, Ok(())); + let ptr = (window.addr() + offset as usize) as *const [u8; 6]; + assert_eq!(unsafe { ptr.read() }, *b"alioth"); + assert_matches!(window.unmap(offset, len), Ok(())); + } else { + assert_matches!(ret, Err(Error::InvalidDaxRange { .. })); + assert_matches!( + window.unmap(offset, len), + Err(Error::InvalidDaxRange { .. }) + ); + } +} + +#[test] +fn dax_range_overflow() { + let page = page_size(); + let window = ArcMemPages::from_anonymous(page as usize, Some(PROT_NONE), None).unwrap(); + assert_matches!( + window.unmap(u64::MAX, page), + Err(Error::InvalidDaxRange { .. }) + ); +} diff --git a/alioth/src/virtio/dev/fs/vu.rs b/alioth/src/virtio/dev/fs/vu.rs index 6f1b2980..22dc47d2 100644 --- a/alioth/src/virtio/dev/fs/vu.rs +++ b/alioth/src/virtio/dev/fs/vu.rs @@ -14,13 +14,13 @@ use std::fs::File; use std::io::ErrorKind; -use std::mem::size_of_val; -use std::os::fd::{AsFd, AsRawFd}; +use std::os::fd::{AsFd, AsRawFd, OwnedFd}; use std::path::Path; +use std::slice; use std::sync::Arc; use std::thread::JoinHandle; -use libc::{MAP_ANONYMOUS, MAP_FAILED, MAP_FIXED, MAP_PRIVATE, MAP_SHARED, PROT_NONE, mmap}; +use libc::PROT_NONE; use mio::event::Event; use mio::unix::SourceFd; use mio::{Interest, Registry, Token}; @@ -28,6 +28,7 @@ use serde::Deserialize; use serde_aco::Help; use zerocopy::{FromZeros, IntoBytes}; +use crate::align_up; use crate::errors::BoxTrace; use crate::fuse::bindings::FuseSetupmappingFlag; use crate::fuse::{self, DaxRegion}; @@ -38,14 +39,13 @@ use crate::virtio::dev::fs::{DAX_SHMEM_ID, FsConfig, FsFeature}; use crate::virtio::dev::{DevSpec, Virtio, WorkerParam}; use crate::virtio::queue::VirtQueue; use crate::virtio::vu::bindings::{ - DeviceConfig, VhostUserMmap, VhostUserMmapFlag, VuBackMsg, VuFeature, + DeviceConfig, Message, VhostUserMmap, VhostUserMmapFlag, VuBackMsg, VuFeature, }; use crate::virtio::vu::conn::VuChannel; use crate::virtio::vu::frontend::VuFrontend; use crate::virtio::vu::{Error, error as vu_error}; use crate::virtio::worker::mio::{ActiveMio, Mio, VirtioMio}; use crate::virtio::{DeviceId, IrqSender, Result}; -use crate::{align_up, ffi}; #[derive(Debug)] pub struct VuFs { @@ -225,71 +225,13 @@ impl VirtioMio for VuFs { .fail()?; }; loop { - let mut fds = [None]; - let msg = channel.recv_msg(&mut fds); - let (request, size) = match msg { - Ok(m) => (m.request, m.size), + let mut fd = None; + let msg = match channel.recv_msg(slice::from_mut(&mut fd)) { + Ok(msg) => msg, Err(Error::System { error, .. }) if error.kind() == ErrorKind::WouldBlock => break, Err(e) => return Err(e)?, }; - let payload: VhostUserMmap = channel.recv_payload()?; - - if size as usize != size_of_val(&payload) { - return vu_error::PayloadSize { - want: size_of_val(&payload), - got: size, - } - .fail()?; - } - match VuBackMsg::from(request) { - VuBackMsg::SHMEM_MAP => { - let [Some(fd)] = fds else { - return vu_error::MissingFd { req: request }.fail()?; - }; - let shm_offset = payload.shm_offset; - let map_addr = dax_region.addr() + shm_offset as usize; - let prot = if payload.flags.contains(VhostUserMmapFlag::RW) { - libc::PROT_READ | libc::PROT_WRITE - } else { - libc::PROT_READ - }; - log::trace!( - "{}: mapping fd {} to offset {shm_offset:#x}, size {:#x}", - self.name(), - fd.as_raw_fd(), - payload.len, - ); - ffi!( - unsafe { - mmap( - map_addr as _, - payload.len as _, - prot, - MAP_SHARED | MAP_FIXED, - fd.as_raw_fd(), - payload.fd_offset as _, - ) - }, - MAP_FAILED - )?; - } - VuBackMsg::SHMEM_UNMAP => { - let shm_offset = payload.shm_offset; - let len = payload.len; - log::trace!( - "{}: unmapping offset {shm_offset:#x}, size {len:#x}", - self.name(), - ); - let map_addr = dax_region.addr() + shm_offset as usize; - let flags = MAP_ANONYMOUS | MAP_PRIVATE | MAP_FIXED; - ffi!( - unsafe { mmap(map_addr as _, len as _, PROT_NONE, flags, -1, 0) }, - MAP_FAILED - )?; - } - _ => unimplemented!("{}: unknown request {request:#x}", self.name()), - } - channel.reply(VuBackMsg::from(request), &0u64, &[])?; + handle_backend_req(self.name(), channel, dax_region, &msg, fd)?; } Ok(()) } @@ -311,6 +253,59 @@ impl VirtioMio for VuFs { } } +/// Handles a request from the backend, given its header and file +/// descriptor. +fn handle_backend_req( + name: &str, + channel: &VuChannel, + dax_region: &ArcMemPages, + msg: &Message, + fd: Option, +) -> Result<()> { + let req = VuBackMsg::from(msg.request); + // The frontend negotiates only the SHMEM requests. The payload of + // another request is unknown, so the channel cannot go on. + if !matches!(req, VuBackMsg::SHMEM_MAP | VuBackMsg::SHMEM_UNMAP) { + return vu_error::UnsupportedReq { req: msg.request }.fail()?; + } + let want = size_of::(); + if msg.size as usize != want { + return vu_error::PayloadSize { + want, + got: msg.size, + } + .fail()?; + } + let payload: VhostUserMmap = channel.recv_payload()?; + let (offset, len) = (payload.shm_offset, payload.len); + let ret = if req == VuBackMsg::SHMEM_MAP { + let Some(fd) = fd else { + return vu_error::MissingFd { req: msg.request }.fail()?; + }; + let mut flag = FuseSetupmappingFlag::READ; + if payload.flags.contains(VhostUserMmapFlag::RW) { + flag |= FuseSetupmappingFlag::WRITE; + } + let file = File::from(fd); + log::trace!( + "{name}: mapping fd {} to offset {offset:#x}, size {len:#x}", + file.as_raw_fd() + ); + dax_region.map(offset, &file, payload.fd_offset, len, flag) + } else { + log::trace!("{name}: unmapping offset {offset:#x}, size {len:#x}"); + dax_region.unmap(offset, len) + }; + if let Err(e) = &ret { + log::error!("{name}: failed to handle {req:?}: {e:?}"); + } + if msg.flag.need_reply() { + let code = if ret.is_ok() { 0 } else { u64::MAX }; + channel.reply(req, &code, &[])?; + } + Ok(()) +} + #[derive(Debug)] pub struct VuDaxRegion { pub channel: Arc, @@ -356,3 +351,7 @@ impl DaxRegion for VuDaxRegion { .box_trace(fuse::error::DaxMapping) } } + +#[cfg(test)] +#[path = "vu_test.rs"] +mod tests; diff --git a/alioth/src/virtio/dev/fs/vu_test.rs b/alioth/src/virtio/dev/fs/vu_test.rs new file mode 100644 index 00000000..ab88a8c6 --- /dev/null +++ b/alioth/src/virtio/dev/fs/vu_test.rs @@ -0,0 +1,118 @@ +// 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::io::{ErrorKind, Read, Write}; +use std::os::fd::AsFd; +use std::os::unix::net::UnixStream; +use std::{slice, thread}; + +use assert_matches::assert_matches; +use libc::PROT_NONE; +use rstest::rstest; +use zerocopy::IntoBytes; + +use crate::mem::mapped::ArcMemPages; +use crate::virtio::dev::fs::vu::handle_backend_req; +use crate::virtio::vu::Error as VuError; +use crate::virtio::vu::bindings::{Message, MessageFlag, VhostUserMmap, VuBackMsg}; +use crate::virtio::vu::conn::VuChannel; +use crate::virtio::{Error, Result}; + +const WINDOW_SIZE: u64 = 0x1000; + +fn fixture_window() -> ArcMemPages { + ArcMemPages::from_anonymous(WINDOW_SIZE as usize, Some(PROT_NONE), None).unwrap() +} + +/// Returns the channel of the frontend and that of the backend. +fn fixture_channels() -> (VuChannel, VuChannel) { + let (front, back) = UnixStream::pair().unwrap(); + (VuChannel { conn: front }, VuChannel { conn: back }) +} + +/// Receives a request from the backend and handles it. +fn handle_req(front: &VuChannel, window: &ArcMemPages) -> Result<()> { + let mut fd = None; + let msg = front.recv_msg(slice::from_mut(&mut fd)).unwrap(); + handle_backend_req("test", front, window, &msg, fd) +} + +#[rstest] +#[case(0, true)] +#[case(WINDOW_SIZE, false)] +fn shmem_map(#[case] offset: u64, #[case] valid: bool) { + let window = fixture_window(); + let (front, back) = fixture_channels(); + let file = tempfile::tempfile().unwrap(); + file.set_len(WINDOW_SIZE).unwrap(); + + let payload = VhostUserMmap { + shm_offset: offset, + len: WINDOW_SIZE, + ..Default::default() + }; + let backend = thread::spawn(move || back.shmem_map(&payload, file.as_fd())); + // A failed request does not stop the frontend. + assert_matches!(handle_req(&front, &window), Ok(())); + let ret = backend.join().unwrap(); + if valid { + assert_matches!(ret, Ok(())); + } else { + assert_matches!(ret, Err(VuError::RequestErr { ret: u64::MAX, .. })); + } +} + +#[test] +fn unsupported_req() { + let window = fixture_window(); + let (front, back) = fixture_channels(); + + let msg = Message { + request: VuBackMsg::CONFIG_CHANGE_MSG.raw(), + flag: MessageFlag::sender(), + size: 0, + }; + (&back.conn).write_all(msg.as_bytes()).unwrap(); + assert_matches!( + handle_req(&front, &window), + Err(Error::Vu { source, .. }) if matches!(*source, VuError::UnsupportedReq { req: 2, .. }) + ); +} + +#[test] +fn no_reply() { + let window = fixture_window(); + let (front, back) = fixture_channels(); + + // The backend does not ask for a reply. + let msg = Message { + request: VuBackMsg::SHMEM_UNMAP.raw(), + flag: MessageFlag(MessageFlag::VERSION_1), + size: size_of::() as u32, + }; + let payload = VhostUserMmap { + len: WINDOW_SIZE, + ..Default::default() + }; + (&back.conn).write_all(msg.as_bytes()).unwrap(); + (&back.conn).write_all(payload.as_bytes()).unwrap(); + assert_matches!(handle_req(&front, &window), Ok(())); + + back.conn.set_nonblocking(true).unwrap(); + let mut buf = [0u8; 1]; + assert_matches!( + (&back.conn).read(&mut buf), + Err(e) if e.kind() == ErrorKind::WouldBlock + ); +} diff --git a/alioth/src/virtio/vu/vu.rs b/alioth/src/virtio/vu/vu.rs index f98f2639..29fe5571 100644 --- a/alioth/src/virtio/vu/vu.rs +++ b/alioth/src/virtio/vu/vu.rs @@ -37,6 +37,8 @@ pub enum Error { System { error: std::io::Error }, #[snafu(display("vhost-user message ({req:#x}) missing fd"))] MissingFd { req: u32 }, + #[snafu(display("Unsupported vhost-user request {req:#x}"))] + UnsupportedReq { req: u32 }, #[snafu(display("Unexpected vhost-user response, want {want}, got {got}"))] Response { want: u32, got: u32 }, #[snafu(display("Unexpected vhost-user message size, want {want}, get {got}"))]