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::{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    // Absolute addresses
137    #[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    // Relative addresses
147    #[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    // Debug Info
159    #[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    // Additional Info
173    #[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        // The backend requires (deobfuscation-api) requires lowercase
280        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// Tests are disabled on macos because we cannot generate the libs
397#[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()) //define in the build.rs
434        );
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()) //define in the build.rs
469        );
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}