From 666a27fc2ab70b906d3200cc037647d5a8d43416 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Wed, 15 Jul 2026 19:25:52 -0700 Subject: [PATCH] feat(attest): support TSM path overrides and detection Detect TDX through configfs TSM providers as well as /dev/tdx_guest, and allow CCEL/RTMR/configfs paths to be overridden via environment variables. Also recognize tdx_guest* providers in dstack-prepare. --- dstack/Cargo.lock | 1 + dstack/cc-eventlog/src/tcg.rs | 10 +- dstack/cc-eventlog/src/tdx.rs | 16 ++++ dstack/dstack-attest/src/attestation.rs | 2 +- dstack/tdx-attest/Cargo.toml | 1 + dstack/tdx-attest/src/dummy.rs | 4 + dstack/tdx-attest/src/linux.rs | 117 ++++++++++++++++++++---- os/common/rootfs/dstack-prepare.sh | 17 +++- 8 files changed, 144 insertions(+), 24 deletions(-) diff --git a/dstack/Cargo.lock b/dstack/Cargo.lock index 65edd0421..c22429043 100644 --- a/dstack/Cargo.lock +++ b/dstack/Cargo.lock @@ -7978,6 +7978,7 @@ dependencies = [ "serde-human-bytes", "serde_json", "sha2 0.10.9", + "tempfile", "thiserror 2.0.18", "tokio", "vsock", diff --git a/dstack/cc-eventlog/src/tcg.rs b/dstack/cc-eventlog/src/tcg.rs index ff0aadc32..f41f7b737 100644 --- a/dstack/cc-eventlog/src/tcg.rs +++ b/dstack/cc-eventlog/src/tcg.rs @@ -7,9 +7,11 @@ use crate::{codecs::VecOf, tdx::TdxEvent}; use anyhow::{Context, Result}; use scale::Decode; +use std::path::PathBuf; /// The path to boottime ccel file. const CCEL_FILE: &str = "/sys/firmware/acpi/tables/data/CCEL"; +const CCEL_FILE_ENV: &str = "DSTACK_CCEL_FILE"; pub const TPM_ALG_ERROR: u16 = 0x0; pub const TPM_ALG_RSA: u16 = 0x1; @@ -300,8 +302,12 @@ impl TcgEventLog { } pub fn decode_from_ccel_file() -> Result { - let data = fs_err::read(CCEL_FILE).context("Failed to read CCEL")?; - Self::decode(&mut data.as_slice()) + let path = std::env::var_os(CCEL_FILE_ENV) + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from(CCEL_FILE)); + let data = fs_err::read(&path) + .with_context(|| format!("failed to read CCEL from {}", path.display()))?; + Self::decode(&mut data.as_slice()).context("failed to decode CCEL") } pub fn to_cc_event_log(&self) -> Result> { diff --git a/dstack/cc-eventlog/src/tdx.rs b/dstack/cc-eventlog/src/tdx.rs index ae3e62c5d..615c32ee9 100644 --- a/dstack/cc-eventlog/src/tdx.rs +++ b/dstack/cc-eventlog/src/tdx.rs @@ -129,6 +129,14 @@ pub fn label_tdx_acpi_data_events(event_logs: &mut [TdxEvent]) { } } +/// Decode a raw CCEL byte stream into dstack's TDX event representation. +pub fn decode_ccel(data: &[u8]) -> Result> { + let mut input = data; + let mut event_logs = TcgEventLog::decode(&mut input)?.to_cc_event_log()?; + label_tdx_acpi_data_events(&mut event_logs); + Ok(event_logs) +} + /// Read both boottime and runtime event logs. pub fn read_event_log() -> Result> { let mut event_logs = TcgEventLog::decode_from_ccel_file()?.to_cc_event_log()?; @@ -172,4 +180,12 @@ mod tests { assert_eq!(events[0].event, ""); assert_eq!(events[4].event, "app-id"); } + + #[test] + fn decodes_the_bundled_ccel() { + let events = decode_ccel(include_bytes!("../samples/ccel.bin")).unwrap(); + assert!(!events.is_empty()); + assert!(events.iter().all(|event| event.imr <= 3)); + assert!(events.iter().all(|event| event.digest().len() == 48)); + } } diff --git a/dstack/dstack-attest/src/attestation.rs b/dstack/dstack-attest/src/attestation.rs index be1da10a3..c7fc6f038 100644 --- a/dstack/dstack-attest/src/attestation.rs +++ b/dstack/dstack-attest/src/attestation.rs @@ -280,7 +280,7 @@ fn choose_dstack_attestation_mode(has_tdx: bool, has_sev_snp: bool) -> Result Result { - let has_tdx = std::path::Path::new("/dev/tdx_guest").exists(); + let has_tdx = tdx_attest::is_tdx_available(); let has_sev_snp = std::path::Path::new("/dev/sev-guest").exists() || has_sev_snp_tsm_provider(); diff --git a/dstack/tdx-attest/Cargo.toml b/dstack/tdx-attest/Cargo.toml index 661b2f5fe..83860d9f3 100644 --- a/dstack/tdx-attest/Cargo.toml +++ b/dstack/tdx-attest/Cargo.toml @@ -32,3 +32,4 @@ insta.workspace = true serde_json.workspace = true dcap-qvl.workspace = true tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } +tempfile.workspace = true diff --git a/dstack/tdx-attest/src/dummy.rs b/dstack/tdx-attest/src/dummy.rs index c6002f6fe..b153474de 100644 --- a/dstack/tdx-attest/src/dummy.rs +++ b/dstack/tdx-attest/src/dummy.rs @@ -49,3 +49,7 @@ pub fn get_report(_report_data: &TdxReportData) -> Result { pub fn extend_rtmr(_index: u32, _event_type: u32, _digest: [u8; 48]) -> Result<()> { Err(TdxAttestError::NotSupported) } + +pub fn is_tdx_available() -> bool { + false +} diff --git a/dstack/tdx-attest/src/linux.rs b/dstack/tdx-attest/src/linux.rs index 92d0d7f70..9820ba792 100644 --- a/dstack/tdx-attest/src/linux.rs +++ b/dstack/tdx-attest/src/linux.rs @@ -33,6 +33,7 @@ const CONFIGFS_BASE: &str = "/sys/kernel/config/tsm/report"; const CONFIGFS_DEFAULT: &str = "/sys/kernel/config/tsm/report/com.intel.dcap"; const CONFIGFS_PATH_ENV: &str = "DCAP_TDX_QUOTE_CONFIGFS_PATH"; const RTMR_SYSFS_BASE: &str = "/sys/devices/virtual/misc/tdx_guest/measurements"; +const RTMR_SYSFS_PATH_ENV: &str = "DCAP_TDX_RTMR_SYSFS_PATH"; const TDX_ATTEST_CONFIG_PATH: &str = "/etc/tdx-attest.conf"; const TDX_REPORT_DATA_SIZE: usize = 64; @@ -166,7 +167,7 @@ static CONFIGFS_MKDIR_TRIED: Mutex = Mutex::new(false); pub fn get_quote(report_data: &TdxReportData) -> Result> { let _guard = TDX_LOCK.lock().map_err(|_| TdxAttestError::Busy)?; - if is_configfs_available() { + if should_try_configfs() { return get_quote_via_configfs(report_data); } @@ -179,8 +180,35 @@ pub fn get_quote(report_data: &TdxReportData) -> Result> { )) } -fn is_configfs_available() -> bool { - Path::new(CONFIGFS_DEFAULT).is_dir() || Path::new(CONFIGFS_BASE).is_dir() +fn should_try_configfs() -> bool { + // An explicit override is authoritative. Let prepare_configfs() report an + // invalid path instead of silently falling back to another quote method. + std::env::var_os(CONFIGFS_PATH_ENV).is_some() + || Path::new(CONFIGFS_DEFAULT).is_dir() + || Path::new(CONFIGFS_BASE).is_dir() +} + +/// Return whether a TDX guest provider is available through either the legacy +/// misc device or the standard TSM report interface. +pub fn is_tdx_available() -> bool { + if Path::new(TDX_GUEST_DEVICE).exists() { + return true; + } + + if let Some(path) = std::env::var_os(CONFIGFS_PATH_ENV) { + return verify_configfs_provider(Path::new(&path)).unwrap_or(false); + } + + if verify_configfs_provider(Path::new(CONFIGFS_DEFAULT)).unwrap_or(false) { + return true; + } + + fs::read_dir(CONFIGFS_BASE) + .ok() + .into_iter() + .flatten() + .filter_map(std::result::Result::ok) + .any(|entry| verify_configfs_provider(&entry.path()).unwrap_or(false)) } fn is_vsock_available() -> bool { @@ -223,8 +251,8 @@ pub fn extend_rtmr(index: u32, _event_type: u32, digest: [u8; 48]) -> Result<()> return Err(TdxAttestError::InvalidRtmrIndex(index)); } - if is_rtmr_sysfs_available() { - return extend_rtmr_via_sysfs(index, &digest); + if let Some(base) = rtmr_sysfs_base()? { + return extend_rtmr_via_sysfs(&base, index, &digest); } if Path::new(TDX_GUEST_DEVICE).exists() { @@ -236,24 +264,37 @@ pub fn extend_rtmr(index: u32, _event_type: u32, digest: [u8; 48]) -> Result<()> )) } -fn is_rtmr_sysfs_available() -> bool { - Path::new(RTMR_SYSFS_BASE).is_dir() +fn rtmr_sysfs_base() -> Result> { + if let Some(path) = std::env::var_os(RTMR_SYSFS_PATH_ENV) { + let path = std::path::PathBuf::from(path); + if path.as_os_str().len() < 240 && path.is_dir() { + return Ok(Some(path)); + } + return Err(TdxAttestError::NotSupported(format!( + "invalid RTMR sysfs path from {RTMR_SYSFS_PATH_ENV}: {}", + path.display() + ))); + } + + Ok(Path::new(RTMR_SYSFS_BASE) + .is_dir() + .then(|| std::path::PathBuf::from(RTMR_SYSFS_BASE))) } -fn extend_rtmr_via_sysfs(index: u32, digest: &[u8; 48]) -> Result<()> { - let path = format!("{}/rtmr{}:sha384", RTMR_SYSFS_BASE, index); +fn extend_rtmr_via_sysfs(base: &Path, index: u32, digest: &[u8; 48]) -> Result<()> { + let path = base.join(format!("rtmr{index}:sha384")); let mut file = OpenOptions::new() .write(true) .open(&path) - .map_err(|e| TdxAttestError::ExtendFailure(format!("open {path}: {e}")))?; + .map_err(|e| TdxAttestError::ExtendFailure(format!("open {}: {e}", path.display())))?; file.write_all(digest).map_err(|e| match e.raw_os_error() { Some(libc::EINVAL) => TdxAttestError::InvalidRtmrIndex(index), Some(libc::EPERM) | Some(libc::EACCES) => { TdxAttestError::ExtendFailure(format!("permission denied for RTMR {index}")) } - _ => TdxAttestError::ExtendFailure(format!("write {path}: {e}")), + _ => TdxAttestError::ExtendFailure(format!("write {}: {e}", path.display())), }) } @@ -334,7 +375,10 @@ fn get_quote_via_configfs(report_data: &TdxReportData) -> Result> { fn prepare_configfs() -> Result { if let Ok(path) = std::env::var(CONFIGFS_PATH_ENV) { - if path.len() < 240 && Path::new(&path).is_dir() && verify_configfs_provider(&path)? { + if path.len() < 240 + && Path::new(&path).is_dir() + && verify_configfs_provider(Path::new(&path))? + { return Ok(path); } return Err(TdxAttestError::NotSupported(format!( @@ -343,7 +387,7 @@ fn prepare_configfs() -> Result { } let default_path = CONFIGFS_DEFAULT; - if Path::new(default_path).is_dir() && verify_configfs_provider(default_path)? { + if Path::new(default_path).is_dir() && verify_configfs_provider(Path::new(default_path))? { return Ok(default_path.to_string()); } @@ -369,7 +413,7 @@ fn prepare_configfs() -> Result { let provider_path = format!("{}/provider", default_path); for i in 0..5 { if Path::new(&provider_path).exists() { - if verify_configfs_provider(default_path)? { + if verify_configfs_provider(Path::new(default_path))? { return Ok(default_path.to_string()); } break; @@ -383,10 +427,11 @@ fn prepare_configfs() -> Result { ))) } -fn verify_configfs_provider(path: &str) -> Result { - let provider_path = format!("{}/provider", path); - let provider = fs::read_to_string(&provider_path) - .map_err(|e| TdxAttestError::Unexpected(format!("read {provider_path}: {e}")))?; +fn verify_configfs_provider(path: &Path) -> Result { + let provider_path = path.join("provider"); + let provider = fs::read_to_string(&provider_path).map_err(|e| { + TdxAttestError::Unexpected(format!("read {}: {e}", provider_path.display())) + })?; Ok(provider.trim().starts_with("tdx_guest")) } @@ -647,3 +692,39 @@ fn parse_qgs_get_quote_response(data: &[u8]) -> Result> { Ok(quote) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn recognizes_tdx_configfs_provider() { + let temp = tempfile::tempdir().unwrap(); + fs::write(temp.path().join("provider"), "tdx_guest_sim\n").unwrap(); + assert!(verify_configfs_provider(temp.path()).unwrap()); + + fs::write(temp.path().join("provider"), "sev_guest\n").unwrap(); + assert!(!verify_configfs_provider(temp.path()).unwrap()); + } + + #[test] + fn invalid_configfs_override_does_not_fall_back() { + let previous = std::env::var_os(CONFIGFS_PATH_ENV); + std::env::set_var( + CONFIGFS_PATH_ENV, + "/definitely/not/a/real/tdx-configfs-provider", + ); + + let result = get_quote(&[0u8; 64]); + + match previous { + Some(value) => std::env::set_var(CONFIGFS_PATH_ENV, value), + None => std::env::remove_var(CONFIGFS_PATH_ENV), + } + assert!(matches!( + result, + Err(TdxAttestError::NotSupported(message)) + if message.contains("invalid configfs path from env") + )); + } +} diff --git a/os/common/rootfs/dstack-prepare.sh b/os/common/rootfs/dstack-prepare.sh index 81e23363a..249bbf9cc 100755 --- a/os/common/rootfs/dstack-prepare.sh +++ b/os/common/rootfs/dstack-prepare.sh @@ -99,10 +99,21 @@ log "Syncing system time..." # Let the chronyd correct the system time immediately; keep booting if chronyd is not ready yet. chronyc makestep || log "Warning: chronyc makestep failed; continuing" -if [[ -e /dev/sev-guest ]] || grep -qw sev_guest /sys/kernel/config/tsm/report/*/provider 2>/dev/null; then +has_tsm_provider() { + local prefix="$1" + local provider value + for provider in /sys/kernel/config/tsm/report/*/provider; do + [[ -r "$provider" ]] || continue + value=$(<"$provider") + [[ "$value" == "$prefix"* ]] && return 0 + done + return 1 +} + +if [[ -e /dev/sev-guest ]] || has_tsm_provider sev_guest; then log "SEV-SNP guest device/TSM provider detected" -elif [[ -e /dev/tdx_guest ]]; then - log "TDX guest device detected" +elif [[ -e /dev/tdx_guest ]] || has_tsm_provider tdx_guest; then + log "TDX guest device/TSM provider detected" elif modprobe sev-guest 2>/dev/null; then log "Loaded sev-guest module" elif modprobe tdx-guest 2>/dev/null; then