blob: 9f934135a401e5b1ade5909c1cfd41a00b1540b9 [file] [log] [blame]
// Copyright 2019 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 file.
use std::cmp::min;
use std::fmt::{self, Debug, Display};
use std::fs::File;
use std::io::{self, Read, Seek, SeekFrom, Write};
use std::sync::Arc;
use async_trait::async_trait;
use base::{
AsRawDescriptors, FileAllocate, FileReadWriteAtVolatile, FileSetLen, FileSync, PunchHole,
SeekHole, WriteZeroesAt,
};
use cros_async::Executor;
use libc::EINVAL;
use remain::sorted;
use vm_memory::GuestMemory;
mod qcow;
pub use qcow::{QcowFile, QCOW_MAGIC};
#[cfg(feature = "composite-disk")]
mod composite;
#[cfg(feature = "composite-disk")]
use composite::{CompositeDiskFile, CDISK_MAGIC, CDISK_MAGIC_LEN};
mod android_sparse;
use android_sparse::{AndroidSparse, SPARSE_HEADER_MAGIC};
#[sorted]
#[derive(Debug)]
pub enum Error {
BlockDeviceNew(base::Error),
ConversionNotSupported,
CreateAndroidSparseDisk(android_sparse::Error),
#[cfg(feature = "composite-disk")]
CreateCompositeDisk(composite::Error),
CreateSingleFileDisk(cros_async::AsyncError),
Fallocate(cros_async::AsyncError),
Fsync(cros_async::AsyncError),
QcowError(qcow::Error),
ReadingData(io::Error),
ReadingHeader(io::Error),
ReadToMem(cros_async::AsyncError),
SeekingFile(io::Error),
SettingFileSize(io::Error),
UnknownType,
WriteFromMem(cros_async::AsyncError),
WriteFromVec(cros_async::AsyncError),
WritingData(io::Error),
}
pub type Result<T> = std::result::Result<T, Error>;
/// A trait for getting the length of a disk image or raw block device.
pub trait DiskGetLen {
/// Get the current length of the disk in bytes.
fn get_len(&self) -> io::Result<u64>;
}
impl DiskGetLen for File {
fn get_len(&self) -> io::Result<u64> {
let mut s = self;
let orig_seek = s.seek(SeekFrom::Current(0))?;
let end = s.seek(SeekFrom::End(0))? as u64;
s.seek(SeekFrom::Start(orig_seek))?;
Ok(end)
}
}
/// The prerequisites necessary to support a block device.
#[rustfmt::skip] // rustfmt won't wrap the long list of trait bounds.
pub trait DiskFile:
FileSetLen
+ DiskGetLen
+ FileSync
+ FileReadWriteAtVolatile
+ PunchHole
+ WriteZeroesAt
+ FileAllocate
+ Send
+ AsRawDescriptors
+ Debug
{
}
impl<
D: FileSetLen
+ DiskGetLen
+ FileSync
+ PunchHole
+ FileReadWriteAtVolatile
+ WriteZeroesAt
+ FileAllocate
+ Send
+ AsRawDescriptors
+ Debug,
> DiskFile for D
{
}
/// A `DiskFile` that can be converted for asychronous access.
pub trait ToAsyncDisk: DiskFile {
/// Convert a boxed self in to a box-wrapped implementaiton of AsyncDisk.
/// Used to convert a standard disk image to an async disk image. This conversion and the
/// inverse are needed so that the `Send` DiskImage can be given to the block thread where it is
/// converted to a non-`Send` AsyncDisk. The AsyncDisk can then be converted back and returned
/// to the main device thread if the block device is destroyed or reset.
fn to_async_disk(self: Box<Self>, ex: &Executor) -> Result<Box<dyn AsyncDisk>>;
}
impl ToAsyncDisk for File {
fn to_async_disk(self: Box<Self>, ex: &Executor) -> Result<Box<dyn AsyncDisk>> {
Ok(Box::new(SingleFileDisk::new(*self, ex)?))
}
}
impl Display for Error {
#[remain::check]
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use self::Error::*;
#[sorted]
match self {
BlockDeviceNew(e) => write!(f, "failed to create block device: {}", e),
ConversionNotSupported => write!(f, "requested file conversion not supported"),
CreateAndroidSparseDisk(e) => write!(f, "failure in android sparse disk: {}", e),
#[cfg(feature = "composite-disk")]
CreateCompositeDisk(e) => write!(f, "failure in composite disk: {}", e),
CreateSingleFileDisk(e) => write!(f, "failure creating single file disk: {}", e),
Fallocate(e) => write!(f, "failure with fallocate: {}", e),
Fsync(e) => write!(f, "failure with fsync: {}", e),
QcowError(e) => write!(f, "failure in qcow: {}", e),
ReadingData(e) => write!(f, "failed to read data: {}", e),
ReadingHeader(e) => write!(f, "failed to read header: {}", e),
ReadToMem(e) => write!(f, "failed to read to memory: {}", e),
SeekingFile(e) => write!(f, "failed to seek file: {}", e),
SettingFileSize(e) => write!(f, "failed to set file size: {}", e),
UnknownType => write!(f, "unknown disk type"),
WriteFromMem(e) => write!(f, "failed to write from memory: {}", e),
WriteFromVec(e) => write!(f, "failed to write from vec: {}", e),
WritingData(e) => write!(f, "failed to write data: {}", e),
}
}
}
/// The variants of image files on the host that can be used as virtual disks.
#[derive(Debug, PartialEq, Eq)]
pub enum ImageType {
Raw,
Qcow2,
CompositeDisk,
AndroidSparse,
}
fn convert_copy<R, W>(reader: &mut R, writer: &mut W, offset: u64, size: u64) -> Result<()>
where
R: Read + Seek,
W: Write + Seek,
{
const CHUNK_SIZE: usize = 65536;
let mut buf = [0; CHUNK_SIZE];
let mut read_count = 0;
reader
.seek(SeekFrom::Start(offset))
.map_err(Error::SeekingFile)?;
writer
.seek(SeekFrom::Start(offset))
.map_err(Error::SeekingFile)?;
loop {
let this_count = min(CHUNK_SIZE as u64, size - read_count) as usize;
let nread = reader
.read(&mut buf[..this_count])
.map_err(Error::ReadingData)?;
writer.write(&buf[..nread]).map_err(Error::WritingData)?;
read_count += nread as u64;
if nread == 0 || read_count == size {
break;
}
}
Ok(())
}
fn convert_reader_writer<R, W>(reader: &mut R, writer: &mut W, size: u64) -> Result<()>
where
R: Read + Seek + SeekHole,
W: Write + Seek,
{
let mut offset = 0;
while offset < size {
// Find the next range of data.
let next_data = match reader.seek_data(offset).map_err(Error::SeekingFile)? {
Some(o) => o,
None => {
// No more data in the file.
break;
}
};
let next_hole = match reader.seek_hole(next_data).map_err(Error::SeekingFile)? {
Some(o) => o,
None => {
// This should not happen - there should always be at least one hole
// after any data.
return Err(Error::SeekingFile(io::Error::from_raw_os_error(EINVAL)));
}
};
let count = next_hole - next_data;
convert_copy(reader, writer, next_data, count)?;
offset = next_hole;
}
Ok(())
}
fn convert_reader<R>(reader: &mut R, dst_file: File, dst_type: ImageType) -> Result<()>
where
R: Read + Seek + SeekHole,
{
let src_size = reader.seek(SeekFrom::End(0)).map_err(Error::SeekingFile)?;
reader
.seek(SeekFrom::Start(0))
.map_err(Error::SeekingFile)?;
// Ensure the destination file is empty before writing to it.
dst_file.set_len(0).map_err(Error::SettingFileSize)?;
match dst_type {
ImageType::Qcow2 => {
let mut dst_writer = QcowFile::new(dst_file, src_size).map_err(Error::QcowError)?;
convert_reader_writer(reader, &mut dst_writer, src_size)
}
ImageType::Raw => {
let mut dst_writer = dst_file;
// Set the length of the destination file to convert it into a sparse file
// of the desired size.
dst_writer
.set_len(src_size)
.map_err(Error::SettingFileSize)?;
convert_reader_writer(reader, &mut dst_writer, src_size)
}
_ => Err(Error::ConversionNotSupported),
}
}
/// Copy the contents of a disk image in `src_file` into `dst_file`.
/// The type of `src_file` is automatically detected, and the output file type is
/// determined by `dst_type`.
pub fn convert(src_file: File, dst_file: File, dst_type: ImageType) -> Result<()> {
let src_type = detect_image_type(&src_file)?;
match src_type {
ImageType::Qcow2 => {
let mut src_reader = QcowFile::from(src_file).map_err(Error::QcowError)?;
convert_reader(&mut src_reader, dst_file, dst_type)
}
ImageType::Raw => {
// src_file is a raw file.
let mut src_reader = src_file;
convert_reader(&mut src_reader, dst_file, dst_type)
}
// TODO(schuffelen): Implement Read + Write + SeekHole for CompositeDiskFile
_ => Err(Error::ConversionNotSupported),
}
}
/// Detect the type of an image file by checking for a valid qcow2 header.
pub fn detect_image_type(file: &File) -> Result<ImageType> {
let mut f = file;
let orig_seek = f.seek(SeekFrom::Current(0)).map_err(Error::SeekingFile)?;
f.seek(SeekFrom::Start(0)).map_err(Error::SeekingFile)?;
let mut magic = [0u8; 4];
f.read_exact(&mut magic).map_err(Error::ReadingHeader)?;
let magic = u32::from_be_bytes(magic);
#[cfg(feature = "composite-disk")]
{
f.seek(SeekFrom::Start(0)).map_err(Error::SeekingFile)?;
let mut cdisk_magic = [0u8; CDISK_MAGIC_LEN];
f.read_exact(&mut cdisk_magic[..])
.map_err(Error::ReadingHeader)?;
if cdisk_magic == CDISK_MAGIC.as_bytes() {
f.seek(SeekFrom::Start(orig_seek))
.map_err(Error::SeekingFile)?;
return Ok(ImageType::CompositeDisk);
}
}
let image_type = if magic == QCOW_MAGIC {
ImageType::Qcow2
} else if magic == SPARSE_HEADER_MAGIC.to_be() {
ImageType::AndroidSparse
} else {
ImageType::Raw
};
f.seek(SeekFrom::Start(orig_seek))
.map_err(Error::SeekingFile)?;
Ok(image_type)
}
/// Check if the image file type can be used for async disk access.
pub fn async_ok(raw_image: &File) -> Result<bool> {
let image_type = detect_image_type(raw_image)?;
Ok(match image_type {
ImageType::Raw => true,
ImageType::Qcow2 | ImageType::AndroidSparse | ImageType::CompositeDisk => false,
})
}
/// Inspect the image file type and create an appropriate disk file to match it.
pub fn create_async_disk_file(raw_image: File) -> Result<Box<dyn ToAsyncDisk>> {
let image_type = detect_image_type(&raw_image)?;
Ok(match image_type {
ImageType::Raw => Box::new(raw_image) as Box<dyn ToAsyncDisk>,
ImageType::Qcow2 | ImageType::AndroidSparse | ImageType::CompositeDisk => {
return Err(Error::UnknownType)
}
})
}
/// Inspect the image file type and create an appropriate disk file to match it.
pub fn create_disk_file(raw_image: File) -> Result<Box<dyn DiskFile>> {
let image_type = detect_image_type(&raw_image)?;
Ok(match image_type {
ImageType::Raw => Box::new(raw_image) as Box<dyn DiskFile>,
ImageType::Qcow2 => {
Box::new(QcowFile::from(raw_image).map_err(Error::QcowError)?) as Box<dyn DiskFile>
}
#[cfg(feature = "composite-disk")]
ImageType::CompositeDisk => {
// Valid composite disk header present
Box::new(CompositeDiskFile::from_file(raw_image).map_err(Error::CreateCompositeDisk)?)
as Box<dyn DiskFile>
}
#[cfg(not(feature = "composite-disk"))]
ImageType::CompositeDisk => return Err(Error::UnknownType),
ImageType::AndroidSparse => {
Box::new(AndroidSparse::from_file(raw_image).map_err(Error::CreateAndroidSparseDisk)?)
as Box<dyn DiskFile>
}
})
}
/// An asynchronously accessible disk.
#[async_trait(?Send)]
pub trait AsyncDisk: DiskGetLen + FileSetLen + FileAllocate {
/// Returns the inner file consuming self.
fn into_inner(self: Box<Self>) -> Box<dyn ToAsyncDisk>;
/// Asynchronously fsyncs any completed operations to the disk.
async fn fsync(&self) -> Result<()>;
/// Reads from the file at 'file_offset' in to memory `mem` at `mem_offsets`.
/// `mem_offsets` is similar to an iovec except relative to the start of `mem`.
async fn read_to_mem<'a>(
&self,
file_offset: u64,
mem: Arc<GuestMemory>,
mem_offsets: &'a [cros_async::MemRegion],
) -> Result<usize>;
/// Writes to the file at 'file_offset' from memory `mem` at `mem_offsets`.
async fn write_from_mem<'a>(
&self,
file_offset: u64,
mem: Arc<GuestMemory>,
mem_offsets: &'a [cros_async::MemRegion],
) -> Result<usize>;
/// Replaces a range of bytes with a hole.
async fn punch_hole(&self, file_offset: u64, length: u64) -> Result<()>;
/// Writes up to `length` bytes of zeroes to the stream, returning how many bytes were written.
async fn write_zeroes_at(&self, file_offset: u64, length: u64) -> Result<()>;
}
use cros_async::IoSourceExt;
/// A disk backed by a single file that implements `AsyncDisk` for access.
pub struct SingleFileDisk {
inner: Box<dyn IoSourceExt<File>>,
}
impl SingleFileDisk {
pub fn new(disk: File, ex: &Executor) -> Result<Self> {
ex.async_from(disk)
.map_err(Error::CreateSingleFileDisk)
.map(|inner| SingleFileDisk { inner })
}
}
impl DiskGetLen for SingleFileDisk {
fn get_len(&self) -> io::Result<u64> {
self.inner.as_source().get_len()
}
}
impl FileSetLen for SingleFileDisk {
fn set_len(&self, len: u64) -> io::Result<()> {
self.inner.as_source().set_len(len)
}
}
impl FileAllocate for SingleFileDisk {
fn allocate(&mut self, offset: u64, len: u64) -> io::Result<()> {
self.inner.as_source_mut().allocate(offset, len)
}
}
#[async_trait(?Send)]
impl AsyncDisk for SingleFileDisk {
fn into_inner(self: Box<Self>) -> Box<dyn ToAsyncDisk> {
Box::new(self.inner.into_source())
}
async fn fsync(&self) -> Result<()> {
self.inner.fsync().await.map_err(Error::Fsync)
}
async fn read_to_mem<'a>(
&self,
file_offset: u64,
mem: Arc<GuestMemory>,
mem_offsets: &'a [cros_async::MemRegion],
) -> Result<usize> {
self.inner
.read_to_mem(file_offset, mem, mem_offsets)
.await
.map_err(Error::ReadToMem)
}
async fn write_from_mem<'a>(
&self,
file_offset: u64,
mem: Arc<GuestMemory>,
mem_offsets: &'a [cros_async::MemRegion],
) -> Result<usize> {
self.inner
.write_from_mem(file_offset, mem, mem_offsets)
.await
.map_err(Error::WriteFromMem)
}
async fn punch_hole(&self, file_offset: u64, length: u64) -> Result<()> {
self.inner
.fallocate(
file_offset,
length,
(libc::FALLOC_FL_PUNCH_HOLE | libc::FALLOC_FL_KEEP_SIZE) as u32,
)
.await
.map_err(Error::Fallocate)
}
async fn write_zeroes_at(&self, file_offset: u64, length: u64) -> Result<()> {
if self
.inner
.fallocate(
file_offset,
length,
(libc::FALLOC_FL_ZERO_RANGE | libc::FALLOC_FL_KEEP_SIZE) as u32,
)
.await
.is_ok()
{
return Ok(());
}
// Fall back to writing zeros if fallocate doesn't work.
let buf_size = min(length, 0x10000);
let mut nwritten = 0;
while nwritten < length {
let remaining = length - nwritten;
let write_size = min(remaining, buf_size) as usize;
let buf = vec![0u8; write_size];
nwritten += self
.inner
.write_from_vec(file_offset + nwritten as u64, buf)
.await
.map(|(n, _)| n as u64)
.map_err(Error::WriteFromVec)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::{File, OpenOptions};
use cros_async::{Executor, MemRegion};
use vm_memory::{GuestAddress, GuestMemory};
#[test]
fn read_async() {
async fn read_zeros_async(ex: &Executor) {
let guest_mem = Arc::new(GuestMemory::new(&[(GuestAddress(0), 4096)]).unwrap());
let f = File::open("/dev/zero").unwrap();
let async_file = SingleFileDisk::new(f, ex).unwrap();
let result = async_file
.read_to_mem(
0,
Arc::clone(&guest_mem),
&[MemRegion { offset: 0, len: 48 }],
)
.await;
assert_eq!(48, result.unwrap());
}
let ex = Executor::new().unwrap();
ex.run_until(read_zeros_async(&ex)).unwrap();
}
#[test]
fn write_async() {
async fn write_zeros_async(ex: &Executor) {
let guest_mem = Arc::new(GuestMemory::new(&[(GuestAddress(0), 4096)]).unwrap());
let f = OpenOptions::new().write(true).open("/dev/null").unwrap();
let async_file = SingleFileDisk::new(f, ex).unwrap();
let result = async_file
.write_from_mem(
0,
Arc::clone(&guest_mem),
&[MemRegion { offset: 0, len: 48 }],
)
.await;
assert_eq!(48, result.unwrap());
}
let ex = Executor::new().unwrap();
ex.run_until(write_zeros_async(&ex)).unwrap();
}
}