diff --git a/src/executor/helpers/apt.rs b/src/executor/helpers/apt.rs index 29a1d5c35..dfcf7adc9 100644 --- a/src/executor/helpers/apt.rs +++ b/src/executor/helpers/apt.rs @@ -87,14 +87,6 @@ where Ok(()) } -/// Returns whether a package is currently installed according to `dpkg`. -pub fn is_package_installed(package: &str) -> bool { - Command::new("dpkg") - .args(["-s", package]) - .output() - .is_ok_and(|output| output.status.success()) -} - pub fn install(system_info: &SystemInfo, packages: &[&str]) -> Result<()> { if !is_system_compatible(system_info) { bail!( diff --git a/src/executor/helpers/debug_file.rs b/src/executor/helpers/debug_file.rs new file mode 100644 index 000000000..0619bed2b --- /dev/null +++ b/src/executor/helpers/debug_file.rs @@ -0,0 +1,156 @@ +use crate::prelude::*; +use object::Object; +use std::path::{Path, PathBuf}; + +/// Search for a separate debug info file, in GDB's order (see [Separate Debug +/// Files]): build-id path first, then `.gnu_debuglink` with CRC32 validation. +/// Build-id wins because it hashes the binary contents, so a match cannot be a +/// false positive, while `.gnu_debuglink` only matches by filename. +/// +/// The searched roots are where Debian/Ubuntu `*-dbg`/`*-dbgsym` packages and +/// NixOS `environment.enableDebugInfo` install debug files. +/// +/// [Separate Debug Files]: https://sourceware.org/gdb/current/onlinedocs/gdb.html/Separate-Debug-Files.html +pub fn find_debug_file(object: &object::File, binary_path: &Path) -> Option { + ["/usr/lib/debug", "/run/current-system/sw/lib/debug"] + .iter() + .map(Path::new) + .filter(|dir| dir.exists()) + .find_map(|dir| find_debug_file_in(object, binary_path, dir)) +} + +fn find_debug_file_in( + object: &object::File, + binary_path: &Path, + debug_dir: &Path, +) -> Option { + if let Some(path) = find_debug_file_by_build_id(object, debug_dir) { + return Some(path); + } + find_debug_file_by_debuglink(object, binary_path, debug_dir) +} + +/// Build-id `a05cfb6313fe06a13c9b4b5cb86c2069faa3951f` resolves to +/// `/.build-id/a0/5cfb6313fe06a13c9b4b5cb86c2069faa3951f.debug`: +/// first byte as subdirectory, the rest as the filename. +fn find_debug_file_by_build_id(object: &object::File, debug_dir: &Path) -> Option { + let build_id = object.build_id().ok()??; + if build_id.is_empty() { + return None; + } + + let hex = build_id + .iter() + .map(|b| format!("{b:02x}")) + .collect::(); + let path = debug_dir + .join(".build-id") + .join(&hex[..2]) + .join(format!("{}.debug", &hex[2..])); + + if path.exists() { + return Some(path); + } + + None +} + +fn find_debug_file_by_debuglink( + object: &object::File, + binary_path: &Path, + debug_dir: &Path, +) -> Option { + let (debuglink, expected_crc) = object.gnu_debuglink().ok()??; + let debuglink = std::str::from_utf8(debuglink).ok()?; + let dir = binary_path.parent()?; + + let candidates = [ + dir.join(debuglink), + dir.join(".debug").join(debuglink), + debug_dir + .join(dir.strip_prefix("/").unwrap_or(dir)) + .join(debuglink), + ]; + + candidates.into_iter().find(|p| { + let Ok(content) = std::fs::read(p) else { + return false; + }; + let actual_crc = crc32fast::hash(&content); + if actual_crc != expected_crc { + trace!( + "CRC mismatch for {}: expected {expected_crc:#x}, got {actual_crc:#x}", + p.display() + ); + return false; + } + true + }) +} + +/// Copy `binary` and `debug_file` in a fresh tempdir, renaming the debug file to +/// match the binary's `.gnu_debuglink` basename so `find_debug_file` resolves +/// the pair. +#[cfg(all(test, target_os = "linux"))] +pub(crate) fn setup_debuglink_tmpdir( + binary: &Path, + debug_file: &Path, +) -> (tempfile::TempDir, PathBuf, PathBuf) { + let src = std::fs::read(binary).unwrap(); + let object = object::File::parse(&*src).unwrap(); + let (debuglink, _crc) = object + .gnu_debuglink() + .unwrap() + .expect("binary has no .gnu_debuglink"); + let debuglink = std::str::from_utf8(debuglink).unwrap(); + + let dir = tempfile::tempdir().unwrap(); + let staged_binary = dir.path().join("binary"); + let staged_debug = dir.path().join(debuglink); + std::fs::copy(binary, &staged_binary).unwrap(); + std::fs::copy(debug_file, &staged_debug).unwrap(); + + (dir, staged_binary, staged_debug) +} + +#[cfg(all(test, target_os = "linux"))] +mod tests { + use super::*; + + // Stripped libc plus its separate debug file, from Ubuntu 22.04's `libc6` + // and `libc6-dbg` packages. + const LIBC_PATH: &str = "testdata/perf_map/libc.so.6"; + const LIBC_DEBUG_PATH: &str = "testdata/perf_map/libc.so.6.debug"; + + #[test] + fn test_find_debug_file_by_build_id() { + let binary_path = Path::new(LIBC_PATH); + let content = std::fs::read(binary_path).unwrap(); + let object = object::File::parse(&*content).unwrap(); + + let build_id = object.build_id().unwrap().unwrap(); + let hex: String = build_id.iter().map(|b| format!("{b:02x}")).collect(); + + let tmp = tempfile::tempdir().unwrap(); + let debug_file_dir = tmp.path().join(".build-id").join(&hex[..2]); + std::fs::create_dir_all(&debug_file_dir).unwrap(); + + let debug_file_path = debug_file_dir.join(format!("{}.debug", &hex[2..])); + std::fs::copy(LIBC_DEBUG_PATH, &debug_file_path).unwrap(); + + let result = find_debug_file_in(&object, binary_path, tmp.path()); + assert_eq!(result, Some(debug_file_path)); + } + + #[test] + fn test_find_debug_file_by_debuglink() { + let (_dir, binary, debug_file) = + setup_debuglink_tmpdir(Path::new(LIBC_PATH), Path::new(LIBC_DEBUG_PATH)); + let content = std::fs::read(&binary).unwrap(); + let object = object::File::parse(&*content).unwrap(); + + let empty_debug_dir = tempfile::tempdir().unwrap(); + let result = find_debug_file_in(&object, &binary, empty_debug_dir.path()); + assert_eq!(result, Some(debug_file)); + } +} diff --git a/src/executor/helpers/mod.rs b/src/executor/helpers/mod.rs index 6efbf5cc8..a372a0d99 100644 --- a/src/executor/helpers/mod.rs +++ b/src/executor/helpers/mod.rs @@ -2,6 +2,7 @@ pub mod apt; #[cfg(target_os = "linux")] pub mod capabilities; pub mod command; +pub mod debug_file; pub mod detect_executable; pub mod env; pub mod get_bench_command; diff --git a/src/executor/valgrind/setup.rs b/src/executor/valgrind/setup.rs index 3db82344b..6adc68e64 100644 --- a/src/executor/valgrind/setup.rs +++ b/src/executor/valgrind/setup.rs @@ -4,11 +4,16 @@ use crate::binary_pins::{ }; use crate::cli::run::helpers::download_pinned_file; use crate::executor::helpers::apt; +use crate::executor::helpers::debug_file; use crate::executor::{ToolInstallStatus, ToolStatus}; use crate::prelude::*; use crate::system::{LinuxDistribution, SupportedOs, SystemInfo}; use semver::Version; -use std::{env, path::Path, process::Command}; +use std::{ + env, + path::{Path, PathBuf}, + process::Command, +}; fn get_codspeed_valgrind_target(system_info: &SystemInfo) -> Result { let SupportedOs::Linux(distro) = &system_info.os else { @@ -173,6 +178,53 @@ fn classify_valgrind_version(version: String) -> ToolInstallStatus { ToolInstallStatus::Installed { version } } +/// Path of the system libc, following the Debian multiarch layout. +fn system_libc_path(system_info: &SystemInfo) -> Option { + let triplet = match system_info.arch.as_str() { + "x86_64" => "x86_64-linux-gnu", + "aarch64" => "aarch64-linux-gnu", + arch => { + debug!("No known multiarch triplet for {arch}"); + return None; + } + }; + Some(PathBuf::from(format!("/lib/{triplet}/libc.so.6"))) +} + +/// Whether a separate debug file can be resolved for `binary`, through the same +/// build-id and `.gnu_debuglink` lookup that GDB and valgrind perform. +fn has_debug_symbols(binary: &Path) -> bool { + let data = match std::fs::read(binary) { + Ok(data) => data, + Err(e) => { + debug!("Failed to read {}: {e}", binary.display()); + return false; + } + }; + let object = match object::File::parse(data.as_slice()) { + Ok(object) => object, + Err(e) => { + debug!("Failed to parse {} as ELF: {e}", binary.display()); + return false; + } + }; + + match debug_file::find_debug_file(&object, binary) { + Some(debug_path) => { + debug!( + "Resolved debug file for {}: {}", + binary.display(), + debug_path.display() + ); + true + } + None => { + debug!("No debug file found for {}", binary.display()); + false + } + } +} + fn is_valgrind_installed(system_info: &SystemInfo) -> bool { if !matches!( get_valgrind_status().status, @@ -181,14 +233,12 @@ fn is_valgrind_installed(system_info: &SystemInfo) -> bool { return false; } - // `libc6-dbg` is only relevant on apt-based systems; on others (e.g. NixOS) - // `dpkg` is absent and would spuriously report it as missing. - if apt::is_system_compatible(system_info) { - apt::is_package_installed("libc6-dbg") - } else { - debug!("Skipping libc6-dbg check on non-apt-based system"); - true + if !apt::is_system_compatible(system_info) { + debug!("Skipping libc debug symbol check on non-apt-based system"); + return true; } + + system_libc_path(system_info).is_some_and(|libc| has_debug_symbols(&libc)) } pub async fn install_valgrind( diff --git a/src/executor/wall_time/profiler/perf/debug_info.rs b/src/executor/wall_time/profiler/perf/debug_info.rs index f1333e032..1022d200f 100644 --- a/src/executor/wall_time/profiler/perf/debug_info.rs +++ b/src/executor/wall_time/profiler/perf/debug_info.rs @@ -1,6 +1,6 @@ -use super::elf_helper::find_debug_file; use super::loaded_module::LoadedModule; use super::module_symbols::ModuleSymbols; +use crate::executor::helpers::debug_file::find_debug_file; use crate::prelude::*; use addr2line::{fallible_iterator::FallibleIterator, gimli}; use object::{Object, ObjectSection}; @@ -265,10 +265,11 @@ mod tests { #[case] binary: &str, #[case] debug_file: &str, ) { - let (_dir, binary, _debug_file) = super::super::elf_helper::setup_debuglink_tmpdir( - Path::new(binary), - Path::new(debug_file), - ); + let (_dir, binary, _debug_file) = + crate::executor::helpers::debug_file::setup_debuglink_tmpdir( + Path::new(binary), + Path::new(debug_file), + ); let module_symbols = ModuleSymbols::from_elf(&binary).unwrap(); assert!(!module_symbols.symbols().is_empty()); diff --git a/src/executor/wall_time/profiler/perf/elf_helper.rs b/src/executor/wall_time/profiler/perf/elf_helper.rs index 639828037..8690355a2 100644 --- a/src/executor/wall_time/profiler/perf/elf_helper.rs +++ b/src/executor/wall_time/profiler/perf/elf_helper.rs @@ -1,10 +1,8 @@ //! Based on this: https://github.com/mstange/samply/blob/4a5afec57b7c68b37ecde12b5a258de523e89463/samply/src/linux_shared/svma_file_range.rs#L8 use anyhow::Context; -use log::trace; use object::Object; use object::ObjectSegment; -use std::path::{Path, PathBuf}; // A file range in an object file, such as a segment or a section, // for which we know the corresponding Stated Virtual Memory Address (SVMA). @@ -190,178 +188,3 @@ pub fn relative_address_base(object_file: &object::File) -> u64 { pub fn compute_base_avma(base_svma: u64, load_bias: u64) -> u64 { base_svma.wrapping_add(load_bias) } - -/// Search for a separate debug info file. -/// -/// Tries two mechanisms in order: -/// 1. **Build-ID path**: `/.build-id//.debug` -/// 2. **`.gnu_debuglink`** with GDB search order and CRC32 validation -/// -/// This is the same order GDB uses (see [Separate Debug Files]). Build-ID is -/// preferred because it's a cryptographic hash of the binary contents, so a -/// match cannot be a false positive — whereas `.gnu_debuglink` matches by -/// filename and relies on a CRC32 check. On Debian/Ubuntu, `*-dbg` and -/// `*-dbgsym` packages install their files under `/usr/lib/debug/.build-id/`, -/// so this path is what actually resolves stripped system libraries in -/// practice. On NixOS, `environment.enableDebugInfo = true` populates the -/// same layout under `/run/current-system/sw/lib/debug`. -/// -/// [Separate Debug Files]: https://sourceware.org/gdb/current/onlinedocs/gdb.html/Separate-Debug-Files.html -pub fn find_debug_file(object: &object::File, binary_path: &Path) -> Option { - ["/usr/lib/debug", "/run/current-system/sw/lib/debug"] - .iter() - .map(Path::new) - .filter(|dir| dir.exists()) - .find_map(|dir| find_debug_file_in(object, binary_path, dir)) -} - -fn find_debug_file_in( - object: &object::File, - binary_path: &Path, - debug_dir: &Path, -) -> Option { - if let Some(path) = find_debug_file_by_build_id(object, debug_dir) { - return Some(path); - } - find_debug_file_by_debuglink(object, binary_path, debug_dir) -} - -/// Tries to find a debug file using the build-id. -/// -/// ## How it works -/// -/// For build-id a05cfb6313fe06a13c9b4b5cb86c2069faa3951f, the debug file lives at: -/// ```text -/// /usr/lib/debug/.build-id/a0/5cfb6313fe06a13c9b4b5cb86c2069faa3951f.debug -/// ^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -/// first byte (2 hex chars) as subdir -/// rest as the filename -/// ``` -fn find_debug_file_by_build_id(object: &object::File, debug_dir: &Path) -> Option { - let build_id = object.build_id().ok()??; - if build_id.is_empty() { - return None; - } - - let hex = build_id - .iter() - .map(|b| format!("{b:02x}")) - .collect::(); - let path = debug_dir - .join(".build-id") - .join(&hex[..2]) - .join(format!("{}.debug", &hex[2..])); - - if path.exists() { - return Some(path); - } - - None -} - -fn find_debug_file_by_debuglink( - object: &object::File, - binary_path: &Path, - debug_dir: &Path, -) -> Option { - let (debuglink, expected_crc) = object.gnu_debuglink().ok()??; - let debuglink = std::str::from_utf8(debuglink).ok()?; - let dir = binary_path.parent()?; - - let candidates = [ - dir.join(debuglink), - dir.join(".debug").join(debuglink), - debug_dir - .join(dir.strip_prefix("/").unwrap_or(dir)) - .join(debuglink), - ]; - - candidates.into_iter().find(|p| { - let Ok(content) = std::fs::read(p) else { - return false; - }; - let actual_crc = crc32fast::hash(&content); - if actual_crc != expected_crc { - trace!( - "CRC mismatch for {}: expected {expected_crc:#x}, got {actual_crc:#x}", - p.display() - ); - return false; - } - true - }) -} - -/// Copy `binary` and `debug_file` in a fresh tempdir, renaming the debug -/// file to match the binary's `.gnu_debuglink` basename so `find_debug_file` -/// resolves the pair. -/// -/// Returns `(TempDir, staged_binary, staged_debug_file)`. Keep the `TempDir` -/// alive for the duration of the test — dropping it removes the files. -#[cfg(all(test, target_os = "linux"))] -pub(super) fn setup_debuglink_tmpdir( - binary: &Path, - debug_file: &Path, -) -> (tempfile::TempDir, PathBuf, PathBuf) { - let src = std::fs::read(binary).unwrap(); - let object = object::File::parse(&*src).unwrap(); - let (debuglink, _crc) = object - .gnu_debuglink() - .unwrap() - .expect("binary has no .gnu_debuglink"); - let debuglink = std::str::from_utf8(debuglink).unwrap(); - - let dir = tempfile::tempdir().unwrap(); - let staged_binary = dir.path().join("binary"); - let staged_debug = dir.path().join(debuglink); - std::fs::copy(binary, &staged_binary).unwrap(); - std::fs::copy(debug_file, &staged_debug).unwrap(); - - (dir, staged_binary, staged_debug) -} - -#[cfg(all(test, target_os = "linux"))] -mod tests { - use super::*; - - // The fixtures `testdata/perf_map/libc.so.6` and `libc.so.6.debug` are the - // stripped libc plus its separate debug file from Ubuntu 22.04's `libc6` - // and `libc6-dbg` packages. - const LIBC_PATH: &str = "testdata/perf_map/libc.so.6"; - const LIBC_DEBUG_PATH: &str = "testdata/perf_map/libc.so.6.debug"; - - #[test] - fn test_find_debug_file_by_build_id() { - // Ubuntu's `libc6-dbg` installs its debug file under - // `/usr/lib/debug/.build-id//.debug`. Reproduce that layout - // in a tempdir and confirm we resolve it via the build-id note. - let binary_path = Path::new(LIBC_PATH); - let content = std::fs::read(binary_path).unwrap(); - let object = object::File::parse(&*content).unwrap(); - - let build_id = object.build_id().unwrap().unwrap(); - let hex: String = build_id.iter().map(|b| format!("{b:02x}")).collect(); - - let tmp = tempfile::tempdir().unwrap(); - let debug_file_dir = tmp.path().join(".build-id").join(&hex[..2]); - std::fs::create_dir_all(&debug_file_dir).unwrap(); - - let debug_file_path = debug_file_dir.join(format!("{}.debug", &hex[2..])); - std::fs::copy(LIBC_DEBUG_PATH, &debug_file_path).unwrap(); - - let result = find_debug_file_in(&object, binary_path, tmp.path()); - assert_eq!(result, Some(debug_file_path)); - } - - #[test] - fn test_find_debug_file_by_debuglink() { - let (_dir, binary, debug_file) = - setup_debuglink_tmpdir(Path::new(LIBC_PATH), Path::new(LIBC_DEBUG_PATH)); - let content = std::fs::read(&binary).unwrap(); - let object = object::File::parse(&*content).unwrap(); - - let empty_debug_dir = tempfile::tempdir().unwrap(); - let result = find_debug_file_in(&object, &binary, empty_debug_dir.path()); - assert_eq!(result, Some(debug_file)); - } -} diff --git a/src/executor/wall_time/profiler/perf/module_symbols.rs b/src/executor/wall_time/profiler/perf/module_symbols.rs index e3341d5fc..e57b9b16a 100644 --- a/src/executor/wall_time/profiler/perf/module_symbols.rs +++ b/src/executor/wall_time/profiler/perf/module_symbols.rs @@ -1,4 +1,5 @@ use super::elf_helper; +use crate::executor::helpers::debug_file; use log::trace; use object::{Object, ObjectSymbol, ObjectSymbolTable}; use runner_shared::module_symbols::SYMBOLS_MAP_SUFFIX; @@ -96,7 +97,7 @@ impl ModuleSymbols { let mut symbols = Self::extract_symbols_from_object(&object); // Merge symbols from a separate debug file if available - if let Some(debug_path) = elf_helper::find_debug_file(&object, path.as_ref()) { + if let Some(debug_path) = debug_file::find_debug_file(&object, path.as_ref()) { trace!( "Merging symbols from debug file {:?} for {:?}", debug_path, @@ -287,7 +288,7 @@ mod tests { // the debug file under a naive fallback. Merging must pick up .symtab // symbols like `_int_malloc` that only live in the debug file — // this is the coverage needed for full libc symbolication. - let (_dir, binary, _debug_file) = elf_helper::setup_debuglink_tmpdir( + let (_dir, binary, _debug_file) = debug_file::setup_debuglink_tmpdir( Path::new("testdata/perf_map/libc.so.6"), Path::new("testdata/perf_map/libc.so.6.debug"), );