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/// Emit a diagnostic log entry with rate limiting.
38///
39/// # Arguments
40/// * `log` - The log entry to emit
41/// * `limit` - Maximum number of log entries to emit per period
42fn emit_log_ratelimited(log: &Log, limit: u32) {
43    if log.debug_level & DEBUG_ERROR != 0 {
44        tracelimit::error_ratelimited!(
45            limit: limit,
46            debug_level = %log.debug_level_str(),
47            ticks = log.ticks(),
48            phase = %log.phase_str(),
49            log_message = log.message_trimmed(),
50            "EFI log entry"
51        )
52    } else if log.debug_level & DEBUG_WARN != 0 {
53        tracelimit::warn_ratelimited!(
54            limit: limit,
55            debug_level = %log.debug_level_str(),
56            ticks = log.ticks(),
57            phase = %log.phase_str(),
58            log_message = log.message_trimmed(),
59            "EFI log entry"
60        )
61    } else {
62        tracelimit::info_ratelimited!(
63            limit: limit,
64            debug_level = %log.debug_level_str(),
65            ticks = log.ticks(),
66            phase = %log.phase_str(),
67            log_message = log.message_trimmed(),
68            "EFI log entry"
69        )
70    }
71}
72
73/// Emit a diagnostic log entry without rate limiting.
74///
75/// # Arguments
76/// * `log` - The log entry to emit
77fn emit_log_unrestricted(log: &Log) {
78    if log.debug_level & DEBUG_ERROR != 0 {
79        tracing::error!(
80            debug_level = %log.debug_level_str(),
81            ticks = log.ticks(),
82            phase = %log.phase_str(),
83            log_message = log.message_trimmed(),
84            "EFI log entry"
85        )
86    } else if log.debug_level & DEBUG_WARN != 0 {
87        tracing::warn!(
88            debug_level = %log.debug_level_str(),
89            ticks = log.ticks(),
90            phase = %log.phase_str(),
91            log_message = log.message_trimmed(),
92            "EFI log entry"
93        )
94    } else {
95        tracing::info!(
96            debug_level = %log.debug_level_str(),
97            ticks = log.ticks(),
98            phase = %log.phase_str(),
99            log_message = log.message_trimmed(),
100            "EFI log entry"
101        )
102    }
103}
104
105/// Log level configuration - encapsulates a u32 mask where u32::MAX means log everything
106#[derive(Debug, Clone, Copy, PartialEq, Eq, Protobuf)]
107#[mesh(transparent)]
108pub struct LogLevel(u32);
109
110impl LogLevel {
111    /// Create default log level configuration (ERROR and WARN only)
112    pub const fn make_default() -> Self {
113        Self(DEBUG_ERROR | DEBUG_WARN)
114    }
115
116    /// Create info log level configuration (ERROR, WARN, and INFO)
117    pub const fn make_info() -> Self {
118        Self(DEBUG_ERROR | DEBUG_WARN | DEBUG_INFO)
119    }
120
121    /// Create full log level configuration (all levels)
122    pub const fn make_full() -> Self {
123        Self(u32::MAX)
124    }
125
126    /// Checks if a raw debug level should be logged based on this log level configuration
127    pub fn should_log(self, raw_debug_level: u32) -> bool {
128        if self.0 == u32::MAX {
129            true // Log everything
130        } else {
131            (raw_debug_level & self.0) != 0
132        }
133    }
134}
135
136impl Default for LogLevel {
137    fn default() -> Self {
138        Self::make_default()
139    }
140}
141
142impl Inspect for LogLevel {
143    fn inspect(&self, req: inspect::Request<'_>) {
144        let human_readable = log::debug_level_to_string(self.0);
145        req.respond()
146            .field("raw_value", self.0)
147            .field("debug_levels", human_readable.as_ref());
148    }
149}
150
151/// Definition of the diagnostics services state
152#[derive(Inspect)]
153pub struct DiagnosticsServices {
154    /// The guest physical address of the diagnostics buffer
155    gpa: Option<Gpa>,
156    /// Whether diagnostics have been processed (prevents reprocessing spam)
157    processed: bool,
158    /// Log level used for filtering
159    log_level: LogLevel,
160}
161
162impl DiagnosticsServices {
163    /// Create a new instance of the diagnostics services
164    pub fn new(log_level: LogLevel) -> DiagnosticsServices {
165        DiagnosticsServices {
166            gpa: None,
167            processed: false,
168            log_level,
169        }
170    }
171
172    /// Reset the diagnostics services state
173    pub fn reset(&mut self) {
174        self.gpa = None;
175        self.processed = false;
176    }
177
178    /// Set the GPA of the diagnostics buffer
179    pub fn set_gpa(&mut self, gpa: u32) {
180        self.gpa = Gpa::new(gpa).ok();
181    }
182
183    /// Processes diagnostics from guest memory
184    ///
185    /// # Arguments
186    /// * `allow_reprocess` - If true, allows processing even if already processed for guest
187    /// * `gm` - Guest memory to read diagnostics from
188    /// * `log_handler` - Function to handle each parsed log entry
189    pub fn process_diagnostics<F>(
190        &mut self,
191        allow_reprocess: bool,
192        gm: &GuestMemory,
193        log_handler: F,
194    ) -> Result<(), ProcessingError>
195    where
196        F: FnMut(&Log),
197    {
198        // Check if processing is allowed
199        if self.processed && !allow_reprocess {
200            tracelimit::warn_ratelimited!("Already processed diagnostics, skipping");
201            return Ok(());
202        }
203
204        // Mark as processed first to prevent guest spam (even on failure)
205        self.processed = true;
206
207        // Delegate to the processor module
208        processor::process_diagnostics_internal(self.gpa, gm, self.log_level, log_handler)
209    }
210}
211
212impl UefiDevice {
213    /// Processes UEFI diagnostics from guest memory.
214    ///
215    /// When a limit is provided, traces are rate-limited to avoid spam.
216    /// When no limit is provided, traces are unrestricted.
217    ///
218    /// # Arguments
219    /// * `allow_reprocess` - If true, allows processing even if already processed for guest
220    /// * `limit` - Maximum number of logs to process per period, or `None` for no limit
221    pub(crate) fn process_diagnostics(&mut self, allow_reprocess: bool, limit: Option<u32>) {
222        if let Err(error) =
223            self.service
224                .diagnostics
225                .process_diagnostics(allow_reprocess, &self.gm, |log| match limit {
226                    Some(limit) => emit_log_ratelimited(log, limit),
227                    None => emit_log_unrestricted(log),
228                })
229        {
230            tracelimit::error_ratelimited!(
231                error = &error as &dyn std::error::Error,
232                "failed to process diagnostics buffer"
233            );
234        }
235    }
236}
237
238mod save_restore {
239    use super::*;
240    use vmcore::save_restore::RestoreError;
241    use vmcore::save_restore::SaveError;
242    use vmcore::save_restore::SaveRestore;
243
244    mod state {
245        use super::LogLevel;
246        use mesh::payload::Protobuf;
247        use vmcore::save_restore::SavedStateRoot;
248
249        #[derive(Protobuf, SavedStateRoot)]
250        #[mesh(package = "firmware.uefi.diagnostics")]
251        pub struct SavedState {
252            #[mesh(1)]
253            pub gpa: Option<u32>,
254            #[mesh(2)]
255            pub did_flush: bool,
256            #[mesh(3)]
257            pub log_level: LogLevel,
258        }
259    }
260
261    impl SaveRestore for DiagnosticsServices {
262        type SavedState = state::SavedState;
263
264        fn save(&mut self) -> Result<Self::SavedState, SaveError> {
265            Ok(state::SavedState {
266                gpa: self.gpa.map(|g| g.get()),
267                did_flush: self.processed,
268                log_level: self.log_level,
269            })
270        }
271
272        fn restore(&mut self, state: Self::SavedState) -> Result<(), RestoreError> {
273            let state::SavedState {
274                gpa,
275                did_flush,
276                log_level,
277            } = state;
278            self.gpa = gpa.and_then(|g| Gpa::new(g).ok());
279            self.processed = did_flush;
280            self.log_level = log_level;
281            Ok(())
282        }
283    }
284}