Skip to content

Commit

Permalink
block: qcow: limit max nesting depth for backing file
Browse files Browse the repository at this point in the history
Impose a limit on the maximum nesting of file formats that can open more
files. For example, a qcow2 file can have a backing file, which could be
another qcow2 file with a backing file (or even the same file as the
original), potentially causing unbounded recursion.

This commit is based on crosvm implementation:
https://chromium.googlesource.com/crosvm/crosvm/+/eb1640e301d66c06e0e0a07886946830f3f2f4fe

Fixes: cloud-hypervisor#6472

Signed-off-by: Yu Li <liyu.yukiteru@bytedance.com>
  • Loading branch information
yukiiiteru committed May 24, 2024
1 parent 7c46e61 commit cd71158
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 41 deletions.
24 changes: 19 additions & 5 deletions block/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ use vmm_sys_util::{ioctl_io_nr, ioctl_ioc_nr};
const SECTOR_SHIFT: u8 = 9;
pub const SECTOR_SIZE: u64 = 0x01 << SECTOR_SHIFT;

/// Nesting depth limit for disk formats that can open other disk files.
pub const MAX_NESTING_DEPTH: u32 = 10;

#[derive(Error, Debug)]
pub enum Error {
#[error("Guest gave us bad memory addresses")]
Expand All @@ -89,6 +92,8 @@ pub enum Error {
GetFileMetadata,
#[error("The requested operation would cause a seek beyond disk end")]
InvalidOffset,
#[error("maximum disk nesting depth exceeded")]
MaxNestingDepthExceeded,
#[error("Failure in qcow: {0}")]
QcowError(qcow::Error),
#[error("Failure in raw file: {0}")]
Expand Down Expand Up @@ -793,14 +798,23 @@ pub trait BlockBackend: Read + Write + Seek + Send + Debug {
}

/// Inspect the image file type and create an appropriate disk file to match it.
pub fn create_disk_file(mut file: File, direct_io: bool) -> Result<Box<dyn BlockBackend>, Error> {
pub fn create_disk_file(
mut file: File,
direct_io: bool,
mut max_nesting_depth: u32,
) -> Result<Box<dyn BlockBackend>, Error> {
if max_nesting_depth == 0 {
return Err(Error::MaxNestingDepthExceeded);
}
max_nesting_depth -= 1;

let image_type = detect_image_type(&mut file).map_err(Error::DetectImageType)?;

Ok(match image_type {
ImageType::Qcow2 => {
Box::new(QcowFile::from(RawFile::new(file, direct_io)).map_err(Error::QcowError)?)
as Box<dyn BlockBackend>
}
ImageType::Qcow2 => Box::new(
QcowFile::from(RawFile::new(file, direct_io), max_nesting_depth)
.map_err(Error::QcowError)?,
) as Box<dyn BlockBackend>,
ImageType::FixedVhd => {
Box::new(FixedVhd::new(file).map_err(Error::FixedVhdError)?) as Box<dyn BlockBackend>
}
Expand Down
84 changes: 50 additions & 34 deletions block/src/qcow/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,7 @@ fn max_refcount_clusters(refcount_order: u32, cluster_size: u32, num_clusters: u
/// # use std::io::{Read, Seek, SeekFrom};
/// # fn test(file: std::fs::File) -> std::io::Result<()> {
/// let mut raw_img = RawFile::new(file, false);
/// let mut q = QcowFile::from(raw_img).expect("Can't open qcow file");
/// let mut q = QcowFile::from(raw_img, block::MAX_NESTING_DEPTH).expect("Can't open qcow file");
/// let mut buf = [0u8; 12];
/// q.seek(SeekFrom::Start(10 as u64))?;
/// q.read(&mut buf[..])?;
Expand All @@ -439,7 +439,7 @@ pub struct QcowFile {

impl QcowFile {
/// Creates a QcowFile from `file`. File must be a valid qcow2 image.
pub fn from(mut file: RawFile) -> Result<QcowFile> {
pub fn from(mut file: RawFile, max_nesting_depth: u32) -> Result<QcowFile> {
let header = QcowHeader::new(&mut file)?;

// Only v2 and v3 files are supported.
Expand Down Expand Up @@ -471,8 +471,9 @@ impl QcowFile {
.read(true)
.open(path)
.map_err(Error::BackingFileIo)?;
let backing_file = crate::create_disk_file(backing_raw_file, direct_io)
.map_err(|e| Error::BackingFileOpen(Box::new(e)))?;
let backing_file =
crate::create_disk_file(backing_raw_file, direct_io, max_nesting_depth)
.map_err(|e| Error::BackingFileOpen(Box::new(e)))?;
Some(backing_file)
} else {
None
Expand Down Expand Up @@ -621,14 +622,16 @@ impl QcowFile {
file: RawFile,
version: u32,
backing_file_name: &str,
backing_file_max_nesting_depth: u32,
) -> Result<QcowFile> {
let direct_io = file.is_direct();
let backing_raw_file = OpenOptions::new()
.read(true)
.open(backing_file_name)
.map_err(Error::BackingFileIo)?;
let backing_file = crate::create_disk_file(backing_raw_file, direct_io)
.map_err(|e| Error::BackingFileOpen(Box::new(e)))?;
let backing_file =
crate::create_disk_file(backing_raw_file, direct_io, backing_file_max_nesting_depth)
.map_err(|e| Error::BackingFileOpen(Box::new(e)))?;
let size = backing_file
.size()
.map_err(|e| Error::BackingFileOpen(Box::new(e)))?;
Expand All @@ -642,7 +645,7 @@ impl QcowFile {
file.rewind().map_err(Error::SeekingFile)?;
header.write_to(&mut file)?;

let mut qcow = Self::from(file)?;
let mut qcow = Self::from(file, 1)?;

// Set the refcount for each refcount table cluster.
let cluster_size = 0x01u64 << qcow.header.cluster_bits;
Expand Down Expand Up @@ -1778,11 +1781,16 @@ where
/// Copy the contents of a disk image in `src_file` into `dst_file`.
/// The type of `src_file` is automatically detected, and the output file type is
/// determined by `dst_type`.
pub fn convert(mut src_file: RawFile, dst_file: RawFile, dst_type: ImageType) -> Result<()> {
pub fn convert(
mut src_file: RawFile,
dst_file: RawFile,
dst_type: ImageType,
src_max_nesting_depth: u32,
) -> Result<()> {
let src_type = detect_image_type(&mut src_file)?;
match src_type {
ImageType::Qcow2 => {
let mut src_reader = QcowFile::from(src_file)?;
let mut src_reader = QcowFile::from(src_file, src_max_nesting_depth)?;
convert_reader(&mut src_reader, dst_file, dst_type)
}
ImageType::Raw => {
Expand Down Expand Up @@ -1810,6 +1818,8 @@ pub fn detect_image_type(file: &mut RawFile) -> Result<ImageType> {

#[cfg(test)]
mod tests {
use crate::MAX_NESTING_DEPTH;

use super::*;
use vmm_sys_util::tempfile::TempFile;
use vmm_sys_util::write_zeroes::WriteZeroes;
Expand Down Expand Up @@ -1907,13 +1917,13 @@ mod tests {
#[test]
fn write_read_start_backing_v2() {
let disk_file = basic_file(&valid_header_v2());
let mut backing = QcowFile::from(disk_file).unwrap();
let mut backing = QcowFile::from(disk_file, MAX_NESTING_DEPTH).unwrap();
backing
.write_all(b"test first bytes")
.expect("Failed to write test string.");
let mut buf = [0u8; 4];
let wrapping_disk_file = basic_file(&valid_header_v2());
let mut wrapping = QcowFile::from(wrapping_disk_file).unwrap();
let mut wrapping = QcowFile::from(wrapping_disk_file, MAX_NESTING_DEPTH).unwrap();
wrapping.set_backing_file(Some(Box::new(backing)));
wrapping.seek(SeekFrom::Start(0)).expect("Failed to seek.");
wrapping.read_exact(&mut buf).expect("Failed to read.");
Expand All @@ -1923,13 +1933,13 @@ mod tests {
#[test]
fn write_read_start_backing_v3() {
let disk_file = basic_file(&valid_header_v3());
let mut backing = QcowFile::from(disk_file).unwrap();
let mut backing = QcowFile::from(disk_file, MAX_NESTING_DEPTH).unwrap();
backing
.write_all(b"test first bytes")
.expect("Failed to write test string.");
let mut buf = [0u8; 4];
let wrapping_disk_file = basic_file(&valid_header_v3());
let mut wrapping = QcowFile::from(wrapping_disk_file).unwrap();
let mut wrapping = QcowFile::from(wrapping_disk_file, MAX_NESTING_DEPTH).unwrap();
wrapping.set_backing_file(Some(Box::new(backing)));
wrapping.seek(SeekFrom::Start(0)).expect("Failed to seek.");
wrapping.read_exact(&mut buf).expect("Failed to read.");
Expand All @@ -1945,7 +1955,8 @@ mod tests {
.write_to(&mut disk_file)
.expect("Failed to write header to temporary file.");
disk_file.rewind().unwrap();
QcowFile::from(disk_file).expect("Failed to create Qcow from default Header");
QcowFile::from(disk_file, MAX_NESTING_DEPTH)
.expect("Failed to create Qcow from default Header");
}

#[test]
Expand All @@ -1957,7 +1968,8 @@ mod tests {
.write_to(&mut disk_file)
.expect("Failed to write header to temporary file.");
disk_file.rewind().unwrap();
QcowFile::from(disk_file).expect("Failed to create Qcow from default Header");
QcowFile::from(disk_file, MAX_NESTING_DEPTH)
.expect("Failed to create Qcow from default Header");
}

#[test]
Expand Down Expand Up @@ -2023,7 +2035,8 @@ mod tests {
let mut header = valid_header_v3();
header[99] = 2;
with_basic_file(&header, |disk_file: RawFile| {
QcowFile::from(disk_file).expect_err("Invalid refcount order worked.");
QcowFile::from(disk_file, MAX_NESTING_DEPTH)
.expect_err("Invalid refcount order worked.");
});
}

Expand All @@ -2032,15 +2045,15 @@ mod tests {
let mut header = valid_header_v3();
header[23] = 3;
with_basic_file(&header, |disk_file: RawFile| {
QcowFile::from(disk_file).expect_err("Failed to create file.");
QcowFile::from(disk_file, MAX_NESTING_DEPTH).expect_err("Failed to create file.");
});
}

#[test]
fn test_header_huge_file() {
let header = test_huge_header();
with_basic_file(&header, |disk_file: RawFile| {
QcowFile::from(disk_file).expect_err("Failed to create file.");
QcowFile::from(disk_file, MAX_NESTING_DEPTH).expect_err("Failed to create file.");
});
}

Expand All @@ -2049,7 +2062,7 @@ mod tests {
let mut header = valid_header_v3();
header[24..32].copy_from_slice(&[0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x1e]);
with_basic_file(&header, |disk_file: RawFile| {
QcowFile::from(disk_file).expect_err("Failed to create file.");
QcowFile::from(disk_file, MAX_NESTING_DEPTH).expect_err("Failed to create file.");
});
}

Expand All @@ -2058,7 +2071,7 @@ mod tests {
let mut header = valid_header_v3();
header[36] = 0x12;
with_basic_file(&header, |disk_file: RawFile| {
QcowFile::from(disk_file).expect_err("Failed to create file.");
QcowFile::from(disk_file, MAX_NESTING_DEPTH).expect_err("Failed to create file.");
});
}

Expand All @@ -2070,7 +2083,7 @@ mod tests {
header[31] = 0;
// 1 TB with the min cluster size makes the arrays too big, it should fail.
with_basic_file(&header, |disk_file: RawFile| {
QcowFile::from(disk_file).expect_err("Failed to create file.");
QcowFile::from(disk_file, MAX_NESTING_DEPTH).expect_err("Failed to create file.");
});
}

Expand All @@ -2084,7 +2097,8 @@ mod tests {
// set cluster_bits
header[23] = 16;
with_basic_file(&header, |disk_file: RawFile| {
let mut qcow = QcowFile::from(disk_file).expect("Failed to create file.");
let mut qcow =
QcowFile::from(disk_file, MAX_NESTING_DEPTH).expect("Failed to create file.");
qcow.seek(SeekFrom::Start(0x100_0000_0000 - 8))
.expect("Failed to seek.");
let value = 0x0000_0040_3f00_ffffu64;
Expand All @@ -2098,7 +2112,8 @@ mod tests {
let mut header = valid_header_v3();
header[56..60].copy_from_slice(&[0x02, 0x00, 0xe8, 0xff]);
with_basic_file(&header, |disk_file: RawFile| {
QcowFile::from(disk_file).expect_err("Created disk with crazy refcount clusters");
QcowFile::from(disk_file, MAX_NESTING_DEPTH)
.expect_err("Created disk with crazy refcount clusters");
});
}

Expand All @@ -2107,14 +2122,15 @@ mod tests {
let mut header = valid_header_v3();
header[48..56].copy_from_slice(&[0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 0x02, 0x00]);
with_basic_file(&header, |disk_file: RawFile| {
QcowFile::from(disk_file).expect_err("Created disk with crazy refcount offset");
QcowFile::from(disk_file, MAX_NESTING_DEPTH)
.expect_err("Created disk with crazy refcount offset");
});
}

#[test]
fn write_read_start() {
with_basic_file(&valid_header_v3(), |disk_file: RawFile| {
let mut q = QcowFile::from(disk_file).unwrap();
let mut q = QcowFile::from(disk_file, MAX_NESTING_DEPTH).unwrap();
q.write_all(b"test first bytes")
.expect("Failed to write test string.");
let mut buf = [0u8; 4];
Expand All @@ -2127,12 +2143,12 @@ mod tests {
#[test]
fn write_read_start_backing_overlap() {
let disk_file = basic_file(&valid_header_v3());
let mut backing = QcowFile::from(disk_file).unwrap();
let mut backing = QcowFile::from(disk_file, MAX_NESTING_DEPTH).unwrap();
backing
.write_all(b"test first bytes")
.expect("Failed to write test string.");
let wrapping_disk_file = basic_file(&valid_header_v3());
let mut wrapping = QcowFile::from(wrapping_disk_file).unwrap();
let mut wrapping = QcowFile::from(wrapping_disk_file, MAX_NESTING_DEPTH).unwrap();
wrapping.set_backing_file(Some(Box::new(backing)));
wrapping.seek(SeekFrom::Start(0)).expect("Failed to seek.");
wrapping
Expand All @@ -2147,7 +2163,7 @@ mod tests {
#[test]
fn offset_write_read() {
with_basic_file(&valid_header_v3(), |disk_file: RawFile| {
let mut q = QcowFile::from(disk_file).unwrap();
let mut q = QcowFile::from(disk_file, MAX_NESTING_DEPTH).unwrap();
let b = [0x55u8; 0x1000];
q.seek(SeekFrom::Start(0xfff2000)).expect("Failed to seek.");
q.write_all(&b).expect("Failed to write test string.");
Expand All @@ -2161,7 +2177,7 @@ mod tests {
#[test]
fn write_zeroes_read() {
with_basic_file(&valid_header_v3(), |disk_file: RawFile| {
let mut q = QcowFile::from(disk_file).unwrap();
let mut q = QcowFile::from(disk_file, MAX_NESTING_DEPTH).unwrap();
// Write some test data.
let b = [0x55u8; 0x1000];
q.seek(SeekFrom::Start(0xfff2000)).expect("Failed to seek.");
Expand All @@ -2187,7 +2203,7 @@ mod tests {
// valid_header uses cluster_bits = 12, which corresponds to a cluster size of 4096.
const CHUNK_SIZE: usize = 4096 * 2 + 512;
with_basic_file(&valid_header_v3(), |disk_file: RawFile| {
let mut q = QcowFile::from(disk_file).unwrap();
let mut q = QcowFile::from(disk_file, MAX_NESTING_DEPTH).unwrap();
// Write some test data.
let b = [0x55u8; CHUNK_SIZE];
q.rewind().expect("Failed to seek.");
Expand All @@ -2208,19 +2224,19 @@ mod tests {
#[test]
fn test_header() {
with_basic_file(&valid_header_v2(), |disk_file: RawFile| {
let q = QcowFile::from(disk_file).unwrap();
let q = QcowFile::from(disk_file, MAX_NESTING_DEPTH).unwrap();
assert_eq!(q.virtual_size(), 0x20_0000_0000);
});
with_basic_file(&valid_header_v3(), |disk_file: RawFile| {
let q = QcowFile::from(disk_file).unwrap();
let q = QcowFile::from(disk_file, MAX_NESTING_DEPTH).unwrap();
assert_eq!(q.virtual_size(), 0x20_0000_0000);
});
}

#[test]
fn read_small_buffer() {
with_basic_file(&valid_header_v3(), |disk_file: RawFile| {
let mut q = QcowFile::from(disk_file).unwrap();
let mut q = QcowFile::from(disk_file, MAX_NESTING_DEPTH).unwrap();
let mut b = [5u8; 16];
q.seek(SeekFrom::Start(1000)).expect("Failed to seek.");
q.read_exact(&mut b).expect("Failed to read.");
Expand All @@ -2232,7 +2248,7 @@ mod tests {
#[test]
fn replay_ext4() {
with_basic_file(&valid_header_v3(), |disk_file: RawFile| {
let mut q = QcowFile::from(disk_file).unwrap();
let mut q = QcowFile::from(disk_file, MAX_NESTING_DEPTH).unwrap();
const BUF_SIZE: usize = 0x1000;
let mut b = [0u8; BUF_SIZE];

Expand Down
7 changes: 5 additions & 2 deletions block/src/qcow_sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

use crate::async_io::{AsyncIo, AsyncIoResult, DiskFile, DiskFileError, DiskFileResult};
use crate::qcow::{QcowFile, RawFile, Result as QcowResult};
use crate::AsyncAdaptor;
use crate::{AsyncAdaptor, MAX_NESTING_DEPTH};
use std::collections::VecDeque;
use std::fs::File;
use std::io::{Seek, SeekFrom};
Expand All @@ -18,7 +18,10 @@ pub struct QcowDiskSync {
impl QcowDiskSync {
pub fn new(file: File, direct_io: bool) -> QcowResult<Self> {
Ok(QcowDiskSync {
qcow_file: Arc::new(Mutex::new(QcowFile::from(RawFile::new(file, direct_io))?)),
qcow_file: Arc::new(Mutex::new(QcowFile::from(
RawFile::new(file, direct_io),
MAX_NESTING_DEPTH,
)?)),
})
}
}
Expand Down

0 comments on commit cd71158

Please sign in to comment.