Skip to content

Commit 501ba9d

Browse files
committed
capi: Add callback for symbolizer
Expose parts of set_process_dispatcher in the capi to enable users to provide alternative ELF file paths for symbolization (e.g., fetched via debuginfod). Signed-off-by: Arne Jansen <arne@die-jansens.de>
1 parent 675e5bf commit 501ba9d

2 files changed

Lines changed: 223 additions & 5 deletions

File tree

capi/include/blazesym.h

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -767,11 +767,43 @@ typedef struct blaze_symbolizer_opts {
767767
* the underlying language does not mangle symbols (such as C).
768768
*/
769769
bool demangle;
770+
/**
771+
* Explicit pad to avoid implicit gaps.
772+
*/
773+
uint8_t _pad_a[4];
774+
/**
775+
* Callback for custom process member path dispatch.
776+
*
777+
* When set, this callback is invoked for each process member that
778+
* has a file path during process symbolization. It allows the
779+
* caller to provide an alternative ELF file path for
780+
* symbolization (e.g., fetched via debuginfod).
781+
*
782+
* The callback receives the `/proc/<pid>/map_files/...` path and
783+
* the symbolic path from `/proc/<pid>/maps`, along with the
784+
* user-provided context pointer
785+
* ([`process_dispatch_ctx`][Self::process_dispatch_ctx]).
786+
*
787+
* The callback should return one of:
788+
* - A `malloc`'d path string to an alternative ELF file to use for
789+
* symbolization. The library will `free` this string after use.
790+
* - `NULL` to use the default symbolization behavior for this member.
791+
*
792+
* Set to `NULL` to disable custom dispatch.
793+
*/
794+
char *(*process_dispatch_cb)(const char *maps_file,
795+
const char *symbolic_path,
796+
void *ctx);
797+
/**
798+
* Opaque context pointer passed to
799+
* [`process_dispatch_cb`][Self::process_dispatch_cb].
800+
*/
801+
void *process_dispatch_ctx;
770802
/**
771803
* Unused member available for future expansion. Must be initialized
772804
* to zero.
773805
*/
774-
uint8_t reserved[20];
806+
uint8_t reserved[24];
775807
} blaze_symbolizer_opts;
776808

777809
/**

capi/src/symbolize.rs

Lines changed: 190 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,20 @@ use std::alloc::alloc;
22
use std::alloc::dealloc;
33
use std::alloc::Layout;
44
use std::ffi::CStr;
5+
use std::ffi::CString;
56
use std::ffi::OsStr;
67
use std::fmt::Debug;
8+
use std::io;
79
use std::mem;
810
use std::ops::Deref as _;
911
use std::os::raw::c_char;
12+
use std::os::raw::c_void;
1013
use std::os::unix::ffi::OsStrExt as _;
1114
use std::path::Path;
1215
use std::path::PathBuf;
1316
use std::ptr;
1417

18+
use blazesym::helper::ElfResolver;
1519
use blazesym::symbolize::cache;
1620
use blazesym::symbolize::source::Elf;
1721
use blazesym::symbolize::source::GsymData;
@@ -21,6 +25,7 @@ use blazesym::symbolize::source::Process;
2125
use blazesym::symbolize::source::Source;
2226
use blazesym::symbolize::CodeInfo;
2327
use blazesym::symbolize::Input;
28+
use blazesym::symbolize::ProcessMemberType;
2429
use blazesym::symbolize::Reason;
2530
use blazesym::symbolize::Sym;
2631
use blazesym::symbolize::Symbolized;
@@ -689,9 +694,39 @@ pub struct blaze_symbolizer_opts {
689694
/// languages are Rust and C++ and the flag will have no effect if
690695
/// the underlying language does not mangle symbols (such as C).
691696
pub demangle: bool,
697+
/// Explicit pad to avoid implicit gaps.
698+
pub _pad_a: [u8; 4],
699+
/// Callback for custom process member path dispatch.
700+
///
701+
/// When set, this callback is invoked for each process member that
702+
/// has a file path during process symbolization. It allows the
703+
/// caller to provide an alternative ELF file path for
704+
/// symbolization (e.g., fetched via debuginfod).
705+
///
706+
/// The callback receives the `/proc/<pid>/map_files/...` path and
707+
/// the symbolic path from `/proc/<pid>/maps`, along with the
708+
/// user-provided context pointer
709+
/// ([`process_dispatch_ctx`][Self::process_dispatch_ctx]).
710+
///
711+
/// The callback should return one of:
712+
/// - A `malloc`'d path string to an alternative ELF file to use for
713+
/// symbolization. The library will `free` this string after use.
714+
/// - `NULL` to use the default symbolization behavior for this member.
715+
///
716+
/// Set to `NULL` to disable custom dispatch.
717+
pub process_dispatch_cb: Option<
718+
unsafe extern "C" fn(
719+
maps_file: *const c_char,
720+
symbolic_path: *const c_char,
721+
ctx: *mut c_void,
722+
) -> *mut c_char,
723+
>,
724+
/// Opaque context pointer passed to
725+
/// [`process_dispatch_cb`][Self::process_dispatch_cb].
726+
pub process_dispatch_ctx: *mut c_void,
692727
/// Unused member available for future expansion. Must be initialized
693728
/// to zero.
694-
pub reserved: [u8; 20],
729+
pub reserved: [u8; 24],
695730
}
696731

697732
impl Default for blaze_symbolizer_opts {
@@ -704,7 +739,10 @@ impl Default for blaze_symbolizer_opts {
704739
code_info: false,
705740
inlined_fns: false,
706741
demangle: false,
707-
reserved: [0; 20],
742+
_pad_a: [0; 4],
743+
process_dispatch_cb: None,
744+
process_dispatch_ctx: ptr::null_mut(),
745+
reserved: [0; 24],
708746
}
709747
}
710748
}
@@ -760,6 +798,9 @@ pub unsafe extern "C" fn blaze_symbolizer_new_opts(
760798
code_info,
761799
inlined_fns,
762800
demangle,
801+
_pad_a: _,
802+
process_dispatch_cb,
803+
process_dispatch_ctx,
763804
reserved: _,
764805
} = opts;
765806

@@ -794,6 +835,41 @@ pub unsafe extern "C" fn blaze_symbolizer_new_opts(
794835
}
795836
};
796837

838+
let builder = if let Some(cb) = process_dispatch_cb {
839+
// Cast the context pointer to usize so the closure is Send.
840+
// The caller is responsible for thread safety of the context.
841+
let ctx = process_dispatch_ctx as usize;
842+
builder.set_process_dispatcher(move |info| {
843+
let ctx = ctx as *mut c_void;
844+
match info.member_entry {
845+
ProcessMemberType::Path(entry) => {
846+
let maps_file = CString::new(entry.maps_file.as_os_str().as_bytes())
847+
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
848+
let sym_path = CString::new(entry.symbolic_path.as_os_str().as_bytes())
849+
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
850+
// SAFETY: The caller guarantees that the callback is safe
851+
// to call with valid C string pointers and the
852+
// provided context.
853+
let result = unsafe { cb(maps_file.as_ptr(), sym_path.as_ptr(), ctx) };
854+
if result.is_null() {
855+
return Ok(None)
856+
}
857+
// SAFETY: The callback is required to return a valid,
858+
// NUL-terminated, `malloc`'d C string.
859+
let path_cstr = unsafe { CStr::from_ptr(result) };
860+
let path = Path::new(OsStr::from_bytes(path_cstr.to_bytes()));
861+
let resolver = ElfResolver::open(path);
862+
// SAFETY: The string was `malloc`'d by the callback.
863+
unsafe { libc::free(result.cast()) };
864+
Ok(Some(Box::new(resolver?)))
865+
}
866+
_ => Ok(None),
867+
}
868+
})
869+
} else {
870+
builder
871+
};
872+
797873
let symbolizer = builder.build();
798874
let symbolizer_box = Box::new(symbolizer);
799875
let () = set_last_err(blaze_err::OK);
@@ -1355,7 +1431,7 @@ mod tests {
13551431
assert_eq!(mem::size_of::<blaze_symbolize_src_process>(), 32);
13561432
assert_eq!(mem::size_of::<blaze_symbolize_src_gsym_data>(), 40);
13571433
assert_eq!(mem::size_of::<blaze_symbolize_src_gsym_file>(), 32);
1358-
assert_eq!(mem::size_of::<blaze_symbolizer_opts>(), 48);
1434+
assert_eq!(mem::size_of::<blaze_symbolizer_opts>(), 72);
13591435
assert_eq!(mem::size_of::<blaze_symbolize_code_info>(), 32);
13601436
assert_eq!(mem::size_of::<blaze_symbolize_inlined_fn>(), 48);
13611437
assert_eq!(mem::size_of::<blaze_sym>(), 104);
@@ -1465,7 +1541,7 @@ mod tests {
14651541
};
14661542
assert_eq!(
14671543
format!("{opts:?}"),
1468-
"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] }"
1544+
"blaze_symbolizer_opts { type_size: 16, debug_dirs: 0x0, debug_dirs_len: 0, auto_reload: false, code_info: false, inlined_fns: false, demangle: true, _pad_a: [0, 0, 0, 0], process_dispatch_cb: None, process_dispatch_ctx: 0x0, reserved: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] }"
14691545
);
14701546
}
14711547

@@ -2302,4 +2378,114 @@ mod tests {
23022378
let () = unsafe { blaze_syms_free(result) };
23032379
let () = unsafe { blaze_symbolizer_free(symbolizer) };
23042380
}
2381+
2382+
/// Helper to test process dispatch callbacks. Creates a symbolizer
2383+
/// with the given callback (ctx set to `0xc0ffee`), symbolizes
2384+
/// the given address, and checks that the symbol name contains
2385+
/// `expected_sym`. If `expected_sym` is `None`, asserts that
2386+
/// symbolization failed.
2387+
unsafe fn symbolize_with_dispatch(
2388+
cb: unsafe extern "C" fn(*const c_char, *const c_char, *mut c_void) -> *mut c_char,
2389+
addr: Addr,
2390+
expected_sym: Option<&str>,
2391+
) {
2392+
let opts = blaze_symbolizer_opts {
2393+
process_dispatch_cb: Some(cb),
2394+
process_dispatch_ctx: 0xc0ffee as *mut c_void,
2395+
..Default::default()
2396+
};
2397+
let symbolizer = unsafe { blaze_symbolizer_new_opts(&opts) };
2398+
assert!(!symbolizer.is_null());
2399+
2400+
let process_src = blaze_symbolize_src_process {
2401+
pid: 0,
2402+
debug_syms: true,
2403+
..Default::default()
2404+
};
2405+
2406+
let addrs = [addr];
2407+
let result = unsafe {
2408+
blaze_symbolize_process_abs_addrs(symbolizer, &process_src, addrs.as_ptr(), addrs.len())
2409+
};
2410+
let () = unsafe { blaze_symbolizer_free(symbolizer) };
2411+
2412+
if let Some(expected) = expected_sym {
2413+
assert!(!result.is_null());
2414+
let result = unsafe { &*result };
2415+
assert_eq!(result.cnt, 1);
2416+
let syms = unsafe { slice::from_raw_parts(result.syms.as_ptr(), result.cnt) };
2417+
let name = unsafe { CStr::from_ptr(syms[0].name) }.to_str().unwrap();
2418+
assert!(name.contains(expected), "{name}");
2419+
let () = unsafe { blaze_syms_free(result) };
2420+
} else {
2421+
assert!(result.is_null());
2422+
}
2423+
}
2424+
2425+
/// Make sure that we can symbolize an address in the current process
2426+
/// using a custom process dispatch callback that returns the
2427+
/// `maps_file` path as-is.
2428+
#[test]
2429+
fn symbolize_in_process_with_dispatch() {
2430+
unsafe extern "C" fn cb(
2431+
maps_file: *const c_char,
2432+
_symbolic_path: *const c_char,
2433+
ctx: *mut c_void,
2434+
) -> *mut c_char {
2435+
assert_eq!(ctx as usize, 0xc0ffee);
2436+
unsafe { libc::strdup(maps_file) }
2437+
}
2438+
2439+
unsafe {
2440+
symbolize_with_dispatch(
2441+
cb,
2442+
symbolize_in_process_with_dispatch as *const () as Addr,
2443+
Some("symbolize_in_process_with_dispatch"),
2444+
)
2445+
};
2446+
}
2447+
2448+
/// Make sure that a dispatch callback returning NULL falls back to
2449+
/// the default symbolization behavior.
2450+
#[test]
2451+
fn symbolize_in_process_with_null_dispatch() {
2452+
unsafe extern "C" fn cb(
2453+
_maps_file: *const c_char,
2454+
_symbolic_path: *const c_char,
2455+
ctx: *mut c_void,
2456+
) -> *mut c_char {
2457+
assert_eq!(ctx as usize, 0xc0ffee);
2458+
ptr::null_mut()
2459+
}
2460+
2461+
unsafe {
2462+
symbolize_with_dispatch(
2463+
cb,
2464+
symbolize_in_process_with_null_dispatch as *const () as Addr,
2465+
Some("symbolize_in_process_with_null_dispatch"),
2466+
)
2467+
};
2468+
}
2469+
2470+
/// Make sure that a dispatch callback returning a non-existent path
2471+
/// causes symbolization to fail for that address.
2472+
#[test]
2473+
fn symbolize_in_process_with_bad_dispatch() {
2474+
unsafe extern "C" fn cb(
2475+
_maps_file: *const c_char,
2476+
_symbolic_path: *const c_char,
2477+
ctx: *mut c_void,
2478+
) -> *mut c_char {
2479+
assert_eq!(ctx as usize, 0xc0ffee);
2480+
unsafe { libc::strdup(c"/no/such/file".as_ptr()) }
2481+
}
2482+
2483+
unsafe {
2484+
symbolize_with_dispatch(
2485+
cb,
2486+
symbolize_in_process_with_bad_dispatch as *const () as Addr,
2487+
None,
2488+
)
2489+
};
2490+
}
23052491
}

0 commit comments

Comments
 (0)