Add methods to create composite disk image.

This can be used like:
crosvm create_composite composite.img partiton1:partition1.img partition2:partition2.img

BUG=b:190503456
TEST=cargo test

Cq-Depend: chromium:2982777, chromium:3008399
Change-Id: I31a9afe1e5f1e2a850ce1f892122150bcf3441b4
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/crosvm/+/2972869
Tested-by: kokoro <noreply+kokoro@google.com>
Reviewed-by: Keiichi Watanabe <keiichiw@chromium.org>
Reviewed-by: Chirantan Ekbote <chirantan@chromium.org>
Commit-Queue: Keiichi Watanabe <keiichiw@chromium.org>
Auto-Submit: Andrew Walbran <qwandor@google.com>
This commit is contained in:
Andrew Walbran 2021-06-17 18:12:14 +01:00 committed by Commit Bot
parent da1f83c0b3
commit fb7c108f15
6 changed files with 919 additions and 10 deletions

47
Cargo.lock generated
View file

@ -151,6 +151,12 @@ version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cloudabi"
version = "0.0.3"
@ -168,6 +174,15 @@ dependencies = [
"data_model",
]
[[package]]
name = "crc32fast"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81156fece84ab6a9f2afdb109ce3ae577e42b1228441eded99bd77f627953b1a"
dependencies = [
"cfg-if 1.0.0",
]
[[package]]
name = "cros_async"
version = "0.1.0"
@ -340,6 +355,7 @@ version = "0.1.0"
dependencies = [
"async-trait",
"base",
"crc32fast",
"cros_async",
"data_model",
"futures",
@ -349,6 +365,7 @@ dependencies = [
"remain",
"tempfile",
"thiserror",
"uuid",
"vm_memory",
]
@ -455,7 +472,7 @@ version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "224c17cf54ffe7e084343f25c7f2881a399bea69862ecaf5bc97f0f6586ba0dc"
dependencies = [
"cfg-if",
"cfg-if 0.1.10",
"log",
"managed",
"num-traits",
@ -471,6 +488,17 @@ dependencies = [
"unicode-width",
]
[[package]]
name = "getrandom"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753"
dependencies = [
"cfg-if 1.0.0",
"libc",
"wasi",
]
[[package]]
name = "gpu_display"
version = "0.1.0"
@ -635,7 +663,7 @@ version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4fcce5fa49cc693c312001daf1d13411c4a5283796bac1084299ea3e567113f"
dependencies = [
"cfg-if",
"cfg-if 0.1.10",
]
[[package]]
@ -1138,6 +1166,15 @@ dependencies = [
"usb_sys",
]
[[package]]
name = "uuid"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
dependencies = [
"getrandom",
]
[[package]]
name = "vfio_sys"
version = "0.1.0"
@ -1227,6 +1264,12 @@ dependencies = [
"tempfile",
]
[[package]]
name = "wasi"
version = "0.10.2+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
[[package]]
name = "winapi"
version = "0.3.9"

View file

@ -8,19 +8,21 @@ edition = "2018"
path = "src/disk.rs"
[features]
composite-disk = ["protos", "protobuf"]
composite-disk = ["crc32fast", "protos", "protobuf", "uuid"]
[dependencies]
async-trait = "0.1.36"
base = { path = "../base" }
crc32fast = { version = "1.2.1", optional = true }
libc = "*"
protobuf = { version = "2.3", optional = true }
remain = "*"
tempfile = "*"
thiserror = "*"
uuid = { version = "0.8.2", features = ["v4"], optional = true }
cros_async = { path = "../cros_async" }
data_model = { path = "../data_model" }
protos = { path = "../protos", optional = true }
protos = { path = "../protos", features = ["composite-disk"], optional = true }
vm_memory = { path = "../vm_memory" }
[dependencies.futures]

View file

@ -3,31 +3,64 @@
// found in the LICENSE file.
use std::cmp::{max, min};
use std::collections::HashSet;
use std::convert::TryInto;
use std::fmt::{self, Display};
use std::fs::{File, OpenOptions};
use std::io::{self, ErrorKind, Read, Seek, SeekFrom};
use std::io::{self, ErrorKind, Read, Seek, SeekFrom, Write};
use std::ops::Range;
use std::path::{Path, PathBuf};
use crate::{create_disk_file, DiskFile, DiskGetLen, ImageType};
use base::{
AsRawDescriptors, FileAllocate, FileReadWriteAtVolatile, FileSetLen, FileSync, PunchHole,
RawDescriptor, WriteZeroesAt,
};
use crc32fast::Hasher;
use data_model::VolatileSlice;
use protos::cdisk_spec;
use protobuf::Message;
use protos::cdisk_spec::{self, ComponentDisk, CompositeDisk, ReadWriteCapability};
use remain::sorted;
use uuid::Uuid;
use crate::gpt::{
self, write_gpt_header, write_protective_mbr, GptPartitionEntry, GPT_BEGINNING_SIZE,
GPT_END_SIZE, GPT_HEADER_SIZE, GPT_NUM_PARTITIONS, GPT_PARTITION_ENTRY_SIZE, SECTOR_SIZE,
};
use crate::{create_disk_file, DiskFile, DiskGetLen, ImageType};
/// The amount of padding needed between the last partition entry and the first partition, to align
/// the partition appropriately. The two sectors are for the MBR and the GPT header.
const PARTITION_ALIGNMENT_SIZE: usize = GPT_BEGINNING_SIZE as usize
- 2 * SECTOR_SIZE as usize
- GPT_NUM_PARTITIONS as usize * GPT_PARTITION_ENTRY_SIZE as usize;
const HEADER_PADDING_LENGTH: usize = SECTOR_SIZE as usize - GPT_HEADER_SIZE as usize;
// Keep all partitions 4k aligned for performance.
const PARTITION_SIZE_SHIFT: u8 = 12;
// Keep the disk size a multiple of 64k for crosvm's virtio_blk driver.
const DISK_SIZE_SHIFT: u8 = 16;
// From https://en.wikipedia.org/wiki/GUID_Partition_Table#Partition_type_GUIDs.
const LINUX_FILESYSTEM_GUID: Uuid = Uuid::from_u128(0x0FC63DAF_8483_4772_8E79_3D69D8477DE4);
const EFI_SYSTEM_PARTITION_GUID: Uuid = Uuid::from_u128(0xC12A7328_F81F_11D2_BA4B_00A0C93EC93B);
#[sorted]
#[derive(Debug)]
pub enum Error {
DiskError(Box<crate::Error>),
DuplicatePartitionLabel(String),
GptError(gpt::Error),
InvalidMagicHeader,
InvalidPath(PathBuf),
InvalidProto(protobuf::ProtobufError),
InvalidSpecification(String),
NoImageFiles(PartitionInfo),
OpenFile(io::Error, String),
ReadSpecificationError(io::Error),
UnalignedReadWrite(PartitionInfo),
UnknownVersion(u64),
UnsupportedComponent(ImageType),
WriteHeader(io::Error),
WriteProto(protobuf::ProtobufError),
}
impl Display for Error {
@ -38,17 +71,39 @@ impl Display for Error {
#[sorted]
match self {
DiskError(e) => write!(f, "failed to use underlying disk: \"{}\"", e),
DuplicatePartitionLabel(label) => {
write!(f, "duplicate GPT partition label \"{}\"", label)
}
GptError(e) => write!(f, "failed to write GPT header: \"{}\"", e),
InvalidMagicHeader => write!(f, "invalid magic header for composite disk format"),
InvalidPath(path) => write!(f, "invalid partition path {:?}", path),
InvalidProto(e) => write!(f, "failed to parse specification proto: \"{}\"", e),
InvalidSpecification(s) => write!(f, "invalid specification: \"{}\"", s),
NoImageFiles(partition) => write!(f, "no image files for partition {:?}", partition),
OpenFile(e, p) => write!(f, "failed to open component file \"{}\": \"{}\"", p, e),
ReadSpecificationError(e) => write!(f, "failed to read specification: \"{}\"", e),
UnalignedReadWrite(partition) => write!(
f,
"Read-write partition {:?} size is not a multiple of {}.",
partition,
1 << PARTITION_SIZE_SHIFT
),
UnknownVersion(v) => write!(f, "unknown version {} in specification", v),
UnsupportedComponent(c) => write!(f, "unsupported component disk type \"{:?}\"", c),
WriteHeader(e) => write!(f, "failed to write composite disk header: \"{}\"", e),
WriteProto(e) => write!(f, "failed to write specification proto: \"{}\"", e),
}
}
}
impl std::error::Error for Error {}
impl From<gpt::Error> for Error {
fn from(e: gpt::Error) -> Self {
Self::GptError(e)
}
}
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug)]
@ -86,11 +141,14 @@ fn range_intersection(a: &Range<u64>, b: &Range<u64>) -> Range<u64> {
}
}
/// The version of the composite disk format supported by this implementation.
const COMPOSITE_DISK_VERSION: u64 = 1;
/// A magic string placed at the beginning of a composite disk file to identify it.
pub static CDISK_MAGIC: &str = "composite_disk\x1d";
pub const CDISK_MAGIC: &str = "composite_disk\x1d";
/// The length of the CDISK_MAGIC string. Created explicitly as a static constant so that it is
/// possible to create a character array of the same length.
pub const CDISK_MAGIC_LEN: usize = 15;
pub const CDISK_MAGIC_LEN: usize = CDISK_MAGIC.len();
impl CompositeDiskFile {
fn new(mut disks: Vec<ComponentDiskPart>) -> Result<CompositeDiskFile> {
@ -128,7 +186,7 @@ impl CompositeDiskFile {
}
let proto: cdisk_spec::CompositeDisk =
protobuf::parse_from_reader(&mut file).map_err(Error::InvalidProto)?;
if proto.get_version() != 1 {
if proto.get_version() != COMPOSITE_DISK_VERSION {
return Err(Error::UnknownVersion(proto.get_version()));
}
let mut open_options = OpenOptions::new();
@ -339,9 +397,280 @@ impl AsRawDescriptors for CompositeDiskFile {
}
}
/// Information about a single image file to be included in a partition.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PartitionFileInfo {
pub path: PathBuf,
pub size: u64,
}
/// Information about a partition to create, including the set of image files which make it up.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PartitionInfo {
pub label: String,
pub files: Vec<PartitionFileInfo>,
pub partition_type: ImagePartitionType,
pub writable: bool,
}
/// Round `val` up to the next multiple of 2**`align_log`.
fn align_to_power_of_2(val: u64, align_log: u8) -> u64 {
let align = 1 << align_log;
((val + (align - 1)) / align) * align
}
impl PartitionInfo {
fn aligned_size(&self) -> u64 {
align_to_power_of_2(
self.files.iter().map(|file| file.size).sum(),
PARTITION_SIZE_SHIFT,
)
}
}
/// The type of partition.
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum ImagePartitionType {
LinuxFilesystem,
EfiSystemPartition,
}
impl ImagePartitionType {
fn guid(self) -> Uuid {
match self {
Self::LinuxFilesystem => LINUX_FILESYSTEM_GUID,
Self::EfiSystemPartition => EFI_SYSTEM_PARTITION_GUID,
}
}
}
/// Write protective MBR and primary GPT table.
fn write_beginning(
file: &mut impl Write,
disk_guid: Uuid,
partitions: &[u8],
partition_entries_crc32: u32,
secondary_table_offset: u64,
disk_size: u64,
) -> Result<()> {
// Write the protective MBR to the first sector.
write_protective_mbr(file, disk_size)?;
// Write the GPT header, and pad out to the end of the sector.
write_gpt_header(
file,
disk_guid,
partition_entries_crc32,
secondary_table_offset,
false,
)?;
file.write_all(&[0; HEADER_PADDING_LENGTH])
.map_err(Error::WriteHeader)?;
// Write partition entries, including unused ones.
file.write_all(partitions).map_err(Error::WriteHeader)?;
// Write zeroes to align the first partition appropriately.
file.write_all(&[0; PARTITION_ALIGNMENT_SIZE])
.map_err(Error::WriteHeader)?;
Ok(())
}
/// Write secondary GPT table.
fn write_end(
file: &mut impl Write,
disk_guid: Uuid,
partitions: &[u8],
partition_entries_crc32: u32,
secondary_table_offset: u64,
disk_size: u64,
) -> Result<()> {
// Write partition entries, including unused ones.
file.write_all(partitions).map_err(Error::WriteHeader)?;
// Write the GPT header, and pad out to the end of the sector.
write_gpt_header(
file,
disk_guid,
partition_entries_crc32,
secondary_table_offset,
true,
)?;
file.write_all(&[0; HEADER_PADDING_LENGTH])
.map_err(Error::WriteHeader)?;
// Pad out to the aligned disk size.
let used_disk_size = secondary_table_offset + GPT_END_SIZE;
let padding = disk_size - used_disk_size;
file.write_all(&vec![0; padding as usize])
.map_err(Error::WriteHeader)?;
Ok(())
}
/// Create the `GptPartitionEntry` for the given partition.
fn create_gpt_entry(partition: &PartitionInfo, offset: u64) -> GptPartitionEntry {
let mut partition_name: Vec<u16> = partition.label.encode_utf16().collect();
partition_name.resize(36, 0);
GptPartitionEntry {
partition_type_guid: partition.partition_type.guid(),
unique_partition_guid: Uuid::new_v4(),
first_lba: offset / SECTOR_SIZE,
last_lba: (offset + partition.aligned_size()) / SECTOR_SIZE - 1,
attributes: 0,
partition_name: partition_name.try_into().unwrap(),
}
}
/// Create one or more `ComponentDisk` proto messages for the given partition.
fn create_component_disks(
partition: &PartitionInfo,
offset: u64,
header_path: &str,
) -> Result<Vec<ComponentDisk>> {
let aligned_size = partition.aligned_size();
if partition.files.is_empty() {
return Err(Error::NoImageFiles(partition.to_owned()));
}
let mut file_size_sum = 0;
let mut component_disks = vec![];
for file in &partition.files {
component_disks.push(ComponentDisk {
offset: offset + file_size_sum,
file_path: file
.path
.to_str()
.ok_or_else(|| Error::InvalidPath(file.path.to_owned()))?
.to_string(),
read_write_capability: if partition.writable {
ReadWriteCapability::READ_WRITE
} else {
ReadWriteCapability::READ_ONLY
},
..ComponentDisk::new()
});
file_size_sum += file.size;
}
if file_size_sum != aligned_size {
if partition.writable {
return Err(Error::UnalignedReadWrite(partition.to_owned()));
} else {
// Fill in the gap by reusing the header file, because we know it is always bigger
// than the alignment size (i.e. GPT_BEGINNING_SIZE > 1 << PARTITION_SIZE_SHIFT).
component_disks.push(ComponentDisk {
offset: offset + file_size_sum,
file_path: header_path.to_owned(),
read_write_capability: ReadWriteCapability::READ_ONLY,
..ComponentDisk::new()
});
}
}
Ok(component_disks)
}
/// Create a new composite disk image containing the given partitions, and write it out to the given
/// files.
pub fn create_composite_disk(
partitions: &[PartitionInfo],
header_path: &Path,
header_file: &mut File,
footer_path: &Path,
footer_file: &mut File,
output_composite: &mut File,
) -> Result<()> {
let header_path = header_path
.to_str()
.ok_or_else(|| Error::InvalidPath(header_path.to_owned()))?
.to_string();
let footer_path = footer_path
.to_str()
.ok_or_else(|| Error::InvalidPath(footer_path.to_owned()))?
.to_string();
let mut composite_proto = CompositeDisk::new();
composite_proto.version = COMPOSITE_DISK_VERSION;
composite_proto.component_disks.push(ComponentDisk {
file_path: header_path.clone(),
offset: 0,
read_write_capability: ReadWriteCapability::READ_ONLY,
..ComponentDisk::new()
});
// Write partitions to a temporary buffer so that we can calculate the CRC, and construct the
// ComponentDisk proto messages at the same time.
let mut partitions_buffer =
[0u8; GPT_NUM_PARTITIONS as usize * GPT_PARTITION_ENTRY_SIZE as usize];
let mut writer: &mut [u8] = &mut partitions_buffer;
let mut next_disk_offset = GPT_BEGINNING_SIZE;
let mut labels = HashSet::with_capacity(partitions.len());
for partition in partitions {
let gpt_entry = create_gpt_entry(partition, next_disk_offset);
if !labels.insert(gpt_entry.partition_name) {
return Err(Error::DuplicatePartitionLabel(partition.label.clone()));
}
gpt_entry.write_bytes(&mut writer)?;
for component_disk in create_component_disks(partition, next_disk_offset, &header_path)? {
composite_proto.component_disks.push(component_disk);
}
next_disk_offset += partition.aligned_size();
}
let secondary_table_offset = next_disk_offset;
let disk_size = align_to_power_of_2(secondary_table_offset + GPT_END_SIZE, DISK_SIZE_SHIFT);
composite_proto.component_disks.push(ComponentDisk {
file_path: footer_path,
offset: secondary_table_offset,
read_write_capability: ReadWriteCapability::READ_ONLY,
..ComponentDisk::new()
});
// Calculate CRC32 of partition entries.
let mut hasher = Hasher::new();
hasher.update(&partitions_buffer);
let partition_entries_crc32 = hasher.finalize();
let disk_guid = Uuid::new_v4();
write_beginning(
header_file,
disk_guid,
&partitions_buffer,
partition_entries_crc32,
secondary_table_offset,
disk_size,
)?;
write_end(
footer_file,
disk_guid,
&partitions_buffer,
partition_entries_crc32,
secondary_table_offset,
disk_size,
)?;
composite_proto.length = disk_size;
output_composite
.write_all(CDISK_MAGIC.as_bytes())
.map_err(Error::WriteHeader)?;
composite_proto
.write_to_writer(output_composite)
.map_err(Error::WriteProto)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::matches;
use base::AsRawDescriptor;
use data_model::VolatileMemory;
use tempfile::tempfile;
@ -561,4 +890,151 @@ mod tests {
}
assert!(input_memory.iter().eq(output_memory.iter()));
}
#[test]
fn beginning_size() {
let mut buffer = vec![];
let partitions = [0u8; GPT_NUM_PARTITIONS as usize * GPT_PARTITION_ENTRY_SIZE as usize];
let disk_size = 1000 * SECTOR_SIZE;
write_beginning(
&mut buffer,
Uuid::from_u128(0x12345678_1234_5678_abcd_12345678abcd),
&partitions,
42,
disk_size - GPT_END_SIZE,
disk_size,
)
.unwrap();
assert_eq!(buffer.len(), GPT_BEGINNING_SIZE as usize);
}
#[test]
fn end_size() {
let mut buffer = vec![];
let partitions = [0u8; GPT_NUM_PARTITIONS as usize * GPT_PARTITION_ENTRY_SIZE as usize];
let disk_size = 1000 * SECTOR_SIZE;
write_end(
&mut buffer,
Uuid::from_u128(0x12345678_1234_5678_abcd_12345678abcd),
&partitions,
42,
disk_size - GPT_END_SIZE,
disk_size,
)
.unwrap();
assert_eq!(buffer.len(), GPT_END_SIZE as usize);
}
#[test]
fn end_size_with_padding() {
let mut buffer = vec![];
let partitions = [0u8; GPT_NUM_PARTITIONS as usize * GPT_PARTITION_ENTRY_SIZE as usize];
let disk_size = 1000 * SECTOR_SIZE;
let padding = 3 * SECTOR_SIZE;
write_end(
&mut buffer,
Uuid::from_u128(0x12345678_1234_5678_abcd_12345678abcd),
&partitions,
42,
disk_size - GPT_END_SIZE - padding,
disk_size,
)
.unwrap();
assert_eq!(buffer.len(), GPT_END_SIZE as usize + padding as usize);
}
/// Creates a composite disk image with no partitions.
#[test]
fn create_composite_disk_empty() {
let mut header_image = tempfile().unwrap();
let mut footer_image = tempfile().unwrap();
let mut composite_image = tempfile().unwrap();
create_composite_disk(
&[],
Path::new("/header_path.img"),
&mut header_image,
Path::new("/footer_path.img"),
&mut footer_image,
&mut composite_image,
)
.unwrap();
}
/// Creates a composite disk image with two partitions.
#[test]
fn create_composite_disk_success() {
let mut header_image = tempfile().unwrap();
let mut footer_image = tempfile().unwrap();
let mut composite_image = tempfile().unwrap();
create_composite_disk(
&[
PartitionInfo {
label: "partition1".to_string(),
files: vec![PartitionFileInfo {
path: "/partition1.img".to_string().into(),
size: 0,
}],
partition_type: ImagePartitionType::LinuxFilesystem,
writable: false,
},
PartitionInfo {
label: "partition2".to_string(),
files: vec![PartitionFileInfo {
path: "/partition2.img".to_string().into(),
size: 0,
}],
partition_type: ImagePartitionType::LinuxFilesystem,
writable: true,
},
],
Path::new("/header_path.img"),
&mut header_image,
Path::new("/footer_path.img"),
&mut footer_image,
&mut composite_image,
)
.unwrap();
}
/// Attempts to create a composite disk image with two partitions with the same label.
#[test]
fn create_composite_disk_duplicate_label() {
let mut header_image = tempfile().unwrap();
let mut footer_image = tempfile().unwrap();
let mut composite_image = tempfile().unwrap();
let result = create_composite_disk(
&[
PartitionInfo {
label: "label".to_string(),
files: vec![PartitionFileInfo {
path: "/partition1.img".to_string().into(),
size: 0,
}],
partition_type: ImagePartitionType::LinuxFilesystem,
writable: false,
},
PartitionInfo {
label: "label".to_string(),
files: vec![PartitionFileInfo {
path: "/partition2.img".to_string().into(),
size: 0,
}],
partition_type: ImagePartitionType::LinuxFilesystem,
writable: true,
},
],
Path::new("/header_path.img"),
&mut header_image,
Path::new("/footer_path.img"),
&mut footer_image,
&mut composite_image,
);
assert!(matches!(result, Err(Error::DuplicatePartitionLabel(label)) if label == "label"));
}
}

View file

@ -26,6 +26,15 @@ pub use qcow::{QcowFile, QCOW_MAGIC};
mod composite;
#[cfg(feature = "composite-disk")]
use composite::{CompositeDiskFile, CDISK_MAGIC, CDISK_MAGIC_LEN};
#[cfg(feature = "composite-disk")]
mod gpt;
#[cfg(feature = "composite-disk")]
pub use composite::{
create_composite_disk, Error as CompositeError, ImagePartitionType, PartitionFileInfo,
PartitionInfo,
};
#[cfg(feature = "composite-disk")]
pub use gpt::Error as GptError;
mod android_sparse;
use android_sparse::{AndroidSparse, SPARSE_HEADER_MAGIC};

275
disk/src/gpt.rs Normal file
View file

@ -0,0 +1,275 @@
// Copyright 2021 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//! Functions for writing GUID Partition Tables for use in a composite disk image.
use std::convert::TryInto;
use std::io::{self, Write};
use std::num::TryFromIntError;
use crc32fast::Hasher;
use remain::sorted;
use thiserror::Error as ThisError;
use uuid::Uuid;
/// The size in bytes of a disk sector (also called a block).
pub const SECTOR_SIZE: u64 = 1 << 9;
/// The size in bytes on an MBR partition entry.
const MBR_PARTITION_ENTRY_SIZE: usize = 16;
/// The size in bytes of a GPT header.
pub const GPT_HEADER_SIZE: u32 = 92;
/// The number of partition entries in the GPT, which is the maximum number of partitions which are
/// supported.
pub const GPT_NUM_PARTITIONS: u32 = 128;
/// The size in bytes of a single GPT partition entry.
pub const GPT_PARTITION_ENTRY_SIZE: u32 = 128;
/// The size in bytes of everything before the first partition: i.e. the MBR, GPT header and GPT
/// partition entries.
pub const GPT_BEGINNING_SIZE: u64 = SECTOR_SIZE * 40;
/// The size in bytes of everything after the last partition: i.e. the GPT partition entries and GPT
/// footer.
pub const GPT_END_SIZE: u64 = SECTOR_SIZE * 33;
#[sorted]
#[derive(ThisError, Debug)]
pub enum Error {
/// The disk size was invalid (too large).
#[error("invalid disk size: {0}")]
InvalidDiskSize(TryFromIntError),
/// There was an error writing data to one of the image files.
#[error("failed to write data: {0}")]
WritingData(io::Error),
}
/// Write a protective MBR for a disk of the given total size (in bytes).
///
/// This should be written at the start of the disk, before the GPT header. It is one `SECTOR_SIZE`
/// long.
pub fn write_protective_mbr(file: &mut impl Write, disk_size: u64) -> Result<(), Error> {
// Bootstrap code
file.write_all(&[0; 446]).map_err(Error::WritingData)?;
// Partition status
file.write_all(&[0x00]).map_err(Error::WritingData)?;
// Begin CHS
file.write_all(&[0; 3]).map_err(Error::WritingData)?;
// Partition type
file.write_all(&[0xEE]).map_err(Error::WritingData)?;
// End CHS
file.write_all(&[0; 3]).map_err(Error::WritingData)?;
let first_lba: u32 = 1;
file.write_all(&first_lba.to_le_bytes())
.map_err(Error::WritingData)?;
let number_of_sectors: u32 = (disk_size / SECTOR_SIZE)
.try_into()
.map_err(Error::InvalidDiskSize)?;
file.write_all(&number_of_sectors.to_le_bytes())
.map_err(Error::WritingData)?;
// Three more empty partitions
file.write_all(&[0; MBR_PARTITION_ENTRY_SIZE * 3])
.map_err(Error::WritingData)?;
// Boot signature
file.write_all(&[0x55, 0xAA]).map_err(Error::WritingData)?;
Ok(())
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
struct GptHeader {
signature: [u8; 8],
revision: [u8; 4],
header_size: u32,
header_crc32: u32,
current_lba: u64,
backup_lba: u64,
first_usable_lba: u64,
last_usable_lba: u64,
disk_guid: Uuid,
partition_entries_lba: u64,
num_partition_entries: u32,
partition_entry_size: u32,
partition_entries_crc32: u32,
}
impl GptHeader {
fn write_bytes(&self, out: &mut impl Write) -> Result<(), Error> {
out.write_all(&self.signature).map_err(Error::WritingData)?;
out.write_all(&self.revision).map_err(Error::WritingData)?;
out.write_all(&self.header_size.to_le_bytes())
.map_err(Error::WritingData)?;
out.write_all(&self.header_crc32.to_le_bytes())
.map_err(Error::WritingData)?;
// Reserved
out.write_all(&[0; 4]).map_err(Error::WritingData)?;
out.write_all(&self.current_lba.to_le_bytes())
.map_err(Error::WritingData)?;
out.write_all(&self.backup_lba.to_le_bytes())
.map_err(Error::WritingData)?;
out.write_all(&self.first_usable_lba.to_le_bytes())
.map_err(Error::WritingData)?;
out.write_all(&self.last_usable_lba.to_le_bytes())
.map_err(Error::WritingData)?;
// GUID is mixed-endian for some reason, so we can't just use `Uuid::as_bytes()`.
write_guid(out, self.disk_guid).map_err(Error::WritingData)?;
out.write_all(&self.partition_entries_lba.to_le_bytes())
.map_err(Error::WritingData)?;
out.write_all(&self.num_partition_entries.to_le_bytes())
.map_err(Error::WritingData)?;
out.write_all(&self.partition_entry_size.to_le_bytes())
.map_err(Error::WritingData)?;
out.write_all(&self.partition_entries_crc32.to_le_bytes())
.map_err(Error::WritingData)?;
Ok(())
}
}
/// Write a GPT header for the disk.
///
/// It may either be a primary header (which should go at LBA 1) or a secondary header (which should
/// go at the end of the disk).
pub fn write_gpt_header(
out: &mut impl Write,
disk_guid: Uuid,
partition_entries_crc32: u32,
secondary_table_offset: u64,
secondary: bool,
) -> Result<(), Error> {
let primary_header_lba = 1;
let secondary_header_lba = (secondary_table_offset + GPT_END_SIZE) / SECTOR_SIZE - 1;
let mut gpt_header = GptHeader {
signature: *b"EFI PART",
revision: [0, 0, 1, 0],
header_size: GPT_HEADER_SIZE,
current_lba: if secondary {
secondary_header_lba
} else {
primary_header_lba
},
backup_lba: if secondary {
primary_header_lba
} else {
secondary_header_lba
},
first_usable_lba: GPT_BEGINNING_SIZE / SECTOR_SIZE,
last_usable_lba: secondary_table_offset / SECTOR_SIZE - 1,
disk_guid,
partition_entries_lba: 2,
num_partition_entries: GPT_NUM_PARTITIONS,
partition_entry_size: GPT_PARTITION_ENTRY_SIZE,
partition_entries_crc32,
header_crc32: 0,
};
// Write once to a temporary buffer to calculate the CRC.
let mut header_without_crc = [0u8; GPT_HEADER_SIZE as usize];
gpt_header.write_bytes(&mut &mut header_without_crc[..])?;
let mut hasher = Hasher::new();
hasher.update(&header_without_crc);
gpt_header.header_crc32 = hasher.finalize();
gpt_header.write_bytes(out)?;
Ok(())
}
/// A GPT entry for a particular partition.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GptPartitionEntry {
pub partition_type_guid: Uuid,
pub unique_partition_guid: Uuid,
pub first_lba: u64,
pub last_lba: u64,
pub attributes: u64,
/// UTF-16LE
pub partition_name: [u16; 36],
}
// This is implemented manually because `Default` isn't implemented in the standard library for
// arrays of more than 32 elements. If that gets implemented (now than const generics are in) then
// we can derive this instead.
impl Default for GptPartitionEntry {
fn default() -> Self {
Self {
partition_type_guid: Default::default(),
unique_partition_guid: Default::default(),
first_lba: 0,
last_lba: 0,
attributes: 0,
partition_name: [0; 36],
}
}
}
impl GptPartitionEntry {
/// Write out the partition table entry. It will take
/// `GPT_PARTITION_ENTRY_SIZE` bytes.
pub fn write_bytes(&self, out: &mut impl Write) -> Result<(), Error> {
write_guid(out, self.partition_type_guid).map_err(Error::WritingData)?;
write_guid(out, self.unique_partition_guid).map_err(Error::WritingData)?;
out.write_all(&self.first_lba.to_le_bytes())
.map_err(Error::WritingData)?;
out.write_all(&self.last_lba.to_le_bytes())
.map_err(Error::WritingData)?;
out.write_all(&self.attributes.to_le_bytes())
.map_err(Error::WritingData)?;
for code_unit in &self.partition_name {
out.write_all(&code_unit.to_le_bytes())
.map_err(Error::WritingData)?;
}
Ok(())
}
}
/// Write a UUID in the mixed-endian format which GPT uses for GUIDs.
fn write_guid(out: &mut impl Write, guid: Uuid) -> Result<(), io::Error> {
let guid_fields = guid.as_fields();
out.write_all(&guid_fields.0.to_le_bytes())?;
out.write_all(&guid_fields.1.to_le_bytes())?;
out.write_all(&guid_fields.2.to_le_bytes())?;
out.write_all(guid_fields.3)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn protective_mbr_size() {
let mut buffer = vec![];
write_protective_mbr(&mut buffer, 1000 * SECTOR_SIZE).unwrap();
assert_eq!(buffer.len(), SECTOR_SIZE as usize);
}
#[test]
fn header_size() {
let mut buffer = vec![];
write_gpt_header(
&mut buffer,
Uuid::from_u128(0x12345678_1234_5678_abcd_12345678abcd),
42,
1000 * SECTOR_SIZE,
false,
)
.unwrap();
assert_eq!(buffer.len(), GPT_HEADER_SIZE as usize);
}
#[test]
fn partition_entry_size() {
let mut buffer = vec![];
GptPartitionEntry::default()
.write_bytes(&mut buffer)
.unwrap();
assert_eq!(buffer.len(), GPT_PARTITION_ENTRY_SIZE as usize);
}
}

View file

@ -36,6 +36,10 @@ use devices::ProtectionType;
#[cfg(feature = "audio")]
use devices::{Ac97Backend, Ac97Parameters};
use disk::QcowFile;
#[cfg(feature = "composite-disk")]
use disk::{
create_composite_disk, create_disk_file, ImagePartitionType, PartitionFileInfo, PartitionInfo,
};
use vm_control::{
client::{
do_modify_battery, do_usb_attach, do_usb_detach, do_usb_list, handle_request, vms_request,
@ -2239,6 +2243,102 @@ fn balloon_stats(mut args: std::env::Args) -> std::result::Result<(), ()> {
}
}
#[cfg(feature = "composite-disk")]
fn create_composite(mut args: std::env::Args) -> std::result::Result<(), ()> {
if args.len() < 1 {
print_help("crosvm create_composite", "PATH [LABEL:PARTITION]..", &[]);
println!("Creates a new composite disk image containing the given partition images");
return Err(());
}
let composite_image_path = args.next().unwrap();
let header_path = format!("{}.header", composite_image_path);
let footer_path = format!("{}.footer", composite_image_path);
let mut composite_image_file = OpenOptions::new()
.create(true)
.read(true)
.write(true)
.truncate(true)
.open(&composite_image_path)
.map_err(|e| {
error!(
"Failed opening composite disk image file at '{}': {}",
composite_image_path, e
);
})?;
let mut header_file = OpenOptions::new()
.create(true)
.read(true)
.write(true)
.truncate(true)
.open(&header_path)
.map_err(|e| {
error!(
"Failed opening header image file at '{}': {}",
header_path, e
);
})?;
let mut footer_file = OpenOptions::new()
.create(true)
.read(true)
.write(true)
.truncate(true)
.open(&footer_path)
.map_err(|e| {
error!(
"Failed opening footer image file at '{}': {}",
footer_path, e
);
})?;
let partitions = args
.into_iter()
.map(|partition_arg| {
if let [label, path] = partition_arg.split(":").collect::<Vec<_>>()[..] {
let partition_file = File::open(path)
.map_err(|e| error!("Failed to open partition image: {}", e))?;
let size = create_disk_file(partition_file)
.map_err(|e| error!("Failed to create DiskFile instance: {}", e))?
.get_len()
.map_err(|e| error!("Failed to get length of partition image: {}", e))?;
Ok(PartitionInfo {
label: label.to_owned(),
files: vec![PartitionFileInfo {
path: Path::new(path).to_owned(),
size,
}],
partition_type: ImagePartitionType::LinuxFilesystem,
writable: false,
})
} else {
error!(
"Must specify label and path for partition '{}', like LABEL:PATH",
partition_arg
);
Err(())
}
})
.collect::<Result<Vec<_>, _>>()?;
create_composite_disk(
&partitions,
&PathBuf::from(header_path),
&mut header_file,
&PathBuf::from(footer_path),
&mut footer_file,
&mut composite_image_file,
)
.map_err(|e| {
error!(
"Failed to create composite disk image at '{}': {}",
composite_image_path, e
);
})?;
Ok(())
}
fn create_qcow2(args: std::env::Args) -> std::result::Result<(), ()> {
let arguments = [
Argument::positional("PATH", "where to create the qcow2 image"),
@ -2459,6 +2559,8 @@ fn print_usage() {
println!(" balloon - Set balloon size of the crosvm instance.");
println!(" balloon_stats - Prints virtio balloon statistics.");
println!(" battery - Modify battery.");
#[cfg(feature = "composite-disk")]
println!(" create_composite - Create a new composite disk image file.");
println!(" create_qcow2 - Create a new qcow2 disk image file.");
println!(" disk - Manage attached virtual disk devices.");
println!(" resume - Resumes the crosvm instance.");
@ -2526,6 +2628,8 @@ fn crosvm_main() -> std::result::Result<(), ()> {
Some("run") => run_vm(args),
Some("balloon") => balloon_vms(args),
Some("balloon_stats") => balloon_stats(args),
#[cfg(feature = "composite-disk")]
Some("create_composite") => create_composite(args),
Some("create_qcow2") => create_qcow2(args),
Some("disk") => disk_cmd(args),
Some("usb") => modify_usb(args),