resolve in src/symbolize/gimli.rs passes cb a Symbol built from name/location references into the current cached Mapping, and then continues iterating the same DWARF state after the callback returns. Because Cache::with_global is reentrant on the same thread, a callback can call clear_symbol_cache() or recursively call resolve() and clear/evict that Mapping while the outer call is still active, leaving both the in-callback &Symbol and the outer frames iterator with dangling references to freed debug data (use-after-free UB).
This can be reproduced by:
fn clear_symbol_cache_during_resolve_callback() {
#[inline(never)]
fn target() -> *mut c_void {
let mut frames = Vec::new();
backtrace::trace(|frame| {
frames.push(frame.ip());
frames.len() < 32
});
frames
.into_iter()
.find(|ip| {
let mut found = false;
backtrace::resolve(*ip, |_| found = true);
found
})
.expect("expected to capture a resolvable frame")
}
let ip = target();
let mut saw_symbol = false;
backtrace::resolve(ip, |symbol| {
saw_symbol = true;
// This reproduces the gimli cache lifetime bug by evicting the mapping
// that backs the borrowed name/location data passed to the callback,
// then touching the borrowed symbol again.
backtrace::clear_symbol_cache();
let _ = symbol.filename().map(|path| path.as_os_str().len());
let _ = symbol.name().map(|name| name.to_string());
});
assert!(saw_symbol, "expected to resolve at least one symbol");
}
cargo test --test smoke clear_symbol_cache_during_resolve_callback --no-run
valgrind --tool=memcheck target/debug/deps/smoke-b6b34c31904af23c clear_symbol_cache_during_resolve_callback
==26174== Invalid read of size 1
==26174== at 0x41E172C: run_utf8_validation (validations.rs:0)
==26174== by 0x41E172C: core::str::converts::from_utf8 (converts.rs:91)
==26174== by 0x4135AC1: <backtrace::symbolize::SymbolName>::new (mod.rs:310)
==26174== by 0x411BBCD: <backtrace::symbolize::gimli::Symbol>::name (gimli.rs:521)
==26174== by 0x4135B95: <backtrace::symbolize::Symbol>::name (mod.rs:208)
==26174== by 0x407EDBA: smoke::clear_symbol_cache_during_resolve_callback::{closure#0} (smoke.rs:272)
==26174== by 0x41540DA: backtrace::symbolize::gimli::resolve::{closure#0} (gimli.rs:449)
==26174== by 0x4154464: backtrace::symbolize::gimli::resolve::{closure#1} (gimli.rs:473)
==26174== by 0x4153AEF: <backtrace::symbolize::gimli::Cache>::with_global::<backtrace::symbolize::gimli::resolve::{closure#1}> (gimli.rs:379)
==26174== by 0x411CD31: backtrace::symbolize::gimli::resolve (gimli.rs:453)
==26174== by 0x408203C: backtrace::symbolize::resolve_unsynchronized::<smoke::clear_symbol_cache_during_resolve_callback::{closure#0}> (mod.rs:162)
==26174== by 0x40823A2: backtrace::symbolize::resolve::<smoke::clear_symbol_cache_during_resolve_callback::{closure#0}> (mod.rs:63)
==26174== by 0x407BE3F: smoke::clear_symbol_cache_during_resolve_callback (smoke.rs:263)
==26174== Address 0x5a35ee6 is not stack'd, malloc'd or (recently) free'd
resolveinsrc/symbolize/gimli.rspassescbaSymbolbuilt fromname/locationreferences into the current cachedMapping, and then continues iterating the same DWARF state after the callback returns. BecauseCache::with_globalis reentrant on the same thread, a callback can callclear_symbol_cache()or recursively callresolve()and clear/evict thatMappingwhile the outer call is still active, leaving both the in-callback&Symboland the outerframesiterator with dangling references to freed debug data (use-after-free UB).This can be reproduced by: