Skip to content

Commit a304dd9

Browse files
committed
dstack-mr: support edk2-stable202505 OVMF event layout
OVMF was upgraded from an untagged 2024-09 snapshot to edk2-stable202505 in commit f9f11f3 on meta-dstack. The newer firmware emits four extra RTMR[0] events (QEMU fw_cfg BootMenu/bootorder, EV_EFI_VARIABLE_AUTHORITY, and a second BootXXXX) and re-formats BootOrder/Boot0000 as full UEFI_VARIABLE_DATA structs. dstack-mr was still computing the legacy 13-event log, so quote replay on 0.5.10 CVMs produced wrong RTMR[0]. Add an OvmfVariant enum in dstack-types and dispatch rtmr0_log on it. The new variant's digests come from a 0.5.10 CVM's actual event log; they're firmware constants for this OVMF build under the standard -kernel boot config. Plumbing: - VmConfig.ovmf_variant: Option<OvmfVariant>: explicit source of truth for verifiers, set by VMM from image.info.ovmf_variant. - ImageInfo.ovmf_variant: ditto in metadata.json. - Verifier prefers vm_config.ovmf_variant; falls back to parsing the version suffix out of vm_config.image (e.g. "dstack-0.5.10") so pre-existing deployments without the field keep working. - dstack-mr CLI adds --dstack-os-version for explicit override. Verified end-to-end against a 0.5.10 CVM: all four measurements (MRTD, RTMR[0-2]) match the quote exactly.
1 parent 53e217c commit a304dd9

9 files changed

Lines changed: 294 additions & 26 deletions

File tree

dstack-mr/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@ fs-err.workspace = true
2525
bon.workspace = true
2626
log.workspace = true
2727
scale.workspace = true
28+
dstack-types.workspace = true
2829

2930
[dev-dependencies]
30-
dstack-types.workspace = true
3131
reqwest = { workspace = true, features = ["blocking"] }
3232
flate2.workspace = true
3333
tar.workspace = true

dstack-mr/cli/src/main.rs

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
use anyhow::{Context, Result};
66
use clap::{Parser, Subcommand};
7-
use dstack_mr::Machine;
7+
use dstack_mr::{Machine, OvmfVariant, ovmf_variant_for_version};
88
use dstack_types::ImageInfo;
99
use fs_err as fs;
1010
use size_parser::parse_memory_size;
@@ -78,6 +78,12 @@ struct MachineConfig {
7878
#[arg(long)]
7979
qemu_version: Option<String>,
8080

81+
/// dstack OS version (MAJOR.MINOR.PATCH), used to pick the OVMF measurement layout.
82+
/// 0.5.10 <= ver < 0.6.0 and ver >= 0.6.1 use the edk2-stable202505 layout; everything
83+
/// else uses the legacy layout. If omitted, falls back to `image_info.version`.
84+
#[arg(long)]
85+
dstack_os_version: Option<String>,
86+
8187
/// Output JSON
8288
#[arg(long)]
8389
json: bool,
@@ -97,7 +103,22 @@ fn main() -> Result<()> {
97103
let firmware_path = parent_dir.join(&image_info.bios).display().to_string();
98104
let kernel_path = parent_dir.join(&image_info.kernel).display().to_string();
99105
let initrd_path = parent_dir.join(&image_info.initrd).display().to_string();
100-
let cmdline = image_info.cmdline + " initrd=initrd";
106+
let cmdline = image_info.cmdline.clone() + " initrd=initrd";
107+
108+
// CLI flag wins, then the explicit `ovmf_variant` in metadata.json,
109+
// and finally the OS version field. Older metadata.json files may
110+
// carry neither, in which case fall back to the default.
111+
let ovmf_variant = if let Some(v) = config.dstack_os_version.as_deref() {
112+
ovmf_variant_for_version(v)
113+
.with_context(|| format!("invalid dstack OS version: {v}"))?
114+
} else if let Some(variant) = image_info.ovmf_variant {
115+
variant
116+
} else if !image_info.version.is_empty() {
117+
ovmf_variant_for_version(&image_info.version)
118+
.with_context(|| format!("invalid dstack OS version: {}", image_info.version))?
119+
} else {
120+
OvmfVariant::default()
121+
};
101122

102123
let machine = Machine::builder()
103124
.cpu_count(config.cpu)
@@ -116,6 +137,7 @@ fn main() -> Result<()> {
116137
.hotplug_off(config.hotplug_off)
117138
.root_verity(config.root_verity)
118139
.maybe_qemu_version(config.qemu_version.clone())
140+
.ovmf_variant(ovmf_variant)
119141
.build();
120142

121143
let measurements = machine

dstack-mr/src/lib.rs

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22
//
33
// SPDX-License-Identifier: Apache-2.0
44

5+
use anyhow::{bail, Context, Result};
56
use serde::{Deserialize, Serialize};
67
use serde_human_bytes as hex_bytes;
78

9+
pub use dstack_types::OvmfVariant;
810
pub use machine::{Machine, TdxMeasurementDetails};
911

1012
use util::{measure_log, measure_sha384, utf16_encode};
@@ -19,6 +21,155 @@ mod num;
1921
mod tdvf;
2022
mod util;
2123

24+
/// Pick the OVMF variant for a given dstack OS version string ("MAJOR.MINOR.PATCH").
25+
///
26+
/// Treats `0.5.10 <= v < 0.6.0` and `v >= 0.6.1` as `Stable202505`, everything else as
27+
/// `Pre202505`. Used as a fallback when `VmConfig::ovmf_variant` is absent.
28+
pub fn ovmf_variant_for_version(version: &str) -> Result<OvmfVariant> {
29+
let parts: Vec<u32> = version
30+
.split('.')
31+
.map(|p| {
32+
p.parse::<u32>()
33+
.with_context(|| format!("invalid version component: {p}"))
34+
})
35+
.collect::<Result<Vec<_>, _>>()?;
36+
if parts.len() != 3 {
37+
bail!("expected MAJOR.MINOR.PATCH, got {version}");
38+
}
39+
let v = (parts[0], parts[1], parts[2]);
40+
let stable = ((0, 5, 10)..(0, 6, 0)).contains(&v) || v >= (0, 6, 1);
41+
Ok(if stable {
42+
OvmfVariant::Stable202505
43+
} else {
44+
OvmfVariant::Pre202505
45+
})
46+
}
47+
48+
/// Extract a dotted version suffix (e.g. "0.5.10") from a dstack image name like
49+
/// `dstack-0.5.10`, `dstack-dev-0.5.10`, or `dstack-nvidia-0.5.10`.
50+
///
51+
/// Returns `None` when the image name does not end with a recognisable
52+
/// `MAJOR.MINOR.PATCH` segment.
53+
pub fn extract_version_from_image_name(image: &str) -> Option<&str> {
54+
let tail = image.rsplit('-').next()?;
55+
let parts: Vec<&str> = tail.split('.').collect();
56+
if parts.len() == 3
57+
&& parts
58+
.iter()
59+
.all(|p| !p.is_empty() && p.parse::<u32>().is_ok())
60+
{
61+
Some(tail)
62+
} else {
63+
None
64+
}
65+
}
66+
67+
/// Pick the OVMF variant from an image name like `dstack-0.5.10`.
68+
///
69+
/// Falls back to `OvmfVariant::default()` (= `Pre202505`) when the image name is
70+
/// missing or doesn't carry a parseable version suffix. Use this only as a
71+
/// fallback for images that pre-date `VmConfig::ovmf_variant`.
72+
pub fn ovmf_variant_for_image(image: Option<&str>) -> OvmfVariant {
73+
image
74+
.and_then(extract_version_from_image_name)
75+
.and_then(|v| ovmf_variant_for_version(v).ok())
76+
.unwrap_or_default()
77+
}
78+
79+
#[cfg(test)]
80+
mod ovmf_variant_tests {
81+
use super::*;
82+
83+
#[test]
84+
fn pre_202505_for_old_versions() {
85+
for v in ["0.4.99", "0.5.7", "0.5.8", "0.5.9", "0.6.0"] {
86+
assert_eq!(
87+
ovmf_variant_for_version(v).unwrap(),
88+
OvmfVariant::Pre202505,
89+
"{v}"
90+
);
91+
}
92+
}
93+
94+
#[test]
95+
fn stable_202505_for_new_versions() {
96+
for v in ["0.5.10", "0.5.99", "0.6.1", "0.6.2", "0.7.0", "1.0.0"] {
97+
assert_eq!(
98+
ovmf_variant_for_version(v).unwrap(),
99+
OvmfVariant::Stable202505,
100+
"{v}"
101+
);
102+
}
103+
}
104+
105+
#[test]
106+
fn rejects_malformed_version() {
107+
assert!(ovmf_variant_for_version("0.5").is_err());
108+
assert!(ovmf_variant_for_version("0.5.10-dev").is_err());
109+
assert!(ovmf_variant_for_version("v0.5.10").is_err());
110+
}
111+
112+
#[test]
113+
fn parses_version_from_image_name() {
114+
assert_eq!(
115+
extract_version_from_image_name("dstack-0.5.10"),
116+
Some("0.5.10")
117+
);
118+
assert_eq!(
119+
extract_version_from_image_name("dstack-dev-0.5.10"),
120+
Some("0.5.10")
121+
);
122+
assert_eq!(
123+
extract_version_from_image_name("dstack-nvidia-0.5.10"),
124+
Some("0.5.10")
125+
);
126+
assert_eq!(
127+
extract_version_from_image_name("dstack-nvidia-dev-0.6.1"),
128+
Some("0.6.1")
129+
);
130+
assert_eq!(extract_version_from_image_name("dstack"), None);
131+
assert_eq!(extract_version_from_image_name("dstack-rc1"), None);
132+
assert_eq!(extract_version_from_image_name("dstack-0.5"), None);
133+
}
134+
135+
#[test]
136+
fn ovmf_variant_for_image_handles_missing_and_unknown() {
137+
assert_eq!(ovmf_variant_for_image(None), OvmfVariant::Pre202505);
138+
assert_eq!(
139+
ovmf_variant_for_image(Some("dstack")),
140+
OvmfVariant::Pre202505
141+
);
142+
assert_eq!(
143+
ovmf_variant_for_image(Some("dstack-0.5.9")),
144+
OvmfVariant::Pre202505
145+
);
146+
assert_eq!(
147+
ovmf_variant_for_image(Some("dstack-0.5.10")),
148+
OvmfVariant::Stable202505
149+
);
150+
assert_eq!(
151+
ovmf_variant_for_image(Some("dstack-nvidia-dev-0.6.1")),
152+
OvmfVariant::Stable202505
153+
);
154+
}
155+
156+
#[test]
157+
fn serializes_with_snake_case() {
158+
assert_eq!(
159+
serde_json::to_string(&OvmfVariant::Pre202505).unwrap(),
160+
"\"pre202505\""
161+
);
162+
assert_eq!(
163+
serde_json::to_string(&OvmfVariant::Stable202505).unwrap(),
164+
"\"stable202505\""
165+
);
166+
assert_eq!(
167+
serde_json::from_str::<OvmfVariant>("\"stable202505\"").unwrap(),
168+
OvmfVariant::Stable202505
169+
);
170+
}
171+
}
172+
22173
/// Contains all the measurement values for TDX.
23174
#[derive(Debug, Clone, Serialize, Deserialize)]
24175
pub struct TdxMeasurements {

dstack-mr/src/machine.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
use crate::acpi::Tables;
66
use crate::tdvf::Tdvf;
77
use crate::util::debug_print_log;
8-
use crate::{kernel, RtmrLogs, TdxMeasurements};
8+
use crate::{kernel, OvmfVariant, RtmrLogs, TdxMeasurements};
99
use crate::{measure_log, measure_sha384};
1010
use anyhow::{bail, Context, Result};
1111
use fs_err as fs;
@@ -32,6 +32,10 @@ pub struct Machine<'a> {
3232
pub root_verity: bool,
3333
#[builder(default)]
3434
pub host_share_mode: String,
35+
/// Selects which OVMF measurement event layout to expect.
36+
/// Defaults to the pre-edk2-stable202505 layout for backwards compatibility.
37+
#[builder(default)]
38+
pub ovmf_variant: OvmfVariant,
3539
}
3640

3741
fn parse_version_tuple(v: &str) -> Result<(u32, u32, u32)> {

dstack-mr/src/tdvf.rs

Lines changed: 68 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use sha2::{Digest, Sha384};
99

1010
use crate::acpi::Tables;
1111
use crate::num::read_le;
12-
use crate::{measure_log, measure_sha384, utf16_encode, Machine, RtmrLog};
12+
use crate::{measure_log, measure_sha384, utf16_encode, Machine, OvmfVariant, RtmrLog};
1313

1414
const PAGE_SIZE: u64 = 0x1000;
1515
const MR_EXTEND_GRANULARITY: usize = 0x100;
@@ -275,33 +275,79 @@ impl<'a> Tdvf<'a> {
275275
pub fn rtmr0_log(&self, machine: &Machine) -> Result<(RtmrLog, Tables)> {
276276
let td_hob_hash = self.measure_td_hob(machine.memory_size)?;
277277
let cfv_image_hash = hex!("344BC51C980BA621AAA00DA3ED7436F7D6E549197DFE699515DFA2C6583D95E6412AF21C097D473155875FFD561D6790");
278-
let boot000_hash = hex!("23ADA07F5261F12F34A0BD8E46760962D6B4D576A416F1FEA1C64BC656B1D28EACF7047AE6E967C58FD2A98BFA74C298");
279278

280279
let tables = machine.build_tables()?;
281280
let acpi_tables_hash = measure_sha384(&tables.tables);
282281
let acpi_rsdp_hash = measure_sha384(&tables.rsdp);
283282
let acpi_loader_hash = measure_sha384(&tables.loader);
284283

285-
// RTMR0 calculation
286-
287-
Ok((
288-
vec![
289-
td_hob_hash,
290-
cfv_image_hash.to_vec(),
291-
measure_tdx_efi_variable("8BE4DF61-93CA-11D2-AA0D-00E098032B8C", "SecureBoot")?,
292-
measure_tdx_efi_variable("8BE4DF61-93CA-11D2-AA0D-00E098032B8C", "PK")?,
293-
measure_tdx_efi_variable("8BE4DF61-93CA-11D2-AA0D-00E098032B8C", "KEK")?,
294-
measure_tdx_efi_variable("D719B2CB-3D3A-4596-A3BC-DAD00E67656F", "db")?,
295-
measure_tdx_efi_variable("D719B2CB-3D3A-4596-A3BC-DAD00E67656F", "dbx")?,
296-
measure_sha384(&[0x00, 0x00, 0x00, 0x00]), // Separator
297-
acpi_loader_hash,
298-
acpi_rsdp_hash,
299-
acpi_tables_hash,
300-
measure_sha384(&[0x00, 0x00]), // BootOrder
301-
boot000_hash.to_vec(),
302-
],
303-
tables,
304-
))
284+
let secureboot_hash =
285+
measure_tdx_efi_variable("8BE4DF61-93CA-11D2-AA0D-00E098032B8C", "SecureBoot")?;
286+
let pk_hash = measure_tdx_efi_variable("8BE4DF61-93CA-11D2-AA0D-00E098032B8C", "PK")?;
287+
let kek_hash = measure_tdx_efi_variable("8BE4DF61-93CA-11D2-AA0D-00E098032B8C", "KEK")?;
288+
let db_hash = measure_tdx_efi_variable("D719B2CB-3D3A-4596-A3BC-DAD00E67656F", "db")?;
289+
let dbx_hash = measure_tdx_efi_variable("D719B2CB-3D3A-4596-A3BC-DAD00E67656F", "dbx")?;
290+
let separator_hash = measure_sha384(&[0x00, 0x00, 0x00, 0x00]);
291+
292+
let log = match machine.ovmf_variant {
293+
OvmfVariant::Pre202505 => {
294+
// Boot0000 = OVMF UiApp (fixed digest for pre-202505 firmware).
295+
let boot000_hash = hex!("23ADA07F5261F12F34A0BD8E46760962D6B4D576A416F1FEA1C64BC656B1D28EACF7047AE6E967C58FD2A98BFA74C298");
296+
vec![
297+
td_hob_hash,
298+
cfv_image_hash.to_vec(),
299+
secureboot_hash,
300+
pk_hash,
301+
kek_hash,
302+
db_hash,
303+
dbx_hash,
304+
separator_hash,
305+
acpi_loader_hash,
306+
acpi_rsdp_hash,
307+
acpi_tables_hash,
308+
measure_sha384(&[0x00, 0x00]), // BootOrder (raw 2 bytes in legacy OVMF)
309+
boot000_hash.to_vec(),
310+
]
311+
}
312+
OvmfVariant::Stable202505 => {
313+
// edk2-stable202505 adds 4 new events and re-formats BootOrder/Boot0000
314+
// in RTMR[0]. Digests captured from a 0.5.10 dstack CVM running the
315+
// standard -kernel boot config (9p host_share_mode); they are firmware
316+
// constants for this OVMF build.
317+
let bootmenu_fwcfg_hash = measure_sha384(&[0x00, 0x00]); // BootMenu fw_cfg = 0x0000
318+
let bootorder_fwcfg_hash =
319+
hex!("53D3DBC00691328371E74C1B1C77BDF1BF4FF79DD1B55222B694FE8341CD8B0ADB3A9AB9286552C681272883B9B0FC1D");
320+
let variable_authority_hash =
321+
hex!("FB66919801F1DFC9C4C273B6A739380790CB0FD3CB706A42F6AC050510EBC8618E7FBA53A1564522F5C6F0DC9E1F41A6");
322+
let boot_order_var_hash =
323+
hex!("52B9A02DE946B947364B57D8210C63113B9058996E2A3BA7CEAD54AF11AE0873B085D1E52BC01E4FEBE57CA05CA1332B");
324+
let boot0000_hash =
325+
hex!("5068E6A9DED2A1C3A8EBB5D26004410EA8670742D8F444C5C3D161B76C66FA23A7B1D2FB3F9840570B675384B5818F2D");
326+
let boot0001_hash =
327+
hex!("DD424F2EEB35F3E8A2C2F50F6CC87FF90B7577E92CE63E13A22869D07D104FD5EA9800E6E4F12C5058FC4EAA78374F20");
328+
vec![
329+
td_hob_hash,
330+
cfv_image_hash.to_vec(),
331+
bootmenu_fwcfg_hash,
332+
bootorder_fwcfg_hash.to_vec(),
333+
secureboot_hash,
334+
pk_hash,
335+
kek_hash,
336+
db_hash,
337+
dbx_hash,
338+
separator_hash,
339+
acpi_loader_hash,
340+
acpi_rsdp_hash,
341+
acpi_tables_hash,
342+
variable_authority_hash.to_vec(),
343+
boot_order_var_hash.to_vec(),
344+
boot0000_hash.to_vec(),
345+
boot0001_hash.to_vec(),
346+
]
347+
}
348+
};
349+
350+
Ok((log, tables))
305351
}
306352

307353
fn measure_td_hob(&self, memory_size: u64) -> Result<Vec<u8>> {

0 commit comments

Comments
 (0)