Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 43 additions & 1 deletion capi/include/blazesym.h
Original file line number Diff line number Diff line change
Expand Up @@ -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/<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.
*/
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.
*/
Expand Down Expand Up @@ -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;

/**
Expand Down
244 changes: 220 additions & 24 deletions capi/src/symbolize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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(
Copy link
Copy Markdown
Collaborator

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 Option to be NULL-able, no?

Copy link
Copy Markdown
Contributor Author

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_dispatch is allowed to be NULL. Is that something about FFI semantics?

Copy link
Copy Markdown
Collaborator

@d-e-s-o d-e-s-o Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, a fn cannot safely be NULL. It's like a reference; can't create it from a NULL ptr. At least that is my understanding.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor Author

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 add Option. Just curious.

Copy link
Copy Markdown
Collaborator

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.

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)]
Expand Down Expand Up @@ -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 {
Expand All @@ -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],
}
}
}
Expand Down Expand Up @@ -760,6 +803,8 @@ pub unsafe extern "C" fn blaze_symbolizer_new_opts(
code_info,
inlined_fns,
demangle,
_reserved1: _,
process_dispatch,
reserved: _,
} = opts;

Expand All @@ -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) };
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please also rename _debug_dirs_len to debug_dirs_len given that it's now used.

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();
Expand Down Expand Up @@ -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] }"
);
}

Expand Down Expand Up @@ -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(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fail to see why this function needs to be unsafe?

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,
)
};
}
}
18 changes: 18 additions & 0 deletions src/elf/resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be only available if the dwarf feature is enabled, same as https://docs.rs/blazesym/latest/blazesym/symbolize/struct.Builder.html#method.set_debug_dirs.

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
Expand Down
Loading
Loading