1#[cfg(unix)]
5use crate::CachedElfResolvers;
6#[cfg(unix)]
7use blazesym::{
8 normalize::Normalizer,
9 symbolize::{source::Source, Input, Symbolized, Symbolizer, TranslateFileOffset},
10 Pid,
11};
12
13use schemars::JsonSchema;
14use serde::{Deserialize, Serialize};
15use symbolic_common::Name;
16use symbolic_demangle::{Demangle, DemangleOptions};
17
18#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
19pub struct StackTrace {
20 pub format: String,
21 pub frames: Vec<StackFrame>,
22 pub incomplete: bool,
23}
24
25const FORMAT_STRING: &str = "Datadog Crashtracker 1.0";
26
27impl StackTrace {
28 pub fn empty() -> Self {
29 Self {
30 format: FORMAT_STRING.to_string(),
31 frames: vec![],
32 incomplete: false,
33 }
34 }
35
36 pub fn from_frames(frames: Vec<StackFrame>, incomplete: bool) -> Self {
37 Self {
38 format: FORMAT_STRING.to_string(),
39 frames,
40 incomplete,
41 }
42 }
43
44 pub fn new_incomplete() -> Self {
45 Self {
46 format: FORMAT_STRING.to_string(),
47 frames: vec![],
48 incomplete: true,
49 }
50 }
51
52 pub fn missing() -> Self {
53 Self {
54 format: FORMAT_STRING.to_string(),
55 frames: vec![],
56 incomplete: true,
57 }
58 }
59}
60
61impl StackTrace {
62 pub fn set_complete(&mut self) -> anyhow::Result<()> {
63 self.incomplete = false;
64 Ok(())
65 }
66
67 pub fn push_frame(&mut self, frame: StackFrame, incomplete: bool) -> anyhow::Result<()> {
68 anyhow::ensure!(
69 self.incomplete,
70 "Can't push a new frame onto a complete stack"
71 );
72 self.frames.push(frame);
73 self.incomplete = incomplete;
74 Ok(())
75 }
76
77 pub fn demangle_names(&mut self) -> anyhow::Result<()> {
78 let mut errors = 0;
79 for frame in &mut self.frames {
80 frame.demangle_name().unwrap_or_else(|e| {
81 frame.comments.push(e.to_string());
82 errors += 1;
83 });
84 }
85 anyhow::ensure!(errors == 0);
86 Ok(())
87 }
88}
89
90#[cfg(unix)]
91impl StackTrace {
92 pub fn normalize_ips(
93 &mut self,
94 normalizer: &Normalizer,
95 pid: Pid,
96 elf_resolvers: &mut CachedElfResolvers,
97 ) -> anyhow::Result<()> {
98 let mut errors = 0;
99 for frame in &mut self.frames {
100 frame
101 .normalize_ip(normalizer, pid, elf_resolvers)
102 .unwrap_or_else(|e| {
103 frame
104 .comments
105 .push(format!("normalize_ip failed with {e:#}"));
106 errors += 1;
107 });
108 }
109 anyhow::ensure!(errors == 0);
110 Ok(())
111 }
112
113 pub fn resolve_names(&mut self, src: &Source, symbolizer: &Symbolizer) -> anyhow::Result<()> {
114 let mut errors = 0;
115 for frame in &mut self.frames {
116 frame.resolve_names(src, symbolizer).unwrap_or_else(|e| {
117 frame
118 .comments
119 .push(format!("resolve_names failed with {e:#}"));
120 errors += 1;
121 });
122 }
123 anyhow::ensure!(errors == 0);
124 Ok(())
125 }
126}
127
128impl Default for StackTrace {
129 fn default() -> Self {
130 Self::missing()
131 }
132}
133
134#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, Default)]
135pub struct StackFrame {
136 #[serde(default, skip_serializing_if = "Option::is_none")]
138 pub ip: Option<String>,
139 #[serde(default, skip_serializing_if = "Option::is_none")]
140 pub module_base_address: Option<String>,
141 #[serde(default, skip_serializing_if = "Option::is_none")]
142 pub sp: Option<String>,
143 #[serde(default, skip_serializing_if = "Option::is_none")]
144 pub symbol_address: Option<String>,
145
146 #[serde(default, skip_serializing_if = "Option::is_none")]
148 pub build_id: Option<String>,
149 #[serde(default, skip_serializing_if = "Option::is_none")]
150 pub build_id_type: Option<BuildIdType>,
151 #[serde(default, skip_serializing_if = "Option::is_none")]
152 pub file_type: Option<FileType>,
153 #[serde(default, skip_serializing_if = "Option::is_none")]
154 pub path: Option<String>,
155 #[serde(default, skip_serializing_if = "Option::is_none")]
156 pub relative_address: Option<String>,
157
158 #[serde(default, skip_serializing_if = "Option::is_none")]
160 pub column: Option<u32>,
161 #[serde(default, skip_serializing_if = "Option::is_none")]
162 pub file: Option<String>,
163 #[serde(default, skip_serializing_if = "Option::is_none")]
164 pub function: Option<String>,
165 #[serde(default, skip_serializing_if = "Option::is_none")]
166 pub line: Option<u32>,
167 #[serde(default, skip_serializing_if = "Option::is_none")]
168 pub type_name: Option<String>,
169 #[serde(default, skip_serializing_if = "Option::is_none")]
170 pub mangled_name: Option<String>,
171
172 #[serde(default, skip_serializing_if = "Vec::is_empty")]
174 pub comments: Vec<String>,
175}
176
177impl StackFrame {
178 pub fn new() -> Self {
179 Self::default()
180 }
181}
182
183#[cfg(unix)]
184impl StackFrame {
185 pub fn normalize_ip(
186 &mut self,
187 normalizer: &Normalizer,
188 pid: Pid,
189 elf_resolvers: &mut CachedElfResolvers,
190 ) -> anyhow::Result<()> {
191 use anyhow::Context;
192 if let Some(ip) = &self.ip {
193 let ip = ip.trim_start_matches("0x");
194 let ip = u64::from_str_radix(ip, 16)?;
195 let normed = normalizer.normalize_user_addrs(pid, &[ip])?;
196 anyhow::ensure!(normed.outputs.len() == 1);
197 let (file_offset, meta_idx) = normed.outputs[0];
198 let meta = &normed.meta[meta_idx];
199 let elf = meta.as_elf().context("Not elf")?;
200 let resolver = elf_resolvers.get_or_insert(&elf.path)?;
201 let virt_address = resolver
202 .file_offset_to_virt_offset(file_offset)?
203 .context("No matching segment found")?;
204
205 self.build_id = elf.build_id.as_ref().map(|x| byte_slice_as_hex(x.as_ref()));
206 self.build_id_type = Some(BuildIdType::GNU);
207 self.file_type = Some(FileType::ELF);
208 self.path = Some(elf.path.to_string_lossy().to_string());
209 self.relative_address = Some(format!("{virt_address:#018x}"));
210 }
211 Ok(())
212 }
213
214 pub fn resolve_names(&mut self, src: &Source, symbolizer: &Symbolizer) -> anyhow::Result<()> {
215 if let Some(ip) = &self.ip {
216 let ip = ip.trim_start_matches("0x");
217 let ip = u64::from_str_radix(ip, 16)?;
218 let input = Input::AbsAddr(ip);
219 match symbolizer.symbolize_single(src, input)? {
220 Symbolized::Sym(s) => {
221 if let Some(c) = s.code_info {
222 self.column = c.column.map(u32::from);
223 self.file = Some(c.to_path().display().to_string());
224 self.line = c.line;
225 }
226 self.function = Some(s.name.into_owned());
227 }
228 Symbolized::Unknown(reason) => {
229 anyhow::bail!("Couldn't symbolize {ip}: {reason}");
230 }
231 }
232 }
233 Ok(())
234 }
235}
236
237impl StackFrame {
238 pub fn demangle_name(&mut self) -> anyhow::Result<()> {
239 if let Some(name) = self.function.take() {
240 match Name::from(&name).demangle(DemangleOptions::name_only()) {
241 Some(demangled) if demangled != name => {
242 self.mangled_name = Some(name);
243 self.function = Some(demangled.to_string());
244 }
245 _ => {
246 self.function = Some(name);
247 }
248 }
249 }
250 Ok(())
251 }
252}
253
254#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
255#[allow(clippy::upper_case_acronyms)]
256#[repr(C)]
257pub enum BuildIdType {
258 GNU,
259 GO,
260 PDB,
261 SHA1,
262}
263
264#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
265#[allow(clippy::upper_case_acronyms)]
266#[repr(C)]
267pub enum FileType {
268 APK,
269 ELF,
270 PE,
271}
272
273#[cfg(unix)]
274fn byte_slice_as_hex(bv: &[u8]) -> String {
275 use std::fmt::Write;
276
277 let mut s = String::with_capacity(bv.len() * 2);
278 for byte in bv {
279 let _ = write!(&mut s, "{byte:02x}");
281 }
282 s
283}
284
285#[cfg(test)]
286impl super::test_utils::TestInstance for StackTrace {
287 fn test_instance(_seed: u64) -> Self {
288 let frames = (0..10).map(StackFrame::test_instance).collect();
289 Self::from_frames(frames, false)
290 }
291}
292
293#[cfg(test)]
294impl super::test_utils::TestInstance for StackFrame {
295 fn test_instance(seed: u64) -> Self {
296 let ip = Some(format!("{seed:#x}"));
297 let module_base_address = None;
298 let sp = None;
299 let symbol_address = None;
300
301 let build_id = Some(format!("abcde{seed:#x}"));
302 let build_id_type = Some(BuildIdType::GNU);
303 let file_type = Some(FileType::ELF);
304 let path = Some(format!("/usr/bin/foo{seed}"));
305 let relative_address = None;
306
307 let column = Some(2 * seed as u32);
308 let file = Some(format!("banana{seed}.rs"));
309 let function = Some(format!("Bar::baz{seed}"));
310 let mangled_name = Some(format!("_ZN3Bar3baz{seed}E"));
311 let line = Some((2 * seed + 1) as u32);
312 let type_name = Some("Bar".to_string());
313 let comments = vec![format!("This is a comment on frame {seed}")];
314 Self {
315 ip,
316 module_base_address,
317 sp,
318 symbol_address,
319 build_id,
320 build_id_type,
321 file_type,
322 path,
323 relative_address,
324 column,
325 file,
326 function,
327 mangled_name,
328 line,
329 comments,
330 type_name,
331 }
332 }
333}
334
335#[cfg(test)]
336mod tests {
337 use super::*;
338
339 #[test]
340 fn test_demangle_rust() {
341 let mut frame = StackFrame::new();
342 frame.function = Some("_ZN3std2rt10lang_start17h7a87e81ecc4a9d6cE".to_string());
343 frame.demangle_name().unwrap();
344 assert_eq!(frame.function, Some("std::rt::lang_start".to_string()));
345 assert_eq!(
346 frame.mangled_name,
347 Some("_ZN3std2rt10lang_start17h7a87e81ecc4a9d6cE".to_string())
348 );
349 }
350
351 #[test]
352 fn test_demangle_cpp() {
353 let mut frame = StackFrame::new();
354 frame.function = Some("_ZN3Foo3barEv".to_string());
355 frame.demangle_name().unwrap();
356 assert_eq!(frame.function, Some("Foo::bar".to_string()));
357 assert_eq!(frame.mangled_name, Some("_ZN3Foo3barEv".to_string()));
358 }
359
360 #[test]
361 fn test_demangle_msvc() {
362 let mut frame = StackFrame::new();
363 frame.function = Some("?bar@Foo@@QEAAXXZ".to_string());
364 frame.demangle_name().unwrap();
365 assert_eq!(frame.function, Some("Foo::bar".to_string()));
366 assert_eq!(frame.mangled_name, Some("?bar@Foo@@QEAAXXZ".to_string()));
367 }
368
369 #[test]
370 fn test_demangle_unmangled() {
371 let mut frame = StackFrame::new();
372 frame.function = Some("main".to_string());
373 frame.demangle_name().unwrap();
374 assert_eq!(frame.function, Some("main".to_string()));
375 assert_eq!(frame.mangled_name, None);
376 }
377
378 #[test]
379 fn test_demangle_empty() {
380 let mut frame = StackFrame::new();
381 frame.demangle_name().unwrap();
382 assert_eq!(frame.function, None);
383 assert_eq!(frame.mangled_name, None);
384 }
385
386 #[test]
387 fn test_demangle_invalid() {
388 let mut frame = StackFrame::new();
389 frame.function = Some("invalid_mangled_name".to_string());
390 frame.demangle_name().unwrap();
391 assert_eq!(frame.function, Some("invalid_mangled_name".to_string()));
392 assert_eq!(frame.mangled_name, None);
393 }
394}
395
396#[cfg(all(unix, not(target_os = "macos"), feature = "generate-unit-test-files"))]
398#[cfg(test)]
399mod unix_test {
400 use super::*;
401 use crate::{get_tests_folder_path, SharedLibrary};
402
403 #[test]
404 #[cfg_attr(miri, ignore)]
405 fn test_normalize_ip() {
406 let test_so = get_tests_folder_path()
407 .expect("Failed to get the tests folder path")
408 .join("libtest.so")
409 .canonicalize()
410 .unwrap();
411
412 let libtest_so =
413 SharedLibrary::open(test_so.to_str().unwrap()).expect("Failed to open library");
414 let address = libtest_so.get_symbol_address("my_function").unwrap();
415 let mut frame = StackFrame::new();
416 frame.ip = Some(address);
417
418 let mut symbolizer = Symbolizer::new();
419 let normalizer = Normalizer::new();
420 frame
421 .normalize_ip(
422 &normalizer,
423 Pid::from(std::process::id()),
424 &mut CachedElfResolvers::new(&mut symbolizer),
425 )
426 .unwrap();
427
428 assert_eq!(frame.build_id_type, Some(BuildIdType::GNU));
429 assert_eq!(frame.file_type, Some(FileType::ELF));
430 assert_eq!(frame.path, Some(test_so.to_string_lossy().to_string()));
431 assert_eq!(
432 frame.build_id,
433 Some("aaaabbbbccccddddeeeeffff0011223344556677".to_string()) );
435 assert!(frame.relative_address.is_some());
436 }
437
438 #[test]
439 #[cfg_attr(miri, ignore)]
440 fn test_normalize_ip_cpp() {
441 let test_so = get_tests_folder_path()
442 .expect("Failed to get the tests folder path")
443 .join("libtest_cpp.so")
444 .canonicalize()
445 .unwrap();
446
447 let libtest_so =
448 SharedLibrary::open(test_so.to_str().unwrap()).expect("Failed to open library");
449 let address = libtest_so.get_symbol_address("_Z12cpp_functionv").unwrap();
450 let mut frame = StackFrame::new();
451 frame.ip = Some(address);
452
453 let mut symbolizer = blazesym::symbolize::Symbolizer::new();
454 let normalizer = Normalizer::new();
455 frame
456 .normalize_ip(
457 &normalizer,
458 Pid::from(std::process::id()),
459 &mut CachedElfResolvers::new(&mut symbolizer),
460 )
461 .unwrap();
462
463 assert_eq!(frame.build_id_type, Some(BuildIdType::GNU));
464 assert_eq!(frame.file_type, Some(FileType::ELF));
465 assert_eq!(frame.path, Some(test_so.to_string_lossy().to_string()));
466 assert_eq!(
467 frame.build_id,
468 Some("0011223344556677aaaabbbbccccddddeeeeffff".to_string()) );
470 assert!(frame.relative_address.is_some());
471 }
472
473 #[test]
474 #[cfg_attr(miri, ignore)]
475 fn test_symbolization() {
476 let test_so = get_tests_folder_path()
477 .expect("Failed to get the tests folder path")
478 .join("libtest.so")
479 .canonicalize()
480 .unwrap();
481
482 let libtest_so =
483 SharedLibrary::open(test_so.to_str().unwrap()).expect("Failed to open library");
484 let address = libtest_so.get_symbol_address("my_function").unwrap();
485 let mut frame = StackFrame::new();
486 frame.ip = Some(address);
487
488 let mut process = blazesym::symbolize::source::Process::new(std::process::id().into());
489 process.map_files = false;
490 let src = blazesym::symbolize::source::Source::Process(process);
491 let symbolizer = blazesym::symbolize::Symbolizer::new();
492 frame.resolve_names(&src, &symbolizer).unwrap();
493
494 assert_eq!(frame.function, Some("my_function".to_string()));
495 let parent_dir = test_so.parent().unwrap();
496 let c_file = parent_dir.join("libtest.c");
497 assert_eq!(frame.file, Some(c_file.to_string_lossy().to_string()));
498 }
499
500 #[test]
501 #[cfg_attr(miri, ignore)]
502 fn test_symbolization_cpp() {
503 let test_so = get_tests_folder_path()
504 .expect("Failed to get the tests folder path")
505 .join("libtest_cpp.so")
506 .canonicalize()
507 .unwrap();
508
509 let libtest_so =
510 SharedLibrary::open(test_so.to_str().unwrap()).expect("Failed to open library");
511 let address = libtest_so
512 .get_symbol_address(
513 "_ZN11MyNamespace16ClassInNamespace21InnerClassInNamespace12InnerMethod1Ev",
514 )
515 .unwrap();
516 let mut frame = StackFrame::new();
517 frame.ip = Some(address);
518
519 let mut process = blazesym::symbolize::source::Process::new(std::process::id().into());
520 process.map_files = false;
521 let src = blazesym::symbolize::source::Source::Process(process);
522 let symbolizer = blazesym::symbolize::Symbolizer::new();
523 frame.resolve_names(&src, &symbolizer).unwrap();
524
525 assert_eq!(
526 frame.function,
527 Some(
528 "MyNamespace::ClassInNamespace::InnerClassInNamespace::InnerMethod1()".to_string()
529 )
530 );
531 let parent_dir = test_so.parent().unwrap();
532 let c_file = parent_dir.join("libtest_cpp.cpp");
533 assert_eq!(frame.file, Some(c_file.to_string_lossy().to_string()));
534 }
535}