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