blob: c1d1adb22d7fdf2d812d29769622167554324efd [file] [log] [blame]
// Copyright (C) 2019 Alibaba Cloud Computing. All rights reserved.
//
// Portions Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
//
// Portions Copyright 2017 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE-BSD-3-Clause file.
//
// SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause
//! Helper structure for working with mmaped memory regions in Unix.
use std::io;
use std::os::unix::io::AsRawFd;
use std::ptr::null_mut;
use std::result;
use crate::bitmap::{Bitmap, BS};
use crate::guest_memory::FileOffset;
use crate::mmap::{check_file_offset, NewBitmap};
use crate::volatile_memory::{self, VolatileMemory, VolatileSlice};
/// Error conditions that may arise when creating a new `MmapRegion` object.
#[derive(Debug, thiserror::Error)]
pub enum Error {
/// The specified file offset and length cause overflow when added.
#[error("The specified file offset and length cause overflow when added")]
InvalidOffsetLength,
/// The specified pointer to the mapping is not page-aligned.
#[error("The specified pointer to the mapping is not page-aligned")]
InvalidPointer,
/// The forbidden `MAP_FIXED` flag was specified.
#[error("The forbidden `MAP_FIXED` flag was specified")]
MapFixed,
/// Mappings using the same fd overlap in terms of file offset and length.
#[error("Mappings using the same fd overlap in terms of file offset and length")]
MappingOverlap,
/// A mapping with offset + length > EOF was attempted.
#[error("The specified file offset and length is greater then file length")]
MappingPastEof,
/// The `mmap` call returned an error.
#[error("{0}")]
Mmap(io::Error),
/// Seeking the end of the file returned an error.
#[error("Error seeking the end of the file: {0}")]
SeekEnd(io::Error),
/// Seeking the start of the file returned an error.
#[error("Error seeking the start of the file: {0}")]
SeekStart(io::Error),
}
pub type Result<T> = result::Result<T, Error>;
/// A factory struct to build `MmapRegion` objects.
pub struct MmapRegionBuilder<B = ()> {
size: usize,
prot: i32,
flags: i32,
file_offset: Option<FileOffset>,
raw_ptr: Option<*mut u8>,
hugetlbfs: Option<bool>,
bitmap: B,
}
impl<B: Bitmap + Default> MmapRegionBuilder<B> {
/// Create a new `MmapRegionBuilder` using the default value for
/// the inner `Bitmap` object.
pub fn new(size: usize) -> Self {
Self::new_with_bitmap(size, B::default())
}
}
impl<B: Bitmap> MmapRegionBuilder<B> {
/// Create a new `MmapRegionBuilder` using the provided `Bitmap` object.
///
/// When instantiating the builder for a region that does not require dirty bitmap
/// bitmap tracking functionality, we can specify a trivial `Bitmap` implementation
/// such as `()`.
pub fn new_with_bitmap(size: usize, bitmap: B) -> Self {
MmapRegionBuilder {
size,
prot: 0,
flags: libc::MAP_ANONYMOUS | libc::MAP_PRIVATE,
file_offset: None,
raw_ptr: None,
hugetlbfs: None,
bitmap,
}
}
/// Create the `MmapRegion` object with the specified mmap memory protection flag `prot`.
pub fn with_mmap_prot(mut self, prot: i32) -> Self {
self.prot = prot;
self
}
/// Create the `MmapRegion` object with the specified mmap `flags`.
pub fn with_mmap_flags(mut self, flags: i32) -> Self {
self.flags = flags;
self
}
/// Create the `MmapRegion` object with the specified `file_offset`.
pub fn with_file_offset(mut self, file_offset: FileOffset) -> Self {
self.file_offset = Some(file_offset);
self
}
/// Create the `MmapRegion` object with the specified `hugetlbfs` flag.
pub fn with_hugetlbfs(mut self, hugetlbfs: bool) -> Self {
self.hugetlbfs = Some(hugetlbfs);
self
}
/// Create the `MmapRegion` object with pre-mmapped raw pointer.
///
/// # Safety
///
/// To use this safely, the caller must guarantee that `raw_addr` and `self.size` define a
/// region within a valid mapping that is already present in the process.
pub unsafe fn with_raw_mmap_pointer(mut self, raw_ptr: *mut u8) -> Self {
self.raw_ptr = Some(raw_ptr);
self
}
/// Build the `MmapRegion` object.
pub fn build(self) -> Result<MmapRegion<B>> {
if self.raw_ptr.is_some() {
return self.build_raw();
}
// Forbid MAP_FIXED, as it doesn't make sense in this context, and is pretty dangerous
// in general.
if self.flags & libc::MAP_FIXED != 0 {
return Err(Error::MapFixed);
}
let (fd, offset) = if let Some(ref f_off) = self.file_offset {
check_file_offset(f_off, self.size)?;
(f_off.file().as_raw_fd(), f_off.start())
} else {
(-1, 0)
};
#[cfg(not(miri))]
// SAFETY: This is safe because we're not allowing MAP_FIXED, and invalid parameters
// cannot break Rust safety guarantees (things may change if we're mapping /dev/mem or
// some wacky file).
let addr = unsafe {
libc::mmap(
null_mut(),
self.size,
self.prot,
self.flags,
fd,
offset as libc::off_t,
)
};
#[cfg(not(miri))]
if addr == libc::MAP_FAILED {
return Err(Error::Mmap(io::Error::last_os_error()));
}
#[cfg(miri)]
if self.size == 0 {
return Err(Error::Mmap(io::Error::from_raw_os_error(libc::EINVAL)));
}
// Miri does not support the mmap syscall, so we use rust's allocator for miri tests
#[cfg(miri)]
let addr = unsafe {
std::alloc::alloc_zeroed(std::alloc::Layout::from_size_align(self.size, 8).unwrap())
};
Ok(MmapRegion {
addr: addr as *mut u8,
size: self.size,
bitmap: self.bitmap,
file_offset: self.file_offset,
prot: self.prot,
flags: self.flags,
owned: true,
hugetlbfs: self.hugetlbfs,
})
}
fn build_raw(self) -> Result<MmapRegion<B>> {
// SAFETY: Safe because this call just returns the page size and doesn't have any side
// effects.
let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) } as usize;
let addr = self.raw_ptr.unwrap();
// Check that the pointer to the mapping is page-aligned.
if (addr as usize) & (page_size - 1) != 0 {
return Err(Error::InvalidPointer);
}
Ok(MmapRegion {
addr,
size: self.size,
bitmap: self.bitmap,
file_offset: self.file_offset,
prot: self.prot,
flags: self.flags,
owned: false,
hugetlbfs: self.hugetlbfs,
})
}
}
/// Helper structure for working with mmaped memory regions in Unix.
///
/// The structure is used for accessing the guest's physical memory by mmapping it into
/// the current process.
///
/// # Limitations
/// When running a 64-bit virtual machine on a 32-bit hypervisor, only part of the guest's
/// physical memory may be mapped into the current process due to the limited virtual address
/// space size of the process.
#[derive(Debug)]
pub struct MmapRegion<B = ()> {
addr: *mut u8,
size: usize,
bitmap: B,
file_offset: Option<FileOffset>,
prot: i32,
flags: i32,
owned: bool,
hugetlbfs: Option<bool>,
}
// SAFETY: Send and Sync aren't automatically inherited for the raw address pointer.
// Accessing that pointer is only done through the stateless interface which
// allows the object to be shared by multiple threads without a decrease in
// safety.
unsafe impl<B: Send> Send for MmapRegion<B> {}
// SAFETY: See comment above.
unsafe impl<B: Sync> Sync for MmapRegion<B> {}
impl<B: NewBitmap> MmapRegion<B> {
/// Creates a shared anonymous mapping of `size` bytes.
///
/// # Arguments
/// * `size` - The size of the memory region in bytes.
pub fn new(size: usize) -> Result<Self> {
MmapRegionBuilder::new_with_bitmap(size, B::with_len(size))
.with_mmap_prot(libc::PROT_READ | libc::PROT_WRITE)
.with_mmap_flags(libc::MAP_ANONYMOUS | libc::MAP_NORESERVE | libc::MAP_PRIVATE)
.build()
}
/// Creates a shared file mapping of `size` bytes.
///
/// # Arguments
/// * `file_offset` - The mapping will be created at offset `file_offset.start` in the file
/// referred to by `file_offset.file`.
/// * `size` - The size of the memory region in bytes.
pub fn from_file(file_offset: FileOffset, size: usize) -> Result<Self> {
MmapRegionBuilder::new_with_bitmap(size, B::with_len(size))
.with_file_offset(file_offset)
.with_mmap_prot(libc::PROT_READ | libc::PROT_WRITE)
.with_mmap_flags(libc::MAP_NORESERVE | libc::MAP_SHARED)
.build()
}
/// Creates a mapping based on the provided arguments.
///
/// # Arguments
/// * `file_offset` - if provided, the method will create a file mapping at offset
/// `file_offset.start` in the file referred to by `file_offset.file`.
/// * `size` - The size of the memory region in bytes.
/// * `prot` - The desired memory protection of the mapping.
/// * `flags` - This argument determines whether updates to the mapping are visible to other
/// processes mapping the same region, and whether updates are carried through to
/// the underlying file.
pub fn build(
file_offset: Option<FileOffset>,
size: usize,
prot: i32,
flags: i32,
) -> Result<Self> {
let mut builder = MmapRegionBuilder::new_with_bitmap(size, B::with_len(size))
.with_mmap_prot(prot)
.with_mmap_flags(flags);
if let Some(v) = file_offset {
builder = builder.with_file_offset(v);
}
builder.build()
}
/// Creates a `MmapRegion` instance for an externally managed mapping.
///
/// This method is intended to be used exclusively in situations in which the mapping backing
/// the region is provided by an entity outside the control of the caller (e.g. the dynamic
/// linker).
///
/// # Arguments
/// * `addr` - Pointer to the start of the mapping. Must be page-aligned.
/// * `size` - The size of the memory region in bytes.
/// * `prot` - Must correspond to the memory protection attributes of the existing mapping.
/// * `flags` - Must correspond to the flags that were passed to `mmap` for the creation of
/// the existing mapping.
///
/// # Safety
///
/// To use this safely, the caller must guarantee that `addr` and `size` define a region within
/// a valid mapping that is already present in the process.
pub unsafe fn build_raw(addr: *mut u8, size: usize, prot: i32, flags: i32) -> Result<Self> {
MmapRegionBuilder::new_with_bitmap(size, B::with_len(size))
.with_raw_mmap_pointer(addr)
.with_mmap_prot(prot)
.with_mmap_flags(flags)
.build()
}
}
impl<B: Bitmap> MmapRegion<B> {
/// Returns a pointer to the beginning of the memory region. Mutable accesses performed
/// using the resulting pointer are not automatically accounted for by the dirty bitmap
/// tracking functionality.
///
/// Should only be used for passing this region to ioctls for setting guest memory.
pub fn as_ptr(&self) -> *mut u8 {
self.addr
}
/// Returns the size of this region.
pub fn size(&self) -> usize {
self.size
}
/// Returns information regarding the offset into the file backing this region (if any).
pub fn file_offset(&self) -> Option<&FileOffset> {
self.file_offset.as_ref()
}
/// Returns the value of the `prot` parameter passed to `mmap` when mapping this region.
pub fn prot(&self) -> i32 {
self.prot
}
/// Returns the value of the `flags` parameter passed to `mmap` when mapping this region.
pub fn flags(&self) -> i32 {
self.flags
}
/// Returns `true` if the mapping is owned by this `MmapRegion` instance.
pub fn owned(&self) -> bool {
self.owned
}
/// Checks whether this region and `other` are backed by overlapping
/// [`FileOffset`](struct.FileOffset.html) objects.
///
/// This is mostly a sanity check available for convenience, as different file descriptors
/// can alias the same file.
pub fn fds_overlap<T: Bitmap>(&self, other: &MmapRegion<T>) -> bool {
if let Some(f_off1) = self.file_offset() {
if let Some(f_off2) = other.file_offset() {
if f_off1.file().as_raw_fd() == f_off2.file().as_raw_fd() {
let s1 = f_off1.start();
let s2 = f_off2.start();
let l1 = self.len() as u64;
let l2 = other.len() as u64;
if s1 < s2 {
return s1 + l1 > s2;
} else {
return s2 + l2 > s1;
}
}
}
}
false
}
/// Set the hugetlbfs of the region
pub fn set_hugetlbfs(&mut self, hugetlbfs: bool) {
self.hugetlbfs = Some(hugetlbfs)
}
/// Returns `true` if the region is hugetlbfs
pub fn is_hugetlbfs(&self) -> Option<bool> {
self.hugetlbfs
}
/// Returns a reference to the inner bitmap object.
pub fn bitmap(&self) -> &B {
&self.bitmap
}
}
impl<B: Bitmap> VolatileMemory for MmapRegion<B> {
type B = B;
fn len(&self) -> usize {
self.size
}
fn get_slice(
&self,
offset: usize,
count: usize,
) -> volatile_memory::Result<VolatileSlice<BS<B>>> {
let _ = self.compute_end_offset(offset, count)?;
Ok(
// SAFETY: Safe because we checked that offset + count was within our range and we only
// ever hand out volatile accessors.
unsafe {
VolatileSlice::with_bitmap(
self.addr.add(offset),
count,
self.bitmap.slice_at(offset),
None,
)
},
)
}
}
impl<B> Drop for MmapRegion<B> {
fn drop(&mut self) {
if self.owned {
// SAFETY: This is safe because we mmap the area at addr ourselves, and nobody
// else is holding a reference to it.
unsafe {
#[cfg(not(miri))]
libc::munmap(self.addr as *mut libc::c_void, self.size);
#[cfg(miri)]
std::alloc::dealloc(
self.addr,
std::alloc::Layout::from_size_align(self.size, 8).unwrap(),
);
}
}
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::undocumented_unsafe_blocks)]
use super::*;
use std::io::Write;
use std::slice;
use std::sync::Arc;
use vmm_sys_util::tempfile::TempFile;
use crate::bitmap::AtomicBitmap;
type MmapRegion = super::MmapRegion<()>;
// Adding a helper method to extract the errno within an Error::Mmap(e), or return a
// distinctive value when the error is represented by another variant.
impl Error {
pub fn raw_os_error(&self) -> i32 {
match self {
Error::Mmap(e) => e.raw_os_error().unwrap(),
_ => std::i32::MIN,
}
}
}
#[test]
fn test_mmap_region_new() {
assert!(MmapRegion::new(0).is_err());
let size = 4096;
let r = MmapRegion::new(4096).unwrap();
assert_eq!(r.size(), size);
assert!(r.file_offset().is_none());
assert_eq!(r.prot(), libc::PROT_READ | libc::PROT_WRITE);
assert_eq!(
r.flags(),
libc::MAP_ANONYMOUS | libc::MAP_NORESERVE | libc::MAP_PRIVATE
);
}
#[test]
fn test_mmap_region_set_hugetlbfs() {
assert!(MmapRegion::new(0).is_err());
let size = 4096;
let r = MmapRegion::new(size).unwrap();
assert_eq!(r.size(), size);
assert!(r.file_offset().is_none());
assert_eq!(r.prot(), libc::PROT_READ | libc::PROT_WRITE);
assert_eq!(
r.flags(),
libc::MAP_ANONYMOUS | libc::MAP_NORESERVE | libc::MAP_PRIVATE
);
assert_eq!(r.is_hugetlbfs(), None);
let mut r = MmapRegion::new(size).unwrap();
r.set_hugetlbfs(false);
assert_eq!(r.size(), size);
assert!(r.file_offset().is_none());
assert_eq!(r.prot(), libc::PROT_READ | libc::PROT_WRITE);
assert_eq!(
r.flags(),
libc::MAP_ANONYMOUS | libc::MAP_NORESERVE | libc::MAP_PRIVATE
);
assert_eq!(r.is_hugetlbfs(), Some(false));
let mut r = MmapRegion::new(size).unwrap();
r.set_hugetlbfs(true);
assert_eq!(r.size(), size);
assert!(r.file_offset().is_none());
assert_eq!(r.prot(), libc::PROT_READ | libc::PROT_WRITE);
assert_eq!(
r.flags(),
libc::MAP_ANONYMOUS | libc::MAP_NORESERVE | libc::MAP_PRIVATE
);
assert_eq!(r.is_hugetlbfs(), Some(true));
}
#[test]
#[cfg(not(miri))] // Miri cannot mmap files
fn test_mmap_region_from_file() {
let mut f = TempFile::new().unwrap().into_file();
let offset: usize = 0;
let buf1 = [1u8, 2, 3, 4, 5];
f.write_all(buf1.as_ref()).unwrap();
let r = MmapRegion::from_file(FileOffset::new(f, offset as u64), buf1.len()).unwrap();
assert_eq!(r.size(), buf1.len() - offset);
assert_eq!(r.file_offset().unwrap().start(), offset as u64);
assert_eq!(r.prot(), libc::PROT_READ | libc::PROT_WRITE);
assert_eq!(r.flags(), libc::MAP_NORESERVE | libc::MAP_SHARED);
let buf2 = unsafe { slice::from_raw_parts(r.as_ptr(), buf1.len() - offset) };
assert_eq!(&buf1[offset..], buf2);
}
#[test]
#[cfg(not(miri))] // Miri cannot mmap files
fn test_mmap_region_build() {
let a = Arc::new(TempFile::new().unwrap().into_file());
let prot = libc::PROT_READ | libc::PROT_WRITE;
let flags = libc::MAP_NORESERVE | libc::MAP_PRIVATE;
let offset = 4096;
let size = 1000;
// Offset + size will overflow.
let r = MmapRegion::build(
Some(FileOffset::from_arc(a.clone(), std::u64::MAX)),
size,
prot,
flags,
);
assert_eq!(format!("{:?}", r.unwrap_err()), "InvalidOffsetLength");
// Offset + size is greater than the size of the file (which is 0 at this point).
let r = MmapRegion::build(
Some(FileOffset::from_arc(a.clone(), offset)),
size,
prot,
flags,
);
assert_eq!(format!("{:?}", r.unwrap_err()), "MappingPastEof");
// MAP_FIXED was specified among the flags.
let r = MmapRegion::build(
Some(FileOffset::from_arc(a.clone(), offset)),
size,
prot,
flags | libc::MAP_FIXED,
);
assert_eq!(format!("{:?}", r.unwrap_err()), "MapFixed");
// Let's resize the file.
assert_eq!(unsafe { libc::ftruncate(a.as_raw_fd(), 1024 * 10) }, 0);
// The offset is not properly aligned.
let r = MmapRegion::build(
Some(FileOffset::from_arc(a.clone(), offset - 1)),
size,
prot,
flags,
);
assert_eq!(r.unwrap_err().raw_os_error(), libc::EINVAL);
// The build should be successful now.
let r =
MmapRegion::build(Some(FileOffset::from_arc(a, offset)), size, prot, flags).unwrap();
assert_eq!(r.size(), size);
assert_eq!(r.file_offset().unwrap().start(), offset);
assert_eq!(r.prot(), libc::PROT_READ | libc::PROT_WRITE);
assert_eq!(r.flags(), libc::MAP_NORESERVE | libc::MAP_PRIVATE);
assert!(r.owned());
let region_size = 0x10_0000;
let bitmap = AtomicBitmap::new(region_size, 0x1000);
let builder = MmapRegionBuilder::new_with_bitmap(region_size, bitmap)
.with_hugetlbfs(true)
.with_mmap_prot(libc::PROT_READ | libc::PROT_WRITE);
assert_eq!(builder.size, region_size);
assert_eq!(builder.hugetlbfs, Some(true));
assert_eq!(builder.prot, libc::PROT_READ | libc::PROT_WRITE);
crate::bitmap::tests::test_volatile_memory(&(builder.build().unwrap()));
}
#[test]
#[cfg(not(miri))] // Causes warnings due to the pointer casts
fn test_mmap_region_build_raw() {
let addr = 0;
let size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) as usize };
let prot = libc::PROT_READ | libc::PROT_WRITE;
let flags = libc::MAP_NORESERVE | libc::MAP_PRIVATE;
let r = unsafe { MmapRegion::build_raw((addr + 1) as *mut u8, size, prot, flags) };
assert_eq!(format!("{:?}", r.unwrap_err()), "InvalidPointer");
let r = unsafe { MmapRegion::build_raw(addr as *mut u8, size, prot, flags).unwrap() };
assert_eq!(r.size(), size);
assert_eq!(r.prot(), libc::PROT_READ | libc::PROT_WRITE);
assert_eq!(r.flags(), libc::MAP_NORESERVE | libc::MAP_PRIVATE);
assert!(!r.owned());
}
#[test]
#[cfg(not(miri))] // Miri cannot mmap files
fn test_mmap_region_fds_overlap() {
let a = Arc::new(TempFile::new().unwrap().into_file());
assert_eq!(unsafe { libc::ftruncate(a.as_raw_fd(), 1024 * 10) }, 0);
let r1 = MmapRegion::from_file(FileOffset::from_arc(a.clone(), 0), 4096).unwrap();
let r2 = MmapRegion::from_file(FileOffset::from_arc(a.clone(), 4096), 4096).unwrap();
assert!(!r1.fds_overlap(&r2));
let r1 = MmapRegion::from_file(FileOffset::from_arc(a.clone(), 0), 5000).unwrap();
assert!(r1.fds_overlap(&r2));
let r2 = MmapRegion::from_file(FileOffset::from_arc(a, 0), 1000).unwrap();
assert!(r1.fds_overlap(&r2));
// Different files, so there's not overlap.
let new_file = TempFile::new().unwrap().into_file();
// Resize before mapping.
assert_eq!(
unsafe { libc::ftruncate(new_file.as_raw_fd(), 1024 * 10) },
0
);
let r2 = MmapRegion::from_file(FileOffset::new(new_file, 0), 5000).unwrap();
assert!(!r1.fds_overlap(&r2));
// R2 is not file backed, so no overlap.
let r2 = MmapRegion::new(5000).unwrap();
assert!(!r1.fds_overlap(&r2));
}
#[test]
fn test_dirty_tracking() {
// Using the `crate` prefix because we aliased `MmapRegion` to `MmapRegion<()>` for
// the rest of the unit tests above.
let m = crate::MmapRegion::<AtomicBitmap>::new(0x1_0000).unwrap();
crate::bitmap::tests::test_volatile_memory(&m);
}
}