diff --git a/block/src/formats/vmdk/engine_sync.rs b/block/src/formats/vmdk/engine_sync.rs new file mode 100644 index 000000000..6f6838792 --- /dev/null +++ b/block/src/formats/vmdk/engine_sync.rs @@ -0,0 +1,293 @@ +// Copyright © 2026, Microsoft Corporation +// +// SPDX-License-Identifier: Apache-2.0 + +use std::os::unix::fs::FileExt; +use std::sync::Arc; +use std::{cmp, io}; + +use vmm_sys_util::eventfd::EventFd; + +use crate::AlignedFile; +use crate::async_io::{ + AsyncIo, AsyncIoCompletion, AsyncIoError, AsyncIoOperation, AsyncIoResult, CompletionCommon, +}; +use crate::formats::vmdk::flat::{ExtentAccess, VmdkExtent}; + +/// Synchronous, extent-aware I/O worker for flat VMDK images. +/// +/// Maps each guest I/O request to one or more backing extents. +/// +/// Async backends (io_uring/AIO) are not supported. +pub(crate) struct FlatVmdkSync { + extents: Arc>, + size: u64, + completions: CompletionCommon, +} + +impl FlatVmdkSync { + pub fn new(extents: Arc>, size: u64) -> Self { + FlatVmdkSync { + extents, + size, + completions: CompletionCommon::new(), + } + } + + // Returns the extent containing virtual `offset`, or `None` if out of range. + fn extent_at(&self, offset: u64) -> Option<&VmdkExtent> { + self.extents + .iter() + .find(|e| offset >= e.virtual_start && offset < e.virtual_start + e.length) + } + + fn check_access(&self, start: u64, total: u64, is_read: bool) -> io::Result<()> { + let end = start + total; + let mut cur = start; + while cur < end { + let extent = self.extent_at(cur).ok_or_else(|| { + io::Error::new(io::ErrorKind::InvalidData, "offset outside any VMDK extent") + })?; + match extent.access { + ExtentAccess::NoAccess => { + return Err(io::Error::new( + io::ErrorKind::PermissionDenied, + format!("VMDK extent at offset {cur} is NOACCESS; request rejected"), + )); + } + ExtentAccess::ReadOnly if !is_read => { + return Err(io::Error::new( + io::ErrorKind::PermissionDenied, + format!("write to read-only VMDK extent at offset {cur} rejected"), + )); + } + _ => {} + } + cur = extent.virtual_start + extent.length; + } + Ok(()) + } + + // Reads or writes a single contiguous segment of one extent through the + // extent's `AlignedFile`. + fn segment_io( + file: &AlignedFile, + file_offset: u64, + op: &mut AsyncIoOperation, + buf_start: usize, + seg_len: usize, + is_read: bool, + ) -> io::Result { + // O_DIRECT unaligned + if file.alignment() != 0 { + return if is_read { + file.read_unaligned(file_offset, seg_len, |data| { + op.write_bytes_at(buf_start, data) + }) + } else { + file.write_unaligned(file_offset, seg_len, |data| { + op.read_bytes_at(buf_start, data) + }) + }; + } + + // Aligned & Buffered + let mut buf = vec![0u8; seg_len]; + let mut done = 0usize; + if is_read { + while done < seg_len { + match file.read_at(&mut buf[done..], file_offset + done as u64) { + Ok(0) => break, // EOF: nothing more to read + Ok(n) => done += n, + Err(ref e) if e.kind() == io::ErrorKind::Interrupted => continue, + Err(e) => return Err(e), + } + } + op.write_bytes_at(buf_start, &buf[..done])?; + Ok(done) + } else { + op.read_bytes_at(buf_start, &mut buf)?; + while done < seg_len { + match file.write_at(&buf[done..], file_offset + done as u64) { + Ok(0) => break, // no progress: avoid spinning forever + Ok(n) => done += n, + Err(ref e) if e.kind() == io::ErrorKind::Interrupted => continue, + Err(e) => return Err(e), + } + } + Ok(done) + } + } + + // Single-extent path: the whole request lives in `extent`. + // + // The guest iovecs are handed to `AlignedFile::{read,write}_vectored_at` + fn single_extent_io( + &self, + extent: &VmdkExtent, + op: &mut AsyncIoOperation, + ) -> io::Result { + let file = extent.file.as_ref().ok_or_else(|| { + io::Error::new( + io::ErrorKind::PermissionDenied, + "VMDK extent is not accessible", + ) + })?; + let file_offset = extent.file_base_offset + (op.offset() as u64 - extent.virtual_start); + let iovecs = op.iovecs(); + + // SAFETY: the iovec buffers are owned by `op` and remain valid for the + // duration of this call. + unsafe { + if op.is_read() { + file.read_vectored_at(iovecs, file_offset) + } else { + file.write_vectored_at(iovecs, file_offset) + } + } + } + + // Slow path: the request straddles >= 2 extents. + // + // A single guest request here maps onto several different backing files, + // Every segment goes through `segment_io` regardless of + // alignment. + fn spanning_io(&self, op: &mut AsyncIoOperation) -> io::Result { + let start = op.offset() as u64; + let total = op.total_len() as u64; + let is_read = op.is_read(); + + let mut done: u64 = 0; + while done < total { + let cur = start + done; + let extent = self.extent_at(cur).ok_or_else(|| { + io::Error::new(io::ErrorKind::InvalidData, "offset outside any VMDK extent") + })?; + let extent_end = extent.virtual_start + extent.length; + // Bytes handled in this extent before reaching its boundary. + let seg_len = cmp::min(total - done, extent_end - cur) as usize; + let file = extent.file.as_ref().ok_or_else(|| { + io::Error::new( + io::ErrorKind::PermissionDenied, + "VMDK extent is not accessible", + ) + })?; + let file_offset = extent.file_base_offset + (cur - extent.virtual_start); + + let n = Self::segment_io(file, file_offset, op, done as usize, seg_len, is_read)?; + done += n as u64; + if n < seg_len { + break; // short read/write + } + } + + Ok(done as usize) + } +} + +impl AsyncIo for FlatVmdkSync { + fn notifier(&self) -> &EventFd { + self.completions.notifier() + } + + fn submit_data_operation(&mut self, mut op: AsyncIoOperation) -> AsyncIoResult<()> { + let start = op.offset() as u64; + let total = op.total_len() as u64; + let is_read = op.is_read(); + + // Bounds check against the virtual disk size (overflow-safe: `start` + // is checked before subtracting it from `size`). + if start > self.size || total > self.size - start { + let error = io::Error::new( + io::ErrorKind::InvalidData, + format!( + "VMDK request [{start}, {}) exceeds virtual size {}", + start + total, + self.size + ), + ); + return Err(if is_read { + AsyncIoError::ReadVectored(error) + } else { + AsyncIoError::WriteVectored(error) + }); + } + + // Reject the request up front if any extent it touches forbids it: + // NOACCESS extents reject all I/O. + if total != 0 + && let Err(error) = self.check_access(start, total, is_read) + { + return Err(if is_read { + AsyncIoError::ReadVectored(error) + } else { + AsyncIoError::WriteVectored(error) + }); + } + + let result = if total == 0 { + Ok(0) + } else if let Some(extent) = self.extent_at(start) { + if start + total <= extent.virtual_start + extent.length { + // Entire request fits in one extent + self.single_extent_io(extent, &mut op) + } else { + // Request crosses an extent boundary + self.spanning_io(&mut op) + } + } else { + Err(io::Error::new( + io::ErrorKind::InvalidData, + "offset outside any VMDK extent", + )) + }; + + let bytes = result.map_err(|e| { + if is_read { + AsyncIoError::ReadVectored(e) + } else { + AsyncIoError::WriteVectored(e) + } + })?; + + self.completions + .complete(AsyncIoCompletion::from_operation(op, bytes as i32)); + Ok(()) + } + + fn fsync(&mut self, user_data: Option) -> AsyncIoResult<()> { + // Flush every extent: a single guest flush must durably persist data + // that may have been written across multiple extent files. + for extent in self.extents.iter() { + // Skip NoAccess extents, which have no open file. + if let Some(file) = extent.file.as_ref() { + file.sync_all().map_err(AsyncIoError::Fsync)?; + } + } + + if let Some(user_data) = user_data { + self.completions + .complete(AsyncIoCompletion::new(user_data, 0, None)); + } + + Ok(()) + } + + fn next_completed_request(&mut self) -> Option { + self.completions.next_completed() + } + + fn punch_hole(&mut self, _offset: u64, _length: u64, _user_data: u64) -> AsyncIoResult<()> { + // Flat VMDK is not sparse-capable (see `SparseCapable` impl), so this + // should never be negotiated by the guest. + Err(AsyncIoError::PunchHole(io::Error::other( + "punch_hole not supported for flat VMDK", + ))) + } + + fn write_zeroes(&mut self, _offset: u64, _length: u64, _user_data: u64) -> AsyncIoResult<()> { + Err(AsyncIoError::WriteZeroes(io::Error::other( + "write_zeroes not supported for flat VMDK", + ))) + } +} diff --git a/block/src/formats/vmdk/flat.rs b/block/src/formats/vmdk/flat.rs index ab5062b45..00cada74f 100644 --- a/block/src/formats/vmdk/flat.rs +++ b/block/src/formats/vmdk/flat.rs @@ -5,8 +5,6 @@ //! Flat VMDK extent layout: opens the data extents referenced by the //! descriptor and maps the virtual disk onto them. -#![allow(dead_code)] - use std::ffi::{CString, OsStr}; use std::fs::{File, OpenOptions}; use std::io; @@ -19,7 +17,7 @@ use std::sync::Arc; use log::warn; use crate::formats::vmdk::descriptor::VmdkDescriptor; -use crate::{AlignedFile, DiskTopology}; +use crate::{AlignedFile, DiskTopology, query_device_size}; const VMDK_SECTOR_SIZE: u64 = 512; @@ -346,18 +344,16 @@ impl FlatVmdk { Arc::clone(&self.extents) } - /// Host allocation size: the sum of every opened extent file's size. - /// `NoAccess` extents contribute 0 to the total. + /// Host allocation size: the sum of every opened extent's actually + /// allocated storage (`st_blocks * 512` for regular files, device size for + /// block devices), so sparse extents are reported correctly. `NoAccess` + /// extents (no open file) contribute 0, as does any extent whose size + /// cannot be queried. pub fn physical_block_size(&self) -> u64 { self.extents .iter() - .map(|extent| { - extent - .file - .as_ref() - .and_then(|f| f.metadata().ok()) - .map_or(0, |m| m.len()) - }) + .filter_map(|extent| extent.file.as_ref()) + .map(|f| query_device_size(f.file()).map_or(0, |(_, physical)| physical)) .sum() } diff --git a/block/src/formats/vmdk/mod.rs b/block/src/formats/vmdk/mod.rs index ae6efcf1b..a7a48d64f 100644 --- a/block/src/formats/vmdk/mod.rs +++ b/block/src/formats/vmdk/mod.rs @@ -1,4 +1,4 @@ -// Copyright 2026 The Cloud Hypervisor Authors. All rights reserved. +// Copyright © 2026, Microsoft Corporation // // SPDX-License-Identifier: Apache-2.0 @@ -8,6 +8,377 @@ //! synchronous, extent-aware I/O. mod descriptor; +mod engine_sync; mod flat; +use std::fs::File; +use std::io; +use std::os::unix::io::AsRawFd; +use std::path::Path; + pub use descriptor::{has_descriptor_header, is_flat_vmdk}; + +use self::engine_sync::FlatVmdkSync; +use self::flat::FlatVmdk; +use crate::async_io::{AsyncIo, BorrowedDiskFd, DiskFileError}; +use crate::error::{BlockError, BlockErrorKind, BlockResult, ErrorOp}; +use crate::{DiskTopology, disk_file}; + +#[derive(Debug)] +pub struct VmdkDisk { + inner: FlatVmdk, +} + +impl VmdkDisk { + /// Builds a Flat VMDK disk backend. + pub fn new(file: File, path: &Path, direct: bool) -> Result { + let inner = FlatVmdk::new(file, path, direct)?; + Ok(VmdkDisk { inner }) + } +} + +impl disk_file::DiskSize for VmdkDisk { + fn logical_size(&self) -> BlockResult { + Ok(self.inner.virtual_block_size()) + } +} + +impl disk_file::PhysicalSize for VmdkDisk { + fn physical_size(&self) -> BlockResult { + Ok(self.inner.physical_block_size()) + } +} + +// Expose the descriptor file's fd for advisory image locking. +impl disk_file::DiskFd for VmdkDisk { + fn fd(&self) -> BorrowedDiskFd<'_> { + BorrowedDiskFd::new(self.inner.as_raw_fd()) + } +} + +impl disk_file::Geometry for VmdkDisk { + fn topology(&self) -> DiskTopology { + self.inner.topology() + } +} + +impl disk_file::SparseCapable for VmdkDisk {} + +// Flat VMDK keeps no in-memory format metadata, so no-op. +impl disk_file::MetadataSync for VmdkDisk {} + +impl disk_file::Resizable for VmdkDisk { + fn resize(&mut self, _size: u64) -> BlockResult<()> { + Err(BlockError::new( + BlockErrorKind::UnsupportedFeature, + DiskFileError::ResizeError(io::Error::other("resize not supported for flat VMDK")), + ) + .with_op(ErrorOp::Resize)) + } +} + +impl disk_file::DiskFile for VmdkDisk {} + +impl disk_file::AsyncDiskFile for VmdkDisk { + fn try_clone(&self) -> BlockResult> { + Ok(Box::new(VmdkDisk { + inner: self.inner.clone(), + })) + } + + fn create_async_io(&self, ring_depth: u32) -> BlockResult> { + // VMDK provides a synchronous, extent-aware worker, so the io_uring ring + // depth is unused here. + let _ = ring_depth; + + Ok(Box::new(FlatVmdkSync::new( + self.inner.extents(), + self.inner.virtual_block_size(), + ))) + } +} + +#[cfg(test)] +mod tests { + use std::io::Write; + use std::os::unix::io::AsRawFd; + use std::path::PathBuf; + + use vmm_sys_util::tempdir::TempDir; + + use super::*; + use crate::disk_file::{AsyncDiskFile, DiskFd, DiskSize, PhysicalSize, Resizable}; + + const SECTOR: u64 = 512; + + // Builds a flat VMDK in `dir`: a descriptor plus one backing data file per + // extent. When `allocate` is true the extent files are filled with real + // blocks (fixed / pre-allocated layout used in practice), otherwise they + // are created sparse via `set_len` (declared length but no allocated + // blocks). `extents` entries are (filename, access, sectors). Returns the + // descriptor path. + fn build_flat_vmdk( + dir: &Path, + create_type: &str, + extents: &[(&str, &str, u64)], + allocate: bool, + ) -> PathBuf { + let mut desc = String::from("# Disk DescriptorFile\n"); + desc.push_str("version=1\n"); + desc.push_str("CID=fffffffe\n"); + desc.push_str("parentCID=ffffffff\n"); + desc.push_str(&format!("createType={create_type}\n")); + desc.push_str("# Extent description\n"); + + for (filename, access, sectors) in extents { + let mut data = File::create(dir.join(filename)).unwrap(); + if allocate { + data.write_all(&vec![0u8; (sectors * SECTOR) as usize]) + .unwrap(); + } else { + data.set_len(sectors * SECTOR).unwrap(); + } + data.sync_all().unwrap(); + desc.push_str(&format!("{access} {sectors} FLAT \"{filename}\"\n")); + } + + desc.push_str("# The Disk Data Base\n"); + desc.push_str("ddb.adapterType = \"ide\"\n"); + + let desc_path = dir.join("disk.vmdk"); + let mut df = File::create(&desc_path).unwrap(); + df.write_all(desc.as_bytes()).unwrap(); + df.sync_all().unwrap(); + desc_path + } + + // Sparse extents. + fn write_flat_vmdk(dir: &Path, create_type: &str, extents: &[(&str, &str, u64)]) -> PathBuf { + build_flat_vmdk(dir, create_type, extents, false) + } + + // Fully pre-allocated extents. + fn write_flat_vmdk_allocated( + dir: &Path, + create_type: &str, + extents: &[(&str, &str, u64)], + ) -> PathBuf { + build_flat_vmdk(dir, create_type, extents, true) + } + + fn open_descriptor(path: &Path) -> File { + File::open(path).unwrap() + } + + // Writes a descriptor referencing `extent_lines` verbatim (no backing data + // files are created). Used to exercise `FlatVmdk::new`'s per-extent + // validation, whose zero-size/overflow checks all run before an extent file + // would be opened. + fn write_descriptor(dir: &Path, create_type: &str, extent_lines: &[&str]) -> PathBuf { + let mut desc = String::from("# Disk DescriptorFile\n"); + desc.push_str("version=1\n"); + desc.push_str("CID=fffffffe\n"); + desc.push_str("parentCID=ffffffff\n"); + desc.push_str(&format!("createType={create_type}\n")); + desc.push_str("# Extent description\n"); + for line in extent_lines { + desc.push_str(line); + desc.push('\n'); + } + desc.push_str("# The Disk Data Base\n"); + desc.push_str("ddb.adapterType = \"ide\"\n"); + + let desc_path = dir.join("disk.vmdk"); + let mut df = File::create(&desc_path).unwrap(); + df.write_all(desc.as_bytes()).unwrap(); + df.sync_all().unwrap(); + desc_path + } + + #[test] + fn logical_and_physical_size_single_extent() { + let dir = TempDir::new_with_prefix("/tmp/vmdk-test").unwrap(); + let path = write_flat_vmdk( + dir.as_path(), + "monolithicFlat", + &[("disk-flat.vmdk", "RW", 2048)], + ); + let disk = VmdkDisk::new(open_descriptor(&path), &path, false).unwrap(); + + assert_eq!(disk.logical_size().unwrap(), 2048 * SECTOR); + // The extent is created sparse (`set_len`), so no blocks are allocated + // and the `st_blocks`-based physical size is 0. + assert_eq!(disk.physical_size().unwrap(), 0); + } + + #[test] + fn logical_size_sums_multiple_extents() { + let dir = TempDir::new_with_prefix("/tmp/vmdk-test").unwrap(); + let path = write_flat_vmdk( + dir.as_path(), + "twoGbMaxExtentFlat", + &[("s001.vmdk", "RW", 2048), ("s002.vmdk", "RW", 1024)], + ); + let disk = VmdkDisk::new(open_descriptor(&path), &path, false).unwrap(); + + assert_eq!(disk.logical_size().unwrap(), (2048 + 1024) * SECTOR); + // Sparse extents: no blocks are allocated, so physical size is 0. + assert_eq!(disk.physical_size().unwrap(), 0); + } + + #[test] + fn physical_size_matches_fully_allocated_extents() { + let dir = TempDir::new_with_prefix("/tmp/vmdk-test").unwrap(); + let path = write_flat_vmdk_allocated( + dir.as_path(), + "twoGbMaxExtentFlat", + &[("s001.vmdk", "RW", 2048), ("s002.vmdk", "RW", 1024)], + ); + let disk = VmdkDisk::new(open_descriptor(&path), &path, false).unwrap(); + + // Fully pre-allocated extents: host allocation (st_blocks) equals the + // declared logical size. + assert_eq!(disk.logical_size().unwrap(), (2048 + 1024) * SECTOR); + assert_eq!(disk.physical_size().unwrap(), (2048 + 1024) * SECTOR); + } + + #[test] + fn fd_exposes_descriptor_file() { + let dir = TempDir::new_with_prefix("/tmp/vmdk-test").unwrap(); + let path = write_flat_vmdk( + dir.as_path(), + "monolithicFlat", + &[("disk-flat.vmdk", "RW", 64)], + ); + let file = open_descriptor(&path); + let expected = file.as_raw_fd(); + + let disk = VmdkDisk::new(file, &path, false).unwrap(); + + assert_eq!(disk.fd().as_raw_fd(), expected); + } + + #[test] + fn resize_is_unsupported() { + let dir = TempDir::new_with_prefix("/tmp/vmdk-test").unwrap(); + let path = write_flat_vmdk( + dir.as_path(), + "monolithicFlat", + &[("disk-flat.vmdk", "RW", 64)], + ); + let mut disk = VmdkDisk::new(open_descriptor(&path), &path, false).unwrap(); + + let err = disk.resize(4096).unwrap_err(); + assert_eq!(err.kind(), BlockErrorKind::UnsupportedFeature); + } + + #[test] + fn try_clone_preserves_size() { + let dir = TempDir::new_with_prefix("/tmp/vmdk-test").unwrap(); + let path = write_flat_vmdk( + dir.as_path(), + "monolithicFlat", + &[("disk-flat.vmdk", "RW", 2048)], + ); + let disk = VmdkDisk::new(open_descriptor(&path), &path, false).unwrap(); + + let cloned = disk.try_clone().unwrap(); + assert_eq!(cloned.logical_size().unwrap(), disk.logical_size().unwrap()); + } + + #[test] + fn create_async_io_builds_worker() { + let dir = TempDir::new_with_prefix("/tmp/vmdk-test").unwrap(); + let path = write_flat_vmdk( + dir.as_path(), + "monolithicFlat", + &[("disk-flat.vmdk", "RW", 2048)], + ); + let disk = VmdkDisk::new(open_descriptor(&path), &path, false).unwrap(); + + // Ring depth is ignored by the synchronous VMDK worker. + disk.create_async_io(0).unwrap(); + } + + #[test] + fn create_async_io_supports_multi_extent() { + let dir = TempDir::new_with_prefix("/tmp/vmdk-test").unwrap(); + let path = write_flat_vmdk( + dir.as_path(), + "twoGbMaxExtentFlat", + &[("s001.vmdk", "RW", 2048), ("s002.vmdk", "RW", 2048)], + ); + let disk = VmdkDisk::new(open_descriptor(&path), &path, false).unwrap(); + + disk.create_async_io(32).unwrap(); + } + + #[test] + fn new_rejects_zero_sector_extent() { + let dir = TempDir::new_with_prefix("/tmp/vmdk-zero-test").unwrap(); + let path = write_descriptor( + dir.as_path(), + "monolithicFlat", + &["RW 0 FLAT \"disk-flat.vmdk\""], + ); + + let err = FlatVmdk::new(open_descriptor(&path), &path, false).unwrap_err(); + assert_eq!(err.kind(), io::ErrorKind::InvalidData); + } + + #[test] + fn new_rejects_extent_size_overflow() { + // size_in_sectors * 512 must fit in a u64. + let dir = TempDir::new_with_prefix("/tmp/vmdk-ovf-size-test").unwrap(); + let line = format!("RW {} FLAT \"disk-flat.vmdk\"", u64::MAX); + let path = write_descriptor(dir.as_path(), "monolithicFlat", &[line.as_str()]); + + let err = FlatVmdk::new(open_descriptor(&path), &path, false).unwrap_err(); + assert_eq!(err.kind(), io::ErrorKind::InvalidData); + } + + #[test] + fn new_rejects_extent_file_offset_overflow() { + // The offset * 512 must fit in a u64. + let dir = TempDir::new_with_prefix("/tmp/vmdk-ovf-offset-test").unwrap(); + let line = format!("RW 1 FLAT \"disk-flat.vmdk\" {}", u64::MAX); + let path = write_descriptor(dir.as_path(), "monolithicFlat", &[line.as_str()]); + + let err = FlatVmdk::new(open_descriptor(&path), &path, false).unwrap_err(); + assert_eq!(err.kind(), io::ErrorKind::InvalidData); + } + + #[test] + fn new_rejects_extent_file_range_overflow() { + // Each of offset*512 and size*512 fits in a u64, but their sum (the last + // byte the extent addresses in its backing file) overflows. + let dir = TempDir::new_with_prefix("/tmp/vmdk-ovf-range-test").unwrap(); + // offset_in_sectors = floor(u64::MAX / 512) => offset bytes = u64::MAX - + // 511, size 1 sector (512 bytes) pushes the end one byte past u64::MAX. + let offset = u64::MAX / 512; + let line = format!("RW 1 FLAT \"disk-flat.vmdk\" {offset}"); + let path = write_descriptor(dir.as_path(), "monolithicFlat", &[line.as_str()]); + + let err = FlatVmdk::new(open_descriptor(&path), &path, false).unwrap_err(); + assert_eq!(err.kind(), io::ErrorKind::InvalidData); + } + + #[test] + fn new_rejects_total_virtual_size_overflow() { + // Two extents whose individual lengths fit in a u64 but whose running + // sum (the total virtual disk size) overflows. size = 2^55 - 1 => + // length = 2^64 - 512, two of them overflow the total. + let dir = TempDir::new_with_prefix("/tmp/vmdk-ovf-total-test").unwrap(); + let size = (1u64 << 55) - 1; + let l1 = format!("NOACCESS {size} FLAT \"s001.vmdk\""); + let l2 = format!("NOACCESS {size} FLAT \"s002.vmdk\""); + let path = write_descriptor( + dir.as_path(), + "twoGbMaxExtentFlat", + &[l1.as_str(), l2.as_str()], + ); + + let err = FlatVmdk::new(open_descriptor(&path), &path, false).unwrap_err(); + assert_eq!(err.kind(), io::ErrorKind::InvalidData); + } +}