diff --git a/block/src/aligned_file.rs b/block/src/aligned_file.rs index c629b9299..df241ae98 100644 --- a/block/src/aligned_file.rs +++ b/block/src/aligned_file.rs @@ -2,13 +2,19 @@ // // SPDX-License-Identifier: Apache-2.0 -use std::fs::File; -use std::io; +use std::fs::{File, Metadata}; +use std::io::{self, Read, Seek, SeekFrom, Write}; +use std::os::fd::{AsFd, BorrowedFd}; use std::os::unix::fs::FileExt; -use std::os::unix::io::AsRawFd; +use std::os::unix::io::{AsRawFd, RawFd}; +use std::result; + +use vmm_sys_util::file_traits::FileSync; +use vmm_sys_util::seek_hole::SeekHole; +use vmm_sys_util::write_zeroes::{PunchHole, WriteZeroesAt}; use crate::aligned_buffer::AlignedBuffer; -use crate::{SECTOR_SIZE, probe_direct_alignment}; +use crate::{BlockBackend, SECTOR_SIZE, probe_direct_alignment, query_device_size}; /// True when `buf_ptr`/`len`/`offset` already satisfy `alignment` /// (`alignment == 0` means no O_DIRECT, so everything is "aligned"). @@ -25,9 +31,10 @@ fn is_aligned(alignment: usize, buf_ptr: usize, len: usize, offset: u64) -> bool /// For unaligned requests under O_DIRECT, I/O is bounced through an /// `AlignedBuffer` (read-modify-write for writes). #[derive(Debug)] -pub(crate) struct AlignedFile { +pub struct AlignedFile { file: File, alignment: usize, + position: u64, } impl AlignedFile { @@ -38,7 +45,11 @@ impl AlignedFile { } else { 0 }; - AlignedFile { file, alignment } + AlignedFile { + file, + alignment, + position: 0, + } } pub fn alignment(&self) -> usize { @@ -57,14 +68,49 @@ impl AlignedFile { Ok(AlignedFile { file: self.file.try_clone()?, alignment: self.alignment, + position: self.position, }) } + pub fn set_len(&self, size: u64) -> io::Result<()> { + self.file.set_len(size) + } + + pub fn metadata(&self) -> io::Result { + self.file.metadata() + } + + pub fn sync_all(&self) -> io::Result<()> { + self.file.sync_all() + } + + pub fn sync_data(&self) -> io::Result<()> { + self.file.sync_data() + } + + pub fn is_direct(&self) -> bool { + self.alignment != 0 + } + + pub fn is_writable(&self) -> bool { + // SAFETY: fcntl with F_GETFL is safe and doesn't modify the file descriptor + let flags = unsafe { libc::fcntl(self.file.as_raw_fd(), libc::F_GETFL) }; + if flags < 0 { + return false; + } + let access_mode = flags & libc::O_ACCMODE; + access_mode == libc::O_WRONLY || access_mode == libc::O_RDWR + } + /// Wrap `file` with an explicit alignment, bypassing the probe. Used by /// tests to force the bounce/RMW path without a real O_DIRECT fd. #[cfg(test)] pub fn with_alignment(file: File, alignment: usize) -> Self { - AlignedFile { file, alignment } + AlignedFile { + file, + alignment, + position: 0, + } } } @@ -97,9 +143,123 @@ impl FileExt for AlignedFile { } } +impl Read for AlignedFile { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + let n = self.read_at(buf, self.position)?; + self.position += n as u64; + Ok(n) + } +} + +impl Write for AlignedFile { + fn write(&mut self, buf: &[u8]) -> io::Result { + let n = self.write_at(buf, self.position)?; + self.position += n as u64; + Ok(n) + } + + fn flush(&mut self) -> io::Result<()> { + self.file.sync_all() + } +} + +impl Seek for AlignedFile { + fn seek(&mut self, pos: SeekFrom) -> io::Result { + let newpos = match pos { + SeekFrom::Start(o) => o, + SeekFrom::Current(d) => self + .position + .checked_add_signed(d) + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "invalid seek"))?, + SeekFrom::End(d) => query_device_size(&self.file)? + .0 + .checked_add_signed(d) + .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "invalid seek"))?, + }; + self.position = newpos; + Ok(newpos) + } +} + +impl WriteZeroesAt for AlignedFile { + fn write_zeroes_at(&mut self, offset: u64, length: usize) -> io::Result { + self.file.write_zeroes_at(offset, length) + } +} + +impl PunchHole for AlignedFile { + fn punch_hole(&mut self, offset: u64, length: u64) -> io::Result<()> { + self.file.punch_hole(offset, length) + } +} + +impl FileSync for AlignedFile { + fn fsync(&mut self) -> io::Result<()> { + self.file.fsync() + } +} + +impl SeekHole for AlignedFile { + fn seek_hole(&mut self, offset: u64) -> io::Result> { + match self.file.seek_hole(offset) { + Ok(pos) => { + if let Some(p) = pos { + self.position = p; + } + Ok(pos) + } + Err(e) => Err(e), + } + } + + fn seek_data(&mut self, offset: u64) -> io::Result> { + match self.file.seek_data(offset) { + Ok(pos) => { + if let Some(p) = pos { + self.position = p; + } + Ok(pos) + } + Err(e) => Err(e), + } + } +} + +impl BlockBackend for AlignedFile { + fn logical_size(&self) -> result::Result { + Ok(query_device_size(&self.file) + .map_err(crate::Error::RawFileError)? + .0) + } + + fn physical_size(&self) -> result::Result { + Ok(query_device_size(&self.file) + .map_err(crate::Error::RawFileError)? + .1) + } +} + +impl Clone for AlignedFile { + fn clone(&self) -> Self { + self.try_clone().expect("AlignedFile cloning failed") + } +} + +impl AsRawFd for AlignedFile { + fn as_raw_fd(&self) -> RawFd { + self.file.as_raw_fd() + } +} + +impl AsFd for AlignedFile { + fn as_fd(&self) -> BorrowedFd<'_> { + self.file.as_fd() + } +} + #[cfg(test)] mod tests { - use std::io::Write; + use std::io::{Read, Seek, SeekFrom, Write}; use std::os::unix::fs::FileExt; use vmm_sys_util::tempfile::TempFile; @@ -115,14 +275,17 @@ mod tests { } fn forced(file: File, alignment: usize) -> AlignedFile { - AlignedFile { file, alignment } + AlignedFile { + file, + alignment, + position: 0, + } } #[test] fn new_probes_alignment_and_accessors() { let tf = pattern_file(8192); - // A tempfile is not O_DIRECT, so probe_direct_alignment reports - // None and new() falls back to SECTOR_SIZE (512). + // Not O_DIRECT, so new() falls back to SECTOR_SIZE (512). let mut af = AlignedFile::new(tf.as_file().try_clone().unwrap(), true); assert_eq!(af.alignment(), 512); let _ = af.file(); @@ -184,4 +347,65 @@ mod tests { let mut buf = vec![0u8; 50]; assert_eq!(af.read_at(&mut buf, 10).unwrap(), 50); } + + #[test] + fn test_unaligned_read_returns_short_read_at_eof() { + let file_size = 100usize; + let tf = pattern_file(file_size); + let mut af = forced(tf.as_file().try_clone().unwrap(), 512); + af.seek(SeekFrom::Start(10)).unwrap(); + + let mut buf = vec![0u8; 200]; + let bytes_read = af.read(&mut buf).unwrap(); + + let expected: Vec = (10..file_size).map(|i| (i % 251) as u8).collect(); + assert_eq!(bytes_read, expected.len()); + assert_eq!(&buf[..bytes_read], &expected[..]); + assert_eq!(af.position, file_size as u64); + } + + #[test] + fn test_unaligned_read_beyond_eof_returns_zero() { + let tf = pattern_file(100); + let mut af = forced(tf.as_file().try_clone().unwrap(), 512); + af.seek(SeekFrom::Start(200)).unwrap(); + + let mut buf = vec![0u8; 16]; + let bytes_read = af.read(&mut buf).unwrap(); + + assert_eq!(bytes_read, 0); + assert_eq!(af.position, 200); + } + + #[test] + fn test_unaligned_write_extends_at_eof() { + let file_size = 100usize; + let tf = pattern_file(file_size); + let mut af = forced(tf.as_file().try_clone().unwrap(), 512); + af.seek(SeekFrom::Start(file_size as u64)).unwrap(); + + let data = b"xyz"; + let bytes_written = af.write(data).unwrap(); + + assert_eq!(bytes_written, data.len()); + assert_eq!(af.position, (file_size + data.len()) as u64); + + let mut readback = vec![0u8; file_size + data.len()]; + tf.as_file().read_exact_at(&mut readback, 0).unwrap(); + let expected_prefix: Vec = (0..file_size).map(|i| (i % 251) as u8).collect(); + assert_eq!(&readback[..file_size], &expected_prefix[..]); + assert_eq!(&readback[file_size..], data); + } + + #[test] + fn test_empty_unaligned_io_is_noop() { + let tf = pattern_file(100); + let mut af = forced(tf.as_file().try_clone().unwrap(), 512); + af.seek(SeekFrom::Start(1)).unwrap(); + + let mut read_buf = []; + assert_eq!(af.read(&mut read_buf).unwrap(), 0); + assert_eq!(af.write(&[]).unwrap(), 0); + assert_eq!(af.position, 1); + } } diff --git a/block/src/formats/qcow/internal/header.rs b/block/src/formats/qcow/internal/header.rs index 4a16b8a00..da85ae80c 100644 --- a/block/src/formats/qcow/internal/header.rs +++ b/block/src/formats/qcow/internal/header.rs @@ -18,8 +18,8 @@ use vmm_sys_util::file_traits::FileSync; use super::decoder::{Decoder, ZlibDecoder, ZstdDecoder}; use super::qcow_raw_file::BeUint; -use super::raw_file::RawFile; use super::{Error, Result, div_round_up_u32, div_round_up_u64}; +use crate::aligned_file::AlignedFile; use crate::error::{BlockError, BlockErrorKind, BlockResult}; #[derive(Copy, Clone, Debug, PartialEq, Eq)] @@ -210,7 +210,7 @@ pub struct QcowHeader { impl QcowHeader { /// Read header extensions, optionally collecting feature names for error reporting. pub(super) fn read_header_extensions( - f: &mut RawFile, + f: &mut AlignedFile, header: &mut QcowHeader, mut feature_table: Option<&mut Vec<(u8, String)>>, ) -> Result<()> { @@ -269,7 +269,7 @@ impl QcowHeader { } /// Creates a QcowHeader from a reference to a file. - pub fn new(f: &mut RawFile) -> Result { + pub fn new(f: &mut AlignedFile) -> Result { f.rewind().map_err(Error::ReadingHeader)?; let magic = u32::read_be(f).map_err(Error::ReadingHeader)?; if magic != QCOW_MAGIC { @@ -277,12 +277,12 @@ impl QcowHeader { } // Reads the next u32 from the file. - fn read_u32_be(f: &mut RawFile) -> Result { + fn read_u32_be(f: &mut AlignedFile) -> Result { u32::read_be(f).map_err(Error::ReadingHeader) } // Reads the next u64 from the file. - fn read_u64_be(f: &mut RawFile) -> Result { + fn read_u64_be(f: &mut AlignedFile) -> Result { u64::read_be(f).map_err(Error::ReadingHeader) } diff --git a/block/src/formats/qcow/internal/mod.rs b/block/src/formats/qcow/internal/mod.rs index bbfc15104..19f5473b6 100644 --- a/block/src/formats/qcow/internal/mod.rs +++ b/block/src/formats/qcow/internal/mod.rs @@ -9,7 +9,6 @@ pub(crate) mod decoder; mod header; pub(crate) mod metadata; pub(crate) mod qcow_raw_file; -mod raw_file; mod refcount; mod util; mod vec_cache; @@ -34,7 +33,6 @@ use header::{ }; use log::warn; use qcow_raw_file::{BeUint, QcowRawFile}; -pub use raw_file::RawFile; use refcount::RefCount; use remain::sorted; use thiserror::Error; @@ -42,6 +40,7 @@ pub(crate) use util::MAX_NESTING_DEPTH; use util::{L1_TABLE_OFFSET_MASK, L2_TABLE_OFFSET_MASK, div_round_up_u32, div_round_up_u64}; use vec_cache::{CacheMap, VecCache}; +use crate::aligned_file::AlignedFile; use crate::error::{BlockError, BlockErrorKind, BlockResult}; #[sorted] @@ -164,7 +163,7 @@ pub type Result = result::Result; /// Concrete backing file variants. pub(crate) enum BackingKind { /// Raw backing file. - Raw(RawFile), + Raw(AlignedFile), /// QCOW2 backing parsed into metadata and raw file. Qcow { inner: Box, @@ -206,7 +205,7 @@ impl BackingFile { ) })?; - let mut raw_file = RawFile::new(backing_raw_file, direct_io); + let mut raw_file = AlignedFile::new(backing_raw_file, direct_io); // Determine backing file format from header extension or auto-detect let backing_format = match config.format { @@ -274,7 +273,7 @@ impl Debug for BackingFile { /// /// Used by [`crate::formats::qcow::QcowDisk`] when opening an image. pub(crate) fn parse_qcow( - mut file: RawFile, + mut file: AlignedFile, max_nesting_depth: u32, sparse: bool, ) -> BlockResult<(metadata::QcowState, Option, bool)> { @@ -874,7 +873,7 @@ fn rebuild_refcounts(raw_file: &mut QcowRawFile, header: QcowHeader) -> BlockRes } /// Detect the type of an image file by checking for a valid qcow2 header. -pub fn detect_image_type(file: &mut RawFile) -> BlockResult { +pub fn detect_image_type(file: &mut AlignedFile) -> BlockResult { let orig_seek = file .stream_position() .map_err(|e| BlockError::new(BlockErrorKind::Io, Error::SeekingFile(e)))?; @@ -975,8 +974,9 @@ mod unit_tests { ] } - fn basic_file(header: &[u8]) -> RawFile { - let mut disk_file: RawFile = RawFile::new(TempFile::new().unwrap().into_file(), false); + fn basic_file(header: &[u8]) -> AlignedFile { + let mut disk_file: AlignedFile = + AlignedFile::new(TempFile::new().unwrap().into_file(), false); disk_file.write_all(header).unwrap(); disk_file.set_len(0x1_0000_0000).unwrap(); disk_file.rewind().unwrap(); @@ -985,7 +985,7 @@ mod unit_tests { fn with_basic_file(header: &[u8], mut testfn: F) where - F: FnMut(RawFile), + F: FnMut(AlignedFile), { testfn(basic_file(header)); // File closed when the function exits. } @@ -1007,7 +1007,7 @@ mod unit_tests { fn try_open_qcow_header(header: &QcowHeader, backing_files: bool) -> BlockResult { let temp = TempFile::new().unwrap(); - let mut raw = RawFile::new(temp.as_file().try_clone().unwrap(), false); + let mut raw = AlignedFile::new(temp.as_file().try_clone().unwrap(), false); header.write_to(&mut raw).expect("write header"); drop(raw); let file = temp.into_file(); @@ -1032,13 +1032,13 @@ mod unit_tests { #[test] fn header_read() { - with_basic_file(&valid_header_v2(), |mut disk_file: RawFile| { + with_basic_file(&valid_header_v2(), |mut disk_file: AlignedFile| { let header = QcowHeader::new(&mut disk_file).expect("Failed to create Header."); assert_eq!(header.version, 2); assert_eq!(header.refcount_order, DEFAULT_REFCOUNT_ORDER); assert_eq!(header.header_size, V2_BARE_HEADER_SIZE); }); - with_basic_file(&valid_header_v3(), |mut disk_file: RawFile| { + with_basic_file(&valid_header_v3(), |mut disk_file: AlignedFile| { let header = QcowHeader::new(&mut disk_file).expect("Failed to create Header."); assert_eq!(header.version, 3); assert_eq!(header.refcount_order, DEFAULT_REFCOUNT_ORDER); @@ -1050,7 +1050,8 @@ mod unit_tests { fn header_v2_with_backing() { let header = QcowHeader::create_for_size_and_path(2, 0x10_0000, Some("/my/path/to/a/file")) .expect("Failed to create header."); - let mut disk_file: RawFile = RawFile::new(TempFile::new().unwrap().into_file(), false); + let mut disk_file: AlignedFile = + AlignedFile::new(TempFile::new().unwrap().into_file(), false); header .write_to(&mut disk_file) .expect("Failed to write header to shm."); @@ -1070,7 +1071,8 @@ mod unit_tests { fn header_v3_with_backing() { let header = QcowHeader::create_for_size_and_path(3, 0x10_0000, Some("/my/path/to/a/file")) .expect("Failed to create header."); - let mut disk_file: RawFile = RawFile::new(TempFile::new().unwrap().into_file(), false); + let mut disk_file: AlignedFile = + AlignedFile::new(TempFile::new().unwrap().into_file(), false); header .write_to(&mut disk_file) .expect("Failed to write header to shm."); @@ -1094,7 +1096,7 @@ mod unit_tests { .expect("Failed to create header."); header.backing_file_offset = offset; header.backing_file_size = size; - let mut disk_file: RawFile = RawFile::new( + let mut disk_file: AlignedFile = AlignedFile::new( TempFile::new() .expect("Failed to create temp file.") .into_file(), @@ -1165,11 +1167,12 @@ mod unit_tests { } /// Helper to create a test file with header extensions - fn create_header_with_extension(ext_type: u32, ext_data: &[u8]) -> (RawFile, QcowHeader) { + fn create_header_with_extension(ext_type: u32, ext_data: &[u8]) -> (AlignedFile, QcowHeader) { let header = QcowHeader::create_for_size_and_path(3, 0x10_0000, None) .expect("Failed to create header."); - let mut disk_file: RawFile = RawFile::new(TempFile::new().unwrap().into_file(), false); + let mut disk_file: AlignedFile = + AlignedFile::new(TempFile::new().unwrap().into_file(), false); header.write_to(&mut disk_file).unwrap(); // Write extension @@ -1304,7 +1307,7 @@ mod unit_tests { /// the file until stack overflow. fn new_self_referential_qcow(path: &Path) -> Result<()> { let header = QcowHeader::create_for_size_and_path(3, 0x10_0000, path.to_str())?; - let mut disk_file = RawFile::new( + let mut disk_file = AlignedFile::new( File::create(path).expect("Failed to create image file."), false, ); @@ -1538,7 +1541,7 @@ mod unit_tests { let file = TempFile::new().unwrap().into_file(); let cluster_size = 0x10000u64; file.set_len(cluster_size * 2).unwrap(); - let raw = RawFile::new(file, false); + let raw = AlignedFile::new(file, false); let mut qcow_raw = QcowRawFile::from(raw, cluster_size, bits).unwrap(); let entries = (cluster_size * 8 / bits) as usize; @@ -1568,7 +1571,7 @@ mod unit_tests { let file = TempFile::new().unwrap().into_file(); let cluster_size = 0x10000u64; file.set_len(cluster_size * 2).unwrap(); - let raw = RawFile::new(file, false); + let raw = AlignedFile::new(file, false); let mut qcow_raw = QcowRawFile::from(raw, cluster_size, bits).unwrap(); let entries = (cluster_size * 8 / bits) as usize; @@ -1597,7 +1600,7 @@ mod unit_tests { let refcount_block_entries = cluster_size * 8 / refcount_bits; file.set_len(cluster_size * 3).unwrap(); - let raw = RawFile::new(file, false); + let raw = AlignedFile::new(file, false); let mut qcow_raw = QcowRawFile::from(raw, cluster_size, refcount_bits).unwrap(); // Set up refcount table pointing to refcount block diff --git a/block/src/formats/qcow/internal/qcow_raw_file.rs b/block/src/formats/qcow/internal/qcow_raw_file.rs index 632b0ad37..bf7c451a9 100644 --- a/block/src/formats/qcow/internal/qcow_raw_file.rs +++ b/block/src/formats/qcow/internal/qcow_raw_file.rs @@ -12,11 +12,11 @@ use std::os::fd::{AsFd, AsRawFd, BorrowedFd, RawFd}; use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; use vmm_sys_util::write_zeroes::WriteZeroes; -use super::RawFile; +use crate::aligned_file::AlignedFile; // Type aliases for the refcount read/write function pointers -type RefcountReader = fn(&mut RawFile, usize) -> io::Result>; -type RefcountWriter = fn(&mut RawFile, &[u64]) -> io::Result<()>; +type RefcountReader = fn(&mut AlignedFile, usize) -> io::Result>; +type RefcountWriter = fn(&mut AlignedFile, &[u64]) -> io::Result<()>; /// Big-endian file access trait. pub(super) trait BeUint: Sized + Copy { @@ -88,7 +88,7 @@ impl BeUint for u64 { } /// Read byte-aligned refcounts. -fn read_refcount(file: &mut RawFile, count: usize) -> io::Result> { +fn read_refcount(file: &mut AlignedFile, count: usize) -> io::Result> { let bytes_per_entry = size_of::(); let mut data = vec![0u8; count * bytes_per_entry]; file.read_exact(&mut data)?; @@ -99,7 +99,7 @@ fn read_refcount(file: &mut RawFile, count: usize) -> io::Result>(file: &mut RawFile, table: &[u64]) -> io::Result<()> +fn write_refcount>(file: &mut AlignedFile, table: &[u64]) -> io::Result<()> where >::Error: Debug, { @@ -114,7 +114,7 @@ where /// Read sub-byte refcounts. Bit 0 is the least significant bit. fn read_refcount_subbyte( - file: &mut RawFile, + file: &mut AlignedFile, count: usize, ) -> io::Result> { const { assert!(BITS == 1 || BITS == 2 || BITS == 4) }; @@ -134,7 +134,10 @@ fn read_refcount_subbyte( } /// Write sub-byte refcounts. Bit 0 is the least significant bit. -fn write_refcount_subbyte(file: &mut RawFile, table: &[u64]) -> io::Result<()> { +fn write_refcount_subbyte( + file: &mut AlignedFile, + table: &[u64], +) -> io::Result<()> { const { assert!(BITS == 1 || BITS == 2 || BITS == 4) }; let entries_per_byte = 8 / BITS; let mask = (1u64 << BITS) - 1; @@ -154,7 +157,7 @@ fn write_refcount_subbyte(file: &mut RawFile, table: &[u64]) /// A qcow file. Allows reading/writing clusters and appending clusters. #[derive(Debug)] pub struct QcowRawFile { - file: RawFile, + file: AlignedFile, cluster_size: u64, cluster_mask: u64, refcount_block_entries: u64, @@ -165,7 +168,7 @@ pub struct QcowRawFile { impl QcowRawFile { /// Creates a `QcowRawFile` from the given `File`, `None` is returned if `cluster_size` is not /// a power of two or refcount_bits is invalid. - pub fn from(file: RawFile, cluster_size: u64, refcount_bits: u64) -> Option { + pub fn from(file: AlignedFile, cluster_size: u64, refcount_bits: u64) -> Option { if !cluster_size.is_power_of_two() { return None; } @@ -289,12 +292,12 @@ impl QcowRawFile { } /// Returns a reference to the underlying file. - pub fn file(&self) -> &RawFile { + pub fn file(&self) -> &AlignedFile { &self.file } /// Returns a mutable reference to the underlying file. - pub fn file_mut(&mut self) -> &mut RawFile { + pub fn file_mut(&mut self) -> &mut AlignedFile { &mut self.file } @@ -394,7 +397,7 @@ mod unit_tests { temp_file.as_file().set_len(FILE_LEN).unwrap(); let file = temp_file.as_file().try_clone().unwrap(); - let raw = RawFile::new(file, false); + let raw = AlignedFile::new(file, false); let qcow_raw = QcowRawFile::from(raw, CLUSTER_SIZE, 16).expect("QcowRawFile::from"); (temp_file, qcow_raw) } diff --git a/block/src/formats/qcow/internal/raw_file.rs b/block/src/formats/qcow/internal/raw_file.rs deleted file mode 100644 index 45f5ed856..000000000 --- a/block/src/formats/qcow/internal/raw_file.rs +++ /dev/null @@ -1,304 +0,0 @@ -// 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. -// -// Copyright © 2020 Intel Corporation -// -// SPDX-License-Identifier: Apache-2.0 AND BSD-3-Clause - -use std::fs::{File, Metadata}; -use std::io::{self, Read, Seek, SeekFrom, Write}; -use std::os::fd::{AsFd, BorrowedFd}; -use std::os::unix::fs::FileExt; -use std::os::unix::io::{AsRawFd, RawFd}; -use std::result; - -use vmm_sys_util::file_traits::FileSync; -use vmm_sys_util::seek_hole::SeekHole; -use vmm_sys_util::write_zeroes::{PunchHole, WriteZeroesAt}; - -use crate::aligned_file::AlignedFile; -use crate::{BlockBackend, query_device_size}; - -#[derive(Debug)] -pub struct RawFile { - aligned: AlignedFile, - position: u64, - direct_io: bool, -} - -impl RawFile { - pub fn new(file: File, direct_io: bool) -> Self { - RawFile { - aligned: AlignedFile::new(file, direct_io), - position: 0, - direct_io, - } - } - - pub fn set_len(&self, size: u64) -> io::Result<()> { - self.aligned.file().set_len(size) - } - - pub fn metadata(&self) -> io::Result { - self.aligned.file().metadata() - } - - pub fn try_clone(&self) -> io::Result { - Ok(RawFile { - aligned: self.aligned.try_clone()?, - position: self.position, - direct_io: self.direct_io, - }) - } - - pub fn sync_all(&self) -> io::Result<()> { - self.aligned.file().sync_all() - } - - pub fn sync_data(&self) -> io::Result<()> { - self.aligned.file().sync_data() - } - - pub fn is_direct(&self) -> bool { - self.direct_io - } - - pub fn alignment(&self) -> usize { - self.aligned.alignment() - } - - /// Returns true if the file was opened with write access. - pub fn is_writable(&self) -> bool { - // SAFETY: fcntl with F_GETFL is safe and doesn't modify the file descriptor - let flags = unsafe { libc::fcntl(self.aligned.file().as_raw_fd(), libc::F_GETFL) }; - if flags < 0 { - return false; - } - let access_mode = flags & libc::O_ACCMODE; - access_mode == libc::O_WRONLY || access_mode == libc::O_RDWR - } -} - -impl Read for RawFile { - fn read(&mut self, buf: &mut [u8]) -> io::Result { - let n = self.aligned.read_at(buf, self.position)?; - self.position += n as u64; - Ok(n) - } -} - -impl Write for RawFile { - fn write(&mut self, buf: &[u8]) -> io::Result { - let n = self.aligned.write_at(buf, self.position)?; - self.position += n as u64; - Ok(n) - } - - fn flush(&mut self) -> io::Result<()> { - self.aligned.file().sync_all() - } -} - -impl Seek for RawFile { - fn seek(&mut self, pos: SeekFrom) -> io::Result { - let newpos = match pos { - SeekFrom::Start(o) => o, - SeekFrom::Current(d) => self - .position - .checked_add_signed(d) - .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "invalid seek"))?, - SeekFrom::End(d) => self - .aligned - .file() - .metadata()? - .len() - .checked_add_signed(d) - .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "invalid seek"))?, - }; - self.position = newpos; - Ok(newpos) - } -} - -impl WriteZeroesAt for RawFile { - fn write_zeroes_at(&mut self, offset: u64, length: usize) -> io::Result { - self.aligned.file_mut().write_zeroes_at(offset, length) - } -} - -impl PunchHole for RawFile { - fn punch_hole(&mut self, offset: u64, length: u64) -> io::Result<()> { - self.aligned.file_mut().punch_hole(offset, length) - } -} - -impl FileSync for RawFile { - fn fsync(&mut self) -> io::Result<()> { - self.aligned.file_mut().fsync() - } -} - -impl SeekHole for RawFile { - fn seek_hole(&mut self, offset: u64) -> io::Result> { - match self.aligned.file_mut().seek_hole(offset) { - Ok(pos) => { - if let Some(p) = pos { - self.position = p; - } - Ok(pos) - } - Err(e) => Err(e), - } - } - - fn seek_data(&mut self, offset: u64) -> io::Result> { - match self.aligned.file_mut().seek_data(offset) { - Ok(pos) => { - if let Some(p) = pos { - self.position = p; - } - Ok(pos) - } - Err(e) => Err(e), - } - } -} - -impl BlockBackend for RawFile { - fn logical_size(&self) -> result::Result { - Ok(query_device_size(self.aligned.file()) - .map_err(crate::Error::RawFileError)? - .0) - } - - fn physical_size(&self) -> result::Result { - Ok(query_device_size(self.aligned.file()) - .map_err(crate::Error::RawFileError)? - .1) - } -} - -impl Clone for RawFile { - fn clone(&self) -> Self { - RawFile { - aligned: self.aligned.try_clone().expect("RawFile cloning failed"), - position: self.position, - direct_io: self.direct_io, - } - } -} - -impl AsRawFd for RawFile { - fn as_raw_fd(&self) -> RawFd { - self.aligned.file().as_raw_fd() - } -} - -impl AsFd for RawFile { - fn as_fd(&self) -> BorrowedFd<'_> { - self.aligned.file().as_fd() - } -} - -impl FileExt for RawFile { - fn read_at(&self, buf: &mut [u8], offset: u64) -> io::Result { - self.aligned.read_at(buf, offset) - } - - fn write_at(&self, buf: &[u8], offset: u64) -> io::Result { - self.aligned.write_at(buf, offset) - } -} - -#[cfg(test)] -mod tests { - use std::fs::File; - use std::io::{Read, Seek, SeekFrom, Write}; - use std::os::unix::fs::FileExt; - - use vmm_sys_util::tempfile::TempFile; - - use super::*; - - const TEST_ALIGNMENT: usize = 512; - - fn create_pattern_file(size: usize) -> TempFile { - let tf = TempFile::new().unwrap(); - let pattern: Vec = (0..size).map(|i| (i % 251) as u8).collect(); - tf.as_file().write_all(&pattern).unwrap(); - tf.as_file().sync_all().unwrap(); - tf - } - - fn raw_with_alignment(file: File) -> RawFile { - // A tempfile is not O_DIRECT, but its aligned probe read still - // succeeds, so RawFile::new selects the smallest candidate (512). - let raw = RawFile::new(file, true); - assert_eq!(raw.alignment(), TEST_ALIGNMENT); - raw - } - - #[test] - fn test_unaligned_read_returns_short_read_at_eof() { - let file_size = 100usize; - let tf = create_pattern_file(file_size); - let mut raw = raw_with_alignment(tf.as_file().try_clone().unwrap()); - raw.seek(SeekFrom::Start(10)).unwrap(); - - let mut buf = vec![0u8; 200]; - let bytes_read = raw.read(&mut buf).unwrap(); - - let expected: Vec = (10..file_size).map(|i| (i % 251) as u8).collect(); - assert_eq!(bytes_read, expected.len()); - assert_eq!(&buf[..bytes_read], &expected[..]); - assert_eq!(raw.position, file_size as u64); - } - - #[test] - fn test_unaligned_read_beyond_eof_returns_zero() { - let tf = create_pattern_file(100); - let mut raw = raw_with_alignment(tf.as_file().try_clone().unwrap()); - raw.seek(SeekFrom::Start(200)).unwrap(); - - let mut buf = vec![0u8; 16]; - let bytes_read = raw.read(&mut buf).unwrap(); - - assert_eq!(bytes_read, 0); - assert_eq!(raw.position, 200); - } - - #[test] - fn test_unaligned_write_extends_at_eof() { - let file_size = 100usize; - let tf = create_pattern_file(file_size); - let mut raw = raw_with_alignment(tf.as_file().try_clone().unwrap()); - raw.seek(SeekFrom::Start(file_size as u64)).unwrap(); - - let data = b"xyz"; - let bytes_written = raw.write(data).unwrap(); - - assert_eq!(bytes_written, data.len()); - assert_eq!(raw.position, (file_size + data.len()) as u64); - - let mut readback = vec![0u8; file_size + data.len()]; - tf.as_file().read_exact_at(&mut readback, 0).unwrap(); - let expected_prefix: Vec = (0..file_size).map(|i| (i % 251) as u8).collect(); - assert_eq!(&readback[..file_size], &expected_prefix[..]); - assert_eq!(&readback[file_size..], data); - } - - #[test] - fn test_empty_unaligned_io_is_noop() { - let tf = create_pattern_file(100); - let mut raw = raw_with_alignment(tf.as_file().try_clone().unwrap()); - raw.seek(SeekFrom::Start(1)).unwrap(); - - let mut read_buf = []; - assert_eq!(raw.read(&mut read_buf).unwrap(), 0); - assert_eq!(raw.write(&[]).unwrap(), 0); - assert_eq!(raw.position, 1); - } -} diff --git a/block/src/formats/qcow/mod.rs b/block/src/formats/qcow/mod.rs index 3ccd849e4..3837f3b3f 100644 --- a/block/src/formats/qcow/mod.rs +++ b/block/src/formats/qcow/mod.rs @@ -30,10 +30,11 @@ use self::internal::metadata::{BackingRead, QcowMetadata}; use self::internal::qcow_raw_file::QcowRawFile; #[cfg(any(test, feature = "test-utils"))] use self::internal::{BackingFileConfig, Error as QcowError, QcowHeader}; -use self::internal::{MAX_NESTING_DEPTH, RawFile, parse_qcow}; +use self::internal::{MAX_NESTING_DEPTH, parse_qcow}; #[cfg(feature = "io_uring")] use self::worker::async_uring::QcowAsync; use self::worker::sync::QcowSync; +use crate::aligned_file::AlignedFile; #[cfg(any(test, feature = "test-utils"))] use crate::async_io::GuestMemoryTarget; use crate::async_io::{AsyncIo, BorrowedDiskFd, DiskFileError}; @@ -91,7 +92,7 @@ impl QcowDisk { } let max_nesting_depth = if backing_files { MAX_NESTING_DEPTH } else { 0 }; - let raw_file = RawFile::new(file, direct_io); + let raw_file = AlignedFile::new(file, direct_io); let (inner, backing_file, sparse) = parse_qcow(raw_file, max_nesting_depth, sparse) .map_err(|e| { let e = if !backing_files && matches!(e.kind(), BlockErrorKind::Overflow) { @@ -163,7 +164,7 @@ pub(crate) fn create_image( { backing_file.format = cfg.format; } - let mut raw = RawFile::new( + let mut raw = AlignedFile::new( file.try_clone() .map_err(|e| BlockError::new(BlockErrorKind::Io, DiskFileError::Clone(e)))?, false, diff --git a/block/src/formats/qcow/worker/sync.rs b/block/src/formats/qcow/worker/sync.rs index 190b1bf65..a7171600f 100644 --- a/block/src/formats/qcow/worker/sync.rs +++ b/block/src/formats/qcow/worker/sync.rs @@ -340,13 +340,14 @@ mod unit_tests { use vmm_sys_util::tempfile::TempFile; use super::*; + use crate::aligned_file::AlignedFile; use crate::async_io::{AsyncIoCompletion, OwnedIoBuffer}; use crate::disk_file::{AsyncDiskFile, DiskSize, Resizable}; use crate::error::BlockErrorKind; use crate::formats::qcow; use crate::formats::qcow::common::unit_tests::compress_allocated_clusters; use crate::formats::qcow::internal::{ - BackingFileConfig, Error as QcowError, ImageType, QcowHeader, RawFile, + BackingFileConfig, Error as QcowError, ImageType, QcowHeader, }; use crate::formats::qcow::{QcowDisk, QcowTempDisk}; @@ -397,7 +398,7 @@ mod unit_tests { } fn qcow_header_is_corrupt(file: &File) -> bool { - let mut raw = RawFile::new(file.try_clone().unwrap(), false); + let mut raw = AlignedFile::new(file.try_clone().unwrap(), false); raw.seek(SeekFrom::Start(0)).unwrap(); QcowHeader::new(&mut raw).unwrap().is_corrupt() } diff --git a/block/src/lib.rs b/block/src/lib.rs index 7b7332155..e5bf9abda 100644 --- a/block/src/lib.rs +++ b/block/src/lib.rs @@ -15,7 +15,7 @@ pub mod factory; mod io_impl; pub use io_impl::{async_io, fcntl, request}; pub(crate) mod aligned_buffer; -pub(crate) mod aligned_file; +pub mod aligned_file; pub mod formats; mod sparse; use std::alloc::{Layout, alloc_zeroed}; @@ -29,6 +29,7 @@ use std::path::Path; use std::str::FromStr; use std::{cmp, mem, result}; +pub use aligned_file::AlignedFile; use formats::qcow::internal as qcow; #[cfg(feature = "io_uring")] use io_uring::{IoUring, Probe, opcode}; diff --git a/vhost_user_block/src/lib.rs b/vhost_user_block/src/lib.rs index 31fdbf17c..f78e39726 100644 --- a/vhost_user_block/src/lib.rs +++ b/vhost_user_block/src/lib.rs @@ -19,8 +19,7 @@ use std::sync::{Arc, Mutex, RwLock, RwLockWriteGuard}; use std::time::Instant; use std::{convert, io, process, result}; -use block::formats::qcow::internal::RawFile; -use block::{Request, RequestType, VirtioBlockConfig, build_serial}; +use block::{AlignedFile, Request, RequestType, VirtioBlockConfig, build_serial}; use libc::EFD_NONBLOCK; use log::{debug, error, info, warn}; use option_parser::{OptionParser, OptionParserError, Toggle}; @@ -85,7 +84,7 @@ impl convert::From for io::Error { } struct VhostUserBlkThread { - disk_image: Arc>, + disk_image: Arc>, serial: Vec, disk_nsectors: u64, event_idx: bool, @@ -96,7 +95,7 @@ struct VhostUserBlkThread { impl VhostUserBlkThread { fn new( - disk_image: Arc>, + disk_image: Arc>, serial: Vec, disk_nsectors: u64, writeback: Arc, @@ -230,7 +229,7 @@ impl VhostUserBlkBackend { options.custom_flags(libc::O_DIRECT); } let image: File = options.open(image_path).unwrap(); - let raw_img = RawFile::new(image, direct); + let raw_img = AlignedFile::new(image, direct); let serial = build_serial(&PathBuf::from(&image_path)); let image = Arc::new(Mutex::new(raw_img));