block: qcow: Add refcount overflow protection

Reject refcount values exceeding the maximum for the image's
refcount_order. This prevents silent truncation when storing
refcounts in narrow widths (e.g., 1-bit max is 1, 4-bit max is 15,
etc.).

Returns RefcountOverflow error with the attempted value, maximum,
and bit width. Propagates as EINVAL to the guest.

Signed-off-by: Anatol Belski <anbelski@linux.microsoft.com>
This commit is contained in:
Anatol Belski 2026-01-23 19:50:10 +01:00 committed by Rob Bradford
parent f8008191d2
commit 6e7f888f5d
2 changed files with 126 additions and 12 deletions

View file

@ -97,6 +97,8 @@ pub enum Error {
ReadingRefCounts(#[source] io::Error),
#[error("Failed to rebuild ref counts")]
RebuildingRefCounts(#[source] io::Error),
#[error("Refcount overflow")]
RefcountOverflow(#[source] refcount::Error),
#[error("Refcount table offset past file end")]
RefcountTableOffEnd,
#[error("Too many clusters specified for refcount")]
@ -932,6 +934,7 @@ impl QcowFile {
refcount_clusters,
refcount_block_entries,
cluster_size,
refcount_bits,
)
.map_err(Error::ReadingRefCounts)?;
@ -1119,18 +1122,36 @@ impl QcowFile {
/// Rebuild the reference count tables.
fn rebuild_refcounts(raw_file: &mut QcowRawFile, header: QcowHeader) -> Result<()> {
fn add_ref(refcounts: &mut [u64], cluster_size: u64, cluster_address: u64) -> Result<()> {
fn add_ref(
refcounts: &mut [u64],
cluster_size: u64,
cluster_address: u64,
max_refcount: u64,
refcount_bits: u64,
) -> Result<()> {
let idx = (cluster_address / cluster_size) as usize;
if idx >= refcounts.len() {
return Err(Error::InvalidClusterIndex);
}
if refcounts[idx] >= max_refcount {
return Err(Error::RefcountOverflow(refcount::Error::RefcountOverflow {
value: refcounts[idx] + 1,
max: max_refcount,
refcount_bits,
}));
}
refcounts[idx] += 1;
Ok(())
}
// Add a reference to the first cluster (header plus extensions).
fn set_header_refcount(refcounts: &mut [u64], cluster_size: u64) -> Result<()> {
add_ref(refcounts, cluster_size, 0)
fn set_header_refcount(
refcounts: &mut [u64],
cluster_size: u64,
max_refcount: u64,
refcount_bits: u64,
) -> Result<()> {
add_ref(refcounts, cluster_size, 0, max_refcount, refcount_bits)
}
// Add references to the L1 table clusters.
@ -1138,12 +1159,20 @@ impl QcowFile {
refcounts: &mut [u64],
header: &QcowHeader,
cluster_size: u64,
max_refcount: u64,
refcount_bits: u64,
) -> Result<()> {
let entries_per_cluster = cluster_size / size_of::<u64>() as u64;
let l1_clusters = div_round_up_u64(u64::from(header.l1_size), entries_per_cluster);
let l1_table_offset = header.l1_table_offset;
for i in 0..l1_clusters {
add_ref(refcounts, cluster_size, l1_table_offset + i * cluster_size)?;
add_ref(
refcounts,
cluster_size,
l1_table_offset + i * cluster_size,
max_refcount,
refcount_bits,
)?;
}
Ok(())
}
@ -1154,6 +1183,8 @@ impl QcowFile {
header: &QcowHeader,
cluster_size: u64,
raw_file: &mut QcowRawFile,
max_refcount: u64,
refcount_bits: u64,
) -> Result<()> {
let l1_table = raw_file
.read_pointer_table(
@ -1166,7 +1197,13 @@ impl QcowFile {
let l2_addr_disk = *l1_table.get(l1_index).ok_or(Error::InvalidIndex)?;
if l2_addr_disk != 0 {
// Add a reference to the L2 table cluster itself.
add_ref(refcounts, cluster_size, l2_addr_disk)?;
add_ref(
refcounts,
cluster_size,
l2_addr_disk,
max_refcount,
refcount_bits,
)?;
// Read the L2 table and find all referenced data clusters.
let l2_table = raw_file
@ -1178,7 +1215,13 @@ impl QcowFile {
.map_err(Error::ReadingPointers)?;
for data_cluster_addr in l2_table {
if data_cluster_addr != 0 {
add_ref(refcounts, cluster_size, data_cluster_addr)?;
add_ref(
refcounts,
cluster_size,
data_cluster_addr,
max_refcount,
refcount_bits,
)?;
}
}
}
@ -1192,6 +1235,8 @@ impl QcowFile {
refcounts: &mut [u64],
header: &QcowHeader,
cluster_size: u64,
max_refcount: u64,
refcount_bits: u64,
) -> Result<()> {
let refcount_table_offset = header.refcount_table_offset;
for i in 0..u64::from(header.refcount_table_clusters) {
@ -1199,6 +1244,8 @@ impl QcowFile {
refcounts,
cluster_size,
refcount_table_offset + i * cluster_size,
max_refcount,
refcount_bits,
)?;
}
Ok(())
@ -1211,6 +1258,8 @@ impl QcowFile {
refcounts: &mut [u64],
cluster_size: u64,
refblock_clusters: u64,
max_refcount: u64,
refcount_bits: u64,
) -> Result<Vec<u64>> {
let mut ref_table = vec![0; refblock_clusters as usize];
let mut first_free_cluster: u64 = 0;
@ -1226,7 +1275,13 @@ impl QcowFile {
}
*refblock_addr = first_free_cluster * cluster_size;
add_ref(refcounts, cluster_size, *refblock_addr)?;
add_ref(
refcounts,
cluster_size,
*refblock_addr,
max_refcount,
refcount_bits,
)?;
first_free_cluster += 1;
}
@ -1293,6 +1348,11 @@ impl QcowFile {
.len();
let refcount_bits = 1u64 << header.refcount_order;
let max_refcount = if refcount_bits == 64 {
u64::MAX
} else {
(1u64 << refcount_bits) - 1
};
let refcount_block_entries = cluster_size * 8 / refcount_bits;
let pointers_per_cluster = cluster_size / size_of::<u64>() as u64;
let data_clusters = div_round_up_u64(header.size, cluster_size);
@ -1325,13 +1385,38 @@ impl QcowFile {
let mut refcounts = vec![0; max_valid_cluster_index as usize];
// Find all references clusters and rebuild refcounts.
set_header_refcount(&mut refcounts, cluster_size)?;
set_l1_refcounts(&mut refcounts, &header, cluster_size)?;
set_data_refcounts(&mut refcounts, &header, cluster_size, raw_file)?;
set_refcount_table_refcounts(&mut refcounts, &header, cluster_size)?;
set_header_refcount(&mut refcounts, cluster_size, max_refcount, refcount_bits)?;
set_l1_refcounts(
&mut refcounts,
&header,
cluster_size,
max_refcount,
refcount_bits,
)?;
set_data_refcounts(
&mut refcounts,
&header,
cluster_size,
raw_file,
max_refcount,
refcount_bits,
)?;
set_refcount_table_refcounts(
&mut refcounts,
&header,
cluster_size,
max_refcount,
refcount_bits,
)?;
// Allocate clusters to store the new reference count blocks.
let ref_table = alloc_refblocks(&mut refcounts, cluster_size, refblock_clusters)?;
let ref_table = alloc_refblocks(
&mut refcounts,
cluster_size,
refblock_clusters,
max_refcount,
refcount_bits,
)?;
// Write updated reference counts and point the reftable at them.
write_refblocks(
@ -1856,6 +1941,9 @@ impl QcowFile {
Err(refcount::Error::ReadingRefCounts(e)) => {
return Err(e);
}
Err(refcount::Error::RefcountOverflow { .. }) => {
return Err(std::io::Error::from_raw_os_error(EINVAL));
}
}
}

View file

@ -29,6 +29,13 @@ pub enum Error {
/// `ReadingRefCounts` - Error reading the file into the refcount cache.
#[error("Failed to read the file into the refcount cache")]
ReadingRefCounts(#[source] io::Error),
/// `RefcountOverflow` - Refcount value exceeds maximum for the refcount width.
#[error("Refcount value {value} exceeds {refcount_bits}-bit max ({max})")]
RefcountOverflow {
value: u64,
max: u64,
refcount_bits: u64,
},
}
pub type Result<T> = std::result::Result<T, Error>;
@ -42,12 +49,15 @@ pub struct RefCount {
refcount_block_entries: u64, // number of refcounts in a cluster.
cluster_size: u64,
max_valid_cluster_offset: u64,
max_refcount: u64, // maximum refcount value for this image's refcount_order
refcount_bits: u64, // number of bits per refcount entry
}
impl RefCount {
/// Creates a `RefCount` from `file`, reading the refcount table from `refcount_table_offset`.
/// `refcount_table_entries` specifies the number of refcount blocks used by this image.
/// `refcount_block_entries` indicates the number of refcounts in each refcount block.
/// `refcount_bits` is the number of bits per refcount (1, 2, 4, 8, 16, 32, or 64).
/// Each refcount table entry points to a refcount block.
pub fn new(
raw_file: &mut QcowRawFile,
@ -55,6 +65,7 @@ impl RefCount {
refcount_table_entries: u64,
refcount_block_entries: u64,
cluster_size: u64,
refcount_bits: u64,
) -> io::Result<RefCount> {
let ref_table = VecCache::from_vec(raw_file.read_pointer_table(
refcount_table_offset,
@ -63,6 +74,11 @@ impl RefCount {
)?);
let max_valid_cluster_index = (ref_table.len() as u64) * refcount_block_entries - 1;
let max_valid_cluster_offset = max_valid_cluster_index * cluster_size;
let max_refcount = if refcount_bits >= 64 {
u64::MAX
} else {
(1u64 << refcount_bits) - 1
};
Ok(RefCount {
ref_table,
refcount_table_offset,
@ -70,6 +86,8 @@ impl RefCount {
refcount_block_entries,
cluster_size,
max_valid_cluster_offset,
max_refcount,
refcount_bits,
})
}
@ -95,6 +113,14 @@ impl RefCount {
refcount: u64,
mut new_cluster: Option<(u64, VecCache<u64>)>,
) -> Result<Option<u64>> {
if refcount > self.max_refcount {
return Err(Error::RefcountOverflow {
value: refcount,
max: self.max_refcount,
refcount_bits: self.refcount_bits,
});
}
let (table_index, block_index) = self.get_refcount_index(cluster_address);
let block_addr_disk = *self.ref_table.get(table_index).ok_or(Error::InvalidIndex)?;