diff --git a/capi/include/blazesym.h b/capi/include/blazesym.h index 679593e16..4935053d9 100644 --- a/capi/include/blazesym.h +++ b/capi/include/blazesym.h @@ -709,6 +709,38 @@ typedef struct blaze_normalize_opts { */ typedef struct blaze_symbolizer blaze_symbolizer; +/** + * Configuration for custom process member dispatch. + * + * When provided to [`blaze_symbolizer_opts`] via + * [`process_dispatch`][blaze_symbolizer_opts::process_dispatch], the + * callback is invoked for each process member that has a file path during + * process symbolization. It allows the caller to provide an alternative + * ELF file path for symbolization. For example, the path may be fetched + * via debuginfod. + * + * The callback receives the `/proc//map_files/...` path and the + * symbolic path from `/proc//maps`, along with the user-provided + * context pointer ([`ctx`][Self::ctx]). + * + * The callback should return one of: + * - A `malloc`'d path string to an alternative ELF file to use for + * symbolization. The library will `free` this string after use. + * - `NULL` to use the default symbolization behavior for this member. + */ +typedef struct blaze_symbolizer_dispatch { + /** + * The dispatch callback function. Must not be `NULL`. + */ + char *(*dispatch_cb)(const char *maps_file, + const char *symbolic_path, + void *ctx); + /** + * Opaque context pointer passed to [`dispatch_cb`][Self::dispatch_cb]. + */ + void *ctx; +} blaze_symbolizer_dispatch; + /** * Options for configuring [`blaze_symbolizer`] objects. */ @@ -771,7 +803,17 @@ typedef struct blaze_symbolizer_opts { * Unused member available for future expansion. Must be initialized * to zero. */ - uint8_t reserved[20]; + uint8_t _reserved1[4]; + /** + * Optional pointer to a [`blaze_symbolizer_dispatch`] struct for custom + * process member dispatch. Set to `NULL` to disable. + */ + const struct blaze_symbolizer_dispatch *process_dispatch; + /** + * Unused member available for future expansion. Must be initialized + * to zero. + */ + uint8_t reserved[8]; } blaze_symbolizer_opts; /** diff --git a/capi/src/symbolize.rs b/capi/src/symbolize.rs index a78df7c49..7ba765f85 100644 --- a/capi/src/symbolize.rs +++ b/capi/src/symbolize.rs @@ -2,16 +2,20 @@ use std::alloc::alloc; use std::alloc::dealloc; use std::alloc::Layout; use std::ffi::CStr; +use std::ffi::CString; use std::ffi::OsStr; use std::fmt::Debug; +use std::io; use std::mem; use std::ops::Deref as _; use std::os::raw::c_char; +use std::os::raw::c_void; use std::os::unix::ffi::OsStrExt as _; use std::path::Path; use std::path::PathBuf; use std::ptr; +use blazesym::helper::ElfResolver; use blazesym::symbolize::cache; use blazesym::symbolize::source::Elf; use blazesym::symbolize::source::GsymData; @@ -21,6 +25,7 @@ use blazesym::symbolize::source::Process; use blazesym::symbolize::source::Source; use blazesym::symbolize::CodeInfo; use blazesym::symbolize::Input; +use blazesym::symbolize::ProcessMemberType; use blazesym::symbolize::Reason; use blazesym::symbolize::Sym; use blazesym::symbolize::Symbolized; @@ -645,6 +650,36 @@ pub(crate) unsafe fn from_cstr(cstr: *const c_char) -> PathBuf { } +/// Configuration for custom process member dispatch. +/// +/// When provided to [`blaze_symbolizer_opts`] via +/// [`process_dispatch`][blaze_symbolizer_opts::process_dispatch], the +/// callback is invoked for each process member that has a file path during +/// process symbolization. It allows the caller to provide an alternative +/// ELF file path for symbolization. For example, the path may be fetched +/// via debuginfod. +/// +/// The callback receives the `/proc//map_files/...` path and the +/// symbolic path from `/proc//maps`, along with the user-provided +/// context pointer ([`ctx`][Self::ctx]). +/// +/// The callback should return one of: +/// - A `malloc`'d path string to an alternative ELF file to use for +/// symbolization. The library will `free` this string after use. +/// - `NULL` to use the default symbolization behavior for this member. +#[repr(C)] +#[derive(Debug)] +pub struct blaze_symbolizer_dispatch { + /// The dispatch callback function. Must not be `NULL`. + pub dispatch_cb: unsafe extern "C" fn( + maps_file: *const c_char, + symbolic_path: *const c_char, + ctx: *mut c_void, + ) -> *mut c_char, + /// Opaque context pointer passed to [`dispatch_cb`][Self::dispatch_cb]. + pub ctx: *mut c_void, +} + /// Options for configuring [`blaze_symbolizer`] objects. #[repr(C)] #[derive(Debug)] @@ -691,7 +726,13 @@ pub struct blaze_symbolizer_opts { pub demangle: bool, /// Unused member available for future expansion. Must be initialized /// to zero. - pub reserved: [u8; 20], + pub _reserved1: [u8; 4], + /// Optional pointer to a [`blaze_symbolizer_dispatch`] struct for custom + /// process member dispatch. Set to `NULL` to disable. + pub process_dispatch: *const blaze_symbolizer_dispatch, + /// Unused member available for future expansion. Must be initialized + /// to zero. + pub reserved: [u8; 8], } impl Default for blaze_symbolizer_opts { @@ -704,7 +745,9 @@ impl Default for blaze_symbolizer_opts { code_info: false, inlined_fns: false, demangle: false, - reserved: [0; 20], + _reserved1: [0; 4], + process_dispatch: ptr::null(), + reserved: [0; 8], } } } @@ -755,11 +798,13 @@ pub unsafe extern "C" fn blaze_symbolizer_new_opts( let blaze_symbolizer_opts { type_size: _, debug_dirs, - debug_dirs_len: _debug_dirs_len, + debug_dirs_len, auto_reload, code_info, inlined_fns, demangle, + _reserved1: _, + process_dispatch, reserved: _, } = opts; @@ -769,29 +814,70 @@ pub unsafe extern "C" fn blaze_symbolizer_new_opts( .enable_inlined_fns(inlined_fns) .enable_demangling(demangle); - let builder = if debug_dirs.is_null() { - builder + let debug_dir_paths = if debug_dirs.is_null() { + None } else { - #[cfg(feature = "dwarf")] - { - // SAFETY: The caller ensures that the pointer is valid and the count - // matches. - let slice = unsafe { slice_from_user_array(debug_dirs, _debug_dirs_len) }; - let iter = slice.iter().map(|cstr| { - Path::new(OsStr::from_bytes( - // SAFETY: The caller ensures that valid C strings are - // provided. - unsafe { CStr::from_ptr(cstr.cast()) }.to_bytes(), - )) - }); - - builder.set_debug_dirs(Some(iter)) - } + // SAFETY: The caller ensures that the pointer is valid and the count + // matches. + let slice = unsafe { slice_from_user_array(debug_dirs, debug_dirs_len) }; + Some( + slice + .iter() + .map(|cstr| { + PathBuf::from(OsStr::from_bytes( + // SAFETY: The caller ensures that valid C strings are + // provided. + unsafe { CStr::from_ptr(cstr.cast()) }.to_bytes(), + )) + }) + .collect::>(), + ) + }; - #[cfg(not(feature = "dwarf"))] - { - builder - } + #[cfg(feature = "dwarf")] + let builder = if let Some(ref dirs) = debug_dir_paths { + builder.set_debug_dirs(Some(dirs.iter().map(PathBuf::as_path))) + } else { + builder + }; + + let builder = if !process_dispatch.is_null() { + // SAFETY: The caller guarantees that the pointer is valid. + let dispatch = unsafe { &*process_dispatch }; + let cb = dispatch.dispatch_cb; + let ctx = dispatch.ctx; + builder.set_process_dispatcher(move |info| { + match info.member_entry { + ProcessMemberType::Path(entry) => { + let maps_file = CString::new(entry.maps_file.as_os_str().as_bytes()) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; + let sym_path = CString::new(entry.symbolic_path.as_os_str().as_bytes()) + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; + // SAFETY: The caller guarantees that the callback is safe + // to call with valid C string pointers and the + // provided context. + let result = unsafe { cb(maps_file.as_ptr(), sym_path.as_ptr(), ctx) }; + if result.is_null() { + return Ok(None) + } + // SAFETY: The callback is required to return a valid, + // NUL-terminated, `malloc`'d C string. + let path_cstr = unsafe { CStr::from_ptr(result) }; + let path = Path::new(OsStr::from_bytes(path_cstr.to_bytes())); + let resolver = if let Some(ref dirs) = debug_dir_paths { + ElfResolver::open_with_debug_dirs(path, dirs) + } else { + ElfResolver::open(path) + }; + // SAFETY: The string was `malloc`'d by the callback. + unsafe { libc::free(result.cast()) }; + Ok(Some(Box::new(resolver?))) + } + _ => Ok(None), + } + }) + } else { + builder }; let symbolizer = builder.build(); @@ -1465,7 +1551,7 @@ mod tests { }; assert_eq!( format!("{opts:?}"), - "blaze_symbolizer_opts { type_size: 16, debug_dirs: 0x0, debug_dirs_len: 0, auto_reload: false, code_info: false, inlined_fns: false, demangle: true, reserved: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] }" + "blaze_symbolizer_opts { type_size: 16, debug_dirs: 0x0, debug_dirs_len: 0, auto_reload: false, code_info: false, inlined_fns: false, demangle: true, _reserved1: [0, 0, 0, 0], process_dispatch: 0x0, reserved: [0, 0, 0, 0, 0, 0, 0, 0] }" ); } @@ -2302,4 +2388,108 @@ mod tests { let () = unsafe { blaze_syms_free(result) }; let () = unsafe { blaze_symbolizer_free(symbolizer) }; } + + /// Symbolize `addr` in the current process using the given dispatch + /// callback. + fn symbolize_with_dispatch( + cb: unsafe extern "C" fn(*const c_char, *const c_char, *mut c_void) -> *mut c_char, + addr: Addr, + expected_sym: Option<&str>, + ) { + let dispatch = blaze_symbolizer_dispatch { + dispatch_cb: cb, + ctx: 0xc0ffee as *mut c_void, + }; + let opts = blaze_symbolizer_opts { + process_dispatch: &dispatch, + ..Default::default() + }; + let symbolizer = unsafe { blaze_symbolizer_new_opts(&opts) }; + assert!(!symbolizer.is_null()); + + let process_src = blaze_symbolize_src_process { + pid: 0, + debug_syms: true, + ..Default::default() + }; + + let addrs = [addr]; + let result = unsafe { + blaze_symbolize_process_abs_addrs(symbolizer, &process_src, addrs.as_ptr(), addrs.len()) + }; + let () = unsafe { blaze_symbolizer_free(symbolizer) }; + + if let Some(expected) = expected_sym { + assert!(!result.is_null()); + let result = unsafe { &*result }; + assert_eq!(result.cnt, 1); + let syms = unsafe { slice::from_raw_parts(result.syms.as_ptr(), result.cnt) }; + let name = unsafe { CStr::from_ptr(syms[0].name) }.to_str().unwrap(); + assert!(name.contains(expected), "{name}"); + let () = unsafe { blaze_syms_free(result) }; + } else { + assert!(result.is_null()); + } + } + + /// Make sure that we can symbolize an address in the current process + /// using a custom process dispatch callback that returns the + /// `maps_file` path as-is. + #[test] + fn symbolize_in_process_with_dispatch() { + unsafe extern "C" fn cb( + maps_file: *const c_char, + _symbolic_path: *const c_char, + ctx: *mut c_void, + ) -> *mut c_char { + assert_eq!(ctx as usize, 0xc0ffee); + unsafe { libc::strdup(maps_file) } + } + + symbolize_with_dispatch( + cb, + symbolize_in_process_with_dispatch as *const () as Addr, + Some("symbolize_in_process_with_dispatch"), + ) + } + + /// Make sure that a dispatch callback returning NULL falls back to + /// the default symbolization behavior. + #[test] + fn symbolize_in_process_with_null_dispatch() { + unsafe extern "C" fn cb( + _maps_file: *const c_char, + _symbolic_path: *const c_char, + ctx: *mut c_void, + ) -> *mut c_char { + assert_eq!(ctx as usize, 0xc0ffee); + ptr::null_mut() + } + + symbolize_with_dispatch( + cb, + symbolize_in_process_with_null_dispatch as *const () as Addr, + Some("symbolize_in_process_with_null_dispatch"), + ) + } + + /// Make sure that a dispatch callback returning a non-existent path + /// causes symbolization to fail for that address. + #[test] + fn symbolize_in_process_with_bad_dispatch() { + unsafe extern "C" fn cb( + _maps_file: *const c_char, + _symbolic_path: *const c_char, + ctx: *mut c_void, + ) -> *mut c_char { + assert_eq!(ctx as usize, 0xc0ffee); + unsafe { libc::strdup(c"/no/such/file".as_ptr()) } + } + + symbolize_with_dispatch( + cb, + symbolize_in_process_with_bad_dispatch as *const () as Addr, + None, + ) + } } diff --git a/src/elf/resolver.rs b/src/elf/resolver.rs index edd52c4d1..63588c844 100644 --- a/src/elf/resolver.rs +++ b/src/elf/resolver.rs @@ -174,6 +174,25 @@ impl ElfResolver { Self::from_parser(parser, Some(&debug_dirs), elf_cache) } + /// Create an `ElfResolver` that loads data from the provided file, + /// using the given directories to search for split debug + /// information. + #[cfg(feature = "dwarf")] + pub fn open_with_debug_dirs(path: P, debug_dirs: D) -> Result + where + P: AsRef, + D: IntoIterator, + DP: AsRef, + { + let path = path.as_ref(); + let parser = Rc::new(ElfParser::open(path)?); + let debug_dirs = debug_dirs + .into_iter() + .map(|p| p.as_ref().to_path_buf()) + .collect::>(); + Self::from_parser(parser, Some(&debug_dirs), None) + } + /// Create a new [`ElfResolver`] using `parser`. /// /// If `debug_dirs` is `Some`, interpret DWARF debug information. If it is diff --git a/tests/suite/symbolize.rs b/tests/suite/symbolize.rs index 16a4a263e..62a1ad6c7 100644 --- a/tests/suite/symbolize.rs +++ b/tests/suite/symbolize.rs @@ -2112,3 +2112,32 @@ fn create_elf_resolver_from_non_existing_path() { assert_eq!(err.kind(), ErrorKind::NotFound); } + +/// Make sure that [`ElfResolver::open_with_debug_dirs`] can be created and +/// registered successfully. +#[cfg(feature = "dwarf")] +#[test] +fn create_elf_resolver_with_debug_dirs() { + let bin_name = Path::new(&env!("CARGO_MANIFEST_DIR")) + .join("data") + .join("test-stable-addrs.bin"); + + let debug_dirs = vec![PathBuf::from("/usr/lib/debug")]; + let resolver = ElfResolver::open_with_debug_dirs(&bin_name, &debug_dirs).unwrap(); + let mut symbolizer = Symbolizer::new(); + let () = symbolizer + .register_elf_resolver(&bin_name, Rc::new(resolver)) + .unwrap(); +} + +/// Make sure that [`ElfResolver::open_with_debug_dirs`] fails for a +/// non-existing file. +#[cfg(feature = "dwarf")] +#[test] +fn create_elf_resolver_with_debug_dirs_non_existing_path() { + let path = Path::new("/This/Path/Does.Not/Exist"); + let debug_dirs = vec![PathBuf::from("/usr/lib/debug")]; + let err = ElfResolver::open_with_debug_dirs(path, &debug_dirs).unwrap_err(); + + assert_eq!(err.kind(), ErrorKind::NotFound); +}