firmware_uefi/service/diagnostics/
mod.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//! This file specifically should only expose the public API of the service;
15//! internal implementation details should be in submodules.
16
17use crate::UefiDevice;
18use gpa::Gpa;
19use guestmem::GuestMemory;
20use inspect::Inspect;
21use log::Log;
22use mesh::payload::Protobuf;
23use processor::ProcessingError;
24use uefi_specs::hyperv::debug_level::DEBUG_ERROR;
25use uefi_specs::hyperv::debug_level::DEBUG_INFO;
26use uefi_specs::hyperv::debug_level::DEBUG_WARN;
27
28mod accumulator;
29mod gpa;
30mod header;
31mod log;
32mod processor;
33
34/// Default number of EfiDiagnosticsLogs emitted per period
35pub const DEFAULT_LOGS_PER_PERIOD: u32 = 150;
36
37/// Number of EfiDiagnosticsLogs emitted per period for watchdog timeouts
38pub const WATCHDOG_LOGS_PER_PERIOD: u32 = 2000;
39
40/// Emit a diagnostic log entry with rate limiting.
41///
42/// # Arguments
43/// * `log` - The log entry to emit
44/// * `limit` - Maximum number of log entries to emit per period
45fn emit_log_ratelimited(log: &Log, limit: u32) {
46    if log.debug_level & DEBUG_ERROR != 0 {
47        tracelimit::error_ratelimited!(
48            limit: limit,
49            debug_level = %log.debug_level_str(),
50            ticks = log.ticks(),
51            phase = %log.phase_str(),
52            log_message = log.message_trimmed(),
53            "EFI log entry"
54        )
55    } else if log.debug_level & DEBUG_WARN != 0 {
56        tracelimit::warn_ratelimited!(
57            limit: limit,
58            debug_level = %log.debug_level_str(),
59            ticks = log.ticks(),
60            phase = %log.phase_str(),
61            log_message = log.message_trimmed(),
62            "EFI log entry"
63        )
64    } else {
65        tracelimit::info_ratelimited!(
66            limit: limit,
67            debug_level = %log.debug_level_str(),
68            ticks = log.ticks(),
69            phase = %log.phase_str(),
70            log_message = log.message_trimmed(),
71            "EFI log entry"
72        )
73    }
74}
75
76/// Emit a diagnostic log entry without rate limiting.
77///
78/// # Arguments
79/// * `log` - The log entry to emit
80fn emit_log_unrestricted(log: &Log) {
81    if log.debug_level & DEBUG_ERROR != 0 {
82        tracing::error!(
83            debug_level = %log.debug_level_str(),
84            ticks = log.ticks(),
85            phase = %log.phase_str(),
86            log_message = log.message_trimmed(),
87            "EFI log entry"
88        )
89    } else if log.debug_level & DEBUG_WARN != 0 {
90        tracing::warn!(
91            debug_level = %log.debug_level_str(),
92            ticks = log.ticks(),
93            phase = %log.phase_str(),
94            log_message = log.message_trimmed(),
95            "EFI log entry"
96        )
97    } else {
98        tracing::info!(
99            debug_level = %log.debug_level_str(),
100            ticks = log.ticks(),
101            phase = %log.phase_str(),
102            log_message = log.message_trimmed(),
103            "EFI log entry"
104        )
105    }
106}
107
108/// Log level configuration - encapsulates a u32 mask where u32::MAX means log everything
109#[derive(Debug, Clone, Copy, PartialEq, Eq, Protobuf)]
110#[mesh(transparent)]
111pub struct LogLevel(u32);
112
113impl LogLevel {
114    /// Create default log level configuration (ERROR and WARN only)
115    pub const fn make_default() -> Self {
116        Self(DEBUG_ERROR | DEBUG_WARN)
117    }
118
119    /// Create info log level configuration (ERROR, WARN, and INFO)
120    pub const fn make_info() -> Self {
121        Self(DEBUG_ERROR | DEBUG_WARN | DEBUG_INFO)
122    }
123
124    /// Create full log level configuration (all levels)
125    pub const fn make_full() -> Self {
126        Self(u32::MAX)
127    }
128
129    /// Checks if a raw debug level should be logged based on this log level configuration
130    pub fn should_log(self, raw_debug_level: u32) -> bool {
131        if self.0 == u32::MAX {
132            true // Log everything
133        } else {
134            (raw_debug_level & self.0) != 0
135        }
136    }
137}
138
139impl Default for LogLevel {
140    fn default() -> Self {
141        Self::make_default()
142    }
143}
144
145impl Inspect for LogLevel {
146    fn inspect(&self, req: inspect::Request<'_>) {
147        let human_readable = log::debug_level_to_string(self.0);
148        req.respond()
149            .field("raw_value", self.0)
150            .field("debug_levels", human_readable.as_ref());
151    }
152}
153
154/// Definition of the diagnostics services state
155#[derive(Inspect)]
156pub struct DiagnosticsServices {
157    /// The guest physical address of the diagnostics buffer
158    gpa: Option<Gpa>,
159    /// Whether diagnostics have been processed (prevents reprocessing spam)
160    processed: bool,
161    /// Log level used for filtering
162    log_level: LogLevel,
163}
164
165impl DiagnosticsServices {
166    /// Create a new instance of the diagnostics services
167    pub fn new(log_level: LogLevel) -> DiagnosticsServices {
168        DiagnosticsServices {
169            gpa: None,
170            processed: false,
171            log_level,
172        }
173    }
174
175    /// Reset the diagnostics services state
176    pub fn reset(&mut self) {
177        self.gpa = None;
178        self.processed = false;
179    }
180
181    /// Set the GPA of the diagnostics buffer
182    pub fn set_gpa(&mut self, gpa: u32) {
183        self.gpa = Gpa::new(gpa).ok();
184    }
185
186    /// Processes diagnostics from guest memory
187    ///
188    /// # Arguments
189    /// * `allow_reprocess` - If true, allows processing even if already processed for guest
190    /// * `gm` - Guest memory to read diagnostics from
191    /// * `log_level_override` - If provided, overrides the configured log level for this processing run
192    /// * `log_handler` - Function to handle each parsed log entry
193    pub fn process_diagnostics<F>(
194        &mut self,
195        allow_reprocess: bool,
196        gm: &GuestMemory,
197        log_level_override: Option<LogLevel>,
198        log_handler: F,
199    ) -> Result<(), ProcessingError>
200    where
201        F: FnMut(&Log),
202    {
203        // Check if processing is allowed
204        if self.processed && !allow_reprocess {
205            tracelimit::warn_ratelimited!("Already processed diagnostics, skipping");
206            return Ok(());
207        }
208
209        // Mark as processed first to prevent guest spam (even on failure)
210        self.processed = true;
211
212        // Use the override log level if provided, otherwise fall back to configured level
213        let effective_log_level = log_level_override.unwrap_or(self.log_level);
214
215        // Delegate to the processor module
216        processor::process_diagnostics_internal(self.gpa, gm, effective_log_level, log_handler)
217    }
218}
219
220/// The output destination for diagnostics.
221pub(crate) enum DiagnosticsEmitter {
222    /// Emit to tracing
223    Tracing { limit: Option<u32> },
224    /// Emit to a string
225    String,
226}
227
228impl UefiDevice {
229    /// Processes UEFI diagnostics from guest memory.
230    ///
231    /// # Arguments
232    /// * `allow_reprocess` - If true, allows processing even if already processed for guest
233    /// * `emitter` - The destination for the diagnostics output
234    /// * `log_level_override` - If provided, overrides the configured log level filter for this run
235    pub(crate) fn process_diagnostics(
236        &mut self,
237        allow_reprocess: bool,
238        emitter: DiagnosticsEmitter,
239        log_level_override: Option<LogLevel>,
240    ) -> Result<Option<String>, ProcessingError> {
241        use std::fmt::Write;
242        let mut output = match emitter {
243            DiagnosticsEmitter::String => Some(String::new()),
244            DiagnosticsEmitter::Tracing { .. } => None,
245        };
246
247        if let Err(error) = self.service.diagnostics.process_diagnostics(
248            allow_reprocess,
249            &self.gm,
250            log_level_override,
251            |log| {
252                if let Some(out) = &mut output {
253                    let _ = writeln!(
254                        out,
255                        "({} ticks) [{}] [{}]: {}",
256                        log.ticks(),
257                        log.debug_level_str(),
258                        log.phase_str(),
259                        log.message_trimmed(),
260                    );
261                } else if let DiagnosticsEmitter::Tracing { limit } = emitter {
262                    match limit {
263                        Some(limit) => emit_log_ratelimited(log, limit),
264                        None => emit_log_unrestricted(log),
265                    }
266                }
267            },
268        ) {
269            match emitter {
270                DiagnosticsEmitter::Tracing { .. } => {
271                    tracelimit::error_ratelimited!(
272                        error = &error as &dyn std::error::Error,
273                        "failed to process diagnostics buffer"
274                    );
275                    // For tracing, we swallow the error after logging it, consistent with previous behavior
276                    return Ok(None);
277                }
278                DiagnosticsEmitter::String => return Err(error),
279            }
280        }
281
282        Ok(output)
283    }
284}
285
286mod save_restore {
287    use super::*;
288    use vmcore::save_restore::RestoreError;
289    use vmcore::save_restore::SaveError;
290    use vmcore::save_restore::SaveRestore;
291
292    mod state {
293        use super::LogLevel;
294        use mesh::payload::Protobuf;
295        use vmcore::save_restore::SavedStateRoot;
296
297        #[derive(Protobuf, SavedStateRoot)]
298        #[mesh(package = "firmware.uefi.diagnostics")]
299        pub struct SavedState {
300            #[mesh(1)]
301            pub gpa: Option<u32>,
302            #[mesh(2)]
303            pub did_flush: bool,
304            #[mesh(3)]
305            pub log_level: LogLevel,
306        }
307    }
308
309    impl SaveRestore for DiagnosticsServices {
310        type SavedState = state::SavedState;
311
312        fn save(&mut self) -> Result<Self::SavedState, SaveError> {
313            Ok(state::SavedState {
314                gpa: self.gpa.map(|g| g.get()),
315                did_flush: self.processed,
316                log_level: self.log_level,
317            })
318        }
319
320        fn restore(&mut self, state: Self::SavedState) -> Result<(), RestoreError> {
321            let state::SavedState {
322                gpa,
323                did_flush,
324                log_level,
325            } = state;
326            self.gpa = gpa.and_then(|g| Gpa::new(g).ok());
327            self.processed = did_flush;
328            self.log_level = log_level;
329            Ok(())
330        }
331    }
332}