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`
|
//! of `FLAT` extent lines, and a disk database (DDB). Only the `monolithicFlat`
|
||||||
//! and `twoGbMaxExtentFlat` create types are recognized.
|
//! and `twoGbMaxExtentFlat` create types are recognized.
|
||||||
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::os::unix::fs::FileExt;
|
use std::os::unix::fs::FileExt;
|
||||||
|
use std::path::Path;
|
||||||
use std::str::Lines;
|
use std::str::Lines;
|
||||||
|
|
||||||
use crate::AlignedFile;
|
use crate::AlignedFile;
|
||||||
@@ -33,7 +33,6 @@ pub enum VMDKDiskType {
|
|||||||
/// Flat VMDK extent line fields.
|
/// Flat VMDK extent line fields.
|
||||||
/// Format of each extent line:
|
/// Format of each extent line:
|
||||||
/// `<access> <sectors> <type> "<file>" [offset]`.
|
/// `<access> <sectors> <type> "<file>" [offset]`.
|
||||||
#[allow(dead_code)]
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct VmdkExtentHeader {
|
pub struct VmdkExtentHeader {
|
||||||
pub access: String,
|
pub access: String,
|
||||||
@@ -44,14 +43,9 @@ pub struct VmdkExtentHeader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Descriptor header fields.
|
/// Descriptor header fields.
|
||||||
#[allow(dead_code)]
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct VmdkDescriptorHeader {
|
pub struct VmdkDescriptorHeader {
|
||||||
pub version: u32,
|
|
||||||
pub cid: u32,
|
|
||||||
pub parent_cid: u32,
|
|
||||||
pub create_type: VMDKDiskType,
|
pub create_type: VMDKDiskType,
|
||||||
pub parent_filename_hint: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ordered list of extents.
|
/// Ordered list of extents.
|
||||||
@@ -59,11 +53,49 @@ pub struct VmdkDescriptorHeader {
|
|||||||
pub struct VmdkDescriptorExtents {
|
pub struct VmdkDescriptorExtents {
|
||||||
pub extents: Vec<VmdkExtentHeader>,
|
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)]
|
#[derive(Debug, Default)]
|
||||||
pub struct VmdkDescriptorDdb {
|
pub struct VmdkDescriptor {
|
||||||
pub entries: HashMap<String, String>,
|
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
|
// Read the whole descriptor into memory through an `AlignedFile` and return it
|
||||||
@@ -115,35 +147,24 @@ pub(crate) fn parse_header<'a>(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
let parts: Vec<&str> = line.split('=').map(|s| s.trim()).collect();
|
let parts: Vec<&str> = line.split('=').map(|s| s.trim()).collect();
|
||||||
if parts.len() == 2 {
|
if parts.len() == 2 && parts[0] == "createType" {
|
||||||
match parts[0] {
|
// Tools such as qemu-img quote the value, strip quotes for comparison.
|
||||||
"version" => header.version = parts[1].parse().unwrap_or(0),
|
header.create_type = match parts[1].trim_matches('"') {
|
||||||
"CID" => header.cid = u32::from_str_radix(parts[1], 16).unwrap_or(0),
|
"monolithicFlat" => VMDKDiskType::MonolithicFlat,
|
||||||
"parentCID" => header.parent_cid = u32::from_str_radix(parts[1], 16).unwrap_or(0),
|
"twoGbMaxExtentFlat" => VMDKDiskType::TwoGbMaxExtentFlat,
|
||||||
"createType" => {
|
_ => VMDKDiskType::CreateTypeUnsupported,
|
||||||
// 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(),
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok((header, last_comment_line))
|
Ok((header, last_comment_line))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn parse_extents_and_ddb(
|
pub(crate) fn parse_extents(
|
||||||
lines: &mut Lines<'_>,
|
lines: &mut Lines<'_>,
|
||||||
last_comment_line: &str,
|
last_comment_line: &str,
|
||||||
) -> io::Result<(VmdkDescriptorExtents, VmdkDescriptorDdb)> {
|
) -> io::Result<VmdkDescriptorExtents> {
|
||||||
let mut extents = VmdkDescriptorExtents::default();
|
let mut extents = VmdkDescriptorExtents::default();
|
||||||
let mut ddb = VmdkDescriptorDdb::default();
|
|
||||||
|
|
||||||
if last_comment_line != VMDK_DESCRIPTOR_EXTENTS {
|
if last_comment_line != VMDK_DESCRIPTOR_EXTENTS {
|
||||||
return Err(io::Error::new(
|
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() {
|
for line in lines.by_ref() {
|
||||||
if line.trim().is_empty() {
|
if line.trim().is_empty() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if line.starts_with('#') {
|
if line.starts_with('#') {
|
||||||
if line == VMDK_DESCRIPTOR_DDB || line == VMDK_DESCRIPTOR_DDB_2 {
|
if line == VMDK_DESCRIPTOR_DDB || line == VMDK_DESCRIPTOR_DDB_2 {
|
||||||
in_extents_section = false;
|
break;
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
return Err(io::Error::new(
|
return Err(io::Error::new(
|
||||||
io::ErrorKind::InvalidData,
|
io::ErrorKind::InvalidData,
|
||||||
"Expected the DDB section comment line",
|
"Expected the DDB section comment line",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if in_extents_section {
|
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
if parts.len() == 4 || parts.len() == 5 {
|
||||||
if parts.len() == 4 || parts.len() == 5 {
|
let size_in_sectors = parts[1].parse::<u64>().map_err(|_| {
|
||||||
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::Error::new(
|
||||||
io::ErrorKind::InvalidData,
|
io::ErrorKind::InvalidData,
|
||||||
format!(
|
format!("VMDK extent offset '{offset}' is not a valid sector count"),
|
||||||
"VMDK extent size '{}' is not a valid sector count",
|
|
||||||
parts[1]
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
})?;
|
})?,
|
||||||
let offset_in_sectors = match parts.get(4) {
|
None => 0,
|
||||||
Some(offset) => offset.parse::<u64>().map_err(|_| {
|
};
|
||||||
io::Error::new(
|
extents.extents.push(VmdkExtentHeader {
|
||||||
io::ErrorKind::InvalidData,
|
access: parts[0].to_string(),
|
||||||
format!("VMDK extent offset '{offset}' is not a valid sector count"),
|
size_in_sectors,
|
||||||
)
|
extent_type: parts[2].to_string(),
|
||||||
})?,
|
filename: parts[3].trim_matches('"').to_string(),
|
||||||
None => 0,
|
offset_in_sectors,
|
||||||
};
|
});
|
||||||
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",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
let parts: Vec<&str> = line.split('=').map(|s| s.trim()).collect();
|
return Err(io::Error::new(
|
||||||
if parts.len() == 2 {
|
io::ErrorKind::InvalidData,
|
||||||
ddb.entries
|
"Malformed VMDK extent line",
|
||||||
.insert(parts[0].to_string(), parts[1].to_string());
|
));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok((extents, ddb))
|
Ok(extents)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true when `prefix` begins with the `# Disk DescriptorFile` header.
|
/// 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),
|
_ => return Ok(false),
|
||||||
}
|
}
|
||||||
|
|
||||||
let extents = match parse_extents_and_ddb(&mut lines, last_line) {
|
let extents = match parse_extents(&mut lines, last_line) {
|
||||||
Ok((extents, _ddb)) => extents,
|
Ok(extents) => extents,
|
||||||
Err(e) if e.kind() == io::ErrorKind::InvalidData => return Ok(false),
|
Err(e) if e.kind() == io::ErrorKind::InvalidData => return Ok(false),
|
||||||
Err(e) => return Err(e),
|
Err(e) => return Err(e),
|
||||||
};
|
};
|
||||||
@@ -265,26 +275,54 @@ mod tests {
|
|||||||
Ok((header, last.to_string()))
|
Ok((header, last.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_body(
|
fn parse_body(last_comment: &str, body: &str) -> io::Result<VmdkDescriptorExtents> {
|
||||||
last_comment: &str,
|
|
||||||
body: &str,
|
|
||||||
) -> io::Result<(VmdkDescriptorExtents, VmdkDescriptorDdb)> {
|
|
||||||
let mut lines = body.lines();
|
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.
|
// Two-stage parse, as `VmdkDescriptor::new` chains it.
|
||||||
fn parse_full(
|
fn parse_full(input: &str) -> io::Result<(VmdkDescriptorHeader, VmdkDescriptorExtents)> {
|
||||||
input: &str,
|
|
||||||
) -> io::Result<(
|
|
||||||
VmdkDescriptorHeader,
|
|
||||||
VmdkDescriptorExtents,
|
|
||||||
VmdkDescriptorDdb,
|
|
||||||
)> {
|
|
||||||
let mut lines = input.lines();
|
let mut lines = input.lines();
|
||||||
let (header, last) = parse_header(&mut lines)?;
|
let (header, last) = parse_header(&mut lines)?;
|
||||||
let (extents, ddb) = parse_extents_and_ddb(&mut lines, last)?;
|
let extents = parse_extents(&mut lines, last)?;
|
||||||
Ok((header, extents, ddb))
|
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]
|
#[test]
|
||||||
@@ -294,7 +332,7 @@ mod tests {
|
|||||||
ddb.adapterType = \"ide\"\n\
|
ddb.adapterType = \"ide\"\n\
|
||||||
ddb.geometry.sectors = \"63\"\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);
|
assert_eq!(extents.extents.len(), 1);
|
||||||
let e = &extents.extents[0];
|
let e = &extents.extents[0];
|
||||||
@@ -302,15 +340,6 @@ mod tests {
|
|||||||
assert_eq!(e.size_in_sectors, 2_097_152);
|
assert_eq!(e.size_in_sectors, 2_097_152);
|
||||||
assert_eq!(e.extent_type, "FLAT");
|
assert_eq!(e.extent_type, "FLAT");
|
||||||
assert_eq!(e.filename, "disk-flat.vmdk");
|
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]
|
#[test]
|
||||||
@@ -321,7 +350,7 @@ mod tests {
|
|||||||
# The Disk Data Base\n\
|
# The Disk Data Base\n\
|
||||||
ddb.adapterType = \"lsilogic\"\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.len(), 3);
|
||||||
assert_eq!(extents.extents[0].filename, "disk-s001.vmdk");
|
assert_eq!(extents.extents[0].filename, "disk-s001.vmdk");
|
||||||
@@ -334,7 +363,7 @@ mod tests {
|
|||||||
// 5-field form: <access> <sectors> <type> <file> <offset>
|
// 5-field form: <access> <sectors> <type> <file> <offset>
|
||||||
let body: &str = "RW 2097152 FLAT \"disk-flat.vmdk\" 0\n";
|
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.len(), 1);
|
||||||
assert_eq!(extents.extents[0].filename, "disk-flat.vmdk");
|
assert_eq!(extents.extents[0].filename, "disk-flat.vmdk");
|
||||||
@@ -345,7 +374,7 @@ mod tests {
|
|||||||
let body: &str = "RDONLY 2097152 FLAT \"ro.vmdk\"\n\
|
let body: &str = "RDONLY 2097152 FLAT \"ro.vmdk\"\n\
|
||||||
NOACCESS 1048576 FLAT \"noaccess.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.len(), 2);
|
||||||
assert_eq!(extents.extents[0].access, "RDONLY");
|
assert_eq!(extents.extents[0].access, "RDONLY");
|
||||||
@@ -394,7 +423,7 @@ mod tests {
|
|||||||
let body: &str = "RW 2097152 FLAT \"disk-flat.vmdk\"\n\
|
let body: &str = "RW 2097152 FLAT \"disk-flat.vmdk\"\n\
|
||||||
\n\
|
\n\
|
||||||
# The Disk Data Base\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.len(), 1);
|
||||||
assert_eq!(extents.extents[0].filename, "disk-flat.vmdk");
|
assert_eq!(extents.extents[0].filename, "disk-flat.vmdk");
|
||||||
}
|
}
|
||||||
@@ -416,9 +445,6 @@ mod tests {
|
|||||||
|
|
||||||
let (header, last) = parse_hdr(input).unwrap();
|
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!(matches!(header.create_type, VMDKDiskType::MonolithicFlat));
|
||||||
assert_eq!(last, "# Extent description");
|
assert_eq!(last, "# Extent description");
|
||||||
}
|
}
|
||||||
@@ -447,15 +473,11 @@ mod tests {
|
|||||||
# The Disk Data Base\n\
|
# The Disk Data Base\n\
|
||||||
ddb.adapterType = \"ide\"\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!(matches!(header.create_type, VMDKDiskType::MonolithicFlat));
|
||||||
assert_eq!(extents.extents.len(), 1);
|
assert_eq!(extents.extents.len(), 1);
|
||||||
assert_eq!(extents.extents[0].access, "RW");
|
assert_eq!(extents.extents[0].access, "RW");
|
||||||
assert_eq!(
|
|
||||||
ddb.entries.get("ddb.adapterType").map(String::as_str),
|
|
||||||
Some("\"ide\"")
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -468,7 +490,7 @@ mod tests {
|
|||||||
RW 4192256 FLAT \"disk-s002.vmdk\"\n\
|
RW 4192256 FLAT \"disk-s002.vmdk\"\n\
|
||||||
# The Disk Data Base\n";
|
# The Disk Data Base\n";
|
||||||
|
|
||||||
let (header, extents, _ddb) = parse_full(input).unwrap();
|
let (header, extents) = parse_full(input).unwrap();
|
||||||
|
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
header.create_type,
|
header.create_type,
|
||||||
@@ -497,7 +519,7 @@ mod tests {
|
|||||||
ddb.virtualHWVersion = \"4\"\n\
|
ddb.virtualHWVersion = \"4\"\n\
|
||||||
ddb.adapterType = \"ide\"\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!(matches!(header.create_type, VMDKDiskType::MonolithicFlat));
|
||||||
assert_eq!(extents.extents.len(), 1);
|
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].size_in_sectors, 6_291_456);
|
||||||
assert_eq!(extents.extents[0].extent_type, "FLAT");
|
assert_eq!(extents.extents[0].extent_type, "FLAT");
|
||||||
assert_eq!(extents.extents[0].filename, "t-flat.vmdk");
|
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.
|
//! synchronous, extent-aware I/O.
|
||||||
|
|
||||||
mod descriptor;
|
mod descriptor;
|
||||||
|
mod flat;
|
||||||
|
|
||||||
pub use descriptor::{has_descriptor_header, is_flat_vmdk};
|
pub use descriptor::{has_descriptor_header, is_flat_vmdk};
|
||||||
|
|||||||
@@ -722,6 +722,7 @@ fn vmm_thread_rules(
|
|||||||
#[cfg(target_arch = "x86_64")]
|
#[cfg(target_arch = "x86_64")]
|
||||||
(libc::SYS_open, vec![]),
|
(libc::SYS_open, vec![]),
|
||||||
(libc::SYS_openat, vec![]),
|
(libc::SYS_openat, vec![]),
|
||||||
|
(libc::SYS_openat2, vec![]),
|
||||||
(libc::SYS_pipe2, vec![]),
|
(libc::SYS_pipe2, vec![]),
|
||||||
#[cfg(target_arch = "x86_64")]
|
#[cfg(target_arch = "x86_64")]
|
||||||
(libc::SYS_poll, vec![]),
|
(libc::SYS_poll, vec![]),
|
||||||
|
|||||||
Reference in New Issue
Block a user