Skip to main content

libdd_crashtracker/crash_info/
stacktrace.rs

1// Copyright 2024-Present Datadog, Inc. https://www.datadoghq.com/
2// SPDX-License-Identifier: Apache-2.0
3
4#[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    // Absolute addresses
140    #[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    // Relative addresses
150    #[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    // Debug Info
162    #[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    // Additional Info
176    #[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        // A process source needs /proc/<pid> to still exist. The crashing process can
226        // be gone by the time we symbolize (the sidecar receiver outlives it), so fall
227        // back to the ELF file and virtual offset that normalize_ip already recorded,
228        // which needs nothing from the live process.
229        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    /// The ELF file and the virtual offset inside it, as recorded by `normalize_ip`.
256    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        // The backend requires (deobfuscation-api) requires lowercase
360        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// Tests are disabled on macos because we cannot generate the libs
477#[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()) //define in the build.rs
514        );
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()) //define in the build.rs
549        );
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    /// Symbolization must still work when the target process is gone, which happens
581    /// whenever the receiver outlives it. `normalize_ip` runs while the process is
582    /// alive, then `resolve_names` falls back to the recorded ELF file and offset.
583    #[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        // A pid that cannot exist, standing in for a process that has already exited:
608        // pid_max caps live pids well below u32::MAX (2^22 at most on Linux), and
609        // Pid::from keeps the value as-is for anything non-zero, so this never resolves
610        // to a live process or to Pid::Slf.
611        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}