From 6dad98361dae3883465755d5600318efc922f964 Mon Sep 17 00:00:00 2001 From: Daniel Hodges Date: Sun, 13 Aug 2023 21:32:49 -0400 Subject: [PATCH] Add support for debuginfod resolver Signed-off-by: Daniel Hodges --- Cargo.toml | 1 + debuginfod/Cargo.toml | 12 +++ debuginfod/examples/debuginfod.rs | 102 ++++++++++++++++++++ debuginfod/src/client.rs | 117 ++++++++++++++++++++++ debuginfod/src/lib.rs | 2 + debuginfod/src/resolver.rs | 155 ++++++++++++++++++++++++++++++ 6 files changed, 389 insertions(+) create mode 100644 debuginfod/Cargo.toml create mode 100644 debuginfod/examples/debuginfod.rs create mode 100644 debuginfod/src/client.rs create mode 100644 debuginfod/src/lib.rs create mode 100644 debuginfod/src/resolver.rs diff --git a/Cargo.toml b/Cargo.toml index 47a33254c..5c0970e0a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ members = [ ".", "capi", "cli", + "debuginfod", ] [package] diff --git a/debuginfod/Cargo.toml b/debuginfod/Cargo.toml new file mode 100644 index 000000000..6369bab41 --- /dev/null +++ b/debuginfod/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "blazesym-debuginfod" +version = "0.0.0" +edition = "2021" + + +[dependencies] +anyhow = "1.0.68" +dirs = "5.0.1" +reqwest = {version = "0.11.18", features = ["blocking", "gzip"]} +blazesym = {path = ".."} + diff --git a/debuginfod/examples/debuginfod.rs b/debuginfod/examples/debuginfod.rs new file mode 100644 index 000000000..c4b4761cd --- /dev/null +++ b/debuginfod/examples/debuginfod.rs @@ -0,0 +1,102 @@ +use std::env; + +use anyhow::bail; +use anyhow::Context as _; +use anyhow::Result; + +use blazesym::symbolize::CodeInfo; +use blazesym::symbolize::Input; +use blazesym::symbolize::Sym; +use blazesym::symbolize::Symbolized; +use blazesym::symbolize::Symbolizer; +use blazesym::Addr; +use blazesym_debuginfod::client::DebugInfodClient; +use blazesym_debuginfod::resolver::DebugInfodResolver; + +const ADDR_WIDTH: usize = 16; + + +fn print_frame(name: &str, addr_info: Option<(Addr, Addr, usize)>, code_info: &Option) { + let code_info = code_info.as_ref().map(|code_info| { + let path = code_info.to_path(); + let path = path.display(); + + match (code_info.line, code_info.column) { + (Some(line), Some(col)) => format!(" {path}:{line}:{col}"), + (Some(line), None) => format!(" {path}:{line}"), + (None, _) => format!(" {path}"), + } + }); + + if let Some((input_addr, addr, offset)) = addr_info { + // If we have various address information bits we have a new symbol. + println!( + "{input_addr:#0width$x}: {name} @ {addr:#x}+{offset:#x}{code_info}", + code_info = code_info.as_deref().unwrap_or(""), + width = ADDR_WIDTH + ) + } else { + // Otherwise we are dealing with an inlined call. + println!( + "{:width$} {name}{code_info} [inlined]", + " ", + code_info = code_info + .map(|info| format!(" @{info}")) + .as_deref() + .unwrap_or(""), + width = ADDR_WIDTH + ) + } +} + + +fn main() -> Result<()> { + let args = env::args().collect::>(); + + if args.len() != 3 { + bail!( + "Usage: {}
", + args.first().map(String::as_str).unwrap_or("addr2ln_pid") + ); + } + let val = env::var("DEBUGINFOD_URLS")?; + let urls = DebugInfodClient::parse_debuginfo_urls(val)?; + + let build_id = &args[1]; + let addr_str = &args[2][..]; + let resolver = DebugInfodResolver::default_resolver(urls)?; + let build_id_str = build_id.as_str(); + let src = resolver.debug_info_from(build_id_str.as_bytes(), None)?; + let symbolizer = Symbolizer::new(); + + let addr = Addr::from_str_radix(addr_str.trim_start_matches("0x"), 16) + .with_context(|| format!("failed to parse address: {addr_str}"))?; + + let addrs = [addr]; + let syms = symbolizer + .symbolize(&src, Input::VirtOffset(&addrs)) + .with_context(|| format!("failed to symbolize address {addr:#x}"))?; + + for (input_addr, sym) in addrs.iter().copied().zip(syms) { + match sym { + Symbolized::Sym(Sym { + name, + addr, + offset, + code_info, + inlined, + .. + }) => { + print_frame(&name, Some((input_addr, addr, offset)), &code_info); + for frame in inlined.iter() { + print_frame(&frame.name, None, &frame.code_info); + } + } + Symbolized::Unknown => { + println!("{input_addr:#0width$x}: ", width = ADDR_WIDTH) + } + } + } + + Ok(()) +} diff --git a/debuginfod/src/client.rs b/debuginfod/src/client.rs new file mode 100644 index 000000000..db7881bad --- /dev/null +++ b/debuginfod/src/client.rs @@ -0,0 +1,117 @@ +use std::io::Write; + +use anyhow::anyhow; +use anyhow::Result; +use reqwest::blocking::Client; +use reqwest::blocking::Request; +use reqwest::Method; +use reqwest::Url; + +#[derive(Debug)] +pub struct DebugInfodClient { + // See: + // https://github.com/seanmonstar/reqwest/issues/988 + base_url: Url, + client: Client, +} + +impl DebugInfodClient { + pub fn new(client: Client, base_url: Url) -> DebugInfodClient { + DebugInfodClient { base_url, client } + } + + /// Helper method to return build_id as a String. + pub fn format_build_id(build_id: &[u8]) -> Result { + Ok(String::from_utf8(build_id.to_vec())?) + } + + pub fn base_path_for(build_id: &[u8]) -> Result { + Ok(format!("{}/", DebugInfodClient::format_build_id(build_id)?)) + } + + /// Returns the debug info from a debuginfod source. Writes are buffered into the writer. + pub fn get_debug_info(&self, build_id: &[u8], dest: &mut impl Write) -> Result<()> { + // /buildid//debuginfo + let url = self + .base_url + .clone() + .join("buildid/")? + .join(DebugInfodClient::base_path_for(build_id)?.as_str())? + .join("debuginfo")?; + let mut res = self.client.execute(Request::new(Method::GET, url))?; + if res.status().is_success() { + res.copy_to(dest)?; + Ok(dest.flush()?) + } else { + Err(anyhow!(format!("request error {}", res.status()))) + } + } + + /// Returns an executable from a debuginfod source. Writes are buffered into + /// the writer. + pub fn get_executable(&self, build_id: &[u8], dest: &mut impl Write) -> Result<()> { + // /buildid//executable + let url = self + .base_url + .join("buildid/")? + .join(DebugInfodClient::base_path_for(build_id)?.as_str())? + .join("executable")?; + let mut res = self.client.execute(Request::new(Method::GET, url))?; + res.copy_to(dest)?; + Ok(dest.flush()?) + } + + /// Returns a vector of URLs based on format of the DEBUGINFOD_URLS + /// environment variable value, which is either a comma separated or + /// whitespace separated list of URLs. + pub fn parse_debuginfo_urls(var: String) -> Result> { + let res: Vec = var.split([',', ' ']).map(|v| v.to_string()).collect(); + let urls: Vec = res.iter().filter_map(|v| Url::parse(v).ok()).collect(); + Ok(urls) + } + + /// Returns a set of default clients based on the get_default_servers method. + pub fn get_default_clients(urls: Vec) -> Result> { + Ok(urls + .into_iter() + .map(|base_url| DebugInfodClient::new(Client::new(), base_url)) + .collect::>()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn build_id() { + let bytes = b"\x47\x91\x72\x34\xfb\xbd\x1c\x72\x88\x68\x4d\x92\x32\xfe\x2a\x27\x9e\x96\xfa"; + let slice: &[u8] = bytes; + let expected = "47917234fbfbd1c7288684d9232fe2a279e96fa4"; + let formatted_build_id = DebugInfodClient::format_build_id(slice).unwrap(); + assert_eq!(formatted_build_id, expected); + } + #[test] + fn parse_debuginfo_urls_ws_separated() { + let vars = "https://debug.infod https://de.bug.info.d"; + let parsed_urls = DebugInfodClient::parse_debuginfo_urls(vars.to_string()).unwrap(); + assert_eq!( + parsed_urls, + vec![ + Url::parse("https://debug.infod").ok().unwrap(), + Url::parse("https://de.bug.info.d").ok().unwrap(), + ], + ); + } + #[test] + fn parse_debuginfo_urls_comma_separated() { + let vars = "https://debug.infod,https://de.bug.info.d"; + let parsed_urls = DebugInfodClient::parse_debuginfo_urls(vars.to_string()).unwrap(); + assert_eq!( + parsed_urls, + vec![ + Url::parse("https://debug.infod").ok().unwrap(), + Url::parse("https://de.bug.info.d").ok().unwrap(), + ], + ); + } +} diff --git a/debuginfod/src/lib.rs b/debuginfod/src/lib.rs new file mode 100644 index 000000000..0273d658a --- /dev/null +++ b/debuginfod/src/lib.rs @@ -0,0 +1,2 @@ +pub mod client; +pub mod resolver; diff --git a/debuginfod/src/resolver.rs b/debuginfod/src/resolver.rs new file mode 100644 index 000000000..c1a2fab80 --- /dev/null +++ b/debuginfod/src/resolver.rs @@ -0,0 +1,155 @@ +use dirs::cache_dir; +use std::fs; +use std::fs::File; +use std::path::Path; +use std::path::PathBuf; + +use crate::client::DebugInfodClient; + +use blazesym::symbolize::Elf; +use blazesym::symbolize::Source; + +use anyhow::anyhow; +use anyhow::Result; +use reqwest::Url; + +#[derive(Debug)] +pub struct DebugInfodResolver { + ignore_cache: bool, + base_path: PathBuf, + clients: Vec, +} + +impl DebugInfodResolver { + /// Returns a DebugInfodResolver resolver. + pub fn new( + clients: Vec, + base_path: PathBuf, + ignore_cache: bool, + ) -> DebugInfodResolver { + DebugInfodResolver { + ignore_cache, + base_path, + clients, + } + } + + pub fn cache_dir() -> Result { + let mut dir = cache_dir() + .or(Some(std::env::temp_dir())) + .expect("Could not create temp dir"); + dir.push("blazesym"); + dir.push("debuginfod"); + Ok(dir) + } + + /// Returns a default DebugInfodResolver resolver suitable for use. Caches + /// resolved objects by default. + pub fn default_resolver(urls: Vec) -> Result { + let clients = DebugInfodClient::get_default_clients(urls)?; + let cache_dir = DebugInfodResolver::cache_dir()?; + std::fs::create_dir_all(&cache_dir)?; + Ok(DebugInfodResolver::new(clients, cache_dir, true)) + } + + fn cached(&self, path: &Path) -> bool { + if path.is_file() { + return true + } + false + } + + fn debuginfo_path(&self, build_id: &[u8], path: Option<&str>) -> Result { + match path { + Some(p) => Ok(PathBuf::from(p.to_string())), + _ => { + let mut path = self.base_path.clone(); + path.push(format!( + "{}.debuginfo", + DebugInfodClient::format_build_id(build_id)? + )); + Ok(path) + } + } + } + + fn executable_path(&self, build_id: &[u8], path: Option<&str>) -> Result { + match path { + Some(p) => Ok(PathBuf::from(p.to_string())), + _ => { + let mut path = self.base_path.clone(); + path.push(DebugInfodClient::format_build_id(build_id)?); + Ok(path) + } + } + } + + /// Returns debug info from a remote debuginfod source. The path can be used to set the result. + pub fn debug_info(&self, build_id: &[u8], path: Option<&str>) -> Result { + let path = self.debuginfo_path(build_id, path)?; + + if !self.ignore_cache && self.cached(&path) { + return Ok(path) + } + + if self.clients.is_empty() { + return Err(anyhow!("No debuginfod clients configured")) + } + + let mut file = File::create(&path)?; + + let success = self + .clients + .iter() + .find(|client| client.get_debug_info(build_id, &mut file).is_ok()); + + if success.is_none() { + fs::remove_file(&path)?; + return Err(anyhow!( + "failed to get debug info for build_id: {}", + DebugInfodClient::format_build_id(build_id)? + )) + } + Ok(path) + } + + /// Returns an executable from a remote debuginfod source. The path can be used to set the + /// result. + pub fn executable(&self, build_id: &[u8], path: Option<&str>) -> Result { + let path = self.executable_path(build_id, path)?; + + if !self.ignore_cache && self.cached(&path) { + return Ok(path) + } + + if self.clients.is_empty() { + return Err(anyhow!("No debuginfod clients configured")) + } + + let mut file = File::create(&path)?; + + let success = self + .clients + .iter() + .find(|client| client.get_executable(build_id, &mut file).is_ok()); + + if success.is_none() { + fs::remove_file(&path)?; + return Err(anyhow!( + "failed to get executable for build_id: {}", + DebugInfodClient::format_build_id(build_id)? + )) + } + Ok(path) + } + + pub fn debug_info_from(&self, build_id: &[u8], path: Option<&str>) -> Result { + let elf_path: PathBuf = self.debug_info(build_id, path)?; + Ok(Source::Elf(Elf::new(elf_path))) + } + + pub fn executable_from(&self, build_id: &[u8], path: Option<&str>) -> Result { + let elf_path: PathBuf = self.executable(build_id, path)?; + Ok(Source::Elf(Elf::new(elf_path))) + } +}