diff --git a/openhcl/underhill_core/src/lib.rs b/openhcl/underhill_core/src/lib.rs index 654904ed30..0369eb17bc 100644 --- a/openhcl/underhill_core/src/lib.rs +++ b/openhcl/underhill_core/src/lib.rs @@ -331,6 +331,7 @@ async fn launch_workers( default_boot_always_attempt: opt.default_boot_always_attempt, guest_state_lifetime: opt.guest_state_lifetime, guest_state_encryption_policy: opt.guest_state_encryption_policy, + efi_diagnostics_log_level: opt.efi_diagnostics_log_level, strict_encryption_policy: opt.strict_encryption_policy, attempt_ak_cert_callback: opt.attempt_ak_cert_callback, enable_vpci_relay: opt.enable_vpci_relay, diff --git a/openhcl/underhill_core/src/options.rs b/openhcl/underhill_core/src/options.rs index 88f2d4a78e..0fa4c4d047 100644 --- a/openhcl/underhill_core/src/options.rs +++ b/openhcl/underhill_core/src/options.rs @@ -84,6 +84,26 @@ impl FromStr for GuestStateEncryptionPolicyCli { } } +#[derive(Clone, Copy, Debug, MeshPayload)] +pub enum EfiDiagnosticsLogLevelCli { + Default, + Info, + Full, +} + +impl FromStr for EfiDiagnosticsLogLevelCli { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + match s { + "DEFAULT" | "0" => Ok(EfiDiagnosticsLogLevelCli::Default), + "INFO" | "1" => Ok(EfiDiagnosticsLogLevelCli::Info), + "FULL" | "2" => Ok(EfiDiagnosticsLogLevelCli::Full), + _ => Err(anyhow::anyhow!("Invalid EFI diagnostics log level: {}", s)), + } + } +} + #[derive(Clone, Debug, MeshPayload, Inspect, InspectMut)] pub enum KeepAliveConfig { EnabledHostAndPrivatePoolPresent, @@ -264,6 +284,11 @@ pub struct Options { /// Specify which guest state encryption policy to use. pub guest_state_encryption_policy: Option, + /// (HCL_EFI_DIAGNOSTICS_LOG_LEVEL=\) + /// Specify the EFI diagnostics log level filter (DEFAULT, INFO, or FULL). + /// Overrides the value in DPS when set. + pub efi_diagnostics_log_level: Option, + /// (HCL_STRICT_ENCRYPTION_POLICY=1) Strict guest state encryption policy. pub strict_encryption_policy: Option, @@ -458,6 +483,14 @@ impl Options { }) .ok() }); + let efi_diagnostics_log_level = read_env("HCL_EFI_DIAGNOSTICS_LOG_LEVEL").and_then(|x| { + x.to_string_lossy() + .parse::() + .map_err(|e| { + tracing::warn!("failed to parse HCL_EFI_DIAGNOSTICS_LOG_LEVEL: {:#}", e) + }) + .ok() + }); let strict_encryption_policy = parse_env_bool_opt("HCL_STRICT_ENCRYPTION_POLICY"); let attempt_ak_cert_callback = parse_env_bool_opt("HCL_ATTEMPT_AK_CERT_CALLBACK"); let enable_vpci_relay = parse_env_bool_opt("OPENHCL_ENABLE_VPCI_RELAY"); @@ -526,6 +559,7 @@ impl Options { default_boot_always_attempt, guest_state_lifetime, guest_state_encryption_policy, + efi_diagnostics_log_level, strict_encryption_policy, attempt_ak_cert_callback, enable_vpci_relay, diff --git a/openhcl/underhill_core/src/worker.rs b/openhcl/underhill_core/src/worker.rs index 9eb32a722c..3ff6ec969a 100644 --- a/openhcl/underhill_core/src/worker.rs +++ b/openhcl/underhill_core/src/worker.rs @@ -46,6 +46,7 @@ use crate::nvme_manager::device::VfioNvmeDriverSpawner; use crate::nvme_manager::manager::NvmeDiskConfig; use crate::nvme_manager::manager::NvmeDiskResolver; use crate::nvme_manager::manager::NvmeManager; +use crate::options::EfiDiagnosticsLogLevelCli; use crate::options::GuestStateEncryptionPolicyCli; use crate::options::GuestStateLifetimeCli; use crate::options::KeepAliveConfig; @@ -309,6 +310,8 @@ pub struct UnderhillEnvCfg { pub guest_state_lifetime: Option, /// Guest state encryption policy pub guest_state_encryption_policy: Option, + /// EFI diagnostics log level filter (overrides DPS value when set) + pub efi_diagnostics_log_level: Option, /// Strict guest state encryption policy pub strict_encryption_policy: Option, /// Attempt to renew the AK cert @@ -1549,6 +1552,16 @@ async fn new_underhill_vm( }; } + if let Some(level) = env_cfg.efi_diagnostics_log_level { + tracing::info!("using HCL_EFI_DIAGNOSTICS_LOG_LEVEL={level:?} from cmdline"); + use get_protocol::dps_json::EfiDiagnosticsLogLevelType; + dps.general.efi_diagnostics_log_level = match level { + EfiDiagnosticsLogLevelCli::Default => EfiDiagnosticsLogLevelType::DEFAULT, + EfiDiagnosticsLogLevelCli::Info => EfiDiagnosticsLogLevelType::INFO, + EfiDiagnosticsLogLevelCli::Full => EfiDiagnosticsLogLevelType::FULL, + }; + } + if let Some(value) = env_cfg.strict_encryption_policy { tracing::info!("using HCL_STRICT_ENCRYPTION_POLICY={value} from cmdline"); dps.general diff --git a/petri/src/vm/hyperv/mod.rs b/petri/src/vm/hyperv/mod.rs index 6395e8323e..de7f1901a2 100644 --- a/petri/src/vm/hyperv/mod.rs +++ b/petri/src/vm/hyperv/mod.rs @@ -189,6 +189,7 @@ impl PetriVmmBackend for HyperVPetriBackend { enable_vpci_boot, secure_boot_enabled, default_boot_always_attempt, + efi_diagnostics_log_level, .. }) = config.firmware.uefi_config() { @@ -216,6 +217,35 @@ impl PetriVmmBackend for HyperVPetriBackend { ); } + // Plumb the EFI diagnostics log level via the OpenHCL command + // line. The corresponding `EfiDiagnosticsLogLevel` WMI property + // is not available on all hosts (e.g. rs_prerelease), so we + // rely on the underhill env var fallback instead. This means + // we currently only support setting the level on OpenHCL-backed + // VMs; for plain Hyper-V UEFI VMs we fail loudly rather than + // silently dropping the setting. + // + // TODO: switch to the WMI property (which would also cover the + // non-OpenHCL path) once host changes are saturated. + let efi_diag_cli = match efi_diagnostics_log_level { + crate::EfiDiagnosticsLogLevel::Default => None, + crate::EfiDiagnosticsLogLevel::Info => Some("INFO"), + crate::EfiDiagnosticsLogLevel::Full => Some("FULL"), + }; + if let Some(cli) = efi_diag_cli { + if !properties.is_openhcl { + anyhow::bail!( + "with_efi_diagnostics_log_level({:?}) is only supported for \ + OpenHCL-backed Hyper-V UEFI VMs in this code path", + efi_diagnostics_log_level + ); + } + append_cmdline( + &mut openhcl_command_line, + format!("HCL_EFI_DIAGNOSTICS_LOG_LEVEL={cli}"), + ); + } + if *enable_vpci_boot { todo!("hyperv nvme boot"); } diff --git a/petri/src/vm/mod.rs b/petri/src/vm/mod.rs index 661e50827d..ca935e9a27 100644 --- a/petri/src/vm/mod.rs +++ b/petri/src/vm/mod.rs @@ -1294,6 +1294,20 @@ impl PetriVmBuilder { self } + /// Sets the UEFI diagnostics log level filter. + /// + /// By default only ERROR and WARN level entries are forwarded to the + /// host tracing infrastructure. Use this to also surface INFO (or all) + /// entries when a test needs to observe them. + pub fn with_efi_diagnostics_log_level(mut self, level: EfiDiagnosticsLogLevel) -> Self { + self.config + .firmware + .uefi_config_mut() + .expect("EFI diagnostics log level is only supported for UEFI firmware.") + .efi_diagnostics_log_level = level; + self + } + /// Sets whether UEFI should always attempt a default boot. pub fn with_default_boot_always_attempt(mut self, enable: bool) -> Self { self.config @@ -2158,6 +2172,8 @@ pub struct UefiConfig { pub default_boot_always_attempt: bool, /// Enable vPCI boot (for NVMe) pub enable_vpci_boot: bool, + /// EFI diagnostics log level filter + pub efi_diagnostics_log_level: EfiDiagnosticsLogLevel, } impl Default for UefiConfig { @@ -2168,10 +2184,26 @@ impl Default for UefiConfig { disable_frontpage: true, default_boot_always_attempt: false, enable_vpci_boot: false, + efi_diagnostics_log_level: EfiDiagnosticsLogLevel::Default, } } } +/// EFI diagnostics log level filter. +/// +/// Controls which UEFI diagnostics log entries are forwarded to the host +/// tracing infrastructure (and thus visible via kmsg / test output). +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub enum EfiDiagnosticsLogLevel { + /// Default log level (ERROR and WARN only). + #[default] + Default, + /// Include INFO logs (ERROR, WARN, and INFO). + Info, + /// All log levels. + Full, +} + /// Control the logging configuration of OpenVMM/OpenHCL. #[derive(Debug, Clone)] pub enum OpenvmmLogConfig { diff --git a/petri/src/vm/openvmm/construct.rs b/petri/src/vm/openvmm/construct.rs index 271a1c4859..c0dba5e2a5 100644 --- a/petri/src/vm/openvmm/construct.rs +++ b/petri/src/vm/openvmm/construct.rs @@ -7,6 +7,7 @@ use super::PetriVmConfigOpenVmm; use super::PetriVmResourcesOpenVmm; use crate::Drive; +use crate::EfiDiagnosticsLogLevel; use crate::Firmware; use crate::IsolationType; use crate::MemoryConfig; @@ -552,7 +553,21 @@ impl PetriVmConfigOpenVmm { debugger_rpc: None, generation_id_recv: None, rtc_delta_milliseconds: 0, - efi_diagnostics_log_level: Default::default(), // TODO: Add config for tests + efi_diagnostics_log_level: match firmware + .uefi_config() + .map(|c| c.efi_diagnostics_log_level) + .unwrap_or_default() + { + EfiDiagnosticsLogLevel::Default => { + openvmm_defs::config::EfiDiagnosticsLogLevelType::Default + } + EfiDiagnosticsLogLevel::Info => { + openvmm_defs::config::EfiDiagnosticsLogLevelType::Info + } + EfiDiagnosticsLogLevel::Full => { + openvmm_defs::config::EfiDiagnosticsLogLevelType::Full + } + }, }; // Make the pipette connection listener. @@ -774,6 +789,7 @@ impl PetriVmConfigSetupCore<'_> { disable_frontpage, default_boot_always_attempt, enable_vpci_boot, + efi_diagnostics_log_level: _, // applied to top-level Config below }, }, ) => { @@ -933,6 +949,7 @@ impl PetriVmConfigSetupCore<'_> { disable_frontpage, default_boot_always_attempt, enable_vpci_boot, + efi_diagnostics_log_level, }, OpenHclConfig { vmbus_redirect, .. }, ) = match self.firmware { @@ -985,7 +1002,17 @@ impl PetriVmConfigSetupCore<'_> { no_persistent_secrets: self.tpm_config.as_ref().is_some_and(|c| c.no_persistent_secrets), igvm_attest_test_config: None, test_gsp_by_id, - efi_diagnostics_log_level: Default::default(), // TODO: make configurable + efi_diagnostics_log_level: match efi_diagnostics_log_level { + EfiDiagnosticsLogLevel::Default => { + get_resources::ged::EfiDiagnosticsLogLevelType::Default + } + EfiDiagnosticsLogLevel::Info => { + get_resources::ged::EfiDiagnosticsLogLevelType::Info + } + EfiDiagnosticsLogLevel::Full => { + get_resources::ged::EfiDiagnosticsLogLevelType::Full + } + }, hv_sint_enabled: false, }; diff --git a/vmm_tests/vmm_tests/tests/tests/multiarch.rs b/vmm_tests/vmm_tests/tests/tests/multiarch.rs index b57fe1f5f1..a0321128ea 100644 --- a/vmm_tests/vmm_tests/tests/tests/multiarch.rs +++ b/vmm_tests/vmm_tests/tests/tests/multiarch.rs @@ -5,6 +5,7 @@ use anyhow::Context; use futures::StreamExt; +use petri::EfiDiagnosticsLogLevel; use petri::MemoryConfig; use petri::PetriHaltReason; use petri::PetriVmBuilder; @@ -480,6 +481,44 @@ async fn efi_diagnostics_no_boot( anyhow::bail!("Did not find expected message in kmsg"); } +/// Test EFI diagnostics with INFO-level logging enabled +/// TODO: +/// - change hyperv tests to use WMI instead of env_cfg once +/// CI runners support it +#[vmm_test_with(noagent( + openvmm_openhcl_uefi_x64(none), + hyperv_openhcl_uefi_x64(none), + hyperv_openhcl_uefi_aarch64(none) +))] +async fn efi_diagnostics_info_level( + config: PetriVmBuilder, +) -> anyhow::Result<()> { + let vm = config + .with_uefi_frontpage(true) + .with_efi_diagnostics_log_level(EfiDiagnosticsLogLevel::Info) + .run_without_agent() + .await?; + + // Marker emitted by `firmware_uefi::service::diagnostics` for every + // UEFI log entry tagged with `DEBUG_INFO`. + // + // Presence of this marker in the kmsg output validates that. + const INFO_MARKER: &str = "debug_level=INFO"; + + let mut kmsg = vm.kmsg().await?; + + while let Some(data) = kmsg.next().await { + let data = data.context("reading kmsg")?; + let msg = kmsg::KmsgParsedEntry::new(&data).unwrap(); + let raw = msg.message.as_raw(); + if raw.contains(INFO_MARKER) { + return Ok(()); + } + } + + anyhow::bail!("Did not find any INFO-level UEFI diagnostics entry ({INFO_MARKER:?}) in kmsg"); +} + /// Boot our guest-test UEFI image, which will run some tests, /// and then purposefully triple fault itself via an expiring /// watchdog timer.