1 // Copyright 2019 The ChromiumOS Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 //! VM disk image file format I/O.
6
7 use std::cmp::min;
8 use std::fmt::Debug;
9 use std::fs::File;
10 use std::io;
11 use std::io::Read;
12 use std::io::Seek;
13 use std::io::SeekFrom;
14 use std::path::Path;
15 use std::sync::Arc;
16
17 use async_trait::async_trait;
18 use base::get_filesystem_type;
19 use base::info;
20 use base::AsRawDescriptors;
21 use base::FileAllocate;
22 use base::FileReadWriteAtVolatile;
23 use base::FileSetLen;
24 use base::PunchHole;
25 use cros_async::AllocateMode;
26 use cros_async::BackingMemory;
27 use cros_async::Executor;
28 use cros_async::IoSource;
29 use thiserror::Error as ThisError;
30
31 mod asynchronous;
32 #[allow(unused)]
33 pub(crate) use asynchronous::AsyncDiskFileWrapper;
34 #[cfg(feature = "qcow")]
35 mod qcow;
36 #[cfg(feature = "qcow")]
37 pub use qcow::QcowFile;
38 #[cfg(feature = "qcow")]
39 pub use qcow::QCOW_MAGIC;
40 mod sys;
41
42 #[cfg(feature = "composite-disk")]
43 mod composite;
44 #[cfg(feature = "composite-disk")]
45 use composite::CompositeDiskFile;
46 #[cfg(feature = "composite-disk")]
47 use composite::CDISK_MAGIC;
48 #[cfg(feature = "composite-disk")]
49 mod gpt;
50 #[cfg(feature = "composite-disk")]
51 pub use composite::create_composite_disk;
52 #[cfg(feature = "composite-disk")]
53 pub use composite::create_zero_filler;
54 #[cfg(feature = "composite-disk")]
55 pub use composite::Error as CompositeError;
56 #[cfg(feature = "composite-disk")]
57 pub use composite::ImagePartitionType;
58 #[cfg(feature = "composite-disk")]
59 pub use composite::PartitionInfo;
60 #[cfg(feature = "composite-disk")]
61 pub use gpt::Error as GptError;
62
63 #[cfg(feature = "android-sparse")]
64 mod android_sparse;
65 #[cfg(feature = "android-sparse")]
66 use android_sparse::AndroidSparse;
67 #[cfg(feature = "android-sparse")]
68 use android_sparse::SPARSE_HEADER_MAGIC;
69
70 /// Nesting depth limit for disk formats that can open other disk files.
71 pub const MAX_NESTING_DEPTH: u32 = 10;
72
73 #[derive(ThisError, Debug)]
74 pub enum Error {
75 #[error("failed to create block device: {0}")]
76 BlockDeviceNew(base::Error),
77 #[error("requested file conversion not supported")]
78 ConversionNotSupported,
79 #[cfg(feature = "android-sparse")]
80 #[error("failure in android sparse disk: {0}")]
81 CreateAndroidSparseDisk(android_sparse::Error),
82 #[cfg(feature = "composite-disk")]
83 #[error("failure in composite disk: {0}")]
84 CreateCompositeDisk(composite::Error),
85 #[error("failure creating single file disk: {0}")]
86 CreateSingleFileDisk(cros_async::AsyncError),
87 #[error("failure with fallocate: {0}")]
88 Fallocate(cros_async::AsyncError),
89 #[error("failure with fsync: {0}")]
90 Fsync(cros_async::AsyncError),
91 #[error("failure with fsync: {0}")]
92 IoFsync(io::Error),
93 #[error("checking host fs type: {0}")]
94 HostFsType(base::Error),
95 #[error("maximum disk nesting depth exceeded")]
96 MaxNestingDepthExceeded,
97 #[error("failure to punch hole: {0}")]
98 PunchHole(io::Error),
99 #[cfg(feature = "qcow")]
100 #[error("failure in qcow: {0}")]
101 QcowError(qcow::Error),
102 #[error("failed to read data: {0}")]
103 ReadingData(io::Error),
104 #[error("failed to read header: {0}")]
105 ReadingHeader(io::Error),
106 #[error("failed to read to memory: {0}")]
107 ReadToMem(cros_async::AsyncError),
108 #[error("failed to seek file: {0}")]
109 SeekingFile(io::Error),
110 #[error("failed to set file size: {0}")]
111 SettingFileSize(io::Error),
112 #[error("unknown disk type")]
113 UnknownType,
114 #[error("failed to write from memory: {0}")]
115 WriteFromMem(cros_async::AsyncError),
116 #[error("failed to write from vec: {0}")]
117 WriteFromVec(cros_async::AsyncError),
118 #[error("failed to write zeroes: {0}")]
119 WriteZeroes(io::Error),
120 #[error("failed to write data: {0}")]
121 WritingData(io::Error),
122 #[error("failed to convert to async: {0}")]
123 ToAsync(cros_async::AsyncError),
124 #[cfg(windows)]
125 #[error("failed to set disk file sparse: {0}")]
126 SetSparseFailure(io::Error),
127 #[error("failure with guest memory access: {0}")]
128 GuestMemory(cros_async::mem::Error),
129 #[error("unsupported operation")]
130 UnsupportedOperation,
131 }
132
133 pub type Result<T> = std::result::Result<T, Error>;
134
135 /// A trait for getting the length of a disk image or raw block device.
136 pub trait DiskGetLen {
137 /// Get the current length of the disk in bytes.
get_len(&self) -> io::Result<u64>138 fn get_len(&self) -> io::Result<u64>;
139 }
140
141 impl DiskGetLen for File {
get_len(&self) -> io::Result<u64>142 fn get_len(&self) -> io::Result<u64> {
143 let mut s = self;
144 let orig_seek = s.seek(SeekFrom::Current(0))?;
145 let end = s.seek(SeekFrom::End(0))? as u64;
146 s.seek(SeekFrom::Start(orig_seek))?;
147 Ok(end)
148 }
149 }
150
151 pub trait PunchHoleMut {
152 /// Replace a range of bytes with a hole.
punch_hole_mut(&mut self, offset: u64, length: u64) -> io::Result<()>153 fn punch_hole_mut(&mut self, offset: u64, length: u64) -> io::Result<()>;
154 }
155
156 impl<T: PunchHole> PunchHoleMut for T {
punch_hole_mut(&mut self, offset: u64, length: u64) -> io::Result<()>157 fn punch_hole_mut(&mut self, offset: u64, length: u64) -> io::Result<()> {
158 self.punch_hole(offset, length)
159 }
160 }
161
162 /// The prerequisites necessary to support a block device.
163 pub trait DiskFile:
164 FileSetLen + DiskGetLen + FileReadWriteAtVolatile + ToAsyncDisk + Send + AsRawDescriptors + Debug
165 {
166 /// Creates a new DiskFile instance that shares the same underlying disk file image. IO
167 /// operations to a DiskFile should affect all DiskFile instances with the same underlying disk
168 /// file image.
169 ///
170 /// `try_clone()` returns [`io::ErrorKind::Unsupported`] Error if a DiskFile does not support
171 /// creating an instance with the same underlying disk file image.
try_clone(&self) -> io::Result<Box<dyn DiskFile>>172 fn try_clone(&self) -> io::Result<Box<dyn DiskFile>> {
173 Err(io::Error::new(
174 io::ErrorKind::Unsupported,
175 "unsupported operation",
176 ))
177 }
178 }
179
180 /// A `DiskFile` that can be converted for asychronous access.
181 pub trait ToAsyncDisk: AsRawDescriptors + DiskGetLen + Send {
182 /// Convert a boxed self in to a box-wrapped implementaiton of AsyncDisk.
183 /// Used to convert a standard disk image to an async disk image. This conversion and the
184 /// inverse are needed so that the `Send` DiskImage can be given to the block thread where it is
185 /// converted to a non-`Send` AsyncDisk. The AsyncDisk can then be converted back and returned
186 /// to the main device thread if the block device is destroyed or reset.
to_async_disk(self: Box<Self>, ex: &Executor) -> Result<Box<dyn AsyncDisk>>187 fn to_async_disk(self: Box<Self>, ex: &Executor) -> Result<Box<dyn AsyncDisk>>;
188 }
189
190 impl ToAsyncDisk for File {
to_async_disk(self: Box<Self>, ex: &Executor) -> Result<Box<dyn AsyncDisk>>191 fn to_async_disk(self: Box<Self>, ex: &Executor) -> Result<Box<dyn AsyncDisk>> {
192 Ok(Box::new(SingleFileDisk::new(*self, ex)?))
193 }
194 }
195
196 /// The variants of image files on the host that can be used as virtual disks.
197 #[derive(Debug, PartialEq, Eq)]
198 pub enum ImageType {
199 Raw,
200 Qcow2,
201 CompositeDisk,
202 AndroidSparse,
203 }
204
log_host_fs_type(file: &File) -> Result<()>205 fn log_host_fs_type(file: &File) -> Result<()> {
206 let fstype = get_filesystem_type(file).map_err(Error::HostFsType)?;
207 info!("Disk image file is hosted on file system type {:x}", fstype);
208 Ok(())
209 }
210
211 /// Detect the type of an image file by checking for a valid header of the supported formats.
detect_image_type(file: &File) -> Result<ImageType>212 pub fn detect_image_type(file: &File) -> Result<ImageType> {
213 let mut f = file;
214 let disk_size = f.get_len().map_err(Error::SeekingFile)?;
215 let orig_seek = f.seek(SeekFrom::Current(0)).map_err(Error::SeekingFile)?;
216 f.seek(SeekFrom::Start(0)).map_err(Error::SeekingFile)?;
217
218 info!("disk size {}, ", disk_size);
219 log_host_fs_type(f)?;
220 // Try to read the disk in a nicely-aligned block size unless the whole file is smaller.
221 const MAGIC_BLOCK_SIZE: usize = 4096;
222 #[repr(align(4096))]
223 struct BlockAlignedBuffer {
224 data: [u8; MAGIC_BLOCK_SIZE],
225 }
226 let mut magic = BlockAlignedBuffer {
227 data: [0u8; MAGIC_BLOCK_SIZE],
228 };
229 let magic_read_len = if disk_size > MAGIC_BLOCK_SIZE as u64 {
230 MAGIC_BLOCK_SIZE
231 } else {
232 // This cast is safe since we know disk_size is less than MAGIC_BLOCK_SIZE (4096) and
233 // therefore is representable in usize.
234 disk_size as usize
235 };
236
237 f.read_exact(&mut magic.data[0..magic_read_len])
238 .map_err(Error::ReadingHeader)?;
239 f.seek(SeekFrom::Start(orig_seek))
240 .map_err(Error::SeekingFile)?;
241
242 #[cfg(feature = "composite-disk")]
243 if let Some(cdisk_magic) = magic.data.get(0..CDISK_MAGIC.len()) {
244 if cdisk_magic == CDISK_MAGIC.as_bytes() {
245 return Ok(ImageType::CompositeDisk);
246 }
247 }
248
249 #[allow(unused_variables)] // magic4 is only used with the qcow or android-sparse features.
250 if let Some(magic4) = magic.data.get(0..4) {
251 #[cfg(feature = "qcow")]
252 if magic4 == QCOW_MAGIC.to_be_bytes() {
253 return Ok(ImageType::Qcow2);
254 }
255 #[cfg(feature = "android-sparse")]
256 if magic4 == SPARSE_HEADER_MAGIC.to_le_bytes() {
257 return Ok(ImageType::AndroidSparse);
258 }
259 }
260
261 Ok(ImageType::Raw)
262 }
263
264 impl DiskFile for File {
try_clone(&self) -> io::Result<Box<dyn DiskFile>>265 fn try_clone(&self) -> io::Result<Box<dyn DiskFile>> {
266 Ok(Box::new(self.try_clone()?))
267 }
268 }
269
270 /// Inspect the image file type and create an appropriate disk file to match it.
create_disk_file( raw_image: File, is_sparse_file: bool, #[allow(unused_variables)] mut max_nesting_depth: u32, #[allow(unused_variables)] image_path: &Path, ) -> Result<Box<dyn DiskFile>>271 pub fn create_disk_file(
272 raw_image: File,
273 is_sparse_file: bool,
274 // max_nesting_depth is only used if the composite-disk or qcow features are enabled.
275 #[allow(unused_variables)] mut max_nesting_depth: u32,
276 // image_path is only used if the composite-disk feature is enabled.
277 #[allow(unused_variables)] image_path: &Path,
278 ) -> Result<Box<dyn DiskFile>> {
279 if max_nesting_depth == 0 {
280 return Err(Error::MaxNestingDepthExceeded);
281 }
282 #[allow(unused_assignments)]
283 {
284 max_nesting_depth -= 1;
285 }
286
287 let image_type = detect_image_type(&raw_image)?;
288 Ok(match image_type {
289 ImageType::Raw => {
290 sys::apply_raw_disk_file_options(&raw_image, is_sparse_file)?;
291 Box::new(raw_image) as Box<dyn DiskFile>
292 }
293 #[cfg(feature = "qcow")]
294 ImageType::Qcow2 => {
295 Box::new(QcowFile::from(raw_image, max_nesting_depth).map_err(Error::QcowError)?)
296 as Box<dyn DiskFile>
297 }
298 #[cfg(feature = "composite-disk")]
299 ImageType::CompositeDisk => {
300 // Valid composite disk header present
301 Box::new(
302 CompositeDiskFile::from_file(
303 raw_image,
304 is_sparse_file,
305 max_nesting_depth,
306 image_path,
307 )
308 .map_err(Error::CreateCompositeDisk)?,
309 ) as Box<dyn DiskFile>
310 }
311 #[cfg(feature = "android-sparse")]
312 ImageType::AndroidSparse => {
313 Box::new(AndroidSparse::from_file(raw_image).map_err(Error::CreateAndroidSparseDisk)?)
314 as Box<dyn DiskFile>
315 }
316 #[allow(unreachable_patterns)]
317 _ => return Err(Error::UnknownType),
318 })
319 }
320
321 /// An asynchronously accessible disk.
322 #[async_trait(?Send)]
323 pub trait AsyncDisk: DiskGetLen + FileSetLen + FileAllocate {
324 /// Returns the inner file consuming self.
into_inner(self: Box<Self>) -> Box<dyn DiskFile>325 fn into_inner(self: Box<Self>) -> Box<dyn DiskFile>;
326
327 /// Asynchronously fsyncs any completed operations to the disk.
fsync(&self) -> Result<()>328 async fn fsync(&self) -> Result<()>;
329
330 /// Reads from the file at 'file_offset' into memory `mem` at `mem_offsets`.
331 /// `mem_offsets` is similar to an iovec except relative to the start of `mem`.
read_to_mem<'a>( &'a self, file_offset: u64, mem: Arc<dyn BackingMemory + Send + Sync>, mem_offsets: &'a [cros_async::MemRegion], ) -> Result<usize>332 async fn read_to_mem<'a>(
333 &'a self,
334 file_offset: u64,
335 mem: Arc<dyn BackingMemory + Send + Sync>,
336 mem_offsets: &'a [cros_async::MemRegion],
337 ) -> Result<usize>;
338
339 /// Writes to the file at 'file_offset' from memory `mem` at `mem_offsets`.
write_from_mem<'a>( &'a self, file_offset: u64, mem: Arc<dyn BackingMemory + Send + Sync>, mem_offsets: &'a [cros_async::MemRegion], ) -> Result<usize>340 async fn write_from_mem<'a>(
341 &'a self,
342 file_offset: u64,
343 mem: Arc<dyn BackingMemory + Send + Sync>,
344 mem_offsets: &'a [cros_async::MemRegion],
345 ) -> Result<usize>;
346
347 /// Replaces a range of bytes with a hole.
punch_hole(&self, file_offset: u64, length: u64) -> Result<()>348 async fn punch_hole(&self, file_offset: u64, length: u64) -> Result<()>;
349
350 /// Writes up to `length` bytes of zeroes to the stream, returning how many bytes were written.
write_zeroes_at(&self, file_offset: u64, length: u64) -> Result<()>351 async fn write_zeroes_at(&self, file_offset: u64, length: u64) -> Result<()>;
352
353 /// Reads from the file at 'file_offset' into `buf`.
354 ///
355 /// Less efficient than `read_to_mem` because of extra copies and allocations.
read_double_buffered(&self, file_offset: u64, buf: &mut [u8]) -> Result<usize>356 async fn read_double_buffered(&self, file_offset: u64, buf: &mut [u8]) -> Result<usize> {
357 let backing_mem = Arc::new(cros_async::VecIoWrapper::from(vec![0u8; buf.len()]));
358 let region = cros_async::MemRegion {
359 offset: 0,
360 len: buf.len(),
361 };
362 let n = self
363 .read_to_mem(file_offset, backing_mem.clone(), &[region])
364 .await?;
365 backing_mem
366 .get_volatile_slice(region)
367 .expect("BUG: the VecIoWrapper shrank?")
368 .sub_slice(0, n)
369 .expect("BUG: read_to_mem return value too large?")
370 .copy_to(buf);
371 Ok(n)
372 }
373
374 /// Writes to the file at 'file_offset' from `buf`.
375 ///
376 /// Less efficient than `write_from_mem` because of extra copies and allocations.
write_double_buffered(&self, file_offset: u64, buf: &[u8]) -> Result<usize>377 async fn write_double_buffered(&self, file_offset: u64, buf: &[u8]) -> Result<usize> {
378 let backing_mem = Arc::new(cros_async::VecIoWrapper::from(buf.to_vec()));
379 let region = cros_async::MemRegion {
380 offset: 0,
381 len: buf.len(),
382 };
383 self.write_from_mem(file_offset, backing_mem, &[region])
384 .await
385 }
386 }
387
388 /// A disk backed by a single file that implements `AsyncDisk` for access.
389 pub struct SingleFileDisk {
390 inner: IoSource<File>,
391 }
392
393 impl SingleFileDisk {
new(disk: File, ex: &Executor) -> Result<Self>394 pub fn new(disk: File, ex: &Executor) -> Result<Self> {
395 ex.async_from(disk)
396 .map_err(Error::CreateSingleFileDisk)
397 .map(|inner| SingleFileDisk { inner })
398 }
399 }
400
401 impl DiskGetLen for SingleFileDisk {
get_len(&self) -> io::Result<u64>402 fn get_len(&self) -> io::Result<u64> {
403 self.inner.as_source().get_len()
404 }
405 }
406
407 impl FileSetLen for SingleFileDisk {
set_len(&self, len: u64) -> io::Result<()>408 fn set_len(&self, len: u64) -> io::Result<()> {
409 self.inner.as_source().set_len(len)
410 }
411 }
412
413 impl FileAllocate for SingleFileDisk {
allocate(&mut self, offset: u64, len: u64) -> io::Result<()>414 fn allocate(&mut self, offset: u64, len: u64) -> io::Result<()> {
415 self.inner.as_source_mut().allocate(offset, len)
416 }
417 }
418
419 #[async_trait(?Send)]
420 impl AsyncDisk for SingleFileDisk {
into_inner(self: Box<Self>) -> Box<dyn DiskFile>421 fn into_inner(self: Box<Self>) -> Box<dyn DiskFile> {
422 Box::new(self.inner.into_source())
423 }
424
fsync(&self) -> Result<()>425 async fn fsync(&self) -> Result<()> {
426 self.inner.fsync().await.map_err(Error::Fsync)
427 }
428
read_to_mem<'a>( &'a self, file_offset: u64, mem: Arc<dyn BackingMemory + Send + Sync>, mem_offsets: &'a [cros_async::MemRegion], ) -> Result<usize>429 async fn read_to_mem<'a>(
430 &'a self,
431 file_offset: u64,
432 mem: Arc<dyn BackingMemory + Send + Sync>,
433 mem_offsets: &'a [cros_async::MemRegion],
434 ) -> Result<usize> {
435 self.inner
436 .read_to_mem(Some(file_offset), mem, mem_offsets)
437 .await
438 .map_err(Error::ReadToMem)
439 }
440
write_from_mem<'a>( &'a self, file_offset: u64, mem: Arc<dyn BackingMemory + Send + Sync>, mem_offsets: &'a [cros_async::MemRegion], ) -> Result<usize>441 async fn write_from_mem<'a>(
442 &'a self,
443 file_offset: u64,
444 mem: Arc<dyn BackingMemory + Send + Sync>,
445 mem_offsets: &'a [cros_async::MemRegion],
446 ) -> Result<usize> {
447 self.inner
448 .write_from_mem(Some(file_offset), mem, mem_offsets)
449 .await
450 .map_err(Error::WriteFromMem)
451 }
452
punch_hole(&self, file_offset: u64, length: u64) -> Result<()>453 async fn punch_hole(&self, file_offset: u64, length: u64) -> Result<()> {
454 self.inner
455 .fallocate(file_offset, length, AllocateMode::PunchHole)
456 .await
457 .map_err(Error::Fallocate)
458 }
459
write_zeroes_at(&self, file_offset: u64, length: u64) -> Result<()>460 async fn write_zeroes_at(&self, file_offset: u64, length: u64) -> Result<()> {
461 if self
462 .inner
463 .fallocate(file_offset, length, AllocateMode::ZeroRange)
464 .await
465 .is_ok()
466 {
467 return Ok(());
468 }
469
470 // Fall back to writing zeros if fallocate doesn't work.
471 let buf_size = min(length, 0x10000);
472 let mut nwritten = 0;
473 while nwritten < length {
474 let remaining = length - nwritten;
475 let write_size = min(remaining, buf_size) as usize;
476 let buf = vec![0u8; write_size];
477 nwritten += self
478 .inner
479 .write_from_vec(Some(file_offset + nwritten as u64), buf)
480 .await
481 .map(|(n, _)| n as u64)
482 .map_err(Error::WriteFromVec)?;
483 }
484 Ok(())
485 }
486 }
487