mirror of
https://github.com/cloud-hypervisor/cloud-hypervisor.git
synced 2026-08-05 02:19:16 +00:00
block: Add flat VMDK extent layout
Defines and implements the layout of a VMDK extent, the region of storage that is used by the virtual disk. Each line in the descriptor file's extent section describes one extent. Signed-off-by: Sumedh Alok Sharma <sumsharma@microsoft.com>
This commit is contained in:
committed by
Wei Liu
parent
7b24354020
commit
0769215d42
@@ -8,10 +8,10 @@
|
||||
//! of `FLAT` extent lines, and a disk database (DDB). Only the `monolithicFlat`
|
||||
//! and `twoGbMaxExtentFlat` create types are recognized.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::io;
|
||||
use std::os::unix::fs::FileExt;
|
||||
use std::path::Path;
|
||||
use std::str::Lines;
|
||||
|
||||
use crate::AlignedFile;
|
||||
@@ -33,7 +33,6 @@ pub enum VMDKDiskType {
|
||||
/// Flat VMDK extent line fields.
|
||||
/// Format of each extent line:
|
||||
/// `<access> <sectors> <type> "<file>" [offset]`.
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Default)]
|
||||
pub struct VmdkExtentHeader {
|
||||
pub access: String,
|
||||
@@ -44,14 +43,9 @@ pub struct VmdkExtentHeader {
|
||||
}
|
||||
|
||||
/// Descriptor header fields.
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Default)]
|
||||
pub struct VmdkDescriptorHeader {
|
||||
pub version: u32,
|
||||
pub cid: u32,
|
||||
pub parent_cid: u32,
|
||||
pub create_type: VMDKDiskType,
|
||||
pub parent_filename_hint: String,
|
||||
}
|
||||
|
||||
/// Ordered list of extents.
|
||||
@@ -59,11 +53,49 @@ pub struct VmdkDescriptorHeader {
|
||||
pub struct VmdkDescriptorExtents {
|
||||
pub extents: Vec<VmdkExtentHeader>,
|
||||
}
|
||||
/// Disk database.
|
||||
#[allow(dead_code)]
|
||||
|
||||
/// Parsed flat VMDK descriptor
|
||||
///
|
||||
/// extents_list: ordered extent list
|
||||
/// base_path: descriptor file's parent directory
|
||||
#[derive(Debug, Default)]
|
||||
pub struct VmdkDescriptorDdb {
|
||||
pub entries: HashMap<String, String>,
|
||||
pub struct VmdkDescriptor {
|
||||
pub base_path: String,
|
||||
pub extents_list: VmdkDescriptorExtents,
|
||||
}
|
||||
|
||||
impl VmdkDescriptor {
|
||||
pub fn new(file: &File, path: &Path) -> io::Result<Self> {
|
||||
// The descriptor's directory anchors the relative extent filenames.
|
||||
let base_path = path
|
||||
.parent()
|
||||
.ok_or_else(|| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"Cannot retrieve parent directory of the file",
|
||||
)
|
||||
})?
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
// Valid descriptor file must be much larger than 4 bytes.
|
||||
if file.metadata()?.len() < 4 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"Invalid VMDK descriptor file: file is empty or too small",
|
||||
));
|
||||
}
|
||||
|
||||
let content = read_descriptor(file)?;
|
||||
let mut lines = content.lines();
|
||||
let (_header, last_line) = parse_header(&mut lines)?;
|
||||
let extents_list = parse_extents(&mut lines, last_line)?;
|
||||
|
||||
Ok(Self {
|
||||
base_path,
|
||||
extents_list,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Read the whole descriptor into memory through an `AlignedFile` and return it
|
||||
@@ -115,35 +147,24 @@ pub(crate) fn parse_header<'a>(
|
||||
break;
|
||||
}
|
||||
let parts: Vec<&str> = line.split('=').map(|s| s.trim()).collect();
|
||||
if parts.len() == 2 {
|
||||
match parts[0] {
|
||||
"version" => header.version = parts[1].parse().unwrap_or(0),
|
||||
"CID" => header.cid = u32::from_str_radix(parts[1], 16).unwrap_or(0),
|
||||
"parentCID" => header.parent_cid = u32::from_str_radix(parts[1], 16).unwrap_or(0),
|
||||
"createType" => {
|
||||
// Tools such as qemu-img quote the value, e.g.
|
||||
// createType="monolithicFlat", strip the quotes.
|
||||
header.create_type = match parts[1].trim_matches('"') {
|
||||
"monolithicFlat" => VMDKDiskType::MonolithicFlat,
|
||||
"twoGbMaxExtentFlat" => VMDKDiskType::TwoGbMaxExtentFlat,
|
||||
_ => VMDKDiskType::CreateTypeUnsupported,
|
||||
}
|
||||
}
|
||||
"parentFileNameHint" => header.parent_filename_hint = parts[1].to_string(),
|
||||
_ => {}
|
||||
}
|
||||
if parts.len() == 2 && parts[0] == "createType" {
|
||||
// Tools such as qemu-img quote the value, strip quotes for comparison.
|
||||
header.create_type = match parts[1].trim_matches('"') {
|
||||
"monolithicFlat" => VMDKDiskType::MonolithicFlat,
|
||||
"twoGbMaxExtentFlat" => VMDKDiskType::TwoGbMaxExtentFlat,
|
||||
_ => VMDKDiskType::CreateTypeUnsupported,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Ok((header, last_comment_line))
|
||||
}
|
||||
|
||||
pub(crate) fn parse_extents_and_ddb(
|
||||
pub(crate) fn parse_extents(
|
||||
lines: &mut Lines<'_>,
|
||||
last_comment_line: &str,
|
||||
) -> io::Result<(VmdkDescriptorExtents, VmdkDescriptorDdb)> {
|
||||
) -> io::Result<VmdkDescriptorExtents> {
|
||||
let mut extents = VmdkDescriptorExtents::default();
|
||||
let mut ddb = VmdkDescriptorDdb::default();
|
||||
|
||||
if last_comment_line != VMDK_DESCRIPTOR_EXTENTS {
|
||||
return Err(io::Error::new(
|
||||
@@ -152,66 +173,55 @@ pub(crate) fn parse_extents_and_ddb(
|
||||
));
|
||||
}
|
||||
|
||||
let mut in_extents_section = true;
|
||||
for line in lines.by_ref() {
|
||||
if line.trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
if line.starts_with('#') {
|
||||
if line == VMDK_DESCRIPTOR_DDB || line == VMDK_DESCRIPTOR_DDB_2 {
|
||||
in_extents_section = false;
|
||||
continue;
|
||||
break;
|
||||
}
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"Expected the DDB section comment line",
|
||||
));
|
||||
}
|
||||
if in_extents_section {
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
if parts.len() == 4 || parts.len() == 5 {
|
||||
let size_in_sectors = parts[1].parse::<u64>().map_err(|_| {
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
if parts.len() == 4 || parts.len() == 5 {
|
||||
let size_in_sectors = parts[1].parse::<u64>().map_err(|_| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"VMDK extent size '{}' is not a valid sector count",
|
||||
parts[1]
|
||||
),
|
||||
)
|
||||
})?;
|
||||
let offset_in_sectors = match parts.get(4) {
|
||||
Some(offset) => offset.parse::<u64>().map_err(|_| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"VMDK extent size '{}' is not a valid sector count",
|
||||
parts[1]
|
||||
),
|
||||
format!("VMDK extent offset '{offset}' is not a valid sector count"),
|
||||
)
|
||||
})?;
|
||||
let offset_in_sectors = match parts.get(4) {
|
||||
Some(offset) => offset.parse::<u64>().map_err(|_| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("VMDK extent offset '{offset}' is not a valid sector count"),
|
||||
)
|
||||
})?,
|
||||
None => 0,
|
||||
};
|
||||
let extent = VmdkExtentHeader {
|
||||
access: parts[0].to_string(),
|
||||
size_in_sectors,
|
||||
extent_type: parts[2].to_string(),
|
||||
filename: parts[3].trim_matches('"').to_string(),
|
||||
offset_in_sectors,
|
||||
};
|
||||
extents.extents.push(extent);
|
||||
} else {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"Malformed VMDK extent line",
|
||||
));
|
||||
}
|
||||
})?,
|
||||
None => 0,
|
||||
};
|
||||
extents.extents.push(VmdkExtentHeader {
|
||||
access: parts[0].to_string(),
|
||||
size_in_sectors,
|
||||
extent_type: parts[2].to_string(),
|
||||
filename: parts[3].trim_matches('"').to_string(),
|
||||
offset_in_sectors,
|
||||
});
|
||||
} else {
|
||||
let parts: Vec<&str> = line.split('=').map(|s| s.trim()).collect();
|
||||
if parts.len() == 2 {
|
||||
ddb.entries
|
||||
.insert(parts[0].to_string(), parts[1].to_string());
|
||||
}
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"Malformed VMDK extent line",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok((extents, ddb))
|
||||
Ok(extents)
|
||||
}
|
||||
|
||||
/// Returns true when `prefix` begins with the `# Disk DescriptorFile` header.
|
||||
@@ -240,8 +250,8 @@ pub fn is_flat_vmdk(f: &mut File) -> io::Result<bool> {
|
||||
_ => return Ok(false),
|
||||
}
|
||||
|
||||
let extents = match parse_extents_and_ddb(&mut lines, last_line) {
|
||||
Ok((extents, _ddb)) => extents,
|
||||
let extents = match parse_extents(&mut lines, last_line) {
|
||||
Ok(extents) => extents,
|
||||
Err(e) if e.kind() == io::ErrorKind::InvalidData => return Ok(false),
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
@@ -265,26 +275,54 @@ mod tests {
|
||||
Ok((header, last.to_string()))
|
||||
}
|
||||
|
||||
fn parse_body(
|
||||
last_comment: &str,
|
||||
body: &str,
|
||||
) -> io::Result<(VmdkDescriptorExtents, VmdkDescriptorDdb)> {
|
||||
fn parse_body(last_comment: &str, body: &str) -> io::Result<VmdkDescriptorExtents> {
|
||||
let mut lines = body.lines();
|
||||
parse_extents_and_ddb(&mut lines, last_comment)
|
||||
parse_extents(&mut lines, last_comment)
|
||||
}
|
||||
|
||||
// Two-stage parse, as `VmdkDescriptor::new` chains it.
|
||||
fn parse_full(
|
||||
input: &str,
|
||||
) -> io::Result<(
|
||||
VmdkDescriptorHeader,
|
||||
VmdkDescriptorExtents,
|
||||
VmdkDescriptorDdb,
|
||||
)> {
|
||||
fn parse_full(input: &str) -> io::Result<(VmdkDescriptorHeader, VmdkDescriptorExtents)> {
|
||||
let mut lines = input.lines();
|
||||
let (header, last) = parse_header(&mut lines)?;
|
||||
let (extents, ddb) = parse_extents_and_ddb(&mut lines, last)?;
|
||||
Ok((header, extents, ddb))
|
||||
let extents = parse_extents(&mut lines, last)?;
|
||||
Ok((header, extents))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_is_unaffected_by_shared_file_offset() {
|
||||
use std::io::{Read, Seek, SeekFrom, Write};
|
||||
|
||||
use vmm_sys_util::tempfile::TempFile;
|
||||
|
||||
// A minimal but complete monolithicFlat descriptor.
|
||||
let descriptor_text = "# Disk DescriptorFile\n\
|
||||
version=1\n\
|
||||
createType=monolithicFlat\n\
|
||||
# Extent description\n\
|
||||
RW 2097152 FLAT \"disk-flat.vmdk\"\n\
|
||||
# The Disk Data Base\n\
|
||||
ddb.adapterType = \"ide\"\n";
|
||||
|
||||
let tmp = TempFile::new().unwrap();
|
||||
let mut file: &File = tmp.as_file();
|
||||
file.write_all(descriptor_text.as_bytes()).unwrap();
|
||||
|
||||
// Advance the shared OS file offset off zero, mimicking an earlier
|
||||
// image-type probe. `&File` is `Copy`, so this moves the same fd's
|
||||
// offset that `VmdkDescriptor::new` will see.
|
||||
file.seek(SeekFrom::Start(0)).unwrap();
|
||||
let mut scratch = [0u8; 8];
|
||||
file.read_exact(&mut scratch).unwrap();
|
||||
assert_ne!(file.stream_position().unwrap(), 0);
|
||||
|
||||
// `read_descriptor` reads positionally (anchored at offset 0), so the
|
||||
// advanced offset must not affect the parse.
|
||||
let descriptor = VmdkDescriptor::new(file, tmp.as_path()).unwrap();
|
||||
assert_eq!(descriptor.extents_list.extents.len(), 1);
|
||||
assert_eq!(
|
||||
descriptor.extents_list.extents[0].filename,
|
||||
"disk-flat.vmdk"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -294,7 +332,7 @@ mod tests {
|
||||
ddb.adapterType = \"ide\"\n\
|
||||
ddb.geometry.sectors = \"63\"\n";
|
||||
|
||||
let (extents, ddb) = parse_body("# Extent description", body).unwrap();
|
||||
let extents = parse_body("# Extent description", body).unwrap();
|
||||
|
||||
assert_eq!(extents.extents.len(), 1);
|
||||
let e = &extents.extents[0];
|
||||
@@ -302,15 +340,6 @@ mod tests {
|
||||
assert_eq!(e.size_in_sectors, 2_097_152);
|
||||
assert_eq!(e.extent_type, "FLAT");
|
||||
assert_eq!(e.filename, "disk-flat.vmdk");
|
||||
|
||||
assert_eq!(
|
||||
ddb.entries.get("ddb.adapterType").map(String::as_str),
|
||||
Some("\"ide\"")
|
||||
);
|
||||
assert_eq!(
|
||||
ddb.entries.get("ddb.geometry.sectors").map(String::as_str),
|
||||
Some("\"63\"")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -321,7 +350,7 @@ mod tests {
|
||||
# The Disk Data Base\n\
|
||||
ddb.adapterType = \"lsilogic\"\n";
|
||||
|
||||
let (extents, _ddb) = parse_body("# Extent description", body).unwrap();
|
||||
let extents = parse_body("# Extent description", body).unwrap();
|
||||
|
||||
assert_eq!(extents.extents.len(), 3);
|
||||
assert_eq!(extents.extents[0].filename, "disk-s001.vmdk");
|
||||
@@ -334,7 +363,7 @@ mod tests {
|
||||
// 5-field form: <access> <sectors> <type> <file> <offset>
|
||||
let body: &str = "RW 2097152 FLAT \"disk-flat.vmdk\" 0\n";
|
||||
|
||||
let (extents, _ddb) = parse_body("# Extent description", body).unwrap();
|
||||
let extents = parse_body("# Extent description", body).unwrap();
|
||||
|
||||
assert_eq!(extents.extents.len(), 1);
|
||||
assert_eq!(extents.extents[0].filename, "disk-flat.vmdk");
|
||||
@@ -345,7 +374,7 @@ mod tests {
|
||||
let body: &str = "RDONLY 2097152 FLAT \"ro.vmdk\"\n\
|
||||
NOACCESS 1048576 FLAT \"noaccess.vmdk\"\n";
|
||||
|
||||
let (extents, _ddb) = parse_body("# Extent description", body).unwrap();
|
||||
let extents = parse_body("# Extent description", body).unwrap();
|
||||
|
||||
assert_eq!(extents.extents.len(), 2);
|
||||
assert_eq!(extents.extents[0].access, "RDONLY");
|
||||
@@ -394,7 +423,7 @@ mod tests {
|
||||
let body: &str = "RW 2097152 FLAT \"disk-flat.vmdk\"\n\
|
||||
\n\
|
||||
# The Disk Data Base\n";
|
||||
let (extents, _ddb) = parse_body("# Extent description", body).unwrap();
|
||||
let extents = parse_body("# Extent description", body).unwrap();
|
||||
assert_eq!(extents.extents.len(), 1);
|
||||
assert_eq!(extents.extents[0].filename, "disk-flat.vmdk");
|
||||
}
|
||||
@@ -416,9 +445,6 @@ mod tests {
|
||||
|
||||
let (header, last) = parse_hdr(input).unwrap();
|
||||
|
||||
assert_eq!(header.version, 1);
|
||||
assert_eq!(header.cid, 0xffff_fffe);
|
||||
assert_eq!(header.parent_cid, 0xffff_ffff);
|
||||
assert!(matches!(header.create_type, VMDKDiskType::MonolithicFlat));
|
||||
assert_eq!(last, "# Extent description");
|
||||
}
|
||||
@@ -447,15 +473,11 @@ mod tests {
|
||||
# The Disk Data Base\n\
|
||||
ddb.adapterType = \"ide\"\n";
|
||||
|
||||
let (header, extents, ddb) = parse_full(input).unwrap();
|
||||
let (header, extents) = parse_full(input).unwrap();
|
||||
|
||||
assert!(matches!(header.create_type, VMDKDiskType::MonolithicFlat));
|
||||
assert_eq!(extents.extents.len(), 1);
|
||||
assert_eq!(extents.extents[0].access, "RW");
|
||||
assert_eq!(
|
||||
ddb.entries.get("ddb.adapterType").map(String::as_str),
|
||||
Some("\"ide\"")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -468,7 +490,7 @@ mod tests {
|
||||
RW 4192256 FLAT \"disk-s002.vmdk\"\n\
|
||||
# The Disk Data Base\n";
|
||||
|
||||
let (header, extents, _ddb) = parse_full(input).unwrap();
|
||||
let (header, extents) = parse_full(input).unwrap();
|
||||
|
||||
assert!(matches!(
|
||||
header.create_type,
|
||||
@@ -497,7 +519,7 @@ mod tests {
|
||||
ddb.virtualHWVersion = \"4\"\n\
|
||||
ddb.adapterType = \"ide\"\n";
|
||||
|
||||
let (header, extents, ddb) = parse_full(input).unwrap();
|
||||
let (header, extents) = parse_full(input).unwrap();
|
||||
|
||||
assert!(matches!(header.create_type, VMDKDiskType::MonolithicFlat));
|
||||
assert_eq!(extents.extents.len(), 1);
|
||||
@@ -505,9 +527,5 @@ mod tests {
|
||||
assert_eq!(extents.extents[0].size_in_sectors, 6_291_456);
|
||||
assert_eq!(extents.extents[0].extent_type, "FLAT");
|
||||
assert_eq!(extents.extents[0].filename, "t-flat.vmdk");
|
||||
assert_eq!(
|
||||
ddb.entries.get("ddb.adapterType").map(String::as_str),
|
||||
Some("\"ide\"")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
637
block/src/formats/vmdk/flat.rs
Normal file
637
block/src/formats/vmdk/flat.rs
Normal file
@@ -0,0 +1,637 @@
|
||||
// Copyright © 2026, Microsoft Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
//! 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;
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
use std::os::unix::io::{AsRawFd, FromRawFd, RawFd};
|
||||
use std::path::{Component, Path};
|
||||
use std::sync::Arc;
|
||||
|
||||
use log::warn;
|
||||
|
||||
use crate::formats::vmdk::descriptor::VmdkDescriptor;
|
||||
use crate::{AlignedFile, DiskTopology};
|
||||
|
||||
const VMDK_SECTOR_SIZE: u64 = 512;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum ExtentAccess {
|
||||
/// "RW": readable and writable.
|
||||
ReadWrite,
|
||||
/// "RDONLY": readable only, writes must be rejected.
|
||||
ReadOnly,
|
||||
/// "NOACCESS": cannot be accessed, reads and writes must be rejected.
|
||||
NoAccess,
|
||||
}
|
||||
|
||||
/// A single Flat VMDK extent
|
||||
///
|
||||
/// `twoGbMaxExtentFlat` images concatenate several of these to form the full
|
||||
/// virtual disk, `monolithicFlat` images have exactly one.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct VmdkExtent {
|
||||
/// Open, alignment-aware handle to this extent's data file. `None` for
|
||||
/// `NoAccess` extents, which are never opened because they cannot be
|
||||
/// accessed.
|
||||
///
|
||||
/// The handle is wrapped in an [`AlignedFile`]
|
||||
pub file: Option<AlignedFile>,
|
||||
/// Access mode declared for this extent in the descriptor.
|
||||
pub access: ExtentAccess,
|
||||
/// First virtual-disk offset (in bytes) backed by this extent.
|
||||
pub virtual_start: u64,
|
||||
/// Length (in bytes) of the virtual-disk range backed by this extent.
|
||||
pub length: u64,
|
||||
/// Starting offset (in bytes) within the backing file for this extent.
|
||||
/// Non-zero when several extents reference the same file at growing
|
||||
/// offsets (e.g. a >2GB file split under `twoGbMaxExtentFlat`).
|
||||
pub file_base_offset: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FlatVmdk {
|
||||
descriptor: Arc<VmdkDescriptor>,
|
||||
// Open handle to the VMDK descriptor file.
|
||||
descriptor_file: Arc<File>,
|
||||
// All opened data extents, in virtual-disk order.
|
||||
extents: Arc<Vec<VmdkExtent>>,
|
||||
size: u64,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
struct OpenHow {
|
||||
flags: u64,
|
||||
mode: u64,
|
||||
resolve: u64,
|
||||
}
|
||||
|
||||
// Splits an untrusted extent `filename` into its `Normal` path components for
|
||||
// the fallback walk, rejecting any `..`/`.` traversal.
|
||||
fn extent_components(filename: &str) -> io::Result<Vec<&OsStr>> {
|
||||
let mut components = Vec::new();
|
||||
for component in Path::new(filename).components() {
|
||||
match component {
|
||||
Component::Normal(name) => components.push(name),
|
||||
Component::RootDir => {}
|
||||
_ => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!(
|
||||
"VMDK extent filename '{filename}' must not contain '..' or '.' path \
|
||||
components"
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
if components.is_empty() {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("VMDK extent filename '{filename}' is empty"),
|
||||
));
|
||||
}
|
||||
Ok(components)
|
||||
}
|
||||
|
||||
// Opens a single VMDK data extent for the descriptor whose directory is
|
||||
// base_path.
|
||||
//
|
||||
// The extent name may be relative to the descriptor or an absolute path. The
|
||||
// only difference between the two is:
|
||||
// - relative -> colocated with descriptor file
|
||||
// - absolute -> the filesystem root
|
||||
// The symlink policy rejects the final component if it is a symlink (O_NOFOLLOW).
|
||||
//
|
||||
// Resolution prefers openat2(2). On kernels without it (< 5.6, ENOSYS) or
|
||||
// where it is blocked (EPERM, e.g. a seccomp filter), it falls back to a
|
||||
// per-component openat walk.
|
||||
fn open_extent(
|
||||
base_path: &str,
|
||||
filename: &str,
|
||||
writable: bool,
|
||||
direct: bool,
|
||||
) -> io::Result<AlignedFile> {
|
||||
let anchor = if Path::new(filename).is_absolute() {
|
||||
"/"
|
||||
} else {
|
||||
base_path
|
||||
};
|
||||
|
||||
let dir = OpenOptions::new()
|
||||
.read(true)
|
||||
.custom_flags(libc::O_DIRECTORY | libc::O_CLOEXEC)
|
||||
.open(anchor)?;
|
||||
|
||||
match open_extent_openat2(dir.as_raw_fd(), filename, writable, direct) {
|
||||
Ok(file) => Ok(AlignedFile::new(file, direct)),
|
||||
Err(e) if matches!(e.raw_os_error(), Some(libc::ENOSYS) | Some(libc::EPERM)) => {
|
||||
let components = extent_components(filename)?;
|
||||
open_extent_walk(dir, &components, writable, direct)
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
fn open_extent_openat2(
|
||||
dir_fd: RawFd,
|
||||
filename: &str,
|
||||
writable: bool,
|
||||
direct: bool,
|
||||
) -> io::Result<File> {
|
||||
let cname = CString::new(filename).map_err(|_| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"VMDK extent filename contains an interior NUL byte",
|
||||
)
|
||||
})?;
|
||||
|
||||
let access = if writable {
|
||||
libc::O_RDWR
|
||||
} else {
|
||||
libc::O_RDONLY
|
||||
};
|
||||
|
||||
let mut flags = access | libc::O_CLOEXEC | libc::O_NOFOLLOW;
|
||||
if direct {
|
||||
flags |= libc::O_DIRECT;
|
||||
}
|
||||
let how = OpenHow {
|
||||
flags: flags as u64,
|
||||
mode: 0,
|
||||
resolve: 0,
|
||||
};
|
||||
|
||||
// SAFETY: FFI syscall. `cname` is NUL-terminated and outlives the call,
|
||||
// `how` is a correctly sized `open_how` passed by pointer, and `dir_fd` is a
|
||||
// valid directory fd.
|
||||
let ret = unsafe {
|
||||
libc::syscall(
|
||||
libc::SYS_openat2,
|
||||
dir_fd,
|
||||
cname.as_ptr(),
|
||||
&how as *const OpenHow,
|
||||
size_of::<OpenHow>(),
|
||||
)
|
||||
};
|
||||
if ret < 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
// SAFETY: `openat2` returned a fresh descriptor we now own exclusively.
|
||||
Ok(unsafe { File::from_raw_fd(ret as RawFd) })
|
||||
}
|
||||
|
||||
fn open_extent_walk(
|
||||
mut dir: File,
|
||||
components: &[&OsStr],
|
||||
writable: bool,
|
||||
direct: bool,
|
||||
) -> io::Result<AlignedFile> {
|
||||
let last = components.len() - 1;
|
||||
for (i, name) in components.iter().enumerate() {
|
||||
let cname = CString::new(name.as_bytes()).map_err(|_| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"VMDK extent filename contains an interior NUL byte",
|
||||
)
|
||||
})?;
|
||||
|
||||
let flags = if i < last {
|
||||
libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC
|
||||
} else {
|
||||
// Final component: the extent file, opened with the declared access
|
||||
// and cache mode, and O_NOFOLLOW so it may not be a symlink either.
|
||||
let access = if writable {
|
||||
libc::O_RDWR
|
||||
} else {
|
||||
libc::O_RDONLY
|
||||
};
|
||||
let mut flags = access | libc::O_NOFOLLOW | libc::O_CLOEXEC;
|
||||
if direct {
|
||||
flags |= libc::O_DIRECT;
|
||||
}
|
||||
flags
|
||||
};
|
||||
|
||||
// SAFETY: `dir` is a valid open directory fd and `cname` is a
|
||||
// NUL-terminated C string that outlives the call.
|
||||
let fd = unsafe { libc::openat(dir.as_raw_fd(), cname.as_ptr(), flags) };
|
||||
if fd < 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
// SAFETY: `fd` is a freshly opened descriptor we now own exclusively.
|
||||
let opened = unsafe { File::from_raw_fd(fd) };
|
||||
|
||||
if i < last {
|
||||
// Reassignment drops the previous directory `File`, closing that fd.
|
||||
dir = opened;
|
||||
} else {
|
||||
return Ok(AlignedFile::new(opened, direct));
|
||||
}
|
||||
}
|
||||
|
||||
unreachable!("extent_components guarantees at least one component")
|
||||
}
|
||||
|
||||
// Builds the error returned when a sector count/offset from the (untrusted)
|
||||
// descriptor, scaled to bytes, does not fit in a u64.
|
||||
fn overflow_error(what: &str) -> io::Error {
|
||||
io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("VMDK {what} overflows a 64-bit byte count"),
|
||||
)
|
||||
}
|
||||
|
||||
impl FlatVmdk {
|
||||
/// Opens a flat VMDK image from its already-open descriptor file.
|
||||
pub fn new(file: File, path: &Path, direct: bool) -> io::Result<Self> {
|
||||
let descriptor = VmdkDescriptor::new(&file, path)?;
|
||||
|
||||
if descriptor.extents_list.extents.is_empty() {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"VMDK descriptor lists no extents",
|
||||
));
|
||||
}
|
||||
|
||||
// Open every data extent and record the virtual-disk byte range it
|
||||
// backs.
|
||||
let mut extents: Vec<VmdkExtent> =
|
||||
Vec::with_capacity(descriptor.extents_list.extents.len());
|
||||
let mut virtual_start: u64 = 0;
|
||||
for extent in &descriptor.extents_list.extents {
|
||||
// A flat extent is a fixed, pre-allocated region, a zero-sector
|
||||
// extent would back an empty virtual range that can never be read
|
||||
// or written
|
||||
if extent.size_in_sectors == 0 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"VMDK flat extent has zero size",
|
||||
));
|
||||
}
|
||||
|
||||
let length = extent
|
||||
.size_in_sectors
|
||||
.checked_mul(VMDK_SECTOR_SIZE)
|
||||
.ok_or_else(|| overflow_error("extent size"))?;
|
||||
let file_base_offset = extent
|
||||
.offset_in_sectors
|
||||
.checked_mul(VMDK_SECTOR_SIZE)
|
||||
.ok_or_else(|| overflow_error("extent file offset"))?;
|
||||
file_base_offset
|
||||
.checked_add(length)
|
||||
.ok_or_else(|| overflow_error("extent file range"))?;
|
||||
|
||||
// Open the backing file using exactly the access declared for this
|
||||
// extent. The VMDK spec defines three values:
|
||||
// "RW" -> read + write
|
||||
// "RDONLY" -> read only
|
||||
// "NOACCESS" -> not accessible, do not open the file at all
|
||||
let (access, extent_file) = match extent.access.as_str() {
|
||||
"RW" => {
|
||||
let f = open_extent(&descriptor.base_path, &extent.filename, true, direct)?;
|
||||
(ExtentAccess::ReadWrite, Some(f))
|
||||
}
|
||||
"RDONLY" => {
|
||||
let f = open_extent(&descriptor.base_path, &extent.filename, false, direct)?;
|
||||
(ExtentAccess::ReadOnly, Some(f))
|
||||
}
|
||||
"NOACCESS" => (ExtentAccess::NoAccess, None),
|
||||
other => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("unsupported VMDK extent access mode '{other}'"),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
extents.push(VmdkExtent {
|
||||
file: extent_file,
|
||||
access,
|
||||
virtual_start,
|
||||
length,
|
||||
file_base_offset,
|
||||
});
|
||||
|
||||
virtual_start = virtual_start
|
||||
.checked_add(length)
|
||||
.ok_or_else(|| overflow_error("total virtual size"))?;
|
||||
}
|
||||
|
||||
// The virtual disk size is the end offset of the last extent.
|
||||
let total_disk_size = virtual_start;
|
||||
|
||||
Ok(Self {
|
||||
descriptor: Arc::new(descriptor),
|
||||
descriptor_file: Arc::new(file),
|
||||
extents: Arc::new(extents),
|
||||
size: total_disk_size,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn virtual_block_size(&self) -> u64 {
|
||||
self.size
|
||||
}
|
||||
|
||||
/// Shared handle to the opened data extents, used to build the I/O worker.
|
||||
pub fn extents(&self) -> Arc<Vec<VmdkExtent>> {
|
||||
Arc::clone(&self.extents)
|
||||
}
|
||||
|
||||
/// Host allocation size: the sum of every opened extent file's size.
|
||||
/// `NoAccess` extents contribute 0 to the total.
|
||||
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())
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
|
||||
/// Sector/cluster geometry reported to the guest.
|
||||
pub fn topology(&self) -> DiskTopology {
|
||||
self.extents
|
||||
.iter()
|
||||
.find_map(|extent| extent.file.as_ref())
|
||||
.map(|f| {
|
||||
DiskTopology::probe(f.file()).unwrap_or_else(|_| {
|
||||
warn!("Unable to get VMDK extent topology. Using default topology");
|
||||
DiskTopology::default()
|
||||
})
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
// Expose the descriptor file's fd as the disk's representative fd.
|
||||
impl AsRawFd for FlatVmdk {
|
||||
fn as_raw_fd(&self) -> RawFd {
|
||||
self.descriptor_file.as_raw_fd()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for FlatVmdk {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
descriptor: Arc::clone(&self.descriptor),
|
||||
descriptor_file: Arc::clone(&self.descriptor_file),
|
||||
extents: Arc::clone(&self.extents),
|
||||
size: self.size,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::fs;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn extent_components_allows_bare_name() {
|
||||
// The common flat-VMDK case: a single co-located extent file.
|
||||
let comps = extent_components("disk-flat.vmdk").unwrap();
|
||||
assert_eq!(comps, [OsStr::new("disk-flat.vmdk")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extent_components_rejects_traversal_and_empty() {
|
||||
// `..`/`.` traversal and empty names are refused. (An absolute path is
|
||||
// decomposed into its Normal components, the leading `/` is skipped and
|
||||
// the caller anchors the walk at the filesystem root.)
|
||||
extent_components("../../etc/passwd").unwrap_err();
|
||||
extent_components("sub/../../escape").unwrap_err();
|
||||
extent_components("extent-1.vmdk/../../").unwrap_err();
|
||||
extent_components("./s001.vmdk").unwrap_err();
|
||||
extent_components("").unwrap_err();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extent_components_decomposes_absolute_path() {
|
||||
// A leading `/` is skipped, the remaining Normal components are walked
|
||||
// from the filesystem root by the caller.
|
||||
let comps = extent_components("/var/lib/layer.erofs").unwrap();
|
||||
assert_eq!(
|
||||
comps,
|
||||
[
|
||||
OsStr::new("var"),
|
||||
OsStr::new("lib"),
|
||||
OsStr::new("layer.erofs")
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// Opens `path` as a directory anchor fd, mirroring how `open_extent` opens
|
||||
// its anchor.
|
||||
fn open_dir(path: &Path) -> File {
|
||||
OpenOptions::new()
|
||||
.read(true)
|
||||
.custom_flags(libc::O_DIRECTORY | libc::O_CLOEXEC)
|
||||
.open(path)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
// Returns false when `openat2(2)` is unavailable.
|
||||
fn openat2_available(res: &io::Result<File>) -> bool {
|
||||
!matches!(
|
||||
res.as_ref().err().and_then(|e| e.raw_os_error()),
|
||||
Some(libc::ENOSYS) | Some(libc::EPERM)
|
||||
)
|
||||
}
|
||||
|
||||
// Opens the same anchor + `filename` with BOTH extent-open implementations
|
||||
// so a single scenario asserts they behave identically:
|
||||
fn open_both(
|
||||
base_path: &Path,
|
||||
filename: &str,
|
||||
writable: bool,
|
||||
direct: bool,
|
||||
) -> (io::Result<File>, io::Result<AlignedFile>) {
|
||||
let anchor: &Path = if Path::new(filename).is_absolute() {
|
||||
Path::new("/")
|
||||
} else {
|
||||
base_path
|
||||
};
|
||||
|
||||
let openat2_dir = open_dir(anchor);
|
||||
let openat2_res = open_extent_openat2(openat2_dir.as_raw_fd(), filename, writable, direct);
|
||||
|
||||
let walk_dir = open_dir(anchor);
|
||||
let walk_res = match extent_components(filename) {
|
||||
Ok(components) => open_extent_walk(walk_dir, &components, writable, direct),
|
||||
Err(e) => Err(e),
|
||||
};
|
||||
|
||||
(openat2_res, walk_res)
|
||||
}
|
||||
|
||||
// Asserts openat2.
|
||||
fn check_openat2(res: &io::Result<File>, expect_ok: bool) {
|
||||
if !openat2_available(res) {
|
||||
return;
|
||||
}
|
||||
assert_eq!(
|
||||
res.is_ok(),
|
||||
expect_ok,
|
||||
"openat2 result did not match expectation (expected ok = {expect_ok})"
|
||||
);
|
||||
}
|
||||
|
||||
// Asserts the per-component walk result.
|
||||
fn check_walk(res: &io::Result<AlignedFile>, expect_ok: bool) {
|
||||
assert_eq!(
|
||||
res.is_ok(),
|
||||
expect_ok,
|
||||
"walk result did not match expectation (expected ok = {expect_ok})"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_extent_opens_regular_file() {
|
||||
use vmm_sys_util::tempdir::TempDir;
|
||||
|
||||
let dir = TempDir::new_with_prefix("/tmp/vmdk-regular-test").unwrap();
|
||||
let base = dir.as_path();
|
||||
fs::write(base.join("disk-flat.vmdk"), b"data").unwrap();
|
||||
|
||||
let (openat2_res, walk_res) = open_both(base, "disk-flat.vmdk", false, false);
|
||||
check_openat2(&openat2_res, true);
|
||||
check_walk(&walk_res, true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_extent_opens_file_in_subdirectory() {
|
||||
use vmm_sys_util::tempdir::TempDir;
|
||||
|
||||
// A relative sub-path resolves beneath the descriptor directory.
|
||||
let dir = TempDir::new_with_prefix("/tmp/vmdk-subdir-test").unwrap();
|
||||
let base = dir.as_path();
|
||||
fs::create_dir(base.join("extents")).unwrap();
|
||||
fs::write(base.join("extents").join("s001.vmdk"), b"data").unwrap();
|
||||
|
||||
let (openat2_res, walk_res) = open_both(base, "extents/s001.vmdk", false, false);
|
||||
check_openat2(&openat2_res, true);
|
||||
check_walk(&walk_res, true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_extent_opens_absolute_path_within_descriptor_dir() {
|
||||
use vmm_sys_util::tempdir::TempDir;
|
||||
|
||||
let dir = TempDir::new_with_prefix("/tmp/vmdk-abs-in-test").unwrap();
|
||||
let base = dir.as_path();
|
||||
fs::write(base.join("gpt_meta_head.img"), b"data").unwrap();
|
||||
let abs = base.join("gpt_meta_head.img");
|
||||
|
||||
let (openat2_res, walk_res) = open_both(base, abs.to_str().unwrap(), false, false);
|
||||
check_openat2(&openat2_res, true);
|
||||
check_walk(&walk_res, true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_extent_opens_absolute_path_outside_descriptor_dir() {
|
||||
use vmm_sys_util::tempdir::TempDir;
|
||||
|
||||
let desc_dir = TempDir::new_with_prefix("/tmp/vmdk-desc-test").unwrap();
|
||||
let layer_dir = TempDir::new_with_prefix("/tmp/vmdk-layer-test").unwrap();
|
||||
fs::write(layer_dir.as_path().join("layer.erofs"), b"data").unwrap();
|
||||
let abs = layer_dir.as_path().join("layer.erofs");
|
||||
|
||||
let (openat2_res, walk_res) =
|
||||
open_both(desc_dir.as_path(), abs.to_str().unwrap(), false, false);
|
||||
check_openat2(&openat2_res, true);
|
||||
check_walk(&walk_res, true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_extent_rejects_symlinked_final_component_relative() {
|
||||
use std::os::unix::fs::symlink;
|
||||
|
||||
use vmm_sys_util::tempdir::TempDir;
|
||||
|
||||
// A bare-named extent that is actually a symlink to a file the guest
|
||||
// must never reach. Both implementations refuse it via O_NOFOLLOW.
|
||||
let dir = TempDir::new_with_prefix("/tmp/vmdk-symlink-test").unwrap();
|
||||
let base = dir.as_path();
|
||||
let target = base.join("target-secret");
|
||||
fs::write(&target, b"secret").unwrap();
|
||||
symlink(&target, base.join("disk-flat.vmdk")).unwrap();
|
||||
|
||||
let (openat2_res, walk_res) = open_both(base, "disk-flat.vmdk", true, false);
|
||||
check_openat2(&openat2_res, false);
|
||||
check_walk(&walk_res, false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_extent_rejects_symlinked_final_component_absolute() {
|
||||
use std::os::unix::fs::symlink;
|
||||
|
||||
use vmm_sys_util::tempdir::TempDir;
|
||||
|
||||
// Even for absolute paths, the extent file itself may not be a symlink.
|
||||
let dir = TempDir::new_with_prefix("/tmp/vmdk-abs-finalsym-test").unwrap();
|
||||
let base = dir.as_path();
|
||||
let target = base.join("target-secret");
|
||||
fs::write(&target, b"secret").unwrap();
|
||||
let link = base.join("extent-link.vmdk");
|
||||
symlink(&target, &link).unwrap();
|
||||
|
||||
let (openat2_res, walk_res) = open_both(base, link.to_str().unwrap(), true, false);
|
||||
check_openat2(&openat2_res, false);
|
||||
check_walk(&walk_res, false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_extent_follows_symlinked_intermediate_directory_relative() {
|
||||
use std::os::unix::fs::symlink;
|
||||
|
||||
use vmm_sys_util::tempdir::TempDir;
|
||||
|
||||
// A relative path may traverse a symlinked intermediate directory
|
||||
// (only the final component is guarded).
|
||||
let real = TempDir::new_with_prefix("/tmp/vmdk-rel-real-test").unwrap();
|
||||
fs::write(real.as_path().join("s001.vmdk"), b"data").unwrap();
|
||||
|
||||
let dir = TempDir::new_with_prefix("/tmp/vmdk-rel-symdir-test").unwrap();
|
||||
let base = dir.as_path();
|
||||
symlink(real.as_path(), base.join("sub")).unwrap();
|
||||
|
||||
let (openat2_res, walk_res) = open_both(base, "sub/s001.vmdk", false, false);
|
||||
check_openat2(&openat2_res, true);
|
||||
check_walk(&walk_res, true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_extent_follows_symlinked_intermediate_directory_absolute() {
|
||||
use std::os::unix::fs::symlink;
|
||||
|
||||
use vmm_sys_util::tempdir::TempDir;
|
||||
|
||||
// An absolute path may likewise traverse a symlinked intermediate
|
||||
// directory (common in container deployments).
|
||||
let real = TempDir::new_with_prefix("/tmp/vmdk-abs-realdir-test").unwrap();
|
||||
fs::write(real.as_path().join("layer.erofs"), b"data").unwrap();
|
||||
|
||||
let dir = TempDir::new_with_prefix("/tmp/vmdk-abs-linkdir-test").unwrap();
|
||||
let base = dir.as_path();
|
||||
symlink(real.as_path(), base.join("link")).unwrap();
|
||||
|
||||
let via_symlink = base.join("link").join("layer.erofs");
|
||||
let (openat2_res, walk_res) = open_both(base, via_symlink.to_str().unwrap(), false, false);
|
||||
check_openat2(&openat2_res, true);
|
||||
check_walk(&walk_res, true);
|
||||
}
|
||||
}
|
||||
@@ -8,5 +8,6 @@
|
||||
//! synchronous, extent-aware I/O.
|
||||
|
||||
mod descriptor;
|
||||
mod flat;
|
||||
|
||||
pub use descriptor::{has_descriptor_header, is_flat_vmdk};
|
||||
|
||||
Reference in New Issue
Block a user