-
Notifications
You must be signed in to change notification settings - Fork 40
capi: Add callback for symbolizer #1535
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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/<pid>/map_files/...` path and the | ||
| /// symbolic path from `/proc/<pid>/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], | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -760,6 +803,8 @@ pub unsafe extern "C" fn blaze_symbolizer_new_opts( | |
| 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) }; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please also rename |
||
| 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::<Vec<_>>(), | ||
| ) | ||
| }; | ||
|
|
||
| #[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,114 @@ 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. | ||
| unsafe fn symbolize_with_dispatch( | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I fail to see why this function needs to be |
||
| 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) } | ||
| } | ||
|
|
||
| unsafe { | ||
| 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() | ||
| } | ||
|
|
||
| unsafe { | ||
| 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()) } | ||
| } | ||
|
|
||
| unsafe { | ||
| symbolize_with_dispatch( | ||
| cb, | ||
| symbolize_in_process_with_bad_dispatch as *const () as Addr, | ||
| None, | ||
| ) | ||
| }; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -174,6 +174,24 @@ 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. | ||
| pub fn open_with_debug_dirs<P, D, DP>(path: P, debug_dirs: D) -> Result<Self> | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this should be only available if the |
||
| where | ||
| P: AsRef<Path>, | ||
| D: IntoIterator<Item = DP>, | ||
| DP: AsRef<Path>, | ||
| { | ||
| 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::<Vec<_>>(); | ||
| 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 | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this should still be wrapped in an
Optionto be NULL-able, no?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I get that. If the callback is not needed,
process_dispatchis allowed to be NULL. Is that something about FFI semantics?Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, a
fncannot safely be NULL. It's like a reference; can't create it from a NULL ptr. At least that is my understanding.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See https://doc.rust-lang.org/nomicon/ffi.html#the-nullable-pointer-optimization
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I get that if we want to allow the callback to be NULL, or even be able to check if it is NULL, we are required to add the
Option. But as we don't allow a NULL cb here, is there still any advantage? As it doesn't have any downsides, I'll just addOption. Just curious.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I see now what you are saying. You are correct. You were referring to the "outer pointer" being NULL. Then we can keep things as-is on this front. Sorry for the confusion.