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    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        // The backend requires (deobfuscation-api) requires lowercase
332        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// Tests are disabled on macos because we cannot generate the libs
449#[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()) //define in the build.rs
486        );
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()) //define in the build.rs
521        );
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}