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