summary refs log tree commit diff
diff options
context:
space:
mode:
authorCody Schuffelen <schuffelen@google.com>2019-07-08 16:14:24 -0700
committerCommit Bot <commit-bot@chromium.org>2019-10-03 00:59:14 +0000
commitf9b035d50c431c2a544ad1cb91ca5121165ae7aa (patch)
treec1b4a9127d8d24502cff87210ccff82bbcb0f2ec
parentb5237bbcf074eb30cf368a138c0835081e747d71 (diff)
downloadcrosvm-f9b035d50c431c2a544ad1cb91ca5121165ae7aa.tar
crosvm-f9b035d50c431c2a544ad1cb91ca5121165ae7aa.tar.gz
crosvm-f9b035d50c431c2a544ad1cb91ca5121165ae7aa.tar.bz2
crosvm-f9b035d50c431c2a544ad1cb91ca5121165ae7aa.tar.lz
crosvm-f9b035d50c431c2a544ad1cb91ca5121165ae7aa.tar.xz
crosvm-f9b035d50c431c2a544ad1cb91ca5121165ae7aa.tar.zst
crosvm-f9b035d50c431c2a544ad1cb91ca5121165ae7aa.zip
Support composite disks.
This adds a new disk file type next to raw files and qcow images that
represent an indirection to further raw disk files. The disk file
itself is a proto file with references to file paths for other disks to
open and their virtual offsets and lengths.

The intention is to make it easy to assemble a single virtual hard disk
out of several distinct partition files. In the particular case of
Cuttlefish running Android in a VM, this is relevant as the Android
build system distributes partitions as separate raw files. While the
simple solution is to pass each partition as a separate raw disk, some
functionality (like the bootloader) assumes there is a partition table
with multiple distinct partitions on a single disk.

Implementing composite disk support in the VMM bridges this gap through
supporting the general-purpose case of a disk built out of multiple
component files.

If desired, this can be extended to support qcow files to support
unusual configurations like a mixed qcow/raw disk.

Enabled with the "composite-disk" feature.

Bug: b/133432409
Change-Id: I2b0c47d92fab13b5dc0ca5a960c7cfd2b7145b87
Signed-off-by: Cody Schuffelen <schuffelen@google.com>
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/crosvm/+/1667767
Reviewed-by: Daniel Verkamp <dverkamp@chromium.org>
Commit-Queue: Daniel Verkamp <dverkamp@chromium.org>
Tested-by: kokoro <noreply+kokoro@google.com>
-rw-r--r--Cargo.lock3
-rw-r--r--Cargo.toml1
-rw-r--r--disk/Cargo.toml6
-rw-r--r--disk/src/composite.rs566
-rw-r--r--disk/src/disk.rs35
-rw-r--r--protos/Cargo.toml1
-rw-r--r--protos/build.rs2
-rw-r--r--protos/src/cdisk_spec.proto18
-rw-r--r--protos/src/lib.rs3
-rw-r--r--qcow_utils/src/qcow_utils.rs1
10 files changed, 636 insertions, 0 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 8017a34..b20bee3 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -171,7 +171,10 @@ dependencies = [
 name = "disk"
 version = "0.1.0"
 dependencies = [
+ "data_model 0.1.0",
  "libc 0.2.44 (registry+https://github.com/rust-lang/crates.io-index)",
+ "protobuf 2.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "protos 0.1.0",
  "qcow 0.1.0",
  "remain 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "sys_util 0.1.0",
diff --git a/Cargo.toml b/Cargo.toml
index 13a3047..bd90dcf 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -37,6 +37,7 @@ tpm = ["devices/tpm"]
 wl-dmabuf = ["devices/wl-dmabuf", "gpu_buffer", "resources/wl-dmabuf"]
 x = ["devices/x"]
 virtio-gpu-next = ["gpu_renderer/virtio-gpu-next"]
+composite-disk = ["protos/composite-disk", "protobuf", "disk/composite-disk"]
 
 [dependencies]
 arch = { path = "arch" }
diff --git a/disk/Cargo.toml b/disk/Cargo.toml
index c26cf4b..0847932 100644
--- a/disk/Cargo.toml
+++ b/disk/Cargo.toml
@@ -7,8 +7,14 @@ edition = "2018"
 [lib]
 path = "src/disk.rs"
 
+[features]
+composite-disk = ["data_model", "protos", "protobuf"]
+
 [dependencies]
 libc = "*"
+protobuf = { version = "2.3", optional = true }
 remain = "*"
+data_model = { path = "../data_model", optional = true }
+protos = { path = "../protos", optional = true }
 qcow = { path = "../qcow" }
 sys_util = { path = "../sys_util" }
diff --git a/disk/src/composite.rs b/disk/src/composite.rs
new file mode 100644
index 0000000..5d6e5a2
--- /dev/null
+++ b/disk/src/composite.rs
@@ -0,0 +1,566 @@
+// Copyright 2019 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.
+
+use std::cmp::{max, min};
+use std::convert::TryFrom;
+use std::fmt::{self, Display};
+use std::fs::{File, OpenOptions};
+use std::io::{self, ErrorKind, Read, Seek, SeekFrom};
+use std::ops::Range;
+use std::os::unix::io::RawFd;
+
+use crate::{create_disk_file, DiskFile, ImageType};
+use data_model::VolatileSlice;
+use protos::cdisk_spec;
+use remain::sorted;
+use sys_util::{AsRawFds, FileReadWriteVolatile, FileSetLen, FileSync, PunchHole, WriteZeroes};
+
+#[sorted]
+#[derive(Debug)]
+pub enum Error {
+    DiskError(Box<crate::Error>),
+    InvalidMagicHeader,
+    InvalidProto(protobuf::ProtobufError),
+    InvalidSpecification(String),
+    OpenFile(io::Error),
+    ReadSpecificationError(io::Error),
+    UnknownVersion(u64),
+    UnsupportedComponent(ImageType),
+}
+
+impl Display for Error {
+    #[remain::check]
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        use self::Error::*;
+
+        #[sorted]
+        match self {
+            DiskError(e) => write!(f, "failed to use underlying disk: \"{}\"", e),
+            InvalidMagicHeader => write!(f, "invalid magic header for composite disk format"),
+            InvalidProto(e) => write!(f, "failed to parse specification proto: \"{}\"", e),
+            InvalidSpecification(s) => write!(f, "invalid specification: \"{}\"", s),
+            OpenFile(e) => write!(f, "failed to open component file: \"{}\"", e),
+            ReadSpecificationError(e) => write!(f, "failed to read specification: \"{}\"", e),
+            UnknownVersion(v) => write!(f, "unknown version {} in specification", v),
+            UnsupportedComponent(c) => write!(f, "unsupported component disk type \"{:?}\"", c),
+        }
+    }
+}
+
+pub type Result<T> = std::result::Result<T, Error>;
+
+struct ComponentDiskPart {
+    file: Box<dyn DiskFile>,
+    offset: u64,
+    length: u64,
+}
+
+impl ComponentDiskPart {
+    fn range(&self) -> Range<u64> {
+        self.offset..(self.offset + self.length)
+    }
+}
+
+/// Represents a composite virtual disk made out of multiple component files. This is described on
+/// disk by a protocol buffer file that lists out the component file locations and their offsets
+/// and lengths on the virtual disk. The spaces covered by the component disks must be contiguous
+/// and not overlapping.
+pub struct CompositeDiskFile {
+    component_disks: Vec<ComponentDiskPart>,
+    cursor_location: u64,
+}
+
+fn ranges_overlap(a: &Range<u64>, b: &Range<u64>) -> bool {
+    // essentially !range_intersection(a, b).is_empty(), but that's experimental
+    let intersection = range_intersection(a, b);
+    intersection.start < intersection.end
+}
+
+fn range_intersection(a: &Range<u64>, b: &Range<u64>) -> Range<u64> {
+    Range {
+        start: max(a.start, b.start),
+        end: min(a.end, b.end),
+    }
+}
+
+/// A magic string placed at the beginning of a composite disk file to identify it.
+pub static 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;
+
+impl CompositeDiskFile {
+    fn new(mut disks: Vec<ComponentDiskPart>) -> Result<CompositeDiskFile> {
+        disks.sort_by(|d1, d2| d1.offset.cmp(&d2.offset));
+        let contiguous_err = disks
+            .windows(2)
+            .map(|s| {
+                if s[0].offset == s[1].offset {
+                    let text = format!("Two disks at offset {}", s[0].offset);
+                    Err(Error::InvalidSpecification(text))
+                } else {
+                    Ok(())
+                }
+            })
+            .find(|r| r.is_err());
+        if let Some(Err(e)) = contiguous_err {
+            return Err(e);
+        }
+        Ok(CompositeDiskFile {
+            component_disks: disks,
+            cursor_location: 0,
+        })
+    }
+
+    /// Set up a composite disk by reading the specification from a file. The file must consist of
+    /// the CDISK_MAGIC string followed by one binary instance of the CompositeDisk protocol
+    /// buffer. Returns an error if it could not read the file or if the specification was invalid.
+    pub fn from_file(mut file: File) -> Result<CompositeDiskFile> {
+        file.seek(SeekFrom::Start(0))
+            .map_err(Error::ReadSpecificationError)?;
+        let mut magic_space = [0u8; CDISK_MAGIC_LEN];
+        file.read_exact(&mut magic_space[..])
+            .map_err(Error::ReadSpecificationError)?;
+        if magic_space != CDISK_MAGIC.as_bytes() {
+            return Err(Error::InvalidMagicHeader);
+        }
+        let proto: cdisk_spec::CompositeDisk =
+            protobuf::parse_from_reader(&mut file).map_err(Error::InvalidProto)?;
+        if proto.get_version() != 1 {
+            return Err(Error::UnknownVersion(proto.get_version()));
+        }
+        let mut open_options = OpenOptions::new();
+        open_options.read(true);
+        let mut disks: Vec<ComponentDiskPart> = proto
+            .get_component_disks()
+            .into_iter()
+            .map(|disk| {
+                open_options.write(
+                    disk.get_read_write_capability() == cdisk_spec::ReadWriteCapability::READ_WRITE,
+                );
+                let file = open_options
+                    .open(disk.get_file_path())
+                    .map_err(Error::OpenFile)?;
+                Ok(ComponentDiskPart {
+                    file: create_disk_file(file).map_err(|e| Error::DiskError(Box::new(e)))?,
+                    offset: disk.get_offset(),
+                    length: 0, // Assigned later
+                })
+            })
+            .collect::<Result<Vec<ComponentDiskPart>>>()?;
+        disks.sort_by(|d1, d2| d1.offset.cmp(&d2.offset));
+        for i in 0..(disks.len() - 1) {
+            let length = disks[i + 1].offset - disks[i].offset;
+            if length == 0 {
+                let text = format!("Two disks at offset {}", disks[i].offset);
+                return Err(Error::InvalidSpecification(text));
+            }
+            if let Some(disk) = disks.get_mut(i) {
+                disk.length = length;
+            } else {
+                let text = format!("Unable to set disk length {}", length);
+                return Err(Error::InvalidSpecification(text));
+            }
+        }
+        let num_disks = disks.len();
+        if let Some(last_disk) = disks.get_mut(num_disks - 1) {
+            if proto.get_length() <= last_disk.offset {
+                let text = format!(
+                    "Full size of disk doesn't match last offset. {} <= {}",
+                    proto.get_length(),
+                    last_disk.offset
+                );
+                return Err(Error::InvalidSpecification(text));
+            }
+            last_disk.length = proto.get_length() - last_disk.offset;
+        } else {
+            let text = format!(
+                "Unable to set last disk length to end at {}",
+                proto.get_length()
+            );
+            return Err(Error::InvalidSpecification(text));
+        }
+
+        CompositeDiskFile::new(disks)
+    }
+
+    fn length(&self) -> u64 {
+        if let Some(disk) = self.component_disks.last() {
+            disk.offset + disk.length
+        } else {
+            0
+        }
+    }
+
+    fn disk_at_offset<'a>(&'a mut self, offset: u64) -> io::Result<&'a mut ComponentDiskPart> {
+        self.component_disks
+            .iter_mut()
+            .find(|disk| disk.range().contains(&offset))
+            .ok_or(io::Error::new(
+                ErrorKind::InvalidData,
+                format!("no disk at offset {}", offset),
+            ))
+    }
+
+    fn disks_in_range<'a>(&'a mut self, range: &Range<u64>) -> Vec<&'a mut ComponentDiskPart> {
+        self.component_disks
+            .iter_mut()
+            .filter(|disk| ranges_overlap(&disk.range(), range))
+            .collect()
+    }
+}
+
+impl FileSetLen for CompositeDiskFile {
+    fn set_len(&self, _len: u64) -> io::Result<()> {
+        Err(io::Error::new(ErrorKind::Other, "unsupported operation"))
+    }
+}
+
+impl FileSync for CompositeDiskFile {
+    fn fsync(&mut self) -> io::Result<()> {
+        for disk in self.component_disks.iter_mut() {
+            disk.file.fsync()?;
+        }
+        Ok(())
+    }
+}
+
+// Implements Read and Write targeting volatile storage for composite disks.
+//
+// Note that reads and writes will return early if crossing component disk boundaries.
+// This is allowed by the read and write specifications, which only say read and write
+// have to return how many bytes were actually read or written. Use read_exact_volatile
+// or write_all_volatile to make sure all bytes are received/transmitted.
+//
+// If one of the component disks does a partial read or write, that also gets passed
+// transparently to the parent.
+impl FileReadWriteVolatile for CompositeDiskFile {
+    fn read_volatile(&mut self, slice: VolatileSlice) -> io::Result<usize> {
+        let cursor_location = self.cursor_location;
+        let disk = self.disk_at_offset(cursor_location)?;
+        disk.file
+            .seek(SeekFrom::Start(cursor_location - disk.offset))?;
+        let subslice = if cursor_location + slice.size() > disk.offset + disk.length {
+            let new_size = disk.offset + disk.length - cursor_location;
+            slice
+                .sub_slice(0, new_size)
+                .map_err(|e| io::Error::new(ErrorKind::InvalidData, format!("{:?}", e)))?
+        } else {
+            slice
+        };
+        let result = disk.file.read_volatile(subslice);
+        if let Ok(size) = result {
+            self.cursor_location += size as u64;
+        }
+        result
+    }
+    fn write_volatile(&mut self, slice: VolatileSlice) -> io::Result<usize> {
+        let cursor_location = self.cursor_location;
+        let disk = self.disk_at_offset(cursor_location)?;
+        disk.file
+            .seek(SeekFrom::Start(cursor_location - disk.offset))?;
+        let subslice = if cursor_location + slice.size() > disk.offset + disk.length {
+            let new_size = disk.offset + disk.length - cursor_location;
+            slice
+                .sub_slice(0, new_size)
+                .map_err(|e| io::Error::new(ErrorKind::InvalidData, format!("{:?}", e)))?
+        } else {
+            slice
+        };
+        let result = disk.file.write_volatile(subslice);
+        if let Ok(size) = result {
+            self.cursor_location += size as u64;
+        }
+        result
+    }
+}
+
+impl PunchHole for CompositeDiskFile {
+    fn punch_hole(&mut self, offset: u64, length: u64) -> io::Result<()> {
+        let range = offset..(offset + length);
+        let disks = self.disks_in_range(&range);
+        for disk in disks {
+            let intersection = range_intersection(&range, &disk.range());
+            if intersection.start >= intersection.end {
+                continue;
+            }
+            let result = disk.file.punch_hole(
+                intersection.start - disk.offset,
+                intersection.end - intersection.start,
+            );
+            if result.is_err() {
+                return result;
+            }
+        }
+        Ok(())
+    }
+}
+
+impl Seek for CompositeDiskFile {
+    fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
+        let cursor_location = match pos {
+            SeekFrom::Start(offset) => Ok(offset),
+            SeekFrom::End(offset) => u64::try_from(self.length() as i64 + offset),
+            SeekFrom::Current(offset) => u64::try_from(self.cursor_location as i64 + offset),
+        }
+        .map_err(|e| io::Error::new(ErrorKind::InvalidData, e))?;
+        self.cursor_location = cursor_location;
+        Ok(cursor_location)
+    }
+}
+
+impl WriteZeroes for CompositeDiskFile {
+    fn write_zeroes(&mut self, length: usize) -> io::Result<usize> {
+        let cursor_location = self.cursor_location;
+        let disk = self.disk_at_offset(cursor_location)?;
+        disk.file
+            .seek(SeekFrom::Start(cursor_location - disk.offset))?;
+        let new_length = if cursor_location + length as u64 > disk.offset + disk.length {
+            (disk.offset + disk.length - cursor_location) as usize
+        } else {
+            length
+        };
+        let result = disk.file.write_zeroes(new_length);
+        if let Ok(size) = result {
+            self.cursor_location += size as u64;
+        }
+        result
+    }
+}
+
+impl AsRawFds for CompositeDiskFile {
+    fn as_raw_fds(&self) -> Vec<RawFd> {
+        self.component_disks
+            .iter()
+            .map(|d| d.file.as_raw_fds())
+            .flatten()
+            .collect()
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use data_model::VolatileMemory;
+    use std::os::unix::io::AsRawFd;
+    use sys_util::SharedMemory;
+
+    #[test]
+    fn block_duplicate_offset_disks() {
+        let file1: File = SharedMemory::new(None).unwrap().into();
+        let file2: File = SharedMemory::new(None).unwrap().into();
+        let disk_part1 = ComponentDiskPart {
+            file: Box::new(file1),
+            offset: 0,
+            length: 100,
+        };
+        let disk_part2 = ComponentDiskPart {
+            file: Box::new(file2),
+            offset: 0,
+            length: 100,
+        };
+        assert!(CompositeDiskFile::new(vec![disk_part1, disk_part2]).is_err());
+    }
+
+    #[test]
+    fn seek_to_end() {
+        let file1: File = SharedMemory::new(None).unwrap().into();
+        let file2: File = SharedMemory::new(None).unwrap().into();
+        let disk_part1 = ComponentDiskPart {
+            file: Box::new(file1),
+            offset: 0,
+            length: 100,
+        };
+        let disk_part2 = ComponentDiskPart {
+            file: Box::new(file2),
+            offset: 100,
+            length: 100,
+        };
+        let mut composite = CompositeDiskFile::new(vec![disk_part1, disk_part2]).unwrap();
+        let location = composite.seek(SeekFrom::End(0)).unwrap();
+        assert_eq!(location, 200);
+    }
+
+    #[test]
+    fn single_file_passthrough() {
+        let file: File = SharedMemory::new(None).unwrap().into();
+        let disk_part = ComponentDiskPart {
+            file: Box::new(file),
+            offset: 0,
+            length: 100,
+        };
+        let mut composite = CompositeDiskFile::new(vec![disk_part]).unwrap();
+        let mut input_memory = [55u8; 5];
+        let input_volatile_memory = &mut input_memory[..];
+        composite
+            .write_all_volatile(input_volatile_memory.get_slice(0, 5).unwrap())
+            .unwrap();
+        composite.seek(SeekFrom::Start(0)).unwrap();
+        let mut output_memory = [0u8; 5];
+        let output_volatile_memory = &mut output_memory[..];
+        composite
+            .read_exact_volatile(output_volatile_memory.get_slice(0, 5).unwrap())
+            .unwrap();
+        assert_eq!(input_memory, output_memory);
+    }
+
+    #[test]
+    fn triple_file_fds() {
+        let file1: File = SharedMemory::new(None).unwrap().into();
+        let file2: File = SharedMemory::new(None).unwrap().into();
+        let file3: File = SharedMemory::new(None).unwrap().into();
+        let mut in_fds = vec![file1.as_raw_fd(), file2.as_raw_fd(), file3.as_raw_fd()];
+        in_fds.sort();
+        let disk_part1 = ComponentDiskPart {
+            file: Box::new(file1),
+            offset: 0,
+            length: 100,
+        };
+        let disk_part2 = ComponentDiskPart {
+            file: Box::new(file2),
+            offset: 100,
+            length: 100,
+        };
+        let disk_part3 = ComponentDiskPart {
+            file: Box::new(file3),
+            offset: 200,
+            length: 100,
+        };
+        let composite = CompositeDiskFile::new(vec![disk_part1, disk_part2, disk_part3]).unwrap();
+        let mut out_fds = composite.as_raw_fds();
+        out_fds.sort();
+        assert_eq!(in_fds, out_fds);
+    }
+
+    #[test]
+    fn triple_file_passthrough() {
+        let file1: File = SharedMemory::new(None).unwrap().into();
+        let file2: File = SharedMemory::new(None).unwrap().into();
+        let file3: File = SharedMemory::new(None).unwrap().into();
+        let disk_part1 = ComponentDiskPart {
+            file: Box::new(file1),
+            offset: 0,
+            length: 100,
+        };
+        let disk_part2 = ComponentDiskPart {
+            file: Box::new(file2),
+            offset: 100,
+            length: 100,
+        };
+        let disk_part3 = ComponentDiskPart {
+            file: Box::new(file3),
+            offset: 200,
+            length: 100,
+        };
+        let mut composite =
+            CompositeDiskFile::new(vec![disk_part1, disk_part2, disk_part3]).unwrap();
+        composite.seek(SeekFrom::Start(50)).unwrap();
+        let mut input_memory = [55u8; 200];
+        let input_volatile_memory = &mut input_memory[..];
+        composite
+            .write_all_volatile(input_volatile_memory.get_slice(0, 200).unwrap())
+            .unwrap();
+        composite.seek(SeekFrom::Start(50)).unwrap();
+        let mut output_memory = [0u8; 200];
+        let output_volatile_memory = &mut output_memory[..];
+        composite
+            .read_exact_volatile(output_volatile_memory.get_slice(0, 200).unwrap())
+            .unwrap();
+        assert!(input_memory.into_iter().eq(output_memory.into_iter()));
+    }
+
+    #[test]
+    fn triple_file_punch_hole() {
+        let file1: File = SharedMemory::new(None).unwrap().into();
+        let file2: File = SharedMemory::new(None).unwrap().into();
+        let file3: File = SharedMemory::new(None).unwrap().into();
+        let disk_part1 = ComponentDiskPart {
+            file: Box::new(file1),
+            offset: 0,
+            length: 100,
+        };
+        let disk_part2 = ComponentDiskPart {
+            file: Box::new(file2),
+            offset: 100,
+            length: 100,
+        };
+        let disk_part3 = ComponentDiskPart {
+            file: Box::new(file3),
+            offset: 200,
+            length: 100,
+        };
+        let mut composite =
+            CompositeDiskFile::new(vec![disk_part1, disk_part2, disk_part3]).unwrap();
+        composite.seek(SeekFrom::Start(0)).unwrap();
+        let mut input_memory = [55u8; 300];
+        let input_volatile_memory = &mut input_memory[..];
+        composite
+            .write_all_volatile(input_volatile_memory.get_slice(0, 300).unwrap())
+            .unwrap();
+        composite.punch_hole(50, 200).unwrap();
+        composite.seek(SeekFrom::Start(0)).unwrap();
+        let mut output_memory = [0u8; 300];
+        let output_volatile_memory = &mut output_memory[..];
+        composite
+            .read_exact_volatile(output_volatile_memory.get_slice(0, 300).unwrap())
+            .unwrap();
+
+        for i in 50..250 {
+            input_memory[i] = 0;
+        }
+        assert!(input_memory.into_iter().eq(output_memory.into_iter()));
+    }
+
+    #[test]
+    fn triple_file_write_zeroes() {
+        let file1: File = SharedMemory::new(None).unwrap().into();
+        let file2: File = SharedMemory::new(None).unwrap().into();
+        let file3: File = SharedMemory::new(None).unwrap().into();
+        let disk_part1 = ComponentDiskPart {
+            file: Box::new(file1),
+            offset: 0,
+            length: 100,
+        };
+        let disk_part2 = ComponentDiskPart {
+            file: Box::new(file2),
+            offset: 100,
+            length: 100,
+        };
+        let disk_part3 = ComponentDiskPart {
+            file: Box::new(file3),
+            offset: 200,
+            length: 100,
+        };
+        let mut composite =
+            CompositeDiskFile::new(vec![disk_part1, disk_part2, disk_part3]).unwrap();
+        composite.seek(SeekFrom::Start(0)).unwrap();
+        let mut input_memory = [55u8; 300];
+        let input_volatile_memory = &mut input_memory[..];
+        composite
+            .write_all_volatile(input_volatile_memory.get_slice(0, 300).unwrap())
+            .unwrap();
+        composite.seek(SeekFrom::Start(50)).unwrap();
+        let mut zeroes_written = 0;
+        while zeroes_written < 200 {
+            zeroes_written += composite.write_zeroes(200 - zeroes_written).unwrap();
+        }
+        composite.seek(SeekFrom::Start(0)).unwrap();
+        let mut output_memory = [0u8; 300];
+        let output_volatile_memory = &mut output_memory[..];
+        composite
+            .read_exact_volatile(output_volatile_memory.get_slice(0, 300).unwrap())
+            .unwrap();
+
+        for i in 50..250 {
+            input_memory[i] = 0;
+        }
+        for i in 0..300 {
+            println!(
+                "input[{0}] = {1}, output[{0}] = {2}",
+                i, input_memory[i], output_memory[i]
+            );
+        }
+        assert!(input_memory.into_iter().eq(output_memory.into_iter()));
+    }
+}
diff --git a/disk/src/disk.rs b/disk/src/disk.rs
index 9f1914d..f63d63c 100644
--- a/disk/src/disk.rs
+++ b/disk/src/disk.rs
@@ -14,10 +14,18 @@ use sys_util::{
     AsRawFds, FileReadWriteVolatile, FileSetLen, FileSync, PunchHole, SeekHole, WriteZeroes,
 };
 
+#[cfg(feature = "composite-disk")]
+mod composite;
+#[cfg(feature = "composite-disk")]
+use composite::{CompositeDiskFile, CDISK_MAGIC, CDISK_MAGIC_LEN};
+
 #[sorted]
 #[derive(Debug)]
 pub enum Error {
     BlockDeviceNew(sys_util::Error),
+    ConversionNotSupported,
+    #[cfg(feature = "composite-disk")]
+    CreateCompositeDisk(composite::Error),
     QcowError(qcow::Error),
     ReadingData(io::Error),
     ReadingHeader(io::Error),
@@ -55,6 +63,9 @@ impl Display for Error {
         #[sorted]
         match self {
             BlockDeviceNew(e) => write!(f, "failed to create block device: {}", e),
+            ConversionNotSupported => write!(f, "requested file conversion not supported"),
+            #[cfg(feature = "composite-disk")]
+            CreateCompositeDisk(e) => write!(f, "failure in composite disk: {}", e),
             QcowError(e) => write!(f, "failure in qcow: {}", e),
             ReadingData(e) => write!(f, "failed to read data: {}", e),
             ReadingHeader(e) => write!(f, "failed to read header: {}", e),
@@ -71,6 +82,7 @@ impl Display for Error {
 pub enum ImageType {
     Raw,
     Qcow2,
+    CompositeDisk,
 }
 
 fn convert_copy<R, W>(reader: &mut R, writer: &mut W, offset: u64, size: u64) -> Result<()>
@@ -159,6 +171,7 @@ where
                 .map_err(Error::SettingFileSize)?;
             convert_reader_writer(reader, &mut dst_writer, src_size)
         }
+        _ => Err(Error::ConversionNotSupported),
     }
 }
 
@@ -177,6 +190,8 @@ pub fn convert(src_file: File, dst_file: File, dst_type: ImageType) -> Result<()
             let mut src_reader = src_file;
             convert_reader(&mut src_reader, dst_file, dst_type)
         }
+        // TODO(schuffelen): Implement Read + Write + SeekHole for CompositeDiskFile
+        _ => Err(Error::ConversionNotSupported),
     }
 }
 
@@ -188,6 +203,18 @@ pub fn detect_image_type(file: &File) -> Result<ImageType> {
     let mut magic = [0u8; 4];
     f.read_exact(&mut magic).map_err(Error::ReadingHeader)?;
     let magic = u32::from_be_bytes(magic);
+    #[cfg(feature = "composite-disk")]
+    {
+        f.seek(SeekFrom::Start(0)).map_err(Error::SeekingFile)?;
+        let mut cdisk_magic = [0u8; CDISK_MAGIC_LEN];
+        f.read_exact(&mut cdisk_magic[..])
+            .map_err(Error::ReadingHeader)?;
+        if cdisk_magic == CDISK_MAGIC.as_bytes() {
+            f.seek(SeekFrom::Start(orig_seek))
+                .map_err(Error::SeekingFile)?;
+            return Ok(ImageType::CompositeDisk);
+        }
+    }
     let image_type = if magic == QCOW_MAGIC {
         ImageType::Qcow2
     } else {
@@ -206,5 +233,13 @@ pub fn create_disk_file(raw_image: File) -> Result<Box<dyn DiskFile>> {
         ImageType::Qcow2 => {
             Box::new(QcowFile::from(raw_image).map_err(Error::QcowError)?) as Box<dyn DiskFile>
         }
+        #[cfg(feature = "composite-disk")]
+        ImageType::CompositeDisk => {
+            // Valid composite disk header present
+            Box::new(CompositeDiskFile::from_file(raw_image).map_err(Error::CreateCompositeDisk)?)
+                as Box<dyn DiskFile>
+        }
+        #[cfg(not(feature = "composite-disk"))]
+        ImageType::CompositeDisk => return Err(Error::UnknownType),
     })
 }
diff --git a/protos/Cargo.toml b/protos/Cargo.toml
index 9fd0e5a..98cd4ae 100644
--- a/protos/Cargo.toml
+++ b/protos/Cargo.toml
@@ -6,6 +6,7 @@ edition = "2018"
 
 [features]
 plugin = ["kvm_sys"]
+composite-disk = []
 trunks = []
 
 [dependencies]
diff --git a/protos/build.rs b/protos/build.rs
index b510e54..2e870b4 100644
--- a/protos/build.rs
+++ b/protos/build.rs
@@ -48,6 +48,8 @@ struct LocalProto {
 static LOCAL_PROTOS: &[LocalProto] = &[
     #[cfg(feature = "plugin")]
     LocalProto { module: "plugin" },
+    #[cfg(feature = "composite-disk")]
+    LocalProto { module: "cdisk_spec" },
 ];
 
 fn main() -> Result<()> {
diff --git a/protos/src/cdisk_spec.proto b/protos/src/cdisk_spec.proto
new file mode 100644
index 0000000..771c7ce
--- /dev/null
+++ b/protos/src/cdisk_spec.proto
@@ -0,0 +1,18 @@
+syntax = "proto3";
+
+enum ReadWriteCapability {
+  READ_ONLY = 0;
+  READ_WRITE = 1;
+}
+
+message ComponentDisk {
+  string file_path = 1;
+  uint64 offset = 2;
+  ReadWriteCapability read_write_capability = 3;
+}
+
+message CompositeDisk {
+  uint64 version = 1;
+  repeated ComponentDisk component_disks = 2;
+  uint64 length = 3;
+};
diff --git a/protos/src/lib.rs b/protos/src/lib.rs
index 3fdbdc0..d73fb03 100644
--- a/protos/src/lib.rs
+++ b/protos/src/lib.rs
@@ -11,3 +11,6 @@ pub mod plugin;
 
 #[cfg(feature = "trunks")]
 pub use generated::trunks;
+
+#[cfg(feature = "composite-disk")]
+pub use generated::cdisk_spec;
diff --git a/qcow_utils/src/qcow_utils.rs b/qcow_utils/src/qcow_utils.rs
index 4c3ede7..1dcf2a4 100644
--- a/qcow_utils/src/qcow_utils.rs
+++ b/qcow_utils/src/qcow_utils.rs
@@ -83,6 +83,7 @@ pub unsafe extern "C" fn expand_disk_image(path: *const c_char, virtual_size: u6
             Ok(f) => Box::new(f),
             Err(_) => return -EINVAL,
         },
+        _ => return -EINVAL,
     };
 
     // For safety against accidentally shrinking the disk image due to a