Skip to content
Merged
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
3 changes: 3 additions & 0 deletions alioth/src/fuse/fuse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ pub enum Error {
DaxMapping {
source: Box<dyn DebugTrace + Send + Sync + 'static>,
},
#[snafu(display("Invalid DAX range: offset {offset:#x}, size {len:#x}"))]
InvalidDaxRange { offset: u64, len: u64 },
}

impl From<&IoError> for Error {
Expand All @@ -78,6 +80,7 @@ impl Error {
Error::FileNotOpened { .. } => libc::EBADF,
Error::InvalidFileHandle { .. } => libc::EBADF,
Error::DaxMapping { .. } => libc::EINVAL,
Error::InvalidDaxRange { .. } => libc::EINVAL,
}
}
}
Expand Down
19 changes: 17 additions & 2 deletions alioth/src/virtio/dev/fs/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<usize> {
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,
Expand All @@ -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) {
Expand Down Expand Up @@ -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) },
Expand Down Expand Up @@ -431,3 +442,7 @@ where
self.fuse.set_dax_region(Box::new(vu_dax_region));
}
}

#[cfg(test)]
#[path = "fs_test.rs"]
mod tests;
68 changes: 68 additions & 0 deletions alioth/src/virtio/dev/fs/fs_test.rs
Original file line number Diff line number Diff line change
@@ -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 { .. })
);
}
133 changes: 66 additions & 67 deletions alioth/src/virtio/dev/fs/vu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,21 @@

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};
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};
Expand All @@ -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 {
Expand Down Expand Up @@ -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(())
}
Expand All @@ -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<OwnedFd>,
) -> 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::<VhostUserMmap>();
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<VuChannel>,
Expand Down Expand Up @@ -356,3 +351,7 @@ impl DaxRegion for VuDaxRegion {
.box_trace(fuse::error::DaxMapping)
}
}

#[cfg(test)]
#[path = "vu_test.rs"]
mod tests;
Loading
Loading