diff --git a/Cargo.lock b/Cargo.lock index dda653c599..116251c8ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1687,6 +1687,7 @@ dependencies = [ name = "firmware_uefi" version = "0.0.0" dependencies = [ + "anyhow", "async-trait", "bitfield-struct 0.10.1", "chipset_device", diff --git a/flowey/flowey_lib_hvlite/src/_jobs/cfg_versions.rs b/flowey/flowey_lib_hvlite/src/_jobs/cfg_versions.rs index 017badd8dc..5bb95e71fa 100644 --- a/flowey/flowey_lib_hvlite/src/_jobs/cfg_versions.rs +++ b/flowey/flowey_lib_hvlite/src/_jobs/cfg_versions.rs @@ -23,7 +23,7 @@ pub const MDBOOK: &str = "0.4.40"; pub const MDBOOK_ADMONISH: &str = "1.18.0"; pub const MDBOOK_MERMAID: &str = "0.14.0"; pub const RUSTUP_TOOLCHAIN: &str = "1.86.0"; -pub const MU_MSVM: &str = "24.0.4"; +pub const MU_MSVM: &str = "25.0.1"; pub const NEXTEST: &str = "0.9.74"; pub const NODEJS: &str = "18.x"; pub const OPENHCL_KERNEL_DEV_VERSION: &str = "6.6.63.2"; diff --git a/vm/devices/firmware/firmware_uefi/Cargo.toml b/vm/devices/firmware/firmware_uefi/Cargo.toml index dca17b1d6d..337956a046 100644 --- a/vm/devices/firmware/firmware_uefi/Cargo.toml +++ b/vm/devices/firmware/firmware_uefi/Cargo.toml @@ -40,6 +40,7 @@ open_enum.workspace = true pal_async.workspace = true ucs2.workspace = true +anyhow.workspace = true async-trait.workspace = true bitfield-struct.workspace = true der = { workspace = true, features = ["derive", "alloc", "oid"], optional = true } diff --git a/vm/devices/firmware/firmware_uefi/src/lib.rs b/vm/devices/firmware/firmware_uefi/src/lib.rs index 13d505d0d7..e77e554f3d 100644 --- a/vm/devices/firmware/firmware_uefi/src/lib.rs +++ b/vm/devices/firmware/firmware_uefi/src/lib.rs @@ -104,6 +104,7 @@ struct UefiDeviceServices { generation_id: service::generation_id::GenerationIdServices, #[inspect(mut)] time: service::time::TimeServices, + diagnostics: service::diagnostics::DiagnosticsServices, } // Begin and end range are inclusive. @@ -199,6 +200,7 @@ impl UefiDevice { generation_id_deps, ), time: service::time::TimeServices::new(time_source), + diagnostics: service::diagnostics::DiagnosticsServices::new(), }, }; Ok(uefi) @@ -252,6 +254,8 @@ impl UefiDevice { ); } } + UefiCommand::SET_EFI_DIAGNOSTICS_GPA => self.service.diagnostics.set_gpa(data), + UefiCommand::PROCESS_EFI_DIAGNOSTICS => self.process_diagnostics(self.gm.clone()), _ => tracelimit::warn_ratelimited!(addr, data, "unknown uefi write"), } } @@ -400,6 +404,10 @@ open_enum::open_enum! { WATCHDOG_RESOLUTION = 0x28, WATCHDOG_COUNT = 0x29, + // EFI Diagnostics + SET_EFI_DIAGNOSTICS_GPA = 0x2B, + PROCESS_EFI_DIAGNOSTICS = 0x2C, + // Event Logging (Windows 8.1 MQ/M0) EVENT_LOG_FLUSH = 0x30, @@ -432,6 +440,7 @@ mod save_restore { use vmcore::save_restore::SaveRestore; mod state { + use crate::service::diagnostics::DiagnosticsServices; use crate::service::event_log::EventLogServices; use crate::service::generation_id::GenerationIdServices; use crate::service::nvram::NvramServices; @@ -457,6 +466,8 @@ mod save_restore { pub generation_id: ::SavedState, #[mesh(6)] pub time: ::SavedState, + #[mesh(7)] + pub diagnostics: ::SavedState, } } @@ -475,6 +486,7 @@ mod save_restore { uefi_watchdog, generation_id, time, + diagnostics, }, address, } = self; @@ -487,6 +499,7 @@ mod save_restore { watchdog: uefi_watchdog.save()?, generation_id: generation_id.save()?, time: time.save()?, + diagnostics: diagnostics.save()?, }) } @@ -499,6 +512,7 @@ mod save_restore { watchdog, generation_id, time, + diagnostics, } = state; self.address = address; @@ -508,6 +522,7 @@ mod save_restore { self.service.uefi_watchdog.restore(watchdog)?; self.service.generation_id.restore(generation_id)?; self.service.time.restore(time)?; + self.service.diagnostics.restore(diagnostics)?; Ok(()) } diff --git a/vm/devices/firmware/firmware_uefi/src/service/diagnostics.rs b/vm/devices/firmware/firmware_uefi/src/service/diagnostics.rs new file mode 100644 index 0000000000..dc53ab137e --- /dev/null +++ b/vm/devices/firmware/firmware_uefi/src/service/diagnostics.rs @@ -0,0 +1,476 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! UEFI diagnostics service +//! +//! This service handles processing of the EFI diagnostics buffer, +//! producing friendly logs for any telemetry during the UEFI boot +//! process. +//! +//! The EFI diagnostics buffer follows the specification of Project Mu's +//! Advanced Logger package, whose relevant types are defined in the Hyper-V +//! specification within the uefi_specs crate. + +#![warn(missing_docs)] + +use crate::UefiDevice; +use anyhow::anyhow; +use guestmem::GuestMemory; +use guestmem::GuestMemoryError; +use inspect::Inspect; +use std::fmt::Debug; +use std::mem::size_of; +use thiserror::Error; +use uefi_specs::hyperv::advanced_logger::AdvancedLoggerInfo; +use uefi_specs::hyperv::advanced_logger::AdvancedLoggerMessageEntryV2; +use uefi_specs::hyperv::advanced_logger::SIG_ENTRY; +use uefi_specs::hyperv::advanced_logger::SIG_HEADER; +use zerocopy::FromBytes; + +/// 8-byte alignment for every entry +const ALIGNMENT: usize = 8; + +/// Alignment mask for the entry +const ALIGNMENT_MASK: usize = ALIGNMENT - 1; + +/// Maximum allowed size of the log buffer +pub const MAX_LOG_BUFFER_SIZE: u32 = 0x400000; // 4MB + +/// Maximum allowed size of a single message +pub const MAX_MESSAGE_LENGTH: u16 = 0x1000; // 4KB + +/// Represents a processed log entry from the EFI diagnostics buffer +#[derive(Debug, Clone)] +pub struct EfiDiagnosticsLog { + /// The debug level of the log entry + pub debug_level: u32, + /// Hypervisor reference ticks elapsed from UEFI + pub ticks: u64, + /// The boot phase that produced this log entry + pub phase: u16, + /// The log message itself + pub message: String, +} + +/// Errors that occur when parsing entries +#[derive(Debug, Error)] +#[allow(missing_docs)] +pub enum EntryParseError { + #[error("Expected: {0:#x}, got: {1:#x}")] + SignatureMismatch(u32, u32), + #[error("Expected non-zero timestamp, got: {0:#x}")] + Timestamp(u64), + #[error("Expected message length < {0:#x}, got: {1:#x}")] + MessageLength(u16, u16), + #[error("Failed to read from buffer slice")] + SliceRead, + #[error("Encountered arithmetic overflow: {0}")] + Overflow(#[from] anyhow::Error), + #[error("Failed to read UTF-8 string: {0}")] + Utf8Error(#[from] std::str::Utf8Error), + #[error("message_end ({0:#x}) exceeds buffer slice length ({1:#x})")] + BadMessageEnd(usize, usize), +} + +/// Represents a single parsed entry from the EFI diagnostics buffer +struct EntryData<'a> { + /// The debug level of the log entry + debug_level: u32, + /// Timestamp of when the log entry was created + time_stamp: u64, + /// The boot phase that produced this log entry + phase: u16, + /// The log message itself + message: &'a str, + /// The size of the entry in bytes (including alignment) + entry_size: usize, +} + +/// Parse a single entry from a buffer slice +fn parse_entry(buffer_slice: &[u8]) -> Result, EntryParseError> { + // Try to parse an entry from the buffer slice + let (entry, _) = AdvancedLoggerMessageEntryV2::read_from_prefix(buffer_slice) + .map_err(|_| EntryParseError::SliceRead)?; + + // Validate the entry signature + let signature = entry.signature.to_le(); + if signature != SIG_ENTRY { + return Err(EntryParseError::SignatureMismatch(SIG_ENTRY, signature)); + } + + // Validate the entry timestamp + if entry.time_stamp == 0 { + return Err(EntryParseError::Timestamp(entry.time_stamp)); + } + + // Validate the message length + if entry.message_len > MAX_MESSAGE_LENGTH { + return Err(EntryParseError::MessageLength( + MAX_MESSAGE_LENGTH, + entry.message_len, + )); + } + + // Copy packed fields to local variables to avoid unaligned access + let message_offset = entry.message_offset; + let message_len = entry.message_len; + + // Calculate message start and end offsets + let message_start = message_offset as usize; + let message_end = message_start + .checked_add(message_len as usize) + .ok_or_else(|| { + EntryParseError::Overflow(anyhow!( + "message_start ({}) + message_len ({})", + message_start, + message_len + )) + })?; + + // Validate message end fits within the buffer slice + if message_end > buffer_slice.len() { + return Err(EntryParseError::BadMessageEnd( + message_end, + buffer_slice.len(), + )); + } + + // Get the message + let message = std::str::from_utf8(&buffer_slice[message_start..message_end])?; + + // Calculate base offset (entry header size + message length) + let base_offset = size_of::() + .checked_add(message_len as usize) + .ok_or_else(|| { + EntryParseError::Overflow(anyhow!( + "size_of:: ({}) + message_len ({})", + size_of::(), + message_len + )) + })?; + + // Add padding for 8-byte alignment + let aligned_offset = base_offset.checked_add(ALIGNMENT_MASK).ok_or_else(|| { + EntryParseError::Overflow(anyhow!( + "base_offset ({}) + ALIGNMENT_MASK ({})", + base_offset, + ALIGNMENT_MASK + )) + })?; + let entry_size = aligned_offset & !ALIGNMENT_MASK; + + Ok(EntryData { + debug_level: entry.debug_level, + time_stamp: entry.time_stamp, + phase: entry.phase, + message, + entry_size, + }) +} + +/// Errors that occur during processing +#[derive(Debug, Error)] +#[allow(missing_docs)] +pub enum DiagnosticsError { + #[error("Failed to parse entry: {0}")] + EntryParse(#[from] EntryParseError), + #[error("Expected: {0:#x}, got: {1:#x}")] + HeaderSignatureMismatch(u32, u32), + #[error("Expected log buffer size < {0:#x}, got: {1:#x}")] + HeaderBufferSize(u32, u32), + #[error("Bad GPA value: {0:#x}")] + BadGpa(u32), + #[error("No GPA set")] + NoGpa, + #[error("Failed to read from guest memory: {0}")] + GuestMemoryRead(#[from] GuestMemoryError), + #[error("Encountered arithmetic overflow: {0}")] + Overflow(#[from] anyhow::Error), + #[error("Expected used log buffer size < {0:#x}, got: {1:#x}")] + BadUsedBufferSize(u32, u32), + #[error("Expected accumulated message length < {0:#x}, got: {1:#x}")] + BadAccumulatedMessageLength(u16, u16), +} + +/// Definition of the diagnostics services state +#[derive(Inspect)] +pub struct DiagnosticsServices { + /// The guest physical address of the diagnostics buffer + gpa: Option, + /// Flag to indicate if we have already processed the buffer + did_process: bool, +} + +impl DiagnosticsServices { + /// Create a new instance of the diagnostics services + pub fn new() -> DiagnosticsServices { + DiagnosticsServices { + gpa: None, + did_process: false, + } + } + + /// Reset the diagnostics services state + pub fn reset(&mut self) { + self.gpa = None; + self.did_process = false; + } + + /// Set the GPA of the diagnostics buffer + pub fn set_gpa(&mut self, gpa: u32) { + self.gpa = match gpa { + 0 => None, + _ => Some(gpa), + } + } + + /// Process the diagnostics buffer + pub fn process_diagnostics( + &self, + gm: &GuestMemory, + mut log_handler: F, + ) -> Result<(), DiagnosticsError> + where + F: FnMut(EfiDiagnosticsLog), + { + // Step 1: Validate GPA + let gpa = match self.gpa { + Some(gpa) if gpa != 0 && gpa != u32::MAX => gpa, + Some(invalid_gpa) => return Err(DiagnosticsError::BadGpa(invalid_gpa)), + None => return Err(DiagnosticsError::NoGpa), + }; + + // Step 2: Get the advanced logger info header + + // Read the header from the guest memory + let header: AdvancedLoggerInfo = gm.read_plain(gpa as u64)?; + + // Validate the header signature + let signature = header.signature.to_le(); + if signature != SIG_HEADER { + return Err(DiagnosticsError::HeaderSignatureMismatch( + SIG_HEADER, signature, + )); + } + + // Validate the log buffer size + if header.log_buffer_size > MAX_LOG_BUFFER_SIZE { + return Err(DiagnosticsError::HeaderBufferSize( + MAX_LOG_BUFFER_SIZE, + header.log_buffer_size, + )); + } + + // Step 3: Read the rest of the used log buffer + + // Copy packed fields to local variables to avoid unaligned access + let log_current_offset = header.log_current_offset; + let log_buffer_offset = header.log_buffer_offset; + + // Calculate used log buffer size + let used_log_buffer_size = log_current_offset + .checked_sub(log_buffer_offset) + .ok_or_else(|| { + DiagnosticsError::Overflow(anyhow!( + "log_current_offset ({:#x}) - log_buffer_offset ({:#x})", + log_current_offset, + log_buffer_offset + )) + })?; + + // If used log buffer size is zero, return early + if used_log_buffer_size == 0 { + tracelimit::info_ratelimited!( + "EFI Diagnostics: Used log buffer size is zero, ending processing" + ); + return Ok(()); + } + + // Validate used log buffer size + if used_log_buffer_size > header.log_buffer_size + || used_log_buffer_size > MAX_LOG_BUFFER_SIZE + { + return Err(DiagnosticsError::BadUsedBufferSize( + MAX_LOG_BUFFER_SIZE, + used_log_buffer_size, + )); + } + + // Calculate start address of the log buffer + let buffer_start_addr = gpa.checked_add(log_buffer_offset).ok_or_else(|| { + DiagnosticsError::Overflow(anyhow!( + "gpa ({:#x}) + log_buffer_offset ({:#x})", + gpa, + log_buffer_offset + )) + })?; + + // Now read the used log buffer into a vector + let mut buffer_data = vec![0u8; used_log_buffer_size as usize]; + gm.read_at(buffer_start_addr as u64, &mut buffer_data)?; + + // Step 4: Parse the log buffer + + // Maintain a slice of the buffer that needs to be processed + let mut buffer_slice = &buffer_data[..]; + + // Message accumulation state + let mut accumulated_message = String::with_capacity(MAX_MESSAGE_LENGTH as usize); + let mut debug_level = 0; + let mut time_stamp = 0; + let mut phase = 0; + let mut is_accumulating = false; + + // Used for tracking what has been processed + let mut bytes_read: usize = 0; + let mut entries_processed: usize = 0; + + // Process the buffer slice until all entries are processed + while !buffer_slice.is_empty() { + // Parse the current etnry + let entry = parse_entry(buffer_slice)?; + + // Handle message accumulation + if !is_accumulating { + debug_level = entry.debug_level; + time_stamp = entry.time_stamp; + phase = entry.phase; + accumulated_message.clear(); + is_accumulating = true; + } + + // Append the message + accumulated_message.push_str(entry.message); + + // Check message length + if accumulated_message.len() > MAX_MESSAGE_LENGTH as usize { + return Err(DiagnosticsError::BadAccumulatedMessageLength( + MAX_MESSAGE_LENGTH, + accumulated_message.len() as u16, + )); + } + + // Handle completed messages (ending with '\n') + if !entry.message.is_empty() && entry.message.ends_with('\n') { + log_handler(EfiDiagnosticsLog { + debug_level, + ticks: time_stamp, + phase, + message: std::mem::take(&mut accumulated_message) + .trim_end_matches(&['\r', '\n'][..]) + .to_string(), + }); + entries_processed += 1; + is_accumulating = false; + } + + // Update bytes read and move to the next entry + bytes_read = bytes_read.checked_add(entry.entry_size).ok_or_else(|| { + DiagnosticsError::Overflow(anyhow!( + "bytes_read ({}) + entry_size ({})", + bytes_read, + entry.entry_size + )) + })?; + + // Advance the buffer to the next entry + if entry.entry_size >= buffer_slice.len() { + break; // End of buffer + } else { + buffer_slice = &buffer_slice[entry.entry_size..]; + } + } + + // Process any remaining accumulated message + if is_accumulating && !accumulated_message.is_empty() { + log_handler(EfiDiagnosticsLog { + debug_level, + ticks: time_stamp, + phase, + message: std::mem::take(&mut accumulated_message) + .trim_end_matches(&['\r', '\n'][..]) + .to_string(), + }); + entries_processed += 1; + } + + // Print summary statistics + tracelimit::info_ratelimited!( + "EFI Diagnostics: Processed {} entries, Read {} bytes", + entries_processed, + bytes_read + ); + + Ok(()) + } +} + +impl UefiDevice { + /// Process the diagnostics buffer and log the entries to tracing + pub(crate) fn process_diagnostics(&mut self, gm: GuestMemory) { + // Do not proceed if we have already processed before + if self.service.diagnostics.did_process { + tracelimit::warn_ratelimited!("EFI Diagnostics: Already processed, skipping"); + return; + } + self.service.diagnostics.did_process = true; + + // Process diagnostics logs and send each directly to tracing + match self.service.diagnostics.process_diagnostics(&gm, |log| { + tracing::info!( + debug_level = log.debug_level, + ticks = log.ticks, + phase = log.phase, + message = %log.message, + "EFI Diagnostics:" + ); + }) { + Ok(_) => {} + Err(error) => { + tracelimit::error_ratelimited!( + error = &error as &dyn std::error::Error, + "EFI Diagnostics: Failed to process diagnostics buffer" + ); + } + } + } +} + +mod save_restore { + use super::*; + use vmcore::save_restore::RestoreError; + use vmcore::save_restore::SaveError; + use vmcore::save_restore::SaveRestore; + + mod state { + use mesh::payload::Protobuf; + use vmcore::save_restore::SavedStateRoot; + + #[derive(Protobuf, SavedStateRoot)] + #[mesh(package = "firmware.uefi.diagnostics")] + pub struct SavedState { + #[mesh(1)] + pub gpa: Option, + #[mesh(2)] + pub did_flush: bool, + } + } + + impl SaveRestore for DiagnosticsServices { + type SavedState = state::SavedState; + + fn save(&mut self) -> Result { + Ok(state::SavedState { + gpa: self.gpa, + did_flush: self.did_process, + }) + } + + fn restore(&mut self, state: Self::SavedState) -> Result<(), RestoreError> { + let state::SavedState { gpa, did_flush } = state; + self.gpa = gpa; + self.did_process = did_flush; + Ok(()) + } + } +} diff --git a/vm/devices/firmware/firmware_uefi/src/service/mod.rs b/vm/devices/firmware/firmware_uefi/src/service/mod.rs index 573bcb962c..6ebb95d60f 100644 --- a/vm/devices/firmware/firmware_uefi/src/service/mod.rs +++ b/vm/devices/firmware/firmware_uefi/src/service/mod.rs @@ -4,6 +4,7 @@ //! Various UEFI device subsystems. pub mod crypto; +pub mod diagnostics; pub mod event_log; pub mod generation_id; pub mod nvram; diff --git a/vm/devices/firmware/firmware_uefi/src/service/nvram/mod.rs b/vm/devices/firmware/firmware_uefi/src/service/nvram/mod.rs index a438b3af4a..18ef3b1a01 100644 --- a/vm/devices/firmware/firmware_uefi/src/service/nvram/mod.rs +++ b/vm/devices/firmware/firmware_uefi/src/service/nvram/mod.rs @@ -569,6 +569,7 @@ impl UefiDevice { vsm.revoke_guest_vsm() } } + self.process_diagnostics(self.gm.clone()); self.service.nvram.services.exit_boot_services(); (EfiStatus::SUCCESS, None) diff --git a/vm/devices/firmware/uefi_specs/src/hyperv/advanced_logger.rs b/vm/devices/firmware/uefi_specs/src/hyperv/advanced_logger.rs new file mode 100644 index 0000000000..7317eb416b --- /dev/null +++ b/vm/devices/firmware/uefi_specs/src/hyperv/advanced_logger.rs @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Types and constants defined by Project Mu's Advanced Logger package, +//! used in the Hyper-V UEFI firmware. + +#![warn(missing_docs)] + +use crate::uefi::time::EFI_TIME; +use zerocopy::FromBytes; +use zerocopy::Immutable; +use zerocopy::KnownLayout; + +/// Advanced Logger Info signature +pub const SIG_HEADER: u32 = u32::from_le_bytes(*b"ALOG"); + +/// Advanced Logger Entry signature +pub const SIG_ENTRY: u32 = u32::from_le_bytes(*b"ALM2"); + +/// UEFI Advanced Logger Info Header, which is shared +/// with the Advanced Logger Package in UEFI. The entries +/// live right after. +#[repr(C, packed)] +#[derive(Debug, Copy, Clone, FromBytes, Immutable, KnownLayout)] +pub struct AdvancedLoggerInfo { + /// Signature 'ALOG' + pub signature: u32, + /// Current Version + pub version: u16, + /// Reserved for future + pub reserved: [u16; 3], + /// Offset from LoggerInfo to start of log + pub log_buffer_offset: u32, + /// Reserved field + pub reserved4: u32, + /// Offset from LoggerInfo to where to store next log entry + pub log_current_offset: u32, + /// Number of bytes of messages missed + pub discarded_size: u32, + /// Size of allocated buffer + pub log_buffer_size: u32, + /// Log in permanent RAM + pub in_permanent_ram: u8, + /// After ExitBootServices + pub at_runtime: u8, + /// After VirtualAddressChange + pub gone_virtual: u8, + /// HdwPort initialized + pub hdw_port_initialized: u8, + /// HdwPort is Disabled + pub hdw_port_disabled: u8, + /// Reserved field + pub reserved2: [u8; 3], + /// Ticks per second for log timing + pub timer_frequency: u64, + /// Ticks when Time Acquired + pub ticks_at_time: u64, + /// Uefi Time Field + pub time: EFI_TIME, + /// Logging level to be printed at hw port + pub hw_print_level: u32, + /// Reserved field + pub reserved3: u32, +} + +/// UEFI Advanced Logger Entry, which is shared with the +/// Advanced Logger Package in UEFI. The messages live +/// right after. +#[repr(C, packed)] +#[derive(Debug, Copy, Clone, FromBytes, Immutable, KnownLayout)] +pub struct AdvancedLoggerMessageEntryV2 { + /// Signature 'ALM2' + pub signature: u32, + /// Major version of the advanced logger message structure. + pub major_version: u8, + /// Minor version of the advanced logger message structure. + pub minor_version: u8, + /// Debug level + pub debug_level: u32, + /// Time stamp + pub time_stamp: u64, + /// Boot phase that produced this message entry + pub phase: u16, + /// Number of bytes in the Message + pub message_len: u16, + /// Offset of the message from the start of the structure. + pub message_offset: u16, + // Rust prevents C flexible array members, but "message_text: [u8; _]" would be here +} diff --git a/vm/devices/firmware/uefi_specs/src/hyperv/mod.rs b/vm/devices/firmware/uefi_specs/src/hyperv/mod.rs index 2cb716e6d2..50bce195ce 100644 --- a/vm/devices/firmware/uefi_specs/src/hyperv/mod.rs +++ b/vm/devices/firmware/uefi_specs/src/hyperv/mod.rs @@ -7,6 +7,7 @@ //! in any public spec. Instead, they are transcribed directly from the Hyper-V //! C/C++ sources. +pub mod advanced_logger; pub mod bios_event_log; pub mod boot_bios_log; pub mod common; diff --git a/vmm_tests/vmm_tests/tests/tests/multiarch.rs b/vmm_tests/vmm_tests/tests/tests/multiarch.rs index e5c5ee30ab..8c273ee32b 100644 --- a/vmm_tests/vmm_tests/tests/tests/multiarch.rs +++ b/vmm_tests/vmm_tests/tests/tests/multiarch.rs @@ -4,6 +4,7 @@ //! Integration tests that run on more than one architecture. use anyhow::Context; +use futures::StreamExt; use get_resources::ged::FirmwareEvent; use hyperv_ic_resources::kvp::KvpRpc; use jiff::SignedDuration; @@ -83,6 +84,37 @@ async fn boot_reset_expected(config: PetriVmConfigOpenVmm) -> anyhow::Result<()> Ok(()) } +/// Test EFI diagnostics with no boot devices on OpenVMM. +/// TODO: +/// - kmsg support in Hyper-V +/// - openhcl_uefi_aarch64 support +/// - uefi_x64 + uefi_aarch64 trace searching support +#[openvmm_test(openhcl_uefi_x64(none))] +async fn efi_diagnostics_no_boot(config: PetriVmConfigOpenVmm) -> anyhow::Result<()> { + let mut vm = config.with_uefi_frontpage(true).run_without_agent().await?; + + // Boot the VM first + vm.wait_for_successful_boot_event().await?; + + // Expected no-boot message. + const NO_BOOT_MSG: &str = "[Bds] Unable to boot!"; + + // Get kmsg stream + let mut kmsg = vm.kmsg().await?; + + // Search for the message + while let Some(data) = kmsg.next().await { + let data = data.context("reading kmsg")?; + let msg = kmsg::KmsgParsedEntry::new(&data)?; + let raw = msg.message.as_raw(); + if raw.contains(NO_BOOT_MSG) { + return Ok(()); + } + } + + anyhow::bail!("Did not find expected message in kmsg"); +} + /// Test the KVP IC. /// /// Windows-only right now, because the Linux images do not include the KVP IC