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 pub fn set_build_id_type(&mut self, build_id_type: BuildIdType) {
254 self.build_id_type = Some(build_id_type);
255 }
256
257 pub fn set_file_type(&mut self, file_type: FileType) {
258 self.file_type = Some(file_type);
259 }
260
261 pub fn with_ip(&mut self, ip: usize) {
262 self.ip = Some(format!("0x{:x}", ip));
263 }
264
265 pub fn with_module_base_address(&mut self, addr: usize) {
266 self.module_base_address = Some(format!("0x{:x}", addr));
267 }
268
269 pub fn with_sp(&mut self, sp: usize) {
270 self.sp = Some(format!("0x{:x}", sp));
271 }
272
273 pub fn with_symbol_address(&mut self, addr: usize) {
274 self.symbol_address = Some(format!("0x{:x}", addr));
275 }
276
277 pub fn with_build_id(&mut self, build_id: String) {
278 self.build_id = Some(build_id);
279 }
280
281 pub fn with_path(&mut self, path: String) {
282 self.path = Some(path);
283 }
284
285 pub fn with_relative_address(&mut self, addr: usize) {
286 self.relative_address = Some(format!("0x{:x}", addr));
287 }
288
289 pub fn with_function(&mut self, function: String) {
290 self.function = Some(function);
291 }
292
293 pub fn with_file(&mut self, file: String) {
294 self.file = Some(file);
295 }
296
297 pub fn with_line(&mut self, line: u32) {
298 self.line = Some(line);
299 }
300
301 pub fn with_column(&mut self, column: u32) {
302 self.column = Some(column);
303 }
304}
305
306#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
307#[allow(clippy::upper_case_acronyms)]
308#[repr(C)]
309pub enum BuildIdType {
310 GNU,
311 GO,
312 PDB,
313 SHA1,
314}
315
316#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
317#[allow(clippy::upper_case_acronyms)]
318#[repr(C)]
319pub enum FileType {
320 APK,
321 ELF,
322 PE,
323}
324
325#[cfg(unix)]
326fn byte_slice_as_hex(bv: &[u8]) -> String {
327 use core::fmt::Write;
328
329 let mut s = String::with_capacity(bv.len() * 2);
330 for byte in bv {
331 let _ = write!(&mut s, "{byte:02x}");
333 }
334 s
335}
336
337#[cfg(test)]
338impl super::test_utils::TestInstance for StackTrace {
339 fn test_instance(_seed: u64) -> Self {
340 let frames = (0..10).map(StackFrame::test_instance).collect();
341 Self::from_frames(frames, false)
342 }
343}
344
345#[cfg(test)]
346impl super::test_utils::TestInstance for StackFrame {
347 fn test_instance(seed: u64) -> Self {
348 let ip = Some(format!("{seed:#x}"));
349 let module_base_address = None;
350 let sp = None;
351 let symbol_address = None;
352
353 let build_id = Some(format!("abcde{seed:#x}"));
354 let build_id_type = Some(BuildIdType::GNU);
355 let file_type = Some(FileType::ELF);
356 let path = Some(format!("/usr/bin/foo{seed}"));
357 let relative_address = None;
358
359 let column = Some(2 * seed as u32);
360 let file = Some(format!("banana{seed}.rs"));
361 let function = Some(format!("Bar::baz{seed}"));
362 let mangled_name = Some(format!("_ZN3Bar3baz{seed}E"));
363 let line = Some((2 * seed + 1) as u32);
364 let type_name = Some("Bar".to_string());
365 let comments = vec![format!("This is a comment on frame {seed}")];
366 Self {
367 ip,
368 module_base_address,
369 sp,
370 symbol_address,
371 build_id,
372 build_id_type,
373 file_type,
374 path,
375 relative_address,
376 column,
377 file,
378 function,
379 mangled_name,
380 line,
381 comments,
382 type_name,
383 }
384 }
385}
386
387#[cfg(test)]
388mod tests {
389 use super::*;
390
391 #[test]
392 fn test_demangle_rust() {
393 let mut frame = StackFrame::new();
394 frame.function = Some("_ZN3std2rt10lang_start17h7a87e81ecc4a9d6cE".to_string());
395 frame.demangle_name().unwrap();
396 assert_eq!(frame.function, Some("std::rt::lang_start".to_string()));
397 assert_eq!(
398 frame.mangled_name,
399 Some("_ZN3std2rt10lang_start17h7a87e81ecc4a9d6cE".to_string())
400 );
401 }
402
403 #[test]
404 fn test_demangle_cpp() {
405 let mut frame = StackFrame::new();
406 frame.function = Some("_ZN3Foo3barEv".to_string());
407 frame.demangle_name().unwrap();
408 assert_eq!(frame.function, Some("Foo::bar".to_string()));
409 assert_eq!(frame.mangled_name, Some("_ZN3Foo3barEv".to_string()));
410 }
411
412 #[test]
413 fn test_demangle_msvc() {
414 let mut frame = StackFrame::new();
415 frame.function = Some("?bar@Foo@@QEAAXXZ".to_string());
416 frame.demangle_name().unwrap();
417 assert_eq!(frame.function, Some("Foo::bar".to_string()));
418 assert_eq!(frame.mangled_name, Some("?bar@Foo@@QEAAXXZ".to_string()));
419 }
420
421 #[test]
422 fn test_demangle_unmangled() {
423 let mut frame = StackFrame::new();
424 frame.function = Some("main".to_string());
425 frame.demangle_name().unwrap();
426 assert_eq!(frame.function, Some("main".to_string()));
427 assert_eq!(frame.mangled_name, None);
428 }
429
430 #[test]
431 fn test_demangle_empty() {
432 let mut frame = StackFrame::new();
433 frame.demangle_name().unwrap();
434 assert_eq!(frame.function, None);
435 assert_eq!(frame.mangled_name, None);
436 }
437
438 #[test]
439 fn test_demangle_invalid() {
440 let mut frame = StackFrame::new();
441 frame.function = Some("invalid_mangled_name".to_string());
442 frame.demangle_name().unwrap();
443 assert_eq!(frame.function, Some("invalid_mangled_name".to_string()));
444 assert_eq!(frame.mangled_name, None);
445 }
446}
447
448#[cfg(all(unix, not(target_os = "macos"), feature = "generate-unit-test-files"))]
450#[cfg(test)]
451mod unix_test {
452 use super::*;
453 use crate::{get_tests_folder_path, SharedLibrary};
454
455 #[test]
456 #[cfg_attr(miri, ignore)]
457 fn test_normalize_ip() {
458 let test_so = get_tests_folder_path()
459 .expect("Failed to get the tests folder path")
460 .join("libtest.so")
461 .canonicalize()
462 .unwrap();
463
464 let libtest_so =
465 SharedLibrary::open(test_so.to_str().unwrap()).expect("Failed to open library");
466 let address = libtest_so.get_symbol_address("my_function").unwrap();
467 let mut frame = StackFrame::new();
468 frame.ip = Some(address);
469
470 let mut symbolizer = Symbolizer::new();
471 let normalizer = Normalizer::new();
472 frame
473 .normalize_ip(
474 &normalizer,
475 Pid::from(std::process::id()),
476 &mut CachedElfResolvers::new(&mut symbolizer),
477 )
478 .unwrap();
479
480 assert_eq!(frame.build_id_type, Some(BuildIdType::GNU));
481 assert_eq!(frame.file_type, Some(FileType::ELF));
482 assert_eq!(frame.path, Some(test_so.to_string_lossy().to_string()));
483 assert_eq!(
484 frame.build_id,
485 Some("aaaabbbbccccddddeeeeffff0011223344556677".to_string()) );
487 assert!(frame.relative_address.is_some());
488 }
489
490 #[test]
491 #[cfg_attr(miri, ignore)]
492 fn test_normalize_ip_cpp() {
493 let test_so = get_tests_folder_path()
494 .expect("Failed to get the tests folder path")
495 .join("libtest_cpp.so")
496 .canonicalize()
497 .unwrap();
498
499 let libtest_so =
500 SharedLibrary::open(test_so.to_str().unwrap()).expect("Failed to open library");
501 let address = libtest_so.get_symbol_address("_Z12cpp_functionv").unwrap();
502 let mut frame = StackFrame::new();
503 frame.ip = Some(address);
504
505 let mut symbolizer = blazesym::symbolize::Symbolizer::new();
506 let normalizer = Normalizer::new();
507 frame
508 .normalize_ip(
509 &normalizer,
510 Pid::from(std::process::id()),
511 &mut CachedElfResolvers::new(&mut symbolizer),
512 )
513 .unwrap();
514
515 assert_eq!(frame.build_id_type, Some(BuildIdType::GNU));
516 assert_eq!(frame.file_type, Some(FileType::ELF));
517 assert_eq!(frame.path, Some(test_so.to_string_lossy().to_string()));
518 assert_eq!(
519 frame.build_id,
520 Some("0011223344556677aaaabbbbccccddddeeeeffff".to_string()) );
522 assert!(frame.relative_address.is_some());
523 }
524
525 #[test]
526 #[cfg_attr(miri, ignore)]
527 fn test_symbolization() {
528 let test_so = get_tests_folder_path()
529 .expect("Failed to get the tests folder path")
530 .join("libtest.so")
531 .canonicalize()
532 .unwrap();
533
534 let libtest_so =
535 SharedLibrary::open(test_so.to_str().unwrap()).expect("Failed to open library");
536 let address = libtest_so.get_symbol_address("my_function").unwrap();
537 let mut frame = StackFrame::new();
538 frame.ip = Some(address);
539
540 let mut process = blazesym::symbolize::source::Process::new(std::process::id().into());
541 process.map_files = false;
542 let src = blazesym::symbolize::source::Source::Process(process);
543 let symbolizer = blazesym::symbolize::Symbolizer::new();
544 frame.resolve_names(&src, &symbolizer).unwrap();
545
546 assert_eq!(frame.function, Some("my_function".to_string()));
547 let parent_dir = test_so.parent().unwrap();
548 let c_file = parent_dir.join("libtest.c");
549 assert_eq!(frame.file, Some(c_file.to_string_lossy().to_string()));
550 }
551
552 #[test]
553 #[cfg_attr(miri, ignore)]
554 fn test_symbolization_cpp() {
555 let test_so = get_tests_folder_path()
556 .expect("Failed to get the tests folder path")
557 .join("libtest_cpp.so")
558 .canonicalize()
559 .unwrap();
560
561 let libtest_so =
562 SharedLibrary::open(test_so.to_str().unwrap()).expect("Failed to open library");
563 let address = libtest_so
564 .get_symbol_address(
565 "_ZN11MyNamespace16ClassInNamespace21InnerClassInNamespace12InnerMethod1Ev",
566 )
567 .unwrap();
568 let mut frame = StackFrame::new();
569 frame.ip = Some(address);
570
571 let mut process = blazesym::symbolize::source::Process::new(std::process::id().into());
572 process.map_files = false;
573 let src = blazesym::symbolize::source::Source::Process(process);
574 let symbolizer = blazesym::symbolize::Symbolizer::new();
575 frame.resolve_names(&src, &symbolizer).unwrap();
576
577 assert_eq!(
578 frame.function,
579 Some(
580 "MyNamespace::ClassInNamespace::InnerClassInNamespace::InnerMethod1()".to_string()
581 )
582 );
583 let parent_dir = test_so.parent().unwrap();
584 let c_file = parent_dir.join("libtest_cpp.cpp");
585 assert_eq!(frame.file, Some(c_file.to_string_lossy().to_string()));
586 }
587}