Skip to main content

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