Skip to main content

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