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