Skip to main content

asan_oracle/
lib.rs

1//! Crash classification, deduplication, and triage.
2//!
3//! Implements SPEC §12 (dedup hash, severity ranking) and §10.3 (CrashReport
4//! types). The oracle is the layer that turns a raw memory-safety event into
5//! a stable, reportable, comparable artifact.
6
7#![forbid(unsafe_code)]
8
9use serde::{Deserialize, Serialize};
10use std::fmt;
11
12pub mod dedup;
13pub mod log_parser;
14pub mod report;
15
16pub use dedup::{dedup_hash, DEDUP_DEFAULT_DEPTH};
17pub use log_parser::{parse as parse_asan_log, parse_one as parse_asan_log_one};
18pub use report::{Backtrace, CrashReport, Frame, Verdict};
19
20/// The classification of a memory-safety violation.
21///
22/// Ordered by the default severity heuristic from SPEC §12.2. The enum
23/// discriminant value is **not** the severity — use [`CrashKind::severity`].
24#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
25#[serde(tag = "kind", rename_all = "snake_case")]
26pub enum CrashKind {
27    HeapBufferOverflow { side: Side },
28    StackBufferOverflow,
29    GlobalBufferOverflow,
30    UseAfterFree { quarantine_residence_ms: u64 },
31    DoubleFree,
32    InvalidFree,
33    StackUseAfterReturn,
34    StackUseAfterScope,
35    MemoryLeak { bytes: u64 },
36    /// A sanitizer error we could not attribute to any known kind.
37    Unknown,
38}
39
40#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
41#[serde(rename_all = "snake_case")]
42pub enum Side {
43    Left,
44    Right,
45}
46
47/// SPEC §12.2 severity ranking. Higher = more severe.
48#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
49#[serde(rename_all = "snake_case")]
50#[repr(u8)]
51pub enum Severity {
52    Info = 0,
53    Low = 1,
54    Medium = 2,
55    High = 3,
56    Critical = 4,
57}
58
59impl fmt::Display for Severity {
60    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61        f.write_str(match self {
62            Severity::Info => "info",
63            Severity::Low => "low",
64            Severity::Medium => "medium",
65            Severity::High => "high",
66            Severity::Critical => "critical",
67        })
68    }
69}
70
71impl CrashKind {
72    /// Default severity per SPEC §12.2.
73    ///
74    /// This is a heuristic ranking of *memory-safety fact*, not exploitability.
75    /// The harness explicitly refuses to predict exploitability (§12.2 final
76    /// paragraph) — downstream analysts are responsible for that judgment.
77    ///
78    /// The explicit one-arm-per-variant form is deliberate: merging arms
79    /// with identical bodies (which clippy suggests) would hide the
80    /// auditable CrashKind → Severity mapping. An editor reading this
81    /// function must see every classification decision at the call site.
82    #[allow(clippy::match_same_arms)]
83    pub const fn severity(&self) -> Severity {
84        match self {
85            // Arbitrary write primitives — top of the ladder.
86            CrashKind::HeapBufferOverflow { side: Side::Right } => Severity::Critical,
87            CrashKind::UseAfterFree { .. } => Severity::Critical,
88            CrashKind::DoubleFree => Severity::High,
89            CrashKind::HeapBufferOverflow { side: Side::Left } => Severity::High,
90            CrashKind::StackBufferOverflow => Severity::High,
91            CrashKind::StackUseAfterReturn => Severity::High,
92            CrashKind::GlobalBufferOverflow => Severity::Medium,
93            CrashKind::StackUseAfterScope => Severity::Medium,
94            CrashKind::InvalidFree => Severity::Medium,
95            CrashKind::MemoryLeak { .. } => Severity::Low,
96            CrashKind::Unknown => Severity::Info,
97        }
98    }
99
100    pub const fn short_name(&self) -> &'static str {
101        match self {
102            CrashKind::HeapBufferOverflow { .. } => "heap-buffer-overflow",
103            CrashKind::StackBufferOverflow => "stack-buffer-overflow",
104            CrashKind::GlobalBufferOverflow => "global-buffer-overflow",
105            CrashKind::UseAfterFree { .. } => "use-after-free",
106            CrashKind::DoubleFree => "double-free",
107            CrashKind::InvalidFree => "invalid-free",
108            CrashKind::StackUseAfterReturn => "stack-use-after-return",
109            CrashKind::StackUseAfterScope => "stack-use-after-scope",
110            CrashKind::MemoryLeak { .. } => "memory-leak",
111            CrashKind::Unknown => "unknown",
112        }
113    }
114}
115
116#[derive(Debug, thiserror::Error)]
117pub enum OracleError {
118    #[error("serialization failed: {0}")]
119    Serialize(#[from] serde_json::Error),
120    #[error("i/o failure: {0}")]
121    Io(#[from] std::io::Error),
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    #[test]
129    fn severity_ordering_matches_spec_12_2() {
130        // SPEC §12.2 ordering: right-overflow ≥ UAF ≥ double-free ≥ OOB read ≥ invalid-free ≥ leak
131        assert!(
132            CrashKind::HeapBufferOverflow { side: Side::Right }.severity()
133                >= CrashKind::UseAfterFree { quarantine_residence_ms: 0 }.severity()
134        );
135        assert!(
136            CrashKind::UseAfterFree { quarantine_residence_ms: 0 }.severity()
137                >= CrashKind::DoubleFree.severity()
138        );
139        assert!(CrashKind::DoubleFree.severity() >= CrashKind::InvalidFree.severity());
140        assert!(CrashKind::InvalidFree.severity() >= CrashKind::MemoryLeak { bytes: 0 }.severity());
141    }
142
143    #[test]
144    fn short_name_is_stable() {
145        // The name is part of our machine-readable output contract.
146        assert_eq!(
147            CrashKind::HeapBufferOverflow { side: Side::Right }.short_name(),
148            "heap-buffer-overflow"
149        );
150    }
151}