Skip to main content

linux_kernel_panic_parser/
lib.rs

1//! Lossless parsing of Linux kernel panic logs, including incomplete excerpts.
2//!
3//! [`parse`] accepts any [`AsRef<str>`] input and owns its result. Unknown lines,
4//! architecture-specific details, whitespace, and line endings are preserved.
5//! [`PanicLog`]'s display output is byte-for-byte identical to its input, so
6//! `parse(log.to_string()) == log` for every valid UTF-8 input.
7//!
8//! ```
9//! use linux_kernel_panic_parser::{parse, LineKind};
10//! let log = parse("[    1.250000] Kernel panic - not syncing: fatal exception\n");
11//! assert_eq!(log.panic_messages().collect::<Vec<_>>(), ["fatal exception"]);
12//! assert_eq!(parse(log.to_string()), log);
13//! assert!(matches!(log.lines()[0].kind(), LineKind::Panic { .. }));
14//! ```
15//!
16//! No architecture is inferred from the machine running this library. Register
17//! names and hexadecimal addresses are retained as strings with no word-size
18//! limit. Recognition is best effort: this is not a validator for every kernel
19//! version or log transport. Syslog/journal wrappers remain raw text; uptime
20//! extraction supports the usual leading printk/dmesg `[seconds.fraction]` form
21//! and `[unix_seconds][seconds.fraction]` pairs.
22
23#![forbid(unsafe_code)]
24#![warn(missing_docs)]
25
26use jiff::{SignedDuration, Timestamp};
27use std::{convert::Infallible, fmt, str::FromStr};
28use winnow::{
29    Parser,
30    ascii::{digit1, space0, space1},
31    combinator::{alt, opt, preceded},
32    error::ModalResult,
33    token::{take_till, take_while},
34};
35
36/// An owned, lossless log. It may contain zero, one, or multiple panics.
37#[derive(Clone, Debug, Eq, PartialEq)]
38pub struct PanicLog {
39    lines: Vec<LogLine>,
40}
41
42impl PanicLog {
43    /// Parse UTF-8 text, including ordinary messages and incomplete excerpts.
44    pub fn parse(input: impl AsRef<str>) -> Self {
45        parse(input)
46    }
47
48    /// Lines in their original order, including blank lines.
49    pub fn lines(&self) -> &[LogLine] {
50        &self.lines
51    }
52
53    /// Recognized panic messages, in their original order.
54    pub fn panic_messages(&self) -> impl Iterator<Item = &str> {
55        self.lines.iter().filter_map(|line| match &line.kind {
56            LineKind::Panic { message } => Some(message.as_str()),
57            _ => None,
58        })
59    }
60
61    /// Whether a `Kernel panic - not syncing:` marker was found.
62    pub fn has_panic(&self) -> bool {
63        self.panic_messages().next().is_some()
64    }
65}
66
67impl fmt::Display for PanicLog {
68    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69        for line in &self.lines {
70            f.write_str(&line.raw)?;
71        }
72        Ok(())
73    }
74}
75
76impl FromStr for PanicLog {
77    type Err = Infallible;
78    fn from_str(s: &str) -> Result<Self, Self::Err> {
79        Ok(parse(s))
80    }
81}
82
83/// One line, including its original terminator (LF, CRLF, CR, or none).
84#[derive(Clone, Debug, Eq, PartialEq)]
85pub struct LogLine {
86    raw: String,
87    message: String,
88    uptime: Option<SignedDuration>,
89    timestamp: Option<Timestamp>,
90    kind: LineKind,
91}
92
93impl LogLine {
94    /// Original line, including its terminator.
95    pub fn as_str(&self) -> &str {
96        &self.raw
97    }
98    /// Content after recognized leading priority, wall-clock, and uptime prefixes, without
99    /// its terminator. Unrecognized prefixes remain part of the message.
100    pub fn message(&self) -> &str {
101        &self.message
102    }
103    /// Elapsed time since boot, when a representable dmesg timestamp is present.
104    pub fn uptime(&self) -> Option<SignedDuration> {
105        self.uptime
106    }
107    /// Wall-clock instant from a leading `[unix_seconds][uptime]` prefix.
108    pub fn timestamp(&self) -> Option<Timestamp> {
109        self.timestamp
110    }
111    /// Best-effort structured interpretation of the message.
112    pub fn kind(&self) -> &LineKind {
113        &self.kind
114    }
115}
116
117impl fmt::Display for LogLine {
118    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
119        f.write_str(&self.raw)
120    }
121}
122
123/// Recognized architecture-independent records. Unrecognized records are kept.
124#[derive(Clone, Debug, Eq, PartialEq)]
125#[non_exhaustive]
126pub enum LineKind {
127    /// A terminal kernel panic marker.
128    Panic {
129        /// Reason following the panic marker.
130        message: String,
131    },
132    /// CPU and task metadata, with the remaining kernel description retained.
133    Cpu {
134        /// Logical CPU identifier.
135        cpu: u64,
136        /// User identifier, when included by the kernel.
137        uid: Option<u64>,
138        /// Task identifier.
139        pid: u64,
140        /// Command name and remaining metadata (names can contain spaces).
141        details: String,
142    },
143    /// A named faulting symbol, such as `RIP: 0010:function+0x1/0x20`.
144    Symbol {
145        /// Architecture-specific label or register name.
146        label: String,
147        /// Optional segment selector, preserved as hexadecimal text.
148        segment: Option<String>,
149        /// Symbol, offset, and size.
150        frame: StackFrame,
151    },
152    /// A diagnostic or machine description with a known label.
153    Diagnostic {
154        /// Label, for example `Oops`, `BUG`, `Hardware name`, or `Code`.
155        label: String,
156        /// Full details after the label, without semantic interpretation.
157        details: String,
158    },
159    /// Loaded modules, including a wrapped continuation line.
160    Modules {
161        /// Module names with any attached taint annotations, in order.
162        modules: Vec<String>,
163        /// Last unloaded module, if reported.
164        last_unloaded: Option<String>,
165        /// Whether this line continues a preceding module list.
166        continuation: bool,
167    },
168    /// An announced reboot delay.
169    Reboot {
170        /// Delay before rebooting.
171        delay: SignedDuration,
172    },
173    /// A call-trace section heading.
174    CallTrace,
175    /// A symbol with hexadecimal offset and size, optionally with an address.
176    Frame(StackFrame),
177    /// Register or page-table entry/value pairs without architecture restrictions.
178    Registers(Vec<Register>),
179    /// A line without a recognized structured interpretation.
180    Unknown,
181}
182
183/// A stack symbol. Textual numbers preserve padding and support any word size.
184#[derive(Clone, Debug, Eq, PartialEq)]
185pub struct StackFrame {
186    /// Optional bracketed address, without brackets or angle brackets.
187    pub address: Option<String>,
188    /// Whether the kernel marked the symbol with `?`.
189    pub uncertain: bool,
190    /// Symbol name.
191    pub symbol: String,
192    /// Hexadecimal offset, including `0x`.
193    pub offset: String,
194    /// Hexadecimal symbol size, including `0x`.
195    pub size: String,
196    /// Remaining text, for example a module name or architecture annotation.
197    pub suffix: String,
198}
199
200/// A register value retained without integer-width assumptions.
201#[derive(Clone, Debug, Eq, PartialEq)]
202pub struct Register {
203    /// Register name as printed by the kernel.
204    pub name: String,
205    /// Value as printed, including any segment selector or parenthesized flags.
206    pub value: String,
207}
208
209/// Parse any borrowed or owned string-like input into an owned log.
210///
211/// Accepts `&str`, `String`, `&String`, `Box<str>`, `Cow<str>`, and other
212/// [`AsRef<str>`] types. Parsing never rejects unknown or truncated records.
213/// Empty input yields no lines. Only valid UTF-8 is accepted by this API.
214pub fn parse(input: impl AsRef<str>) -> PanicLog {
215    let mut input = input.as_ref();
216    let mut lines = Vec::new();
217    let mut in_modules = false;
218    while !input.is_empty() {
219        // This grammar consumes at least one character, including blank lines.
220        let remaining = input;
221        let raw = match raw_line.parse_next(&mut input) {
222            Ok(raw) => raw,
223            Err(_) => {
224                // Preserve all remaining text even if the line grammar changes
225                // to reject some input in the future.
226                input = "";
227                remaining
228            }
229        };
230        let content = raw.trim_end_matches(['\r', '\n']);
231        let mut body = content;
232        if let Ok(rest) = priority.parse_next(&mut body) {
233            body = rest;
234        } else {
235            body = content;
236        }
237        let wall_clock = wall_clock_prefix.parse_next(&mut &*body).ok();
238        let wall_timestamp = wall_clock.map(|(instant, rest)| {
239            body = rest;
240            instant
241        });
242        let before_time = body;
243        let uptime = match timestamp.parse_next(&mut body) {
244            Ok(time) => Some(time),
245            Err(_) => {
246                body = before_time;
247                None
248            }
249        };
250        let mut kind = classify(body);
251        if in_modules
252            && matches!(kind, LineKind::Unknown)
253            && let Ok(value) = module_list(true).parse(body.trim())
254        {
255            kind = value;
256        }
257        in_modules = matches!(kind, LineKind::Modules { .. });
258        lines.push(LogLine {
259            raw: raw.to_owned(),
260            message: body.to_owned(),
261            uptime,
262            timestamp: wall_timestamp,
263            kind,
264        });
265    }
266    PanicLog { lines }
267}
268
269fn raw_line<'a>(input: &mut &'a str) -> ModalResult<&'a str> {
270    (take_till(0.., ['\r', '\n']), opt(alt(("\r\n", "\n", "\r"))))
271        .take()
272        .parse_next(input)
273}
274
275fn priority<'a>(input: &mut &'a str) -> ModalResult<&'a str> {
276    (
277        "<",
278        take_while(1.., |c: char| c.is_ascii_digit()),
279        ">",
280        space0,
281    )
282        .parse_next(input)?;
283    Ok(*input)
284}
285
286fn timestamp(input: &mut &str) -> ModalResult<SignedDuration> {
287    let (_, _, seconds, fraction, _, _) = (
288        "[",
289        space0,
290        digit1,
291        opt((".", take_while(1..=9, |c: char| c.is_ascii_digit()))),
292        "]",
293        space0,
294    )
295        .parse_next(input)?;
296    let seconds = seconds
297        .parse::<i64>()
298        .map_err(|_| winnow::error::ErrMode::Backtrack(winnow::error::ContextError::new()))?;
299    let nanos = fraction.map_or(0, |(_, digits)| {
300        // The grammar limits this to at most nine ASCII digits, so the
301        // accumulated value and its nanosecond scaling fit in i32.
302        digits
303            .bytes()
304            .fold(0_i32, |value, digit| value * 10 + i32::from(digit - b'0'))
305            * 10_i32.pow(9 - digits.len() as u32)
306    });
307    Ok(SignedDuration::new(seconds, nanos))
308}
309
310fn wall_clock_prefix<'a>(input: &mut &'a str) -> ModalResult<(Timestamp, &'a str)> {
311    let (_, seconds, _) = ("[", digit1, "]").parse_next(input)?;
312    let instant = seconds
313        .parse::<i64>()
314        .ok()
315        .and_then(|s| Timestamp::from_second(s).ok())
316        .ok_or_else(|| winnow::error::ErrMode::Backtrack(winnow::error::ContextError::new()))?;
317    // Only interpret the first bracket as wall time when followed by a valid
318    // uptime. A single [seconds] bracket remains a normal dmesg prefix.
319    timestamp.parse_next(&mut &**input)?;
320    Ok((instant, *input))
321}
322
323fn cpu(input: &mut &str) -> ModalResult<LineKind> {
324    let (_, cpu, _, uid, _, pid, _, _) = (
325        "CPU:",
326        preceded(space0, digit1),
327        space1,
328        opt(("UID:", preceded(space0, digit1), space1)),
329        "PID:",
330        preceded(space0, digit1),
331        space1,
332        "Comm:",
333    )
334        .parse_next(input)?;
335    let number = |s: &str| {
336        s.parse::<u64>()
337            .map_err(|_| winnow::error::ErrMode::Backtrack(winnow::error::ContextError::new()))
338    };
339    Ok(LineKind::Cpu {
340        cpu: number(cpu)?,
341        uid: uid.map(|(_, value, _)| number(value)).transpose()?,
342        pid: number(pid)?,
343        details: input.trim_start().to_owned(),
344    })
345}
346
347fn hex<'a>(input: &mut &'a str) -> ModalResult<&'a str> {
348    ("0x", take_while(1.., |c: char| c.is_ascii_hexdigit()))
349        .take()
350        .parse_next(input)
351}
352
353fn frame(input: &mut &str) -> ModalResult<StackFrame> {
354    space0.parse_next(input)?;
355    let address = opt((
356        "[<",
357        take_while(1.., |c: char| c.is_ascii_hexdigit()),
358        ">]",
359        space0,
360    ))
361    .parse_next(input)?;
362    let uncertain = opt(("?", space0)).parse_next(input)?.is_some();
363    let (symbol, _, offset, _, size) = (
364        take_while(1.., |c: char| !c.is_whitespace() && c != '+'),
365        "+",
366        hex,
367        "/",
368        hex,
369    )
370        .parse_next(input)?;
371    // Reject partial sizes such as 0x10garbage while allowing module suffixes.
372    if input.chars().next().is_some_and(|c| !c.is_whitespace()) {
373        return Err(winnow::error::ErrMode::Backtrack(
374            winnow::error::ContextError::new(),
375        ));
376    }
377    Ok(StackFrame {
378        address: address.map(|(_, value, _, _)| value.to_owned()),
379        uncertain,
380        symbol: symbol.to_owned(),
381        offset: offset.to_owned(),
382        size: size.to_owned(),
383        suffix: input.trim_start().to_owned(),
384    })
385}
386
387fn register_value<'a>(input: &mut &'a str) -> ModalResult<&'a str> {
388    (
389        opt("0x"),
390        take_while(1.., |c: char| c.is_ascii_hexdigit()),
391        opt((
392            ":",
393            opt("0x"),
394            take_while(1.., |c: char| c.is_ascii_hexdigit()),
395        )),
396        opt(("(", take_while(1.., |c: char| c.is_ascii_hexdigit()), ")")),
397    )
398        .take()
399        .parse_next(input)
400}
401
402fn named_symbol(input: &mut &str) -> ModalResult<LineKind> {
403    let (label, _, _) = (
404        take_while(1.., |c: char| c.is_ascii_alphanumeric() || c == '_'),
405        ":",
406        space0,
407    )
408        .parse_next(input)?;
409    let segment = opt((take_while(1.., |c: char| c.is_ascii_hexdigit()), ":")).parse_next(input)?;
410    Ok(LineKind::Symbol {
411        label: label.to_owned(),
412        segment: segment.map(|(s, _)| s.to_owned()),
413        frame: frame.parse_next(input)?,
414    })
415}
416
417fn module_list(continuation: bool) -> impl FnMut(&mut &str) -> ModalResult<LineKind> {
418    move |input| {
419        let mut modules = Vec::new();
420        let mut last_unloaded = None;
421        while !input.is_empty() {
422            if input.starts_with("[last unloaded:") {
423                let (_, _, name, _, _) = (
424                    "[last unloaded:",
425                    space0,
426                    take_while(1.., |c: char| {
427                        c.is_ascii_alphanumeric() || "_-+.".contains(c)
428                    }),
429                    "]",
430                    space0,
431                )
432                    .parse_next(input)?;
433                last_unloaded = Some(name.to_owned());
434                break;
435            }
436            let (name, separator) = (
437                (
438                    take_while(1.., |c: char| {
439                        c.is_ascii_alphanumeric() || "_-+.".contains(c)
440                    }),
441                    opt(("(", take_while(1.., |c: char| c.is_ascii_alphabetic()), ")")),
442                )
443                    .take(),
444                space0,
445            )
446                .parse_next(input)?;
447            if separator.is_empty() && !input.is_empty() {
448                return Err(winnow::error::ErrMode::Backtrack(
449                    winnow::error::ContextError::new(),
450                ));
451            }
452            modules.push(name.to_owned());
453        }
454        if continuation && modules.is_empty() && last_unloaded.is_none() {
455            return Err(winnow::error::ErrMode::Backtrack(
456                winnow::error::ContextError::new(),
457            ));
458        }
459        Ok(LineKind::Modules {
460            modules,
461            last_unloaded,
462            continuation,
463        })
464    }
465}
466
467fn reboot(input: &mut &str) -> ModalResult<LineKind> {
468    let (_, seconds, _, _) =
469        ("Rebooting in ", digit1, " seconds", alt(("..", ".", ""))).parse_next(input)?;
470    let seconds = seconds
471        .parse::<i64>()
472        .map_err(|_| winnow::error::ErrMode::Backtrack(winnow::error::ContextError::new()))?;
473    Ok(LineKind::Reboot {
474        delay: SignedDuration::from_secs(seconds),
475    })
476}
477
478fn registers(input: &mut &str) -> ModalResult<Vec<Register>> {
479    let mut values = Vec::new();
480    while !input.is_empty() {
481        let (name, _, value, separator) = (
482            take_while(1.., |c: char| c.is_ascii_alphanumeric() || c == '_'),
483            alt((preceded(":", space0), space1)),
484            register_value,
485            space0,
486        )
487            .parse_next(input)?;
488        if separator.is_empty() && !input.is_empty() {
489            return Err(winnow::error::ErrMode::Backtrack(
490                winnow::error::ContextError::new(),
491            ));
492        }
493        values.push(Register {
494            name: name.to_owned(),
495            value: value.to_owned(),
496        });
497    }
498    if values.is_empty() {
499        return Err(winnow::error::ErrMode::Backtrack(
500            winnow::error::ContextError::new(),
501        ));
502    }
503    Ok(values)
504}
505
506fn classify(body: &str) -> LineKind {
507    let body = body.trim();
508    // Searching also recognizes panic markers inside syslog/journal wrappers.
509    if let Some((_, message)) = body.split_once("Kernel panic - not syncing:") {
510        return LineKind::Panic {
511            message: message.trim_start().to_owned(),
512        };
513    }
514    if matches!(
515        body,
516        "Call Trace:" | "Call trace:" | "Backtrace:" | "Stack backtrace:"
517    ) {
518        return LineKind::CallTrace;
519    }
520    if let Ok(value) = cpu.parse_next(&mut &*body) {
521        return value;
522    }
523    if let Ok(value) = named_symbol.parse_next(&mut &*body) {
524        return value;
525    }
526    if let Some(modules) = body.strip_prefix("Modules linked in:")
527        && let Ok(value) = module_list(false).parse(modules.trim())
528    {
529        return value;
530    }
531    if let Ok(value) = reboot.parse(body) {
532        return value;
533    }
534    for label in [
535        "Oops",
536        "BUG",
537        "#PF",
538        "Tainted",
539        "Hardware name",
540        "Workqueue",
541        "Code",
542        "Kernel Offset",
543    ] {
544        if let Some(details) = body
545            .strip_prefix(label)
546            .and_then(|rest| rest.strip_prefix(':'))
547        {
548            return LineKind::Diagnostic {
549                label: label.to_owned(),
550                details: details.trim_start().to_owned(),
551            };
552        }
553    }
554    if let Ok(value) = frame.parse_next(&mut &*body) {
555        return LineKind::Frame(value);
556    }
557    if let Ok(value) = registers.parse(body) {
558        return LineKind::Registers(value);
559    }
560    LineKind::Unknown
561}