///|
pub(all) struct AuditIssue {
severity : String
code : String
offset : UInt64
message : String
}
///|
pub(all) struct AuditReport {
issues : Array[AuditIssue]
scanned_bytes : UInt64
mappings : Int32
complete : Bool
}
///|
pub fn AuditReport::ok(self : AuditReport) -> Bool {
if !self.complete {
return false
}
for issue in self.issues {
if issue.severity == "error" {
return false
}
}
true
}
///|
fn overlaps(
a : UInt64,
a_length : UInt64,
b : UInt64,
b_length : UInt64,
) -> Bool {
a < b + b_length && b < a + a_length
}
///|
/// Validated metadata union used for O(log n) data/metadata overlap queries.
fn merged_regions(
regions : Array[HostRegion],
) -> Array[HostRegion] raise QcowError {
let sorted = regions.copy()
sorted.sort_by(fn(a, b) {
if a.offset < b.offset {
-1
} else if a.offset > b.offset {
1
} else {
0
}
})
let result : Array[HostRegion] = []
for region in sorted {
ignore(add(region.offset, region.length))
if region.length == 0UL {
continue
}
if !result.is_empty() {
let last = result[result.length() - 1]
let end = last.offset + last.length
if region.offset <= end {
let next_end = region.offset + region.length
if next_end > end {
result[result.length() - 1] = {
..last,
length: next_end - last.offset,
}
}
continue
}
}
result.push({ ..region, role: "metadata", })
}
result
}
///|
fn overlaps_metadata(
regions : Array[HostRegion],
offset : UInt64,
length : UInt64,
) -> Bool {
let mut low = 0
let mut high = regions.length()
while low < high {
let middle = low + (high - low) / 2
let region = regions[middle]
if region.offset + region.length <= offset {
low = middle + 1
} else {
high = middle
}
}
low < regions.length() &&
overlaps(offset, length, regions[low].offset, regions[low].length)
}
///|
/// Audit active guest mappings and metadata references without modifying bytes.
/// This checks referenced counts are nonzero, not exact global refcount equality:
/// VM-state and bitmap allocations are outside this audit's ownership model.
pub fn Image::audit(
self : Image,
verify_data~ : Bool = false,
max_issues~ : Int32 = 1000,
) -> AuditReport raise QcowError {
if max_issues <= 0 || max_issues > 1000000 {
raise Invalid("invalid audit issue limit")
}
let issues = self.layout_issues(max_issues~)
if issues.length() >= max_issues {
return { issues, scanned_bytes: 0UL, mappings: 0, complete: false, }
}
let metadata = merged_regions(self.metadata_regions())
let cluster = self.cluster_size().to_uint64()
let counted : Map[UInt64, Bool] = Map([])
let mut operations = 0
for region in metadata {
let first = region.offset / cluster
let last = ceiling(region.offset + region.length, cluster)
for index = first; index < last; index = index + 1UL {
if operations >= self.limits.max_scan_clusters {
return { issues, scanned_bytes: 0UL, mappings: 0, complete: false, }
}
operations = operations + 1
let offset = index * cluster
if self.refcount_at(offset) == 0UL {
issues.push({
severity: "error",
code: "unreferenced-metadata",
offset,
message: "referenced metadata cluster has zero refcount",
})
if issues.length() >= max_issues {
return { issues, scanned_bytes: 0UL, mappings: 0, complete: false, }
}
}
}
}
let mut position = 0UL
let mut mappings = 0
while position < self.virtual_size() {
if mappings >= self.limits.max_scan_clusters {
return { issues, scanned_bytes: position, mappings, complete: false, }
}
let extent = self.extent_at(position)
let host = match (extent.kind, extent.physical) {
(Data, Some(value)) => Some((value, extent.length))
(Compressed, Some(value)) =>
Some((value, extent.stored_length.to_uint64()))
_ => None
}
if host is Some((offset, length)) {
if overlaps_metadata(metadata, offset, length) {
issues.push({
severity: "error",
code: "data-overlaps-metadata",
offset: position,
message: "guest data points into image metadata",
})
}
let first = offset / cluster
let last = ceiling(offset + length, cluster)
for index = first; index < last; index = index + 1UL {
if !counted.contains(index) {
counted[index] = true
let count = self.refcount_at(index * cluster)
if count == 0UL {
issues.push({
severity: "error",
code: "unreferenced-data",
offset: position,
message: "referenced guest cluster has zero refcount",
})
}
if counted.length() > self.limits.max_scan_clusters {
return {
issues,
scanned_bytes: position,
mappings,
complete: false,
}
}
}
}
}
if verify_data {
let mut verified = 0UL
while verified < extent.length {
let count = min64(
extent.length - verified,
self.limits.max_read.to_uint64(),
).to_int()
ignore(self.read_at(position + verified, count))
verified = verified + count.to_uint64()
}
}
position = position + extent.length
mappings = mappings + 1
if issues.length() >= max_issues {
return { issues, scanned_bytes: position, mappings, complete: false, }
}
}
{ issues, scanned_bytes: position, mappings, complete: true, }
}
///|
pub(all) struct Reference {
guest_offset : UInt64
guest_length : UInt64
physical_offset : UInt64
physical_length : UInt64
compressed : Bool
}
///|
/// Reverse mapping of a host byte range to active local guest references.
/// Backing data is not included because its addresses belong to other files.
pub fn Image::references_to(
self : Image,
host_offset : UInt64,
host_length : UInt64,
) -> Array[Reference] raise QcowError {
self.source.check_range(host_offset, host_length)
let result : Array[Reference] = []
if host_length == 0UL {
return result
}
ignore(
self.visit_extents(0UL, self.virtual_size(), extent => {
match extent.physical {
Some(physical) => {
let stored_length = if extent.kind is Compressed {
extent.stored_length.to_uint64()
} else {
extent.length
}
if overlaps(host_offset, host_length, physical, stored_length) {
result.push({
guest_offset: extent.offset,
guest_length: extent.length,
physical_offset: physical,
physical_length: stored_length,
compressed: extent.kind is Compressed,
})
if result.length() >= self.limits.max_map_entries {
raise LimitExceeded
}
}
}
None => ()
}
true
}),
)
result
}
///|
/// Metadata role lookup for diagnostics at an exact host address.
pub fn Image::metadata_at(
self : Image,
offset : UInt64,
) -> Array[HostRegion] raise QcowError {
if offset >= self.physical_size() {
raise OutOfBounds
}
let result : Array[HostRegion] = []
for region in self.metadata_regions() {
if offset >= region.offset && offset - region.offset < region.length {
result.push(region)
}
}
result
}