diff --git a/Cargo.lock b/Cargo.lock index fde8abf90c..e6931d0a3d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/disk/Cargo.toml b/disk/Cargo.toml index 8c49b45dc8..da386f2687 100644 --- a/disk/Cargo.toml +++ b/disk/Cargo.toml @@ -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] diff --git a/disk/src/composite.rs b/disk/src/composite.rs index efa5e1de83..5b88caa221 100644 --- a/disk/src/composite.rs +++ b/disk/src/composite.rs @@ -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), + 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 for Error { + fn from(e: gpt::Error) -> Self { + Self::GptError(e) + } +} + pub type Result = std::result::Result; #[derive(Debug)] @@ -86,11 +141,14 @@ fn range_intersection(a: &Range, b: &Range) -> Range { } } +/// 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) -> Result { @@ -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, + 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 = 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> { + 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")); + } } diff --git a/disk/src/disk.rs b/disk/src/disk.rs index 792075fe6d..ef6ee177cd 100644 --- a/disk/src/disk.rs +++ b/disk/src/disk.rs @@ -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}; diff --git a/disk/src/gpt.rs b/disk/src/gpt.rs new file mode 100644 index 0000000000..39e6d590c0 --- /dev/null +++ b/disk/src/gpt.rs @@ -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); + } +} diff --git a/src/main.rs b/src/main.rs index ada571501e..d188dcbb40 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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::>()[..] { + 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::, _>>()?; + + 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),