Skip to content

Self backtrace of deleted binaries #748

@Jongy

Description

@Jongy

In case the main binary is deleted, getting a backtrace from it fails, because the path we try to work against is the result of readlink("/proc/self/exe"):

let name = if is_static {
// don't try to look up our name from /proc/self/maps, it'll get silly
env::current_exe().unwrap_or_default().into_os_string()
} else if is_main && no_given_name {
infer_current_exe(&maps, dlpi_addr as usize)
} else {

(which is with (deleted) suffix, and in any case, the path no longer exists).

If we just use /proc/self/exe directly - things work. This example demonstrates that:

$ cat src/main.rs
fn inner() {
    // Delete our own binary from disk.
    let exe = std::env::current_exe().unwrap();
    std::fs::remove_file(&exe).unwrap();

    // Capture and print the backtrace.
    let bt = backtrace::Backtrace::new();
    eprintln!("\n{bt:?}");
}

fn main() {
    inner();
}
$ cat Cargo.toml
[package]
name = "bt-deleted-demo"
version = "0.1.0"
edition = "2021"

[dependencies]
backtrace = { path = "/home/jong/src/rust/backtrace-rs" }

[profile.release]
debug = true            # keep debug info (DWARF)
strip = "debuginfo"     # then strip DWARF, keeping .symtab (like `strip --strip-debug`)

When run against master (28ec93b) I get:

   0: <unknown>
   1: <unknown>
   2: <unknown>
   3: <unknown>
   4: <unknown>
   5: __libc_start_call_main
             at ./csu/../sysdeps/nptl/libc_start_call_main.h:58:16
   6: __libc_start_main_impl
             at ./csu/../csu/libc-start.c:360:3
   7: <unknown>

I apply this patch:

diff --git i/src/symbolize/gimli/libs_dl_iterate_phdr.rs w/src/symbolize/gimli/libs_dl_iterate_phdr.rs
index 2d1da7c..e5c21e2 100644
--- i/src/symbolize/gimli/libs_dl_iterate_phdr.rs
+++ w/src/symbolize/gimli/libs_dl_iterate_phdr.rs
@@ -72,11 +72,8 @@ unsafe extern "C" fn callback(
     let no_given_name = dlpi_name.is_null()
         // SAFETY: we just checked for null
         || unsafe { *dlpi_name == 0 };
-    let name = if is_static {
-        // don't try to look up our name from /proc/self/maps, it'll get silly
-        env::current_exe().unwrap_or_default().into_os_string()
-    } else if is_main && no_given_name {
-        infer_current_exe(&maps, dlpi_addr as usize)
+    let name = if is_static || (is_main && no_given_name) {
+        OsString::from("/proc/self/exe")
     } else {
         // this fallback works even if we are main, because some platforms give the name anyways
         if dlpi_name.is_null() {

and get:

   0: bt_deleted_demo::main
   1: std::sys::backtrace::__rust_begin_short_backtrace
   2: std::rt::lang_start::{{closure}}
   3: std::rt::lang_start_internal
   4: main
   5: __libc_start_call_main
             at ./csu/../sysdeps/nptl/libc_start_call_main.h:58:16
   6: __libc_start_main_impl
             at ./csu/../csu/libc-start.c:360:3
   7: _start

This is relevant in one of our apps, where the binary might be deleted in some cases but we want the self-backtrace to keep working as expected.
Do you think it's a valid change? I'm happy to submit it. We need some handling around what #488 fixed (when /proc/self/exe is the interpreter we should still use /proc/pid/maps and not /proc/self/exe) but other than that I believe this fix is okay.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions