Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions dstack/crates/qemu-acpi/fixtures/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,12 @@ ninja qemu-system-x86_64
There was no genuine upstream QEMU 11.1 release at the time of this audit, so
the `11.1` compatibility profile remains pinned to the production compatibility
fork rather than being described as an upstream release profile.

## Versions past the newest profile

A QEMU version newer than the newest profile here is generated with that
profile (`Compatibility::LATEST`) rather than rejected: most releases leave the
Q35 ACPI ABI untouched, and when one does change it the generated blobs stop
matching the measured ones, which is strictly more informative than refusing to
generate. Adding a profile therefore means adding its fixtures **and** moving
`Compatibility::LATEST` to it.
7 changes: 6 additions & 1 deletion dstack/crates/qemu-acpi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
//! This crate intentionally models the observable ACPI ABI rather than a
//! virtual machine. Its output is expected to match QEMU byte for byte for a
//! supported compatibility profile and machine topology.
//!
//! QEMU releases newer than the newest modeled profile are generated with that
//! profile (see [`QemuVersion::compatibility`]) instead of being rejected, so a
//! QEMU upgrade that leaves the ACPI ABI alone keeps working and one that does
//! not surfaces as a blob mismatch the caller can act on.

mod aml_patch;
mod cpu;
Expand All @@ -33,7 +38,7 @@ pub struct AcpiBlobs {
pub enum Error {
#[error(transparent)]
Topology(#[from] TopologyError),
#[error("unsupported QEMU compatibility profile: {0}")]
#[error("no ACPI compatibility profile for QEMU {0}; releases older than 8.0 are not modeled")]
UnsupportedVersion(QemuVersion),
#[error("malformed generated ACPI tables: missing {0}")]
MalformedTables(String),
Expand Down
65 changes: 65 additions & 0 deletions dstack/crates/qemu-acpi/src/profile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,19 @@ impl QemuVersion {
}
}

/// Map a QEMU version onto the ACPI compatibility profile to generate with.
///
/// Versions newer than the newest modeled profile fall back to
/// [`Compatibility::LATEST`] rather than failing: most QEMU releases do not
/// change the Q35 ACPI ABI, so extrapolating is right far more often than
/// not, and when it is wrong the generated blobs simply do not match the
/// measured ones. Refusing to generate turns every such deployment into a
/// verification error, which is the same outcome with less information, so
/// the caller is left to decide what a mismatch means.
///
/// Versions older than 8.0 return `None`: their ABI was never modeled, and
/// clamping *down* to the oldest profile would be a guess in the direction
/// where QEMU's ACPI output is known to differ.
pub const fn compatibility(self) -> Option<Compatibility> {
match (self.major, self.minor) {
(8, _) => Some(Compatibility::V8),
Expand All @@ -29,6 +42,7 @@ impl QemuVersion {
(10, _) => Some(Compatibility::V10),
(11, 0) => Some(Compatibility::V11_0),
(11, 1..) => Some(Compatibility::V11_1),
(12.., _) => Some(Compatibility::LATEST),
_ => None,
}
}
Expand Down Expand Up @@ -76,3 +90,54 @@ pub enum Compatibility {
V11_0,
V11_1,
}

impl Compatibility {
/// Newest modeled profile, used for QEMU versions released after it.
/// Update this together with every new profile.
pub const LATEST: Self = Self::V11_1;
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn versions_map_to_their_own_profile() {
let cases = [
((8, 2, 2), Compatibility::V8),
((9, 1, 0), Compatibility::V9Pre92),
((9, 2, 1), Compatibility::V9_2),
((10, 0, 0), Compatibility::V10),
((11, 0, 3), Compatibility::V11_0),
((11, 1, 0), Compatibility::V11_1),
];
for ((major, minor, micro), expected) in cases {
assert_eq!(
QemuVersion::new(major, minor, micro).compatibility(),
Some(expected),
"{major}.{minor}.{micro}"
);
}
}

/// A QEMU release newer than the newest modeled profile must still produce
/// blobs: if its ACPI ABI is unchanged they match, and if it changed the
/// caller sees a digest mismatch instead of a "cannot generate" error.
#[test]
fn versions_newer_than_the_newest_profile_clamp_to_it() {
for version in [
QemuVersion::new(11, 9, 0),
QemuVersion::new(12, 0, 0),
QemuVersion::new(99, 4, 1),
] {
assert_eq!(version.compatibility(), Some(Compatibility::LATEST));
}
}
Comment thread
kvinwang marked this conversation as resolved.

#[test]
fn versions_older_than_the_oldest_profile_are_rejected() {
for version in [QemuVersion::new(7, 2, 0), QemuVersion::new(0, 0, 0)] {
assert_eq!(version.compatibility(), None);
}
}
}
16 changes: 15 additions & 1 deletion dstack/crates/qemu-acpi/src/tables.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ fn write_bytes(data: &mut [u8], offset: usize, value: &[u8], context: &str) -> R
/// DSDT with `iasl`, map the relevant ASL object back to its AML byte sequence,
/// and record the package-length or insertion offset relative to the DSDT
/// signature. Verify every offset against the reference binary and its
/// one-device output. Never infer offsets from a Rust-generated blob.
/// one-device output. Never infer offsets from a Rust-generated blob. A new
/// profile also has to move `Compatibility::LATEST`, which decides what newer
/// QEMU versions are generated with.
struct Layout {
/// Trimmed QEMU `etc/acpi/tables` fixture used as the immutable template.
base: &'static [u8],
Expand Down Expand Up @@ -618,6 +620,18 @@ mod tests {
}
}

/// A QEMU release past the newest modeled profile generates with that
/// profile instead of erroring, so an ABI-preserving upgrade keeps
/// verifying and an ABI-changing one surfaces as a blob mismatch.
#[test]
fn unmodeled_newer_versions_generate_with_the_latest_profile() -> Result<(), Error> {
let latest = build(&config(1, 0))?;
let mut newer = config(1, 0);
newer.qemu_version = QemuVersion::new(12, 0, 0);
assert_eq!(build(&newer)?, latest);
Ok(())
}

#[test]
fn one_nic_matches_qemu_byte_for_byte() -> Result<(), Error> {
let actual = build(&config(1, 0))?;
Expand Down
Loading