relay_server/utils/
native.rs

1//! Utility methods for native event processing.
2//!
3//! These functions are invoked by the `EnvelopeProcessor`, and are used to prepare native event
4//! payloads. See [`process_minidump`] and [`process_apple_crash_report`] for more information.
5
6use std::collections::BTreeMap;
7use std::error::Error;
8
9use chrono::{TimeZone, Utc};
10use minidump::{
11    MinidumpAnnotation, MinidumpCrashpadInfo, MinidumpModuleList, Module, StabilityReport,
12};
13use relay_event_schema::protocol::{
14    ClientSdkInfo, Context, Contexts, Event, Exception, JsonLenientString, Level, Mechanism,
15    StabilityReportContext, Values,
16};
17use relay_protocol::{Annotated, Value};
18
19type Minidump<'a> = minidump::Minidump<'a, &'a [u8]>;
20
21/// Placeholder payload fragments indicating a native event.
22///
23/// These payload attributes tell the processing pipeline that the event requires attachment
24/// processing and serve as defaults for failed events. When updating these values, also check the
25/// processing pipeline in Sentry.
26///
27/// The [`mechanism_type`](Self::mechanism_type) field is the most important field, as this is the
28/// primary indicator for processing. All other fields are mere defaults.
29#[derive(Debug)]
30struct NativePlaceholder {
31    /// The `exception.type` attribute value rendered in the issue.
32    exception_type: &'static str,
33    /// The default `exception.value` shown in the issue if processing fails.
34    exception_value: &'static str,
35    /// The `exception.mechanism.type` attribute, which is the primary indicator for processing.
36    mechanism_type: &'static str,
37}
38
39/// Writes a placeholder to indicate that this event has an associated minidump or an apple
40/// crash report.
41///
42/// This will indicate to the ingestion pipeline that this event will need to be processed. The
43/// payload can be checked via `is_minidump_event`.
44fn write_native_placeholder(event: &mut Event, placeholder: NativePlaceholder) {
45    // Events must be native platform.
46    let platform = event.platform.value_mut();
47    *platform = Some("native".to_owned());
48
49    // Assume that this minidump is the result of a crash and assign the fatal
50    // level. Note that the use of `setdefault` here doesn't generally allow the
51    // user to override the minidump's level as processing will overwrite it
52    // later.
53    event.level.get_or_insert_with(|| Level::Fatal);
54
55    // Create a placeholder exception. This signals normalization that this is an
56    // error event and also serves as a placeholder if processing of the minidump
57    // fails.
58    let exceptions = event
59        .exceptions
60        .value_mut()
61        .get_or_insert_with(Values::default)
62        .values
63        .value_mut()
64        .get_or_insert_with(Vec::new);
65
66    exceptions.clear(); // clear previous errors if any
67
68    exceptions.push(Annotated::new(Exception {
69        ty: Annotated::new(placeholder.exception_type.to_owned()),
70        value: Annotated::new(JsonLenientString(placeholder.exception_value.to_owned())),
71        mechanism: Annotated::new(Mechanism {
72            ty: Annotated::from(placeholder.mechanism_type.to_owned()),
73            handled: Annotated::from(false),
74            synthetic: Annotated::from(true),
75            ..Mechanism::default()
76        }),
77        ..Exception::default()
78    }));
79}
80
81/// Generates crashpad contexts for annotations stored in the minidump.
82///
83/// Returns an error if either the minidump module list or the crashpad information stream cannot be
84/// loaded from the minidump. Returns `Ok(())` in all other cases, including when no annotations are
85/// present.
86///
87/// Crashpad has global annotations, and per-module annotations. For each of these, a separate
88/// context of type "crashpad" is added, which contains the annotations as key-value mapping. List
89/// annotations are added to an "annotations" JSON list.
90fn write_crashpad_annotations(
91    event: &mut Event,
92    minidump: &Minidump<'_>,
93) -> Result<(), minidump::Error> {
94    let module_list = minidump.get_stream::<MinidumpModuleList>()?;
95    let crashpad_info = match minidump.get_stream::<MinidumpCrashpadInfo>() {
96        Err(minidump::Error::StreamNotFound) => return Ok(()),
97        result => result?,
98    };
99
100    let contexts = event.contexts.get_or_insert_with(Contexts::new);
101
102    if !crashpad_info.simple_annotations.is_empty() {
103        // First, create a generic crashpad context with top-level simple annotations. This context does
104        // not need a type field, since its type matches the the key.
105        let crashpad_context = crashpad_info
106            .simple_annotations
107            .into_iter()
108            .map(|(key, value)| (key, Annotated::new(Value::from(value))))
109            .collect();
110
111        contexts.insert("crashpad".to_owned(), Context::Other(crashpad_context));
112    }
113
114    match minidump.get_stream::<StabilityReport>() {
115        Ok(stability_report) => {
116            contexts.add(StabilityReportContext::from(stability_report));
117        }
118        Err(minidump::Error::StreamNotFound) => {
119            // Ignore missing stability report stream.
120        }
121        Err(err) => {
122            relay_log::debug!(
123                error = &err as &dyn Error,
124                "failed to parse stability report"
125            );
126        }
127    };
128
129    if crashpad_info.module_list.is_empty() {
130        return Ok(());
131    }
132
133    let modules = module_list.iter().collect::<Vec<_>>();
134
135    for module_info in crashpad_info.module_list {
136        // Resolve the actual module entry in the minidump module list. This entry should always
137        // exist and crashpad module info with an invalid link can be discarded. Since this is
138        // non-essential information, we skip gracefully and only emit debug logs.
139        let module = match modules.get(module_info.module_index) {
140            Some(module) => module,
141            None => {
142                relay_log::debug!(
143                    module_index = module_info.module_index,
144                    "Skipping invalid minidump module index",
145                );
146                continue;
147            }
148        };
149
150        // Use the basename of the code file (library or executable name) as context name. The
151        // context type must be set explicitly in this case, which will render in Sentry as
152        // "Module.dll (crashpad)".
153        let code_file = module.code_file();
154        let (_, module_name) = symbolic_common::split_path(&code_file);
155
156        let mut module_context = BTreeMap::new();
157        module_context.insert(
158            "type".to_owned(),
159            Annotated::new(Value::String("crashpad".to_owned())),
160        );
161
162        for (key, value) in module_info.simple_annotations {
163            module_context.insert(key, Annotated::new(Value::String(value)));
164        }
165
166        for (key, annotation) in module_info.annotation_objects {
167            if let MinidumpAnnotation::String(value) = annotation {
168                module_context.insert(key, Annotated::new(Value::String(value)));
169            }
170        }
171
172        if !module_info.list_annotations.is_empty() {
173            // Annotation lists do not maintain a key-value mapping, so instead write them to an
174            // "annotations" key within the module context. This will render as a JSON list in Sentry.
175            let annotation_list = module_info
176                .list_annotations
177                .into_iter()
178                .map(|s| Annotated::new(Value::String(s)))
179                .collect();
180
181            module_context.insert(
182                "annotations".to_owned(),
183                Annotated::new(Value::Array(annotation_list)),
184            );
185        }
186
187        contexts.insert(module_name.to_owned(), Context::Other(module_context));
188    }
189
190    Ok(())
191}
192
193/// Extracts information from the minidump and writes it into the given event.
194///
195/// This function operates at best-effort. It always attaches the placeholder and returns
196/// successfully, even if the minidump or part of its data cannot be parsed.
197pub fn process_minidump(event: &mut Event, data: &[u8]) {
198    let placeholder = NativePlaceholder {
199        exception_type: "Minidump",
200        exception_value: "Invalid Minidump",
201        mechanism_type: "minidump",
202    };
203    write_native_placeholder(event, placeholder);
204
205    let minidump = match Minidump::read(data) {
206        Ok(minidump) => minidump,
207        Err(err) => {
208            relay_log::debug!(error = &err as &dyn Error, "failed to parse minidump");
209            return;
210        }
211    };
212
213    let client_sdk_name = if minidump.get_stream::<MinidumpCrashpadInfo>().is_ok() {
214        "minidump.crashpad"
215    } else if minidump
216        .get_stream::<minidump::MinidumpBreakpadInfo>()
217        .is_ok()
218    {
219        "minidump.breakpad"
220    } else {
221        "minidump.unknown"
222    };
223
224    // Add sdk information for analytics.
225    event.client_sdk.get_or_insert_with(|| ClientSdkInfo {
226        name: Annotated::new(client_sdk_name.to_owned()),
227        version: "0.0.0".to_owned().into(),
228        ..ClientSdkInfo::default()
229    });
230
231    // Use the minidump's timestamp as the event's primary time. This timestamp can lie multiple
232    // days in the past, in which case the event may be rejected in store normalization.
233    let timestamp = Utc
234        .timestamp_opt(minidump.header.time_date_stamp.into(), 0)
235        .latest();
236
237    if let Some(timestamp) = timestamp {
238        event.timestamp.set_value(Some(timestamp.into()));
239    }
240
241    // Write annotations from the crashpad info stream, but skip gracefully on error. Annotations
242    // are non-essential to processing.
243    if let Err(err) = write_crashpad_annotations(event, &minidump) {
244        // TODO: Consider adding an event error for failed annotation extraction.
245        relay_log::debug!(
246            error = &err as &dyn Error,
247            "failed to parse minidump module list"
248        );
249    }
250}
251
252/// Writes minimal information into the event to indicate it is associated with an Apple Crash
253/// Report.
254pub fn process_apple_crash_report(event: &mut Event, _data: &[u8]) {
255    let placeholder = NativePlaceholder {
256        exception_type: "AppleCrashReport",
257        exception_value: "Invalid Apple Crash Report",
258        mechanism_type: "applecrashreport",
259    };
260    write_native_placeholder(event, placeholder);
261}