From 05aad40802e09bc340727ee9dc9c86ced4f4a101 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Thu, 10 Sep 2026 00:25:26 -0700 Subject: [PATCH] fix(vmm): let auto TDX attestation follow the image, not guest RAM `auto` refused lite below 3 GiB, exempting exactly 2 GiB. That came from QEMU's setup-header rewrite, which moves the initrd with guest RAM and so makes the patched kernel Authenticode hash memory-dependent. Images whose OVMF normalizes the setup header measure the shipped `bzImage` instead, which no guest RAM size can move -- and that is every image the build system can now produce: `os/image/assemble.sh` hardcodes `kernel_header_normalized` and the OVMF build fails when the normalization patch does not apply. So the heuristic now only costs deployments the cheaper verification path: a 1 GiB CVM on a current image got legacy despite carrying everything lite needs. Drop it and follow `image_supports_lite` alone; `resolve` no longer takes the memory size. A pre-normalization image is the one case this stops covering. The no-download verifier still rejects those below the threshold, with an error that names the memory sizes and says to re-emit the image, so the failure is explicit rather than a silent mismatch, and `tdx_attestation_variant = "legacy"` keeps one running as is. --- dstack/vmm/src/app.rs | 12 +++---- dstack/vmm/src/config.rs | 69 +++++++++++++++++++--------------------- dstack/vmm/vmm.toml | 6 ++-- 3 files changed, 42 insertions(+), 45 deletions(-) diff --git a/dstack/vmm/src/app.rs b/dstack/vmm/src/app.rs index baa947078..959aebec9 100644 --- a/dstack/vmm/src/app.rs +++ b/dstack/vmm/src/app.rs @@ -1734,7 +1734,7 @@ fn make_vm_config( tdx_attestation_variant_from_requirements(requirements).unwrap_or_else(|| { cfg.cvm .tdx_attestation_variant - .resolve(manifest.memory, image_supports_tdx_lite(image)) + .resolve(image_supports_tdx_lite(image)) }) } else { dstack_types::TdxAttestationVariant::Legacy @@ -2565,17 +2565,17 @@ mod tests { Ok(()) } + /// 1 GiB used to fall back to legacy: it is below 3 GiB and not the 2 GiB + /// exemption. That heuristic existed for images whose OVMF leaves the setup + /// header for QEMU to rewrite, which the build system no longer produces. #[test] - fn tdx_auto_variant_uses_legacy_for_low_non_2g_memory() -> Result<()> { + fn tdx_auto_variant_uses_lite_for_low_non_2g_memory() -> Result<()> { let config = test_tdx_config()?; let manifest = test_manifest(1024); let image = test_tdx_image(true); let vm_config = make_vm_config(&config, &manifest, &image, &hex_of(0x22, 32), None, None)?; - assert!(vm_config.get("tdx_attestation_variant").is_none()); - // tdx_measurement is attached whenever the image supports it, even - // when the resolved variant is legacy, so a verifier can still - // choose lite verification for this boot. + assert_eq!(vm_config["tdx_attestation_variant"], "lite"); assert!(vm_config.get("tdx_measurement").is_some()); assert_eq!( vm_config["os_image_hash"] diff --git a/dstack/vmm/src/config.rs b/dstack/vmm/src/config.rs index 2e82e67c6..81d9d2cb3 100644 --- a/dstack/vmm/src/config.rs +++ b/dstack/vmm/src/config.rs @@ -272,20 +272,31 @@ pub enum TdxAttestationVariantConfig { } impl TdxAttestationVariantConfig { - const TWO_GIB_MIB: u32 = 2 * 1024; - const THREE_GIB_MIB: u32 = 3 * 1024; - - pub fn resolve(self, memory_mib: u32, image_supports_lite: bool) -> TdxAttestationVariant { + /// `auto` follows the image and nothing else. + /// + /// It used to also refuse lite below 3 GiB, exempting exactly 2 GiB, + /// because QEMU's setup-header rewrite moves the initrd with guest RAM and + /// so makes the patched kernel Authenticode hash memory-dependent. Images + /// whose OVMF normalizes the setup header measure the shipped `bzImage` + /// instead, which no guest RAM size can move, and that is every image the + /// build system can now produce: it hardcodes `kernel_header_normalized` + /// and fails when the OVMF patch does not apply. + /// + /// A pre-normalization image is the one case this no longer covers. The + /// no-download verifier still rejects those below the threshold, with an + /// error naming the memory sizes and telling the operator to re-emit the + /// image, so the failure is explicit rather than a silent mismatch. Set + /// `tdx_attestation_variant = "legacy"` to keep running one as is. + pub fn resolve(self, image_supports_lite: bool) -> TdxAttestationVariant { + use TdxAttestationVariant::{Legacy, Lite}; match self { - Self::Legacy => TdxAttestationVariant::Legacy, - Self::Lite => TdxAttestationVariant::Lite, + Self::Legacy => Legacy, + Self::Lite => Lite, Self::Auto => { - if memory_mib < Self::THREE_GIB_MIB && memory_mib != Self::TWO_GIB_MIB { - TdxAttestationVariant::Legacy - } else if image_supports_lite { - TdxAttestationVariant::Lite + if image_supports_lite { + Lite } else { - TdxAttestationVariant::Legacy + Legacy } } } @@ -360,9 +371,10 @@ pub struct CvmConfig { /// TDX attestation/hash scheme policy. `legacy` keeps the existing /// digest.txt measurement path; `lite` opts into split measurement CBOR; - /// `auto` selects `legacy` for - /// CVMs below 3 GiB except exactly 2 GiB, otherwise uses `lite` when the - /// image carries TDX measurement material and falls back to `legacy`. + /// `auto` uses `lite` when the image carries TDX measurement material and + /// falls back to `legacy` when it does not. See + /// [`TdxAttestationVariantConfig::resolve`] for why memory size no longer + /// takes part. #[serde(default)] pub tdx_attestation_variant: TdxAttestationVariantConfig, @@ -1164,30 +1176,13 @@ mod tests { use dstack_types::TdxAttestationVariant::{Legacy, Lite}; - // Explicit settings bypass auto heuristics. - assert_eq!( - TdxAttestationVariantConfig::Legacy.resolve(2048, true), - Legacy - ); - assert_eq!(TdxAttestationVariantConfig::Lite.resolve(1024, false), Lite); - - // Auto avoids lite for sub-3 GiB memory sizes except exactly 2 GiB. - assert_eq!( - TdxAttestationVariantConfig::Auto.resolve(1024, true), - Legacy - ); - assert_eq!( - TdxAttestationVariantConfig::Auto.resolve(2816, true), - Legacy - ); - assert_eq!(TdxAttestationVariantConfig::Auto.resolve(2048, true), Lite); + // Explicit settings bypass auto entirely. + assert_eq!(TdxAttestationVariantConfig::Legacy.resolve(true), Legacy); + assert_eq!(TdxAttestationVariantConfig::Lite.resolve(false), Lite); - // At 3 GiB and above, auto follows image support. - assert_eq!(TdxAttestationVariantConfig::Auto.resolve(3072, true), Lite); - assert_eq!( - TdxAttestationVariantConfig::Auto.resolve(3072, false), - Legacy - ); + // Auto follows image support alone; memory no longer takes part. + assert_eq!(TdxAttestationVariantConfig::Auto.resolve(true), Lite); + assert_eq!(TdxAttestationVariantConfig::Auto.resolve(false), Legacy); } fn default_config() -> Config { diff --git a/dstack/vmm/vmm.toml b/dstack/vmm/vmm.toml index c7b7accf9..27889ee89 100644 --- a/dstack/vmm/vmm.toml +++ b/dstack/vmm/vmm.toml @@ -84,8 +84,10 @@ qemu_hotplug_off = false # TDX attestation/hash scheme policy: # - "legacy": digest.txt + legacy verifier # - "lite": digest.txt + measurement.tdx.cbor + no-QEMU verifier -# - "auto": legacy for CVM memory below 3 GiB except exactly 2 GiB; otherwise -# lite when the image supports it, legacy when it does not. +# - "auto": lite when the image ships measurement.tdx.cbor, legacy when it does +# not. Memory size does not take part: the kernel hash only depended on it +# for images whose OVMF leaves the setup header for QEMU to rewrite, and the +# build system no longer produces those. tdx_attestation_variant = "auto" host_share_mode = "9p"