From a6494e104b73f8727eaf070b2faf88128bf44a65 Mon Sep 17 00:00:00 2001 From: "Surply, Pierre" Date: Sun, 23 Aug 2026 18:10:17 +0200 Subject: [PATCH] lib: add support for split Event log records When a WHEA record hits a certain size, it may be split and logged as several event in the Event Logs. The actual CPER must then be reconstructed using the SeqNum field of the EventData before being decoded. This change refactors the Event Log extraction logic to take this scenario into account. Signed-off-by: Surply, Pierre --- lib/src/metadata.rs | 3 +- lib/src/source/event_log/win.rs | 129 ++++++++++++++++++++++++-------- 2 files changed, 100 insertions(+), 32 deletions(-) diff --git a/lib/src/metadata.rs b/lib/src/metadata.rs index 0d8ccca..ebbffa2 100644 --- a/lib/src/metadata.rs +++ b/lib/src/metadata.rs @@ -14,7 +14,7 @@ use crate::cper::CperSectionBody; use crate::source::CrashLogSource; /// Crash Log Metadata -#[derive(Default)] +#[derive(Default, Clone)] pub struct Metadata { /// Name of the computer where the Crash Log has been extracted from. pub computer: Option, @@ -28,6 +28,7 @@ pub struct Metadata { } /// Crash Log Extraction Time +#[derive(Clone)] pub struct Time { pub year: u16, pub month: u8, diff --git a/lib/src/source/event_log/win.rs b/lib/src/source/event_log/win.rs index 87c7123..8db86a3 100644 --- a/lib/src/source/event_log/win.rs +++ b/lib/src/source/event_log/win.rs @@ -2,8 +2,9 @@ // SPDX-License-Identifier: MIT use crate::CrashLog; -use crate::metadata; +use crate::metadata::{Metadata, Time}; use std::alloc::{Layout, alloc, dealloc}; +use std::collections::{BTreeMap, HashMap}; use std::ffi::c_void; use std::ops::{Deref, Drop}; use std::path::Path; @@ -138,30 +139,85 @@ fn evt_render_values(context: &EvtHandle, event: &EvtHandle) -> Result Result { - let mut time = SYSTEMTIME::default(); - unsafe { +#[derive(Default)] +struct EvtRecord { + id: u32, + metadata: Metadata, + chunks: BTreeMap>, +} + +impl EvtRecord { + fn new(id: u32) -> Self { + Self { + id, + ..Default::default() + } + } + + fn add_chunk(&mut self, seq_num: u32, chunk: &[u8]) { + if self.chunks.insert(seq_num, chunk.to_vec()).is_some() { + log::warn!( + "Chunk {seq_num} of the record {} is defined several times in the Event Log.", + self.id + ); + } + } + + fn set_time(&mut self, filetimeval: u64) { + let mut time = SYSTEMTIME::default(); let filetime = FILETIME { - dwHighDateTime: (filetime.Anonymous.FileTimeVal >> 32) as u32, - dwLowDateTime: (filetime.Anonymous.FileTimeVal & 0xFFFFFFFF) as u32, + dwHighDateTime: (filetimeval >> 32) as u32, + dwLowDateTime: (filetimeval & 0xFFFFFFFF) as u32, + }; + let res = unsafe { + FileTimeToSystemTime(&filetime as *const FILETIME, &mut time as *mut SYSTEMTIME) }; - FileTimeToSystemTime(&filetime as *const FILETIME, &mut time as *mut SYSTEMTIME)? - } - Ok(metadata::Metadata { - time: Some(metadata::Time { + if let Err(err) = res { + log::warn!( + "Failed to convert filetime to system time for event record {}: {err}", + self.id + ); + return; + } + + self.metadata.time = Some(Time { year: time.wYear, month: time.wMonth as u8, day: time.wDay as u8, hour: time.wHour as u8, minute: time.wMinute as u8, - }), - computer: unsafe { computer.Anonymous.StringVal.to_string().ok() }, - ..Default::default() - }) + }); + } + + fn set_computer(&mut self, computer: PCWSTR) { + self.metadata.computer = unsafe { computer.to_string().ok() }; + } + + fn into_crashlog(self) -> Option { + let mut binary = Vec::new(); + + for (i, (seq_num, chunk)) in self.chunks.into_iter().enumerate() { + if i as u32 != seq_num { + log::warn!( + "Event record {} is incomplete. Chunk {i} is missing.", + self.id + ); + return None; + } + + binary.extend(chunk); + } + + let mut crashlog = CrashLog::from_slice(&binary) + .inspect_err(|err| { + log::warn!("Error while decoding Crash Log read from Event Logs: {err}") + }) + .ok()?; + + crashlog.metadata = self.metadata.clone(); + Some(crashlog) + } } fn query_crashlogs(path: PCWSTR, query: PCWSTR, query_flags: u32) -> Result> { @@ -171,6 +227,8 @@ fn query_crashlogs(path: PCWSTR, query: PCWSTR, query_flags: u32) -> Result Result(values[0].Anonymous.BinaryVal, values[0].Count as usize) }; - - match CrashLog::from_slice(binary) { - Ok(mut crashlog) => { - crashlog.metadata = metadata_from_evt_values(values[1], values[2])?; - crashlogs.push(crashlog) - } - Err(err) => { - log::warn!("Error while decoding Crash Log read from Event Logs: {err}") - } + let record_id = unsafe { values[1].Anonymous.UInt32Val }; + let seq_num = unsafe { values[2].Anonymous.UInt32Val }; + let filetimeval = unsafe { values[3].Anonymous.FileTimeVal }; + let computer = unsafe { values[4].Anonymous.StringVal }; + + records.entry(record_id).or_insert_with(|| { + let mut record = EvtRecord::new(record_id); + record.set_time(filetimeval); + record.set_computer(computer); + record + }); + + if let Some(record) = records.get_mut(&record_id) { + record.add_chunk(seq_num, binary); } } } - Ok(crashlogs) + Ok(records + .into_values() + .filter_map(|record| record.into_crashlog()) + .collect()) } pub(super) fn extract_crashlogs(path: Option<&Path>) -> Result> {