libdd-crashtracker 3.0.0

Detects program crashes and reports them to datadog backend.
Documentation
// Copyright 2024-Present Datadog, Inc. https://www.datadoghq.com/
// SPDX-License-Identifier: Apache-2.0
use super::stacktrace::StackTrace;
#[cfg(unix)]
use alloc::rc::Rc;
#[cfg(unix)]
use blazesym::helper::ElfResolver;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[cfg(unix)]
use std::collections::HashMap;
#[cfg(unix)]
use std::path::PathBuf;

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct ErrorData {
    pub is_crash: bool,
    pub kind: ErrorKind,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub message: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub thread_name: Option<String>,
    pub source_type: SourceType,
    pub stack: StackTrace,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub threads: Option<Vec<ThreadData>>,
}

#[cfg(unix)]
pub struct CachedElfResolvers<'a> {
    symbolizer: &'a mut blazesym::symbolize::Symbolizer,
    elf_resolvers: HashMap<PathBuf, Rc<ElfResolver>>,
}

#[cfg(unix)]
impl<'a> CachedElfResolvers<'a> {
    pub fn new(symbolizer: &'a mut blazesym::symbolize::Symbolizer) -> Self {
        Self {
            symbolizer,
            elf_resolvers: HashMap::new(),
        }
    }

    pub fn get_or_insert(&mut self, file_path: &PathBuf) -> anyhow::Result<Rc<ElfResolver>> {
        use anyhow::Context;
        let entry = self.elf_resolvers.entry(file_path.clone());

        match entry {
            std::collections::hash_map::Entry::Occupied(o) => Ok(o.get().clone()),
            std::collections::hash_map::Entry::Vacant(v) => {
                let resolver = ElfResolver::open(file_path).with_context(|| {
                    format!(
                        "ElfResolver::open failed for '{}'",
                        file_path.to_string_lossy()
                    )
                });

                match resolver {
                    Ok(resolver) => {
                        let resolver = Rc::new(resolver);
                        // even if the symbolizer failed at registering the elf resolver, we still
                        // cache it to avoid trying to open it again
                        let _ = self
                            .symbolizer
                            .register_elf_resolver(file_path.as_path(), Rc::clone(&resolver));
                        v.insert(Rc::clone(&resolver));
                        Ok(resolver)
                    }
                    Err(e) => Err(e),
                }
            }
        }
    }
}

#[cfg(unix)]
impl ErrorData {
    pub fn normalize_ips(&mut self, pid: u32) -> anyhow::Result<()> {
        let mut symbolizer = blazesym::symbolize::Symbolizer::new();
        let mut elf_resolvers = CachedElfResolvers::new(&mut symbolizer);
        let normalizer = blazesym::normalize::Normalizer::builder()
            .enable_vma_caching(true)
            .enable_build_ids(true)
            .enable_build_id_caching(true)
            .build();
        self.normalize_ips_impl(pid, &normalizer, &mut elf_resolvers)
    }

    pub(crate) fn normalize_ips_impl(
        &mut self,
        pid: u32,
        normalizer: &blazesym::normalize::Normalizer,
        elf_resolvers: &mut CachedElfResolvers,
    ) -> anyhow::Result<()> {
        let mut errors = 0;
        let pid = pid.into();
        self.stack
            .normalize_ips(normalizer, pid, elf_resolvers)
            .unwrap_or_else(|_| errors += 1);

        if let Some(threads) = &mut self.threads {
            for thread in threads {
                thread
                    .stack
                    .normalize_ips(normalizer, pid, elf_resolvers)
                    .unwrap_or_else(|_| errors += 1);
            }
        }
        anyhow::ensure!(
            errors == 0,
            "Failed to normalize ips, see frame comments for details"
        );
        Ok(())
    }

    pub(crate) fn create_symbolizer_source<'a>(
        pid: u32,
    ) -> blazesym::symbolize::source::Source<'a> {
        let mut process = blazesym::symbolize::source::Process::new(pid.into());
        // https://github.com/libbpf/blazesym/issues/518
        process.map_files = false;
        blazesym::symbolize::source::Source::Process(process)
    }

    pub(crate) fn create_normalizer() -> blazesym::normalize::Normalizer {
        blazesym::normalize::Normalizer::builder()
            .enable_vma_caching(true)
            .enable_build_ids(true)
            .enable_build_id_caching(true)
            .build()
    }

    pub fn resolve_names(&mut self, pid: u32) -> anyhow::Result<()> {
        let src = Self::create_symbolizer_source(pid);
        let symbolizer = blazesym::symbolize::Symbolizer::new();
        self.resolve_names_impl(&symbolizer, &src)
    }

    pub(crate) fn resolve_names_impl(
        &mut self,
        symbolizer: &blazesym::symbolize::Symbolizer,
        src: &blazesym::symbolize::source::Source,
    ) -> anyhow::Result<()> {
        let mut errors = 0;
        self.stack
            .resolve_names(src, symbolizer)
            .unwrap_or_else(|_| errors += 1);

        if let Some(threads) = &mut self.threads {
            for thread in threads {
                thread
                    .stack
                    .resolve_names(src, symbolizer)
                    .unwrap_or_else(|_| errors += 1);
            }
        }
        anyhow::ensure!(
            errors == 0,
            "Failed to resolve names, see frame comments for details"
        );
        Ok(())
    }
}

impl ErrorData {
    pub fn demangle_names(&mut self) -> anyhow::Result<()> {
        let mut errors = 0;
        self.stack.demangle_names().unwrap_or_else(|_| errors += 1);
        if let Some(threads) = &mut self.threads {
            for thread in threads {
                thread
                    .stack
                    .demangle_names()
                    .unwrap_or_else(|_| errors += 1);
            }
        }
        anyhow::ensure!(
            errors == 0,
            "Failed to demangle names, see frame comments for details"
        );
        Ok(())
    }
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub enum SourceType {
    Crashtracking,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[repr(C)]
pub enum ErrorKind {
    Panic,
    UnhandledException,
    UnixSignal,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct ThreadData {
    pub crashed: bool,
    pub name: String,
    pub stack: StackTrace,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub state: Option<String>,
}

#[cfg(test)]
impl super::test_utils::TestInstance for ErrorData {
    fn test_instance(seed: u64) -> Self {
        Self {
            is_crash: true,
            kind: ErrorKind::UnixSignal,
            message: Some(format!("Test crash message for seed {seed}")),
            thread_name: Some(format!("test-thread-{seed}")),
            source_type: SourceType::Crashtracking,
            stack: StackTrace::test_instance(seed),
            threads: None,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::crash_info::test_utils::TestInstance;

    #[test]
    fn error_data_without_threads_omits_threads_field() {
        let error = ErrorData {
            is_crash: true,
            kind: ErrorKind::UnixSignal,
            message: None,
            thread_name: None,
            source_type: SourceType::Crashtracking,
            stack: StackTrace::missing(),
            threads: None,
        };
        let json = serde_json::to_value(&error).unwrap();
        assert!(json.get("threads").is_none());
    }

    #[test]
    fn error_data_threads_serializes_as_thread_array() {
        let thread = ThreadData {
            crashed: false,
            name: "worker".to_string(),
            stack: StackTrace::test_instance(1),
            state: Some("S".to_string()),
        };
        let error = ErrorData {
            is_crash: true,
            kind: ErrorKind::UnixSignal,
            message: None,
            thread_name: None,
            source_type: SourceType::Crashtracking,
            stack: StackTrace::missing(),
            threads: Some(vec![thread.clone()]),
        };
        let json = serde_json::to_value(&error).unwrap();
        let threads = json["threads"].as_array().unwrap();
        assert_eq!(threads.len(), 1);
        assert_eq!(threads[0]["crashed"], false);
        assert_eq!(threads[0]["name"], "worker");
        assert_eq!(threads[0]["state"], "S");
        assert!(threads[0]["stack"].is_object());
    }
}