firmware_uefi/service/
diagnostics.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! UEFI diagnostics service
5//!
6//! This service handles processing of the EFI diagnostics buffer,
7//! producing friendly logs for any telemetry during the UEFI boot
8//! process.
9//!
10//! The EFI diagnostics buffer follows the specification of Project Mu's
11//! Advanced Logger package, whose relevant types are defined in the Hyper-V
12//! specification within the uefi_specs crate.
13
14#![warn(missing_docs)]
15
16use crate::UefiDevice;
17use guestmem::GuestMemory;
18use guestmem::GuestMemoryError;
19use inspect::Inspect;
20use std::borrow::Cow;
21use std::collections::BTreeMap;
22use std::fmt::Debug;
23use std::mem::size_of;
24use thiserror::Error;
25use uefi_specs::hyperv::advanced_logger::AdvancedLoggerInfo;
26use uefi_specs::hyperv::advanced_logger::AdvancedLoggerMessageEntryV2;
27use uefi_specs::hyperv::advanced_logger::PHASE_NAMES;
28use uefi_specs::hyperv::advanced_logger::SIG_ENTRY;
29use uefi_specs::hyperv::advanced_logger::SIG_HEADER;
30use uefi_specs::hyperv::debug_level::DEBUG_ERROR;
31use uefi_specs::hyperv::debug_level::DEBUG_FLAG_NAMES;
32use uefi_specs::hyperv::debug_level::DEBUG_WARN;
33use zerocopy::FromBytes;
34
35/// 8-byte alignment for every entry
36const ALIGNMENT: usize = 8;
37
38/// Alignment mask for the entry
39const ALIGNMENT_MASK: usize = ALIGNMENT - 1;
40
41/// Maximum allowed size of the log buffer
42pub const MAX_LOG_BUFFER_SIZE: u32 = 0x400000; // 4MB
43
44/// Maximum allowed size of a single message
45pub const MAX_MESSAGE_LENGTH: u16 = 0x1000; // 4KB
46
47// Suppress logs that contain these known error/warning messages.
48// These messages are the result of known issues with our UEFI firmware that do
49// not seem to affect the guest.
50// TODO: Fix UEFI to resolve this errors/warnings
51const SUPPRESS_LOGS: [&str; 3] = [
52    "WARNING: There is mismatch of supported HashMask (0x2 - 0x7) between modules",
53    "that are linking different HashInstanceLib instances!",
54    "ConvertPages: failed to find range",
55];
56
57/// Represents a processed log entry from the EFI diagnostics buffer
58#[derive(Debug, Clone)]
59pub struct EfiDiagnosticsLog<'a> {
60    /// The debug level of the log entry
61    pub debug_level: u32,
62    /// Hypervisor reference ticks elapsed from UEFI
63    pub ticks: u64,
64    /// The boot phase that produced this log entry
65    pub phase: u16,
66    /// The log message itself
67    pub message: &'a str,
68}
69
70/// Converts a debug level to a human-readable string
71fn debug_level_to_string(debug_level: u32) -> Cow<'static, str> {
72    // Borrow directly from the table if only one flag is set
73    if debug_level.count_ones() == 1 {
74        if let Some(&(_, name)) = DEBUG_FLAG_NAMES
75            .iter()
76            .find(|&&(flag, _)| flag == debug_level)
77        {
78            return Cow::Borrowed(name);
79        }
80    }
81
82    // Handle combined flags or unknown debug levels
83    let flags: Vec<&str> = DEBUG_FLAG_NAMES
84        .iter()
85        .filter(|&&(flag, _)| debug_level & flag != 0)
86        .map(|&(_, name)| name)
87        .collect();
88
89    if flags.is_empty() {
90        Cow::Borrowed("UNKNOWN")
91    } else {
92        Cow::Owned(flags.join("+"))
93    }
94}
95
96/// Converts a phase value to a human-readable string
97fn phase_to_string(phase: u16) -> &'static str {
98    PHASE_NAMES
99        .iter()
100        .find(|&&(phase_raw, _)| phase_raw == phase)
101        .map(|&(_, name)| name)
102        .unwrap_or("UNKNOWN")
103}
104
105/// Errors that occur when parsing entries
106#[derive(Debug, Error)]
107#[allow(missing_docs)]
108pub enum EntryParseError {
109    #[error("Expected: {0:#x}, got: {1:#x}")]
110    SignatureMismatch(u32, u32),
111    #[error("Expected non-zero timestamp, got: {0:#x}")]
112    Timestamp(u64),
113    #[error("Expected message length < {0:#x}, got: {1:#x}")]
114    MessageLength(u16, u16),
115    #[error("Failed to read from buffer slice")]
116    SliceRead,
117    #[error("Arithmetic overflow in {0}")]
118    Overflow(&'static str),
119    #[error("Failed to read UTF-8 string: {0}")]
120    Utf8Error(#[from] std::str::Utf8Error),
121    #[error("message_end ({0:#x}) exceeds buffer slice length ({1:#x})")]
122    BadMessageEnd(usize, usize),
123}
124
125/// Represents a single parsed entry from the EFI diagnostics buffer
126struct EntryData<'a> {
127    /// The debug level of the log entry
128    debug_level: u32,
129    /// Timestamp of when the log entry was created
130    time_stamp: u64,
131    /// The boot phase that produced this log entry
132    phase: u16,
133    /// The log message itself
134    message: &'a str,
135    /// The size of the entry in bytes (including alignment)
136    entry_size: usize,
137}
138
139/// Parse a single entry from a buffer slice
140fn parse_entry(buffer_slice: &[u8]) -> Result<EntryData<'_>, EntryParseError> {
141    // Try to parse an entry from the buffer slice and validate it
142    let (entry, _) = AdvancedLoggerMessageEntryV2::read_from_prefix(buffer_slice)
143        .map_err(|_| EntryParseError::SliceRead)?;
144
145    let signature = entry.signature;
146    if signature != u32::from_le_bytes(SIG_ENTRY) {
147        return Err(EntryParseError::SignatureMismatch(
148            u32::from_le_bytes(SIG_ENTRY),
149            signature,
150        ));
151    }
152
153    if entry.message_len > MAX_MESSAGE_LENGTH {
154        return Err(EntryParseError::MessageLength(
155            MAX_MESSAGE_LENGTH,
156            entry.message_len,
157        ));
158    }
159
160    let message_offset = entry.message_offset;
161    let message_len = entry.message_len;
162
163    // Calculate message start and end offsets for boundary validation
164    let message_start = message_offset as usize;
165    let message_end = message_start
166        .checked_add(message_len as usize)
167        .ok_or(EntryParseError::Overflow("message_end"))?;
168
169    if message_end > buffer_slice.len() {
170        return Err(EntryParseError::BadMessageEnd(
171            message_end,
172            buffer_slice.len(),
173        ));
174    }
175
176    let message = std::str::from_utf8(&buffer_slice[message_start..message_end])?;
177
178    // Calculate size of the entry to find the offset of the next entry
179    let base_offset = size_of::<AdvancedLoggerMessageEntryV2>()
180        .checked_add(message_len as usize)
181        .ok_or(EntryParseError::Overflow("base_offset"))?;
182
183    // Add padding for 8-byte alignment
184    let aligned_offset = base_offset
185        .checked_add(ALIGNMENT_MASK)
186        .ok_or(EntryParseError::Overflow("aligned_offset"))?;
187    let entry_size = aligned_offset & !ALIGNMENT_MASK;
188
189    Ok(EntryData {
190        debug_level: entry.debug_level,
191        time_stamp: entry.time_stamp,
192        phase: entry.phase,
193        message,
194        entry_size,
195    })
196}
197
198/// Errors that occur during processing
199#[derive(Debug, Error)]
200#[allow(missing_docs)]
201pub enum DiagnosticsError {
202    #[error("Failed to parse entry: {0}")]
203    EntryParse(#[from] EntryParseError),
204    #[error("Expected: {0:#x}, got: {1:#x}")]
205    HeaderSignatureMismatch(u32, u32),
206    #[error("Expected log buffer size < {0:#x}, got: {1:#x}")]
207    HeaderBufferSize(u32, u32),
208    #[error("Bad GPA value: {0:#x}")]
209    BadGpa(u32),
210    #[error("No GPA set")]
211    NoGpa,
212    #[error("Failed to read from guest memory: {0}")]
213    GuestMemoryRead(#[from] GuestMemoryError),
214    #[error("Arithmetic overflow in {0}")]
215    Overflow(&'static str),
216    #[error("Expected used log buffer size < {0:#x}, got: {1:#x}")]
217    BadUsedBufferSize(u32, u32),
218    #[error("Expected accumulated message length < {0:#x}, got: {1:#x}")]
219    BadAccumulatedMessageLength(u16, u16),
220}
221
222/// Definition of the diagnostics services state
223#[derive(Inspect)]
224pub struct DiagnosticsServices {
225    /// The guest physical address of the diagnostics buffer
226    gpa: Option<u32>,
227    /// Flag to indicate if we have already processed the buffer
228    did_process: bool,
229}
230
231impl DiagnosticsServices {
232    /// Create a new instance of the diagnostics services
233    pub fn new() -> DiagnosticsServices {
234        DiagnosticsServices {
235            gpa: None,
236            did_process: false,
237        }
238    }
239
240    /// Reset the diagnostics services state
241    pub fn reset(&mut self) {
242        self.gpa = None;
243        self.did_process = false;
244    }
245
246    /// Set the GPA of the diagnostics buffer
247    pub fn set_gpa(&mut self, gpa: u32) {
248        self.gpa = match gpa {
249            0 => None,
250            _ => Some(gpa),
251        }
252    }
253
254    /// Process the diagnostics buffer
255    pub fn process_diagnostics<F>(
256        &mut self,
257        gm: &GuestMemory,
258        mut log_handler: F,
259    ) -> Result<(), DiagnosticsError>
260    where
261        F: FnMut(EfiDiagnosticsLog<'_>),
262    {
263        // Validate the GPA
264        let gpa = match self.gpa {
265            Some(gpa) if gpa != 0 && gpa != u32::MAX => gpa,
266            Some(invalid_gpa) => return Err(DiagnosticsError::BadGpa(invalid_gpa)),
267            None => return Err(DiagnosticsError::NoGpa),
268        };
269
270        // Read and validate the header from the guest memory
271        let header: AdvancedLoggerInfo = gm.read_plain(gpa as u64)?;
272
273        let signature = header.signature;
274        if signature != u32::from_le_bytes(SIG_HEADER) {
275            return Err(DiagnosticsError::HeaderSignatureMismatch(
276                u32::from_le_bytes(SIG_HEADER),
277                signature,
278            ));
279        }
280
281        if header.log_buffer_size > MAX_LOG_BUFFER_SIZE {
282            return Err(DiagnosticsError::HeaderBufferSize(
283                MAX_LOG_BUFFER_SIZE,
284                header.log_buffer_size,
285            ));
286        }
287
288        // Calculate the used portion of the log buffer
289        let used_log_buffer_size = header
290            .log_current_offset
291            .checked_sub(header.log_buffer_offset)
292            .ok_or_else(|| DiagnosticsError::Overflow("used_log_buffer_size"))?;
293
294        // Early exit if there is no buffer to process
295        if used_log_buffer_size == 0 {
296            tracelimit::info_ratelimited!(
297                "EFI diagnostics' used log buffer size is 0, ending processing"
298            );
299            return Ok(());
300        }
301
302        if used_log_buffer_size > header.log_buffer_size
303            || used_log_buffer_size > MAX_LOG_BUFFER_SIZE
304        {
305            return Err(DiagnosticsError::BadUsedBufferSize(
306                MAX_LOG_BUFFER_SIZE,
307                used_log_buffer_size,
308            ));
309        }
310
311        // Calculate start address of the log buffer
312        let buffer_start_addr = gpa
313            .checked_add(header.log_buffer_offset)
314            .ok_or_else(|| DiagnosticsError::Overflow("buffer_start_addr"))?;
315
316        // Now read the used log buffer into a vector
317        let mut buffer_data = vec![0u8; used_log_buffer_size as usize];
318        gm.read_at(buffer_start_addr as u64, &mut buffer_data)?;
319
320        // Maintain a slice of the buffer that needs to be processed
321        let mut buffer_slice = &buffer_data[..];
322
323        // Message accumulation state
324        let mut accumulated_message = String::with_capacity(MAX_MESSAGE_LENGTH as usize);
325        let mut debug_level = 0;
326        let mut time_stamp = 0;
327        let mut phase = 0;
328        let mut is_accumulating = false;
329
330        // Used for tracking what has been processed
331        let mut bytes_read: usize = 0;
332        let mut entries_processed: usize = 0;
333
334        let mut suppressed_logs = BTreeMap::new();
335
336        // Process the buffer slice until all entries are processed
337        while !buffer_slice.is_empty() {
338            let entry = parse_entry(buffer_slice)?;
339
340            // Handle message accumulation
341            if !is_accumulating {
342                debug_level = entry.debug_level;
343                time_stamp = entry.time_stamp;
344                phase = entry.phase;
345                accumulated_message.clear();
346                is_accumulating = true;
347            }
348
349            accumulated_message.push_str(entry.message);
350            if accumulated_message.len() > MAX_MESSAGE_LENGTH as usize {
351                return Err(DiagnosticsError::BadAccumulatedMessageLength(
352                    MAX_MESSAGE_LENGTH,
353                    accumulated_message.len() as u16,
354                ));
355            }
356
357            // Handle completed messages (ending with '\n')
358            if !entry.message.is_empty() && entry.message.ends_with('\n') {
359                let mut suppress = false;
360                for log in SUPPRESS_LOGS {
361                    if accumulated_message.contains(log) {
362                        suppressed_logs
363                            .entry(log)
364                            .and_modify(|c| *c += 1)
365                            .or_insert(1);
366                        suppress = true;
367                    }
368                }
369                if !suppress {
370                    log_handler(EfiDiagnosticsLog {
371                        debug_level,
372                        ticks: time_stamp,
373                        phase,
374                        message: accumulated_message.trim_end_matches(&['\r', '\n'][..]),
375                    });
376                }
377                is_accumulating = false;
378                entries_processed += 1;
379            }
380
381            // Update bytes read and move to the next entry
382            bytes_read = bytes_read
383                .checked_add(entry.entry_size)
384                .ok_or_else(|| DiagnosticsError::Overflow("bytes_read"))?;
385
386            if entry.entry_size >= buffer_slice.len() {
387                break; // End of buffer
388            } else {
389                buffer_slice = &buffer_slice[entry.entry_size..];
390            }
391        }
392
393        // Process any remaining accumulated message
394        if is_accumulating && !accumulated_message.is_empty() {
395            log_handler(EfiDiagnosticsLog {
396                debug_level,
397                ticks: time_stamp,
398                phase,
399                message: accumulated_message.trim_end_matches(&['\r', '\n'][..]),
400            });
401            entries_processed += 1;
402        }
403
404        for (substring, count) in suppressed_logs {
405            tracelimit::warn_ratelimited!(substring, count, "suppressed logs")
406        }
407
408        // Print summary statistics
409        tracelimit::info_ratelimited!(entries_processed, bytes_read, "processed EFI log entries");
410
411        Ok(())
412    }
413}
414
415impl UefiDevice {
416    /// Process the diagnostics buffer and log the entries to tracing
417    pub(crate) fn process_diagnostics(&mut self) {
418        // Do not proceed if we have already processed before
419        if self.service.diagnostics.did_process {
420            tracelimit::warn_ratelimited!("Already processed diagnostics, skipping");
421            return;
422        }
423        self.service.diagnostics.did_process = true;
424
425        // Process diagnostics logs and send each directly to tracing
426        match self
427            .service
428            .diagnostics
429            .process_diagnostics(&self.gm, |log| {
430                let debug_level_str = debug_level_to_string(log.debug_level);
431                let phase_str = phase_to_string(log.phase);
432
433                match log.debug_level {
434                    DEBUG_WARN => tracing::warn!(
435                        debug_level = %debug_level_str,
436                        ticks = log.ticks,
437                        phase = %phase_str,
438                        log_message = log.message,
439                        "EFI log entry"
440                    ),
441                    DEBUG_ERROR => tracing::error!(
442                        debug_level = %debug_level_str,
443                        ticks = log.ticks,
444                        phase = %phase_str,
445                        log_message = log.message,
446                        "EFI log entry"
447                    ),
448                    _ => tracing::info!(
449                        debug_level = %debug_level_str,
450                        ticks = log.ticks,
451                        phase = %phase_str,
452                        log_message = log.message,
453                        "EFI log entry"
454                    ),
455                }
456            }) {
457            Ok(_) => {}
458            Err(error) => {
459                tracelimit::error_ratelimited!(
460                    error = &error as &dyn std::error::Error,
461                    "Failed to process diagnostics buffer"
462                );
463            }
464        }
465    }
466}
467
468mod save_restore {
469    use super::*;
470    use vmcore::save_restore::RestoreError;
471    use vmcore::save_restore::SaveError;
472    use vmcore::save_restore::SaveRestore;
473
474    mod state {
475        use mesh::payload::Protobuf;
476        use vmcore::save_restore::SavedStateRoot;
477
478        #[derive(Protobuf, SavedStateRoot)]
479        #[mesh(package = "firmware.uefi.diagnostics")]
480        pub struct SavedState {
481            #[mesh(1)]
482            pub gpa: Option<u32>,
483            #[mesh(2)]
484            pub did_flush: bool,
485        }
486    }
487
488    impl SaveRestore for DiagnosticsServices {
489        type SavedState = state::SavedState;
490
491        fn save(&mut self) -> Result<Self::SavedState, SaveError> {
492            Ok(state::SavedState {
493                gpa: self.gpa,
494                did_flush: self.did_process,
495            })
496        }
497
498        fn restore(&mut self, state: Self::SavedState) -> Result<(), RestoreError> {
499            let state::SavedState { gpa, did_flush } = state;
500            self.gpa = gpa;
501            self.did_process = did_flush;
502            Ok(())
503        }
504    }
505}