block: vhdx: Use AlignedFile for O_DIRECT-safe I/O

By redirecting VHDx I/O through the AlignedFile the required RMW
semantics can be achieved for writes less than the logical block size
whilt reusing the same logic used for other backend implementations.

Assisted-by: Claude:Opus-4.8
Signed-off-by: Rob Bradford <rbradford@meta.com>
This commit is contained in:
Rob Bradford
2026-06-12 10:23:22 +01:00
parent 80a0393edd
commit b4c1d85327
9 changed files with 169 additions and 115 deletions

View File

@@ -68,6 +68,13 @@ impl AlignedFile {
alignment: self.alignment, alignment: self.alignment,
}) })
} }
/// 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 }
}
} }
impl FileExt for AlignedFile { impl FileExt for AlignedFile {

View File

@@ -114,7 +114,7 @@ fn open_vhdx(
) -> BlockResult<Box<dyn AsyncFullDiskFile>> { ) -> BlockResult<Box<dyn AsyncFullDiskFile>> {
info!("Opening VHDX disk file with synchronous backend"); info!("Opening VHDX disk file with synchronous backend");
Ok(Box::new( Ok(Box::new(
VhdxDisk::new(file).map_err(|e| e.with_path(options.path))?, VhdxDisk::new(file, options.direct).map_err(|e| e.with_path(options.path))?,
)) ))
} }

View File

@@ -2,17 +2,17 @@
// //
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
use std::fs::File;
use std::io::{self, Seek, SeekFrom};
use std::mem::size_of; use std::mem::size_of;
use std::result; use std::os::unix::fs::FileExt;
use std::{io, result};
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; use byteorder::{ByteOrder, LittleEndian};
use remain::sorted; use remain::sorted;
use thiserror::Error; use thiserror::Error;
use super::header::RegionTableEntry; use super::header::RegionTableEntry;
use super::metadata::DiskSpec; use super::metadata::DiskSpec;
use crate::aligned_file::AlignedFile;
// Payload BAT Entry States // Payload BAT Entry States
pub const PAYLOAD_BLOCK_NOT_PRESENT: u64 = 0; pub const PAYLOAD_BLOCK_NOT_PRESENT: u64 = 0;
@@ -48,7 +48,7 @@ pub struct BatEntry(pub u64);
impl BatEntry { impl BatEntry {
// Read all BAT entries presented on the disk and insert them to a vector // Read all BAT entries presented on the disk and insert them to a vector
pub fn collect_bat_entries( pub fn collect_bat_entries(
f: &mut File, f: &AlignedFile,
disk_spec: &DiskSpec, disk_spec: &DiskSpec,
bat_entry: &RegionTableEntry, bat_entry: &RegionTableEntry,
) -> Result<Vec<BatEntry>> { ) -> Result<Vec<BatEntry>> {
@@ -64,14 +64,10 @@ impl BatEntry {
let mut bat: Vec<BatEntry> = Vec::with_capacity(bat_entry.length as usize); let mut bat: Vec<BatEntry> = Vec::with_capacity(bat_entry.length as usize);
let offset = bat_entry.file_offset; let offset = bat_entry.file_offset;
for i in 0..entry_count { for i in 0..entry_count {
f.seek(SeekFrom::Start(offset + i * size_of::<u64>() as u64)) let mut entry = [0u8; size_of::<u64>()];
f.read_exact_at(&mut entry, offset + i * size_of::<u64>() as u64)
.map_err(VhdxBatError::ReadBat)?; .map_err(VhdxBatError::ReadBat)?;
bat.insert(i as usize, BatEntry(LittleEndian::read_u64(&entry)));
let bat_entry = BatEntry(
f.read_u64::<LittleEndian>()
.map_err(VhdxBatError::ReadBat)?,
);
bat.insert(i as usize, bat_entry);
} }
Ok(bat) Ok(bat)
@@ -85,13 +81,11 @@ impl BatEntry {
// Routine for writing BAT entries to the disk // Routine for writing BAT entries to the disk
pub fn write_bat_entries( pub fn write_bat_entries(
f: &mut File, f: &AlignedFile,
bat_offset: u64, bat_offset: u64,
bat_entries: &[BatEntry], bat_entries: &[BatEntry],
) -> Result<()> { ) -> Result<()> {
for i in 0..bat_entries.len() as u64 { for i in 0..bat_entries.len() as u64 {
f.seek(SeekFrom::Start(bat_offset + i * size_of::<u64>() as u64))
.map_err(VhdxBatError::WriteBat)?;
let bat_entry = match bat_entries.get(i as usize) { let bat_entry = match bat_entries.get(i as usize) {
Some(entry) => entry.0, Some(entry) => entry.0,
None => { None => {
@@ -99,7 +93,9 @@ impl BatEntry {
} }
}; };
f.write_u64::<LittleEndian>(bat_entry) let mut buf = [0u8; size_of::<u64>()];
LittleEndian::write_u64(&mut buf, bat_entry);
f.write_all_at(&buf, bat_offset + i * size_of::<u64>() as u64)
.map_err(VhdxBatError::WriteBat)?; .map_err(VhdxBatError::WriteBat)?;
} }
Ok(()) Ok(())

View File

@@ -3,16 +3,17 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
use std::collections::btree_map::BTreeMap; use std::collections::btree_map::BTreeMap;
use std::fs::File;
use std::io::{self, Read, Seek, SeekFrom, Write};
use std::mem::size_of; use std::mem::size_of;
use std::{result, slice}; use std::os::unix::fs::FileExt;
use std::{io, result, slice};
use byteorder::{ByteOrder, LittleEndian, ReadBytesExt}; use byteorder::{ByteOrder, LittleEndian};
use remain::sorted; use remain::sorted;
use thiserror::Error; use thiserror::Error;
use uuid::Uuid; use uuid::Uuid;
use crate::aligned_file::AlignedFile;
const VHDX_SIGN: u64 = 0x656C_6966_7864_6876; // "vhdxfile" const VHDX_SIGN: u64 = 0x656C_6966_7864_6876; // "vhdxfile"
const HEADER_SIGN: u32 = 0x6461_6568; // "head" const HEADER_SIGN: u32 = 0x6461_6568; // "head"
const REGION_SIGN: u32 = 0x6967_6572; // "regi" const REGION_SIGN: u32 = 0x6967_6572; // "regi"
@@ -72,14 +73,6 @@ pub enum VhdxHeaderError {
RegionOverlap, RegionOverlap,
#[error("Reserved region has non-zero value")] #[error("Reserved region has non-zero value")]
ReservedIsNonZero, ReservedIsNonZero,
#[error("Failed to seek in File Type Identifier {0}")]
SeekFileTypeIdentifier(#[source] io::Error),
#[error("Failed to seek in headers {0}")]
SeekHeader(#[source] io::Error),
#[error("Failed to seek in region table entries {0}")]
SeekRegionTableEntries(#[source] io::Error),
#[error("Failed to seek in region table header {0}")]
SeekRegionTableHeader(#[source] io::Error),
#[error("We do not recognize this entry")] #[error("We do not recognize this entry")]
UnrecognizedRegionEntry, UnrecognizedRegionEntry,
#[error("Failed to write header {0}")] #[error("Failed to write header {0}")]
@@ -95,12 +88,11 @@ pub struct FileTypeIdentifier {
impl FileTypeIdentifier { impl FileTypeIdentifier {
/// Reads the File Type Identifier structure from a reference VHDx file /// Reads the File Type Identifier structure from a reference VHDx file
pub fn new(f: &mut File) -> Result<FileTypeIdentifier> { pub fn new(f: &AlignedFile) -> Result<FileTypeIdentifier> {
f.seek(SeekFrom::Start(FILE_START)) let mut buf = [0u8; size_of::<u64>()];
.map_err(VhdxHeaderError::SeekFileTypeIdentifier)?; f.read_exact_at(&mut buf, FILE_START)
let _signature = f
.read_u64::<LittleEndian>()
.map_err(VhdxHeaderError::ReadFileTypeIdentifier)?; .map_err(VhdxHeaderError::ReadFileTypeIdentifier)?;
let _signature = LittleEndian::read_u64(&buf);
if _signature != VHDX_SIGN { if _signature != VHDX_SIGN {
return Err(VhdxHeaderError::InvalidVHDXSign); return Err(VhdxHeaderError::InvalidVHDXSign);
} }
@@ -126,13 +118,11 @@ pub struct Header {
impl Header { impl Header {
/// Reads the Header structure from a reference VHDx file /// Reads the Header structure from a reference VHDx file
pub fn new(f: &mut File, start: u64) -> Result<Header> { pub fn new(f: &AlignedFile, start: u64) -> Result<Header> {
// Read the whole header into a buffer. We will need it for // Read the whole header into a buffer. We will need it for
// calculating checksum. // calculating checksum.
let mut buffer = [0; HEADER_SIZE as usize]; let mut buffer = [0; HEADER_SIZE as usize];
f.seek(SeekFrom::Start(start)) f.read_exact_at(&mut buffer, start)
.map_err(VhdxHeaderError::SeekHeader)?;
f.read_exact(&mut buffer)
.map_err(VhdxHeaderError::ReadHeader)?; .map_err(VhdxHeaderError::ReadHeader)?;
// SAFETY: buffer is of correct size and has been successfully filled. // SAFETY: buffer is of correct size and has been successfully filled.
@@ -159,7 +149,7 @@ impl Header {
/// Creates and returns new updated header from the provided current header /// Creates and returns new updated header from the provided current header
fn update_header( fn update_header(
f: &mut File, f: &AlignedFile,
current_header: &Header, current_header: &Header,
change_data_guid: bool, change_data_guid: bool,
mut file_write_guid: u128, mut file_write_guid: u128,
@@ -193,9 +183,8 @@ impl Header {
new_header.checksum = calculate_checksum(&mut buffer, size_of::<u32>()); new_header.checksum = calculate_checksum(&mut buffer, size_of::<u32>());
new_header.write_to_buffer(&mut buffer); new_header.write_to_buffer(&mut buffer);
f.seek(SeekFrom::Start(start)) f.write_all_at(&buffer, start)
.map_err(VhdxHeaderError::SeekHeader)?; .map_err(VhdxHeaderError::WriteHeader)?;
f.write(&buffer).map_err(VhdxHeaderError::WriteHeader)?;
Ok(new_header) Ok(new_header)
} }
@@ -212,13 +201,11 @@ struct RegionTableHeader {
impl RegionTableHeader { impl RegionTableHeader {
/// Reads the Region Table Header structure from a reference VHDx file /// Reads the Region Table Header structure from a reference VHDx file
pub fn new(f: &mut File, start: u64) -> Result<RegionTableHeader> { pub fn new(f: &AlignedFile, start: u64) -> Result<RegionTableHeader> {
// Read the whole header into a buffer. We will need it for calculating // Read the whole header into a buffer. We will need it for calculating
// checksum. // checksum.
let mut buffer = [0u8; REGION_SIZE as usize]; let mut buffer = [0u8; REGION_SIZE as usize];
f.seek(SeekFrom::Start(start)) f.read_exact_at(&mut buffer, start)
.map_err(VhdxHeaderError::SeekRegionTableHeader)?;
f.read_exact(&mut buffer)
.map_err(VhdxHeaderError::ReadRegionTableHeader)?; .map_err(VhdxHeaderError::ReadRegionTableHeader)?;
// SAFETY: buffer is of correct size and has been successfully filled. // SAFETY: buffer is of correct size and has been successfully filled.
@@ -253,7 +240,7 @@ pub struct RegionInfo {
impl RegionInfo { impl RegionInfo {
/// Collect all entries in a BTreeMap from the Region Table and identifies /// Collect all entries in a BTreeMap from the Region Table and identifies
/// BAT and metadata regions /// BAT and metadata regions
pub fn new(f: &mut File, region_start: u64, entry_count: u32) -> Result<RegionInfo> { pub fn new(f: &AlignedFile, region_start: u64, entry_count: u32) -> Result<RegionInfo> {
let mut bat_entry: Option<RegionTableEntry> = None; let mut bat_entry: Option<RegionTableEntry> = None;
let mut mdr_entry: Option<RegionTableEntry> = None; let mut mdr_entry: Option<RegionTableEntry> = None;
@@ -261,13 +248,12 @@ impl RegionInfo {
let mut region_entries = BTreeMap::new(); let mut region_entries = BTreeMap::new();
let mut buffer = [0; REGION_SIZE as usize]; let mut buffer = [0; REGION_SIZE as usize];
// Seek after the Region Table Header // Read after the Region Table Header
f.seek(SeekFrom::Start( f.read_exact_at(
&mut buffer,
region_start + size_of::<RegionTableHeader>() as u64, region_start + size_of::<RegionTableHeader>() as u64,
)) )
.map_err(VhdxHeaderError::SeekRegionTableEntries)?; .map_err(VhdxHeaderError::ReadRegionTableEntries)?;
f.read_exact(&mut buffer)
.map_err(VhdxHeaderError::ReadRegionTableEntries)?;
for _ in 0..entry_count { for _ in 0..entry_count {
let entry = let entry =
@@ -366,7 +352,7 @@ pub struct VhdxHeader {
impl VhdxHeader { impl VhdxHeader {
/// Creates a VhdxHeader from a reference to a file /// Creates a VhdxHeader from a reference to a file
pub fn new(f: &mut File) -> Result<VhdxHeader> { pub fn new(f: &AlignedFile) -> Result<VhdxHeader> {
Ok(VhdxHeader { Ok(VhdxHeader {
_file_type_identifier: FileTypeIdentifier::new(f)?, _file_type_identifier: FileTypeIdentifier::new(f)?,
header_1: Header::new(f, HEADER_1_START)?, header_1: Header::new(f, HEADER_1_START)?,
@@ -403,7 +389,7 @@ impl VhdxHeader {
/// current one. Returns both headers as a tuple sequenced the way it was /// current one. Returns both headers as a tuple sequenced the way it was
/// received from the parameter list. /// received from the parameter list.
fn update_header( fn update_header(
f: &mut File, f: &AlignedFile,
header_1: Result<Header>, header_1: Result<Header>,
header_2: Result<Header>, header_2: Result<Header>,
guid: u128, guid: u128,
@@ -426,7 +412,7 @@ impl VhdxHeader {
// Update the provided headers according to the spec // Update the provided headers according to the spec
fn update_headers( fn update_headers(
f: &mut File, f: &AlignedFile,
header_1: Result<Header>, header_1: Result<Header>,
header_2: Result<Header>, header_2: Result<Header>,
guid: u128, guid: u128,
@@ -436,7 +422,7 @@ impl VhdxHeader {
VhdxHeader::update_header(f, Ok(header_1), Ok(header_2), guid) VhdxHeader::update_header(f, Ok(header_1), Ok(header_2), guid)
} }
pub fn update(&mut self, f: &mut File) -> Result<()> { pub fn update(&mut self, f: &AlignedFile) -> Result<()> {
let headers = VhdxHeader::update_headers(f, Ok(self.header_1), Ok(self.header_2), 0)?; let headers = VhdxHeader::update_headers(f, Ok(self.header_1), Ok(self.header_2), 0)?;
self.header_1 = headers.0; self.header_1 = headers.0;
self.header_2 = headers.1; self.header_2 = headers.1;

View File

@@ -2,15 +2,15 @@
// //
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
use std::fs::File; use std::os::unix::fs::FileExt;
use std::io::{self, Read, Seek, SeekFrom, Write}; use std::{io, result};
use std::result;
use remain::sorted; use remain::sorted;
use thiserror::Error; use thiserror::Error;
use super::bat::{self, BatEntry, VhdxBatError}; use super::bat::{self, BatEntry, VhdxBatError};
use super::metadata::{self, DiskSpec}; use super::metadata::{self, DiskSpec};
use crate::aligned_file::AlignedFile;
const SECTOR_SIZE: u64 = 512; const SECTOR_SIZE: u64 = 512;
@@ -87,7 +87,7 @@ impl Sector {
/// VHDx IO read routine: requires relative sector index and count for the /// VHDx IO read routine: requires relative sector index and count for the
/// requested data. /// requested data.
pub fn read( pub fn read(
f: &mut File, f: &AlignedFile,
buf: &mut [u8], buf: &mut [u8],
disk_spec: &DiskSpec, disk_spec: &DiskSpec,
bat: &[BatEntry], bat: &[BatEntry],
@@ -115,11 +115,10 @@ pub fn read(
| bat::PAYLOAD_BLOCK_UNMAPPED | bat::PAYLOAD_BLOCK_UNMAPPED
| bat::PAYLOAD_BLOCK_ZERO => {} | bat::PAYLOAD_BLOCK_ZERO => {}
bat::PAYLOAD_BLOCK_FULLY_PRESENT => { bat::PAYLOAD_BLOCK_FULLY_PRESENT => {
f.seek(SeekFrom::Start(sector.file_offset)) f.read_exact_at(
.map_err(VhdxIoError::ReadSectorBlock)?;
f.read_exact(
&mut buf &mut buf
[read_count..(read_count + (sector.free_sectors * SECTOR_SIZE) as usize)], [read_count..(read_count + (sector.free_sectors * SECTOR_SIZE) as usize)],
sector.file_offset,
) )
.map_err(VhdxIoError::ReadSectorBlock)?; .map_err(VhdxIoError::ReadSectorBlock)?;
} }
@@ -140,7 +139,7 @@ pub fn read(
/// VHDx IO write routine: requires relative sector index and count for the /// VHDx IO write routine: requires relative sector index and count for the
/// requested data. /// requested data.
pub fn write( pub fn write(
f: &mut File, f: &AlignedFile,
buf: &[u8], buf: &[u8],
disk_spec: &mut DiskSpec, disk_spec: &mut DiskSpec,
bat_offset: u64, bat_offset: u64,
@@ -173,7 +172,9 @@ pub fn write(
.checked_add(disk_spec.block_size as u64) .checked_add(disk_spec.block_size as u64)
.ok_or(VhdxIoError::InvalidDiskSize)?; .ok_or(VhdxIoError::InvalidDiskSize)?;
f.set_len(new_size).map_err(VhdxIoError::ResizeFile)?; f.file()
.set_len(new_size)
.map_err(VhdxIoError::ResizeFile)?;
disk_spec.image_size = new_size; disk_spec.image_size = new_size;
let new_bat_entry = let new_bat_entry =
@@ -185,10 +186,9 @@ pub fn write(
break; break;
} }
f.seek(SeekFrom::Start(file_offset)) f.write_all_at(
.map_err(VhdxIoError::ReadSectorBlock)?;
f.write_all(
&buf[write_count..(write_count + (sector.free_sectors * SECTOR_SIZE) as usize)], &buf[write_count..(write_count + (sector.free_sectors * SECTOR_SIZE) as usize)],
file_offset,
) )
.map_err(VhdxIoError::ReadSectorBlock)?; .map_err(VhdxIoError::ReadSectorBlock)?;
} }
@@ -197,10 +197,9 @@ pub fn write(
break; break;
} }
f.seek(SeekFrom::Start(sector.file_offset)) f.write_all_at(
.map_err(VhdxIoError::ReadSectorBlock)?;
f.write_all(
&buf[write_count..(write_count + (sector.free_sectors * SECTOR_SIZE) as usize)], &buf[write_count..(write_count + (sector.free_sectors * SECTOR_SIZE) as usize)],
sector.file_offset,
) )
.map_err(VhdxIoError::ReadSectorBlock)?; .map_err(VhdxIoError::ReadSectorBlock)?;
} }

View File

@@ -2,17 +2,17 @@
// //
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
use std::fs::File;
use std::io::{self, Read, Seek, SeekFrom};
use std::mem::size_of; use std::mem::size_of;
use std::result; use std::os::unix::fs::FileExt;
use std::{io, result};
use byteorder::{LittleEndian, ReadBytesExt}; use byteorder::{ByteOrder, LittleEndian};
use remain::sorted; use remain::sorted;
use thiserror::Error; use thiserror::Error;
use uuid::Uuid; use uuid::Uuid;
use super::header::RegionTableEntry; use super::header::RegionTableEntry;
use crate::aligned_file::AlignedFile;
const METADATA_SIGN: u64 = 0x6174_6164_6174_656D; const METADATA_SIGN: u64 = 0x6174_6164_6174_656D;
const METADATA_ENTRY_SIZE: usize = 32; const METADATA_ENTRY_SIZE: usize = 32;
@@ -103,17 +103,18 @@ pub struct DiskSpec {
impl DiskSpec { impl DiskSpec {
/// Parse all metadata from the provided file and store info in DiskSpec /// Parse all metadata from the provided file and store info in DiskSpec
/// structure. /// structure.
pub fn new(f: &mut File, metadata_region: &RegionTableEntry) -> Result<DiskSpec> { pub fn new(f: &AlignedFile, metadata_region: &RegionTableEntry) -> Result<DiskSpec> {
let mut disk_spec = DiskSpec::default(); let mut disk_spec = DiskSpec::default();
let mut metadata_presence: u16 = 0; let mut metadata_presence: u16 = 0;
let mut offset = 0; let mut offset = 0;
let metadata = f.metadata().map_err(VhdxMetadataError::ReadMetadata)?; let metadata = f
.file()
.metadata()
.map_err(VhdxMetadataError::ReadMetadata)?;
disk_spec.image_size = metadata.len(); disk_spec.image_size = metadata.len();
let mut buffer = [0u8; METADATA_TABLE_MAX_SIZE]; let mut buffer = [0u8; METADATA_TABLE_MAX_SIZE];
f.seek(SeekFrom::Start(metadata_region.file_offset)) f.read_exact_at(&mut buffer, metadata_region.file_offset)
.map_err(VhdxMetadataError::ReadMetadata)?;
f.read_exact(&mut buffer)
.map_err(VhdxMetadataError::ReadMetadata)?; .map_err(VhdxMetadataError::ReadMetadata)?;
let metadata_header = let metadata_header =
@@ -124,18 +125,16 @@ impl DiskSpec {
let metadata_entry = let metadata_entry =
MetadataTableEntry::new(&buffer[offset..offset + size_of::<MetadataTableEntry>()])?; MetadataTableEntry::new(&buffer[offset..offset + size_of::<MetadataTableEntry>()])?;
f.seek(SeekFrom::Start( let item_offset = metadata_region.file_offset + metadata_entry.offset as u64;
metadata_region.file_offset + metadata_entry.offset as u64,
))
.map_err(VhdxMetadataError::ReadMetadata)?;
if metadata_entry.item_id if metadata_entry.item_id
== Uuid::parse_str(METADATA_FILE_PARAMETER) == Uuid::parse_str(METADATA_FILE_PARAMETER)
.map_err(VhdxMetadataError::InvalidUuid)? .map_err(VhdxMetadataError::InvalidUuid)?
{ {
disk_spec.block_size = f let mut item = [0u8; 2 * size_of::<u32>()];
.read_u32::<LittleEndian>() f.read_exact_at(&mut item, item_offset)
.map_err(VhdxMetadataError::ReadMetadata)?; .map_err(VhdxMetadataError::ReadMetadata)?;
disk_spec.block_size = LittleEndian::read_u32(&item[0..4]);
// MUST be at least 1 MiB and not greater than 256 MiB // MUST be at least 1 MiB and not greater than 256 MiB
if disk_spec.block_size < BLOCK_SIZE_MIN || disk_spec.block_size > BLOCK_SIZE_MAX { if disk_spec.block_size < BLOCK_SIZE_MIN || disk_spec.block_size > BLOCK_SIZE_MAX {
@@ -147,9 +146,7 @@ impl DiskSpec {
return Err(VhdxMetadataError::InvalidBlockSize); return Err(VhdxMetadataError::InvalidBlockSize);
} }
let bits = f let bits = LittleEndian::read_u32(&item[4..8]);
.read_u32::<LittleEndian>()
.map_err(VhdxMetadataError::ReadMetadata)?;
disk_spec.has_parent = bits & BLOCK_HAS_PARENT != 0; disk_spec.has_parent = bits & BLOCK_HAS_PARENT != 0;
metadata_presence |= METADATA_FILE_PARAMETER_PRESENT; metadata_presence |= METADATA_FILE_PARAMETER_PRESENT;
@@ -157,27 +154,30 @@ impl DiskSpec {
== Uuid::parse_str(METADATA_VIRTUAL_DISK_SIZE) == Uuid::parse_str(METADATA_VIRTUAL_DISK_SIZE)
.map_err(VhdxMetadataError::InvalidUuid)? .map_err(VhdxMetadataError::InvalidUuid)?
{ {
disk_spec.virtual_disk_size = f let mut item = [0u8; size_of::<u64>()];
.read_u64::<LittleEndian>() f.read_exact_at(&mut item, item_offset)
.map_err(VhdxMetadataError::ReadMetadata)?; .map_err(VhdxMetadataError::ReadMetadata)?;
disk_spec.virtual_disk_size = LittleEndian::read_u64(&item);
metadata_presence |= METADATA_VIRTUAL_DISK_SIZE_PRESENT; metadata_presence |= METADATA_VIRTUAL_DISK_SIZE_PRESENT;
} else if metadata_entry.item_id } else if metadata_entry.item_id
== Uuid::parse_str(METADATA_VIRTUAL_DISK_ID) == Uuid::parse_str(METADATA_VIRTUAL_DISK_ID)
.map_err(VhdxMetadataError::InvalidUuid)? .map_err(VhdxMetadataError::InvalidUuid)?
{ {
disk_spec.disk_id = f let mut item = [0u8; size_of::<u128>()];
.read_u128::<LittleEndian>() f.read_exact_at(&mut item, item_offset)
.map_err(VhdxMetadataError::ReadMetadata)?; .map_err(VhdxMetadataError::ReadMetadata)?;
disk_spec.disk_id = LittleEndian::read_u128(&item);
metadata_presence |= METADATA_VIRTUAL_DISK_ID_PRESENT; metadata_presence |= METADATA_VIRTUAL_DISK_ID_PRESENT;
} else if metadata_entry.item_id } else if metadata_entry.item_id
== Uuid::parse_str(METADATA_LOGICAL_SECTOR_SIZE) == Uuid::parse_str(METADATA_LOGICAL_SECTOR_SIZE)
.map_err(VhdxMetadataError::InvalidUuid)? .map_err(VhdxMetadataError::InvalidUuid)?
{ {
disk_spec.logical_sector_size = f let mut item = [0u8; size_of::<u32>()];
.read_u32::<LittleEndian>() f.read_exact_at(&mut item, item_offset)
.map_err(VhdxMetadataError::ReadMetadata)?; .map_err(VhdxMetadataError::ReadMetadata)?;
disk_spec.logical_sector_size = LittleEndian::read_u32(&item);
if !(disk_spec.logical_sector_size == 512 || disk_spec.logical_sector_size == 4096) if !(disk_spec.logical_sector_size == 512 || disk_spec.logical_sector_size == 4096)
{ {
return Err(VhdxMetadataError::InvalidLogicalSectorSize); return Err(VhdxMetadataError::InvalidLogicalSectorSize);
@@ -188,9 +188,10 @@ impl DiskSpec {
== Uuid::parse_str(METADATA_PHYSICAL_SECTOR_SIZE) == Uuid::parse_str(METADATA_PHYSICAL_SECTOR_SIZE)
.map_err(VhdxMetadataError::InvalidUuid)? .map_err(VhdxMetadataError::InvalidUuid)?
{ {
disk_spec.physical_sector_size = f let mut item = [0u8; size_of::<u32>()];
.read_u32::<LittleEndian>() f.read_exact_at(&mut item, item_offset)
.map_err(VhdxMetadataError::ReadMetadata)?; .map_err(VhdxMetadataError::ReadMetadata)?;
disk_spec.physical_sector_size = LittleEndian::read_u32(&item);
if !(disk_spec.physical_sector_size == 512 if !(disk_spec.physical_sector_size == 512
|| disk_spec.physical_sector_size == 4096) || disk_spec.physical_sector_size == 4096)
{ {

View File

@@ -20,6 +20,7 @@ use self::header::{RegionInfo, RegionTableEntry, VhdxHeader, VhdxHeaderError};
use self::io::VhdxIoError; use self::io::VhdxIoError;
use self::metadata::{DiskSpec, VhdxMetadataError}; use self::metadata::{DiskSpec, VhdxMetadataError};
use crate::BlockBackend; use crate::BlockBackend;
use crate::aligned_file::AlignedFile;
mod bat; mod bat;
mod header; mod header;
@@ -49,7 +50,7 @@ pub type Result<T> = result::Result<T, VhdxError>;
#[derive(Debug)] #[derive(Debug)]
pub struct Vhdx { pub struct Vhdx {
file: File, aligned: AlignedFile,
vhdx_header: VhdxHeader, vhdx_header: VhdxHeader,
region_entries: BTreeMap<u64, u64>, region_entries: BTreeMap<u64, u64>,
bat_entry: RegionTableEntry, bat_entry: RegionTableEntry,
@@ -63,11 +64,13 @@ pub struct Vhdx {
impl Vhdx { impl Vhdx {
/// Parse the Vhdx header, BAT, and metadata from a file and store info /// Parse the Vhdx header, BAT, and metadata from a file and store info
// in Vhdx structure. // in Vhdx structure.
pub fn new(mut file: File) -> Result<Vhdx> { pub fn new(file: File, direct_io: bool) -> Result<Vhdx> {
let vhdx_header = VhdxHeader::new(&mut file).map_err(VhdxError::ParseVhdxHeader)?; let aligned = AlignedFile::new(file, direct_io);
let vhdx_header = VhdxHeader::new(&aligned).map_err(VhdxError::ParseVhdxHeader)?;
let collected_entries = RegionInfo::new( let collected_entries = RegionInfo::new(
&mut file, &aligned,
header::REGION_TABLE_1_START, header::REGION_TABLE_1_START,
vhdx_header.region_entry_count(), vhdx_header.region_entry_count(),
) )
@@ -77,12 +80,12 @@ impl Vhdx {
let mdr_entry = collected_entries.mdr_entry; let mdr_entry = collected_entries.mdr_entry;
let disk_spec = let disk_spec =
DiskSpec::new(&mut file, &mdr_entry).map_err(VhdxError::ParseVhdxMetadata)?; DiskSpec::new(&aligned, &mdr_entry).map_err(VhdxError::ParseVhdxMetadata)?;
let bat_entries = BatEntry::collect_bat_entries(&mut file, &disk_spec, &bat_entry) let bat_entries = BatEntry::collect_bat_entries(&aligned, &disk_spec, &bat_entry)
.map_err(VhdxError::ReadBatEntry)?; .map_err(VhdxError::ReadBatEntry)?;
Ok(Vhdx { Ok(Vhdx {
file, aligned,
vhdx_header, vhdx_header,
region_entries: collected_entries.region_entries, region_entries: collected_entries.region_entries,
bat_entry, bat_entry,
@@ -107,7 +110,7 @@ impl Read for Vhdx {
let sector_index = self.current_offset / self.disk_spec.logical_sector_size as u64; let sector_index = self.current_offset / self.disk_spec.logical_sector_size as u64;
let result = io::read( let result = io::read(
&mut self.file, &self.aligned,
buf, buf,
&self.disk_spec, &self.disk_spec,
&self.bat_entries, &self.bat_entries,
@@ -128,7 +131,7 @@ impl Read for Vhdx {
impl Write for Vhdx { impl Write for Vhdx {
fn flush(&mut self) -> IoResult<()> { fn flush(&mut self) -> IoResult<()> {
self.file.flush() self.aligned.file_mut().flush()
} }
/// Wrapper function to satisfy Write trait implementation for VHDx disk. /// Wrapper function to satisfy Write trait implementation for VHDx disk.
@@ -140,12 +143,12 @@ impl Write for Vhdx {
if self.first_write { if self.first_write {
self.first_write = false; self.first_write = false;
self.vhdx_header self.vhdx_header
.update(&mut self.file) .update(&self.aligned)
.map_err(|e| IoError::other(format!("Failed to update VHDx header: {e}")))?; .map_err(|e| IoError::other(format!("Failed to update VHDx header: {e}")))?;
} }
let result = io::write( let result = io::write(
&mut self.file, &self.aligned,
buf, buf,
&mut self.disk_spec, &mut self.disk_spec,
self.bat_entry.file_offset, self.bat_entry.file_offset,
@@ -210,7 +213,8 @@ impl BlockBackend for Vhdx {
} }
fn physical_size(&self) -> result::Result<u64, crate::Error> { fn physical_size(&self) -> result::Result<u64, crate::Error> {
self.file self.aligned
.file()
.metadata() .metadata()
.map(|m| m.len()) .map(|m| m.len())
.map_err(crate::Error::GetFileMetadata) .map_err(crate::Error::GetFileMetadata)
@@ -220,7 +224,7 @@ impl BlockBackend for Vhdx {
impl Clone for Vhdx { impl Clone for Vhdx {
fn clone(&self) -> Self { fn clone(&self) -> Self {
Vhdx { Vhdx {
file: self.file.try_clone().unwrap(), aligned: self.aligned.try_clone().unwrap(),
vhdx_header: self.vhdx_header.clone(), vhdx_header: self.vhdx_header.clone(),
region_entries: self.region_entries.clone(), region_entries: self.region_entries.clone(),
bat_entry: self.bat_entry, bat_entry: self.bat_entry,
@@ -235,7 +239,7 @@ impl Clone for Vhdx {
impl AsRawFd for Vhdx { impl AsRawFd for Vhdx {
fn as_raw_fd(&self) -> RawFd { fn as_raw_fd(&self) -> RawFd {
self.file.as_raw_fd() self.aligned.file().as_raw_fd()
} }
} }
@@ -250,3 +254,64 @@ pub(crate) fn uuid_from_guid(buf: &[u8]) -> Uuid {
buf[8..16].try_into().unwrap(), buf[8..16].try_into().unwrap(),
) )
} }
#[cfg(test)]
mod tests {
use std::process::Command;
use vmm_sys_util::tempfile::TempFile;
use super::*;
/// Generate a small dynamic VHDX with `qemu-img`. Returns `None` (and the
/// test is skipped) when `qemu-img` is unavailable, e.g. in minimal CI.
fn dynamic_vhdx(size_mib: u64) -> Option<TempFile> {
let tf = TempFile::new().unwrap();
let path = tf.as_path();
let status = Command::new("qemu-img")
.args(["create", "-f", "vhdx", "-o", "subformat=dynamic"])
.arg(path)
.arg(format!("{size_mib}M"))
.status();
match status {
Ok(s) if s.success() => Some(tf),
_ => None,
}
}
/// An unaligned sector write under a forced O_DIRECT alignment must go
/// through `AlignedFile`'s read-modify-write bounce (the data block and the
/// BAT update both land at unaligned host offsets) and read back intact.
#[test]
fn unaligned_write_is_rmw() {
let Some(tf) = dynamic_vhdx(16) else {
eprintln!("skipping unaligned_write_is_rmw: qemu-img unavailable");
return;
};
let file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(tf.as_path())
.unwrap();
let mut vhdx = Vhdx::new(file, false).unwrap();
// Force a non-zero alignment so all of vhdx's positioned I/O exercises
// the bounce/RMW path even though the tempfile is not really O_DIRECT.
vhdx.aligned = AlignedFile::with_alignment(vhdx.aligned.file().try_clone().unwrap(), 512);
let sector = vhdx.disk_spec.logical_sector_size as usize;
let data: Vec<u8> = (0..sector).map(|i| ((i + 1) % 251) as u8).collect();
// Write at virtual offset 0 (allocates a new data block + rewrites BAT).
vhdx.seek(SeekFrom::Start(0)).unwrap();
assert_eq!(vhdx.write(&data).unwrap(), data.len());
vhdx.flush().unwrap();
// Read it back through a fresh, forced-alignment handle.
let mut readback = vec![0u8; sector];
vhdx.seek(SeekFrom::Start(0)).unwrap();
assert_eq!(vhdx.read(&mut readback).unwrap(), readback.len());
assert_eq!(readback, data);
}
}

View File

@@ -38,9 +38,9 @@ pub struct VhdxDisk {
} }
impl VhdxDisk { impl VhdxDisk {
pub fn new(f: File) -> BlockResult<Self> { pub fn new(f: File, direct_io: bool) -> BlockResult<Self> {
Ok(VhdxDisk { Ok(VhdxDisk {
vhdx_file: Arc::new(Mutex::new(Vhdx::new(f).map_err(|e| { vhdx_file: Arc::new(Mutex::new(Vhdx::new(f, direct_io).map_err(|e| {
let kind = match &e { let kind = match &e {
VhdxError::NotVhdx(_) VhdxError::NotVhdx(_)
| VhdxError::ParseVhdxHeader(_) | VhdxError::ParseVhdxHeader(_)

View File

@@ -22,7 +22,7 @@ fuzz_target!(|bytes: &[u8]| -> Corpus {
disk_file.write_all(&bytes[..]).unwrap(); disk_file.write_all(&bytes[..]).unwrap();
disk_file.seek(SeekFrom::Start(0)).unwrap(); disk_file.seek(SeekFrom::Start(0)).unwrap();
let mut vhdx = match Vhdx::new(disk_file) { let mut vhdx = match Vhdx::new(disk_file, false) {
Ok(vhdx) => vhdx, Ok(vhdx) => vhdx,
Err(_) => return Corpus::Reject, Err(_) => return Corpus::Reject,
}; };