Full Gemini Codebase Audit Report Appendix
Unsafe Rust Review: backtrace (v0_3)
Overall Safety Assessment
The backtrace crate makes extensive use of unsafe code to interface with system-level backtracing libraries (like libunwind and dbghelp.dll) and manually parses debug information (via gimli/object). While the system-level interfaces are generally well-encapsulated (for example, using re-entrant process-wide named mutexes on Windows and stack-based panicky "bombs" to prevent panic unwinding through foreign C frames on Unix), there is a critical soundness vulnerability in the symbolication cache design.
A combination of re-entrant global locking and a fixed-size LRU cache for DWARF mapping metadata allows safe Rust callbacks to easily trigger use-after-free (UAF) and aliasing violations. In addition, there is a lack of # Safety documentation for public unsafe functions and many internal unsafe blocks lack proper safety comments.
Due to the critical UAF vulnerability, the crate is Unsound (Critical risk).
Critical Findings
Use-After-Free (UAF) and Aliasing Violation in Gimli Symbolication Cache 🔴 🤦
- Severity: 🔴 High
- Threat Vector: 🤦 Accidental Misuse
- Bug Type: Use-After-Free & Aliasing Violation
- Vulnerability Type: Use-After-Free & Aliasing Violation
- Location:
src/symbolize/gimli.rs (and associated files src/symbolize/mod.rs / src/lib.rs)
Description
The symbolication backend using gimli maintains a process-wide global cache of parsed DWARF mappings:
static mut MAPPINGS_CACHE: Option<Cache> = None;
Access to this cache is protected by a re-entrant global lock:
pub fn resolve<F: FnMut(&Symbol)>(addr: *mut c_void, cb: F) {
let _guard = crate::lock::lock();
unsafe { resolve_unsynchronized(addr, cb) }
}
Because the lock is re-entrant, a thread that already holds the lock can acquire it again without blocking.
During resolve, the cache is borrowed mutably via Cache::with_global:
unsafe fn with_global(f: impl FnOnce(&mut Self)) {
static mut MAPPINGS_CACHE: Option<Cache> = None;
unsafe {
f(MAPPINGS_CACHE.get_or_insert_with(Cache::new))
}
}
Inside the closure f, the symbol for an address is resolved. This yields a DWARF context cx and stash stash borrowed from one of the active mappings in the cache:
let (cx, stash) = match cache.mapping_for_lib(lib) {
Some((cx, stash)) => (cx, stash),
None => return,
};
The resolve closure then calls the user's callback cb while still borrowing the cache:
if let Ok(mut frames) = cx.find_frames(stash, addr as u64) {
while let Ok(Some(frame)) = frames.next() {
...
call(Symbol::Frame { ... }); // calls cb
}
}
Since the callback runs inside the borrow of cache and under the re-entrant lock, it can execute arbitrary safe Rust code, including calling other backtrace APIs. This allows two separate soundness exploits:
Exploitation Scenario 1: clear_symbol_cache() inside callback
If the safe callback cb calls the public safe function clear_symbol_cache(), it triggers the following sequence:
clear_symbol_cache acquires the re-entrant lock (which immediately succeeds).
- It calls
Cache::with_global(|cache| cache.mappings.clear()).
- This creates a second mutable reference
&mut Cache to MAPPINGS_CACHE while the outer &mut Cache is still live, violating Rust's aliasing rules (instant UB).
cache.mappings.clear() drops all Mappings in the cache. This drops the Stash and the Mmap associated with the active mapping, unmapping the DWARF files and deallocating memory.
clear_symbol_cache returns.
- The outer
resolve function continues iterating while let Ok(Some(frame)) = frames.next(). However, frames holds references to the unmapped/dropped cx and stash!
- This causes a use-after-free when
frames.next() is called.
Exploitation Scenario 2: Recursive resolve() inside callback
The LRU cache (cache.mappings) has a fixed capacity of 4:
const MAPPINGS_CACHE_SIZE: usize = 4;
If the callback cb recursively calls resolve for addresses in 4 or more different shared libraries, it will insert new mappings into the LRU cache. This will evict the oldest mapping (which is the mapping currently being used by the outer resolve frame).
- When the oldest mapping is evicted, it is dropped, unmapping its
Mmap and deallocating its Stash.
- The outer
resolve frame continues execution after the recursive calls return.
- It attempts to read from
cx or stash, dereferencing pointers to unmapped memory (UAF).
Proof of Concept (PoC)
The following safe Rust code is sufficient to trigger the UAF (on Unix/Linux):
fn main() {
// Resolve any address
backtrace::resolve(main as *mut _, |symbol| {
// Trigger cache clearing while resolve is active
backtrace::clear_symbol_cache();
});
}
Critical Findings
Use-After-Free (UAF) and Aliasing Violation in Gimli Symbolication Cache 🔴 🤦
- Severity: 🔴 High
- Threat Vector: 🤦 Accidental Misuse
- Bug Type: Use-After-Free & Aliasing Violation
[Duplicate entry - see main description above]
Fishy Findings
Stack-Allocated Pointers with 'static Lifetime Lie 🟡 🤸
- Severity: 🟡 Low
- Threat Vector: 🤸 Deliberate Contortion
- Bug Type: Invalid Lifetime
- Location:
src/symbolize/dbghelp.rs (lines 37-47, 225-314)
Description
In src/symbolize/dbghelp.rs, the Windows MSVC symbolication backend copies the symbol name into a stack-allocated buffer:
let mut name_buffer = [0_u8; 256];
let mut name_len = unsafe { WideCharToMultiByte(..., name_buffer.as_mut_ptr(), ...) };
...
let name = ptr::addr_of!(name_buffer[..name_len]);
Then, it constructs the Symbol struct:
cb(&super::Symbol {
inner: Symbol {
name, // raw pointer to stack-allocated `name_buffer`
addr: unsafe { (*info).Address } as *mut _,
line: lineno,
filename,
_filename_cache: unsafe { cache(filename) },
_marker: marker::PhantomData,
},
})
However, super::Symbol is declared as:
pub struct Symbol {
inner: imp::Symbol<'static>,
}
This forces the compiler to treat Symbol::inner as having a 'static lifetime.
Thus, we are constructing a Symbol<'static> where the name raw pointer points to a local variable name_buffer on the stack that will be deallocated as soon as do_resolve returns.
Although this is technically safe because super::Symbol is only handed out as a reference &super::Symbol with a short anonymous lifetime, and there are no public APIs allowing the user to copy or clone it, it is a highly fragile design. It lies about lifetimes to the compiler and relies on the API not exposing ways to leak the internal state.
Missing Safety Comments
Lack of # Safety Documentation on Public Unsafe APIs 🟡 🤦
- Severity: 🟡 Low
- Threat Vector: 🤦 Accidental Misuse
- Bug Type: Missing Documentation
The following public functions are marked unsafe but completely lack a # Safety section in their documentation explaining their preconditions:
trace_unsynchronized (in src/backtrace/mod.rs)
resolve_unsynchronized (in src/symbolize/mod.rs)
resolve_frame_unsynchronized (in src/symbolize/mod.rs)
They are unsafe because they require external synchronization to prevent concurrent mutable access to global static state (like MAPPINGS_CACHE on Unix or dbghelp.dll initialization on Windows). This contract must be documented.
Lack of // SAFETY: Comments on Unsafe Blocks 🟡 🤦
- Severity: 🟡 Low
- Threat Vector: 🤦 Accidental Misuse
- Bug Type: Missing Safety Comment
Multiple unsafe operations throughout the codebase are performed without documenting why they are safe:
- Unix
Mmap::map & munmap: src/symbolize/gimli/mmap_unix.rs calls FFI functions mmap64 and munmap without documenting safety preconditions or explaining how the mapped memory's validity is maintained.
- Android
Mmap::map from ZIP: src/symbolize/gimli/elf.rs maps a ZIP-embedded library without safety comments.
dl_iterate_phdr: src/symbolize/gimli/libs_dl_iterate_phdr.rs calls FFI libc::dl_iterate_phdr and casts raw pointers without any // SAFETY: comments.
- MSVC API Calls:
src/dbghelp.rs makes numerous Windows FFI calls (e.g. CreateMutexA, WaitForSingleObjectEx, LoadLibraryA) and performs atomic operations on raw pointer pointers without safety justifications.
Note
This finding was identified during an agentic unsafe Rust code review performed by Gemini AI, followed by human review and verification.
The Issue
In
backtracev0_3, the symbolication backend usinggimlimaintains a process-wide global cache of DWARF mappings (static mut MAPPINGS_CACHE) at src/symbolize/gimli.rs#L17-L26, protected by a re-entrant mutex (crate::lock::lock()) inbacktrace::resolveatbacktrace-rs/src/symbolize/mod.rs
Lines 98 to 105 in d902726
When resolving symbols,
Cache::with_globalborrowsMAPPINGS_CACHEmutably and obtains a DWARF contextcxand stashstashfrom an active cached mapping. It then executes the caller's closurecb(&Symbol)while this mutable cache borrow remains live.backtrace-rs/src/symbolize/gimli.rs
Lines 130 to 142 in d902726
Because the global lock is re-entrant and
cbruns synchronously inside the live borrow ofMAPPINGS_CACHE, re-entrant calls in safe code trigger memory safety violations:clear_symbol_cache(): Ifcbcallsbacktrace::clear_symbol_cache(), it creates a second concurrent&mut Cachereference toMAPPINGS_CACHE(Aliasing Violation) and clearscache.mappings. This drops all cachedMappings and unmaps the DWARF file (Mmap), deallocatingcxandstash. Whencbreturns,resolvecontinues iteratingframes.next()on dangling pointers (Use-After-Free).MAPPINGS_CACHE_SIZE = 4). Ifcbrecursively resolves symbols across 4+ libraries, new mappings evict and drop the oldest mapping actively in use by the outerresolveframe (Use-After-Free).Minimal Reproduction (Segfault)
Suggested Fix
Apply necessary runtime bounds checks or formalize the
unsafe fnsafety contract pre-conditions.Note
The full audit report below also contains additional minor findings (such as missing safety comments or undocumented FFI assumptions) that are probably worth fixing as well but not the primary goal of this issue. The audit report has not been human-reviewed, it may contain misleading claims.
Full Gemini Codebase Audit Report Appendix
Unsafe Rust Review:
backtrace(v0_3)Overall Safety Assessment
The
backtracecrate makes extensive use of unsafe code to interface with system-level backtracing libraries (likelibunwindanddbghelp.dll) and manually parses debug information (viagimli/object). While the system-level interfaces are generally well-encapsulated (for example, using re-entrant process-wide named mutexes on Windows and stack-based panicky "bombs" to prevent panic unwinding through foreign C frames on Unix), there is a critical soundness vulnerability in the symbolication cache design.A combination of re-entrant global locking and a fixed-size LRU cache for DWARF mapping metadata allows safe Rust callbacks to easily trigger use-after-free (UAF) and aliasing violations. In addition, there is a lack of
# Safetydocumentation for public unsafe functions and many internalunsafeblocks lack proper safety comments.Due to the critical UAF vulnerability, the crate is Unsound (Critical risk).
Critical Findings
Use-After-Free (UAF) and Aliasing Violation in Gimli Symbolication Cache 🔴 🤦
src/symbolize/gimli.rs(and associated filessrc/symbolize/mod.rs/src/lib.rs)Description
The symbolication backend using
gimlimaintains a process-wide global cache of parsed DWARF mappings:Access to this cache is protected by a re-entrant global lock:
Because the lock is re-entrant, a thread that already holds the lock can acquire it again without blocking.
During
resolve, the cache is borrowed mutably viaCache::with_global:Inside the closure
f, the symbol for an address is resolved. This yields a DWARF contextcxand stashstashborrowed from one of the active mappings in the cache:The resolve closure then calls the user's callback
cbwhile still borrowing the cache:Since the callback runs inside the borrow of
cacheand under the re-entrant lock, it can execute arbitrary safe Rust code, including calling other backtrace APIs. This allows two separate soundness exploits:Exploitation Scenario 1:
clear_symbol_cache()inside callbackIf the safe callback
cbcalls the public safe functionclear_symbol_cache(), it triggers the following sequence:clear_symbol_cacheacquires the re-entrant lock (which immediately succeeds).Cache::with_global(|cache| cache.mappings.clear()).&mut CachetoMAPPINGS_CACHEwhile the outer&mut Cacheis still live, violating Rust's aliasing rules (instant UB).cache.mappings.clear()drops allMappings in the cache. This drops theStashand theMmapassociated with the active mapping, unmapping the DWARF files and deallocating memory.clear_symbol_cachereturns.resolvefunction continues iteratingwhile let Ok(Some(frame)) = frames.next(). However,framesholds references to the unmapped/droppedcxandstash!frames.next()is called.Exploitation Scenario 2: Recursive
resolve()inside callbackThe LRU cache (
cache.mappings) has a fixed capacity of 4:If the callback
cbrecursively callsresolvefor addresses in 4 or more different shared libraries, it will insert new mappings into the LRU cache. This will evict the oldest mapping (which is the mapping currently being used by the outerresolveframe).Mmapand deallocating itsStash.resolveframe continues execution after the recursive calls return.cxorstash, dereferencing pointers to unmapped memory (UAF).Proof of Concept (PoC)
The following safe Rust code is sufficient to trigger the UAF (on Unix/Linux):
Critical Findings
Use-After-Free (UAF) and Aliasing Violation in Gimli Symbolication Cache 🔴 🤦
[Duplicate entry - see main description above]
Fishy Findings
Stack-Allocated Pointers with
'staticLifetime Lie 🟡 🤸src/symbolize/dbghelp.rs(lines 37-47, 225-314)Description
In
src/symbolize/dbghelp.rs, the Windows MSVC symbolication backend copies the symbol name into a stack-allocated buffer:Then, it constructs the
Symbolstruct:However,
super::Symbolis declared as:This forces the compiler to treat
Symbol::inneras having a'staticlifetime.Thus, we are constructing a
Symbol<'static>where thenameraw pointer points to a local variablename_bufferon the stack that will be deallocated as soon asdo_resolvereturns.Although this is technically safe because
super::Symbolis only handed out as a reference&super::Symbolwith a short anonymous lifetime, and there are no public APIs allowing the user to copy or clone it, it is a highly fragile design. It lies about lifetimes to the compiler and relies on the API not exposing ways to leak the internal state.Missing Safety Comments
Lack of
# SafetyDocumentation on Public Unsafe APIs 🟡 🤦The following public functions are marked
unsafebut completely lack a# Safetysection in their documentation explaining their preconditions:trace_unsynchronized(insrc/backtrace/mod.rs)resolve_unsynchronized(insrc/symbolize/mod.rs)resolve_frame_unsynchronized(insrc/symbolize/mod.rs)They are unsafe because they require external synchronization to prevent concurrent mutable access to global static state (like
MAPPINGS_CACHEon Unix ordbghelp.dllinitialization on Windows). This contract must be documented.Lack of
// SAFETY:Comments on Unsafe Blocks 🟡 🤦Multiple unsafe operations throughout the codebase are performed without documenting why they are safe:
Mmap::map&munmap:src/symbolize/gimli/mmap_unix.rscalls FFI functionsmmap64andmunmapwithout documenting safety preconditions or explaining how the mapped memory's validity is maintained.Mmap::mapfrom ZIP:src/symbolize/gimli/elf.rsmaps a ZIP-embedded library without safety comments.dl_iterate_phdr:src/symbolize/gimli/libs_dl_iterate_phdr.rscalls FFIlibc::dl_iterate_phdrand casts raw pointers without any// SAFETY:comments.src/dbghelp.rsmakes numerous Windows FFI calls (e.g.CreateMutexA,WaitForSingleObjectEx,LoadLibraryA) and performs atomic operations on raw pointer pointers without safety justifications.