Skip to content

-musl platforms do not include unwind tables for libc #134592

Open
@RomanHargrave

Description

In certain cases, it may be desirable to capture a backtrace from a signal handler which includes stack frames prior to signal handling.

Consider this program, which attempts to demonstrate the problem in a concise way - please note that the actual implementation is more complex, in order to deal with signal safety:

use libc::{
    c_int, c_void, getpid, getuid, pid_t, sigaction, sigemptyset, sigset_t, size_t, syscall, uid_t,
    SYS_rt_tgsigqueueinfo, SA_SIGINFO, SIGRTMIN,
};
use std::{
    backtrace::Backtrace,
    mem::MaybeUninit,
    ptr::{addr_of, addr_of_mut, null_mut},
};

// please do not use this definition of siginfo_t in live code
#[repr(C)]
struct siginfo_t {
    si_signo: c_int,
    _si_errno: c_int,
    si_code: c_int,
    si_pid: pid_t,
    si_uid: uid_t,
    si_ptr: *mut c_void,
    _si_pad: [c_int; (128 / size_of::<c_int>()) - 3],
}

unsafe extern "C" fn handle_signal(_signo: c_int, _info: *mut siginfo_t, _ucontext: *const c_void) {
    // neither of these operations are signal safe
    let bt = Backtrace::force_capture();
    dbg!(bt);
}

fn main() {
    let sa_mask = unsafe {
        let mut sa_mask = MaybeUninit::<sigset_t>::uninit();
        sigemptyset(sa_mask.as_mut_ptr());
        sa_mask.assume_init()
    };

    let sa = sigaction {
        sa_sigaction: handle_signal as size_t,
        sa_mask,
        sa_flags: SA_SIGINFO,
        sa_restorer: None,
    };

    // please do not blindly copy this code for use in real world applications, it is meant to be
    // brief and functional, not strictly correct.
    unsafe {
        let _ = sigaction(SIGRTMIN(), addr_of!(sa), null_mut());

        let mut si: siginfo_t = MaybeUninit::zeroed().assume_init();
        si.si_signo = SIGRTMIN();
        si.si_code = -1; // SI_QUEUE
        si.si_pid = getpid();
        si.si_uid = getuid();

        let _ = syscall(
            SYS_rt_tgsigqueueinfo,
            getpid(),
            getpid(),
            SIGRTMIN(),
            addr_of_mut!(si),
        );
    }
}

When built for x86_64-unknown-linux-gnu, it produces the following output:

[src/main.rs:24:5] bt = Backtrace [
    { fn: "sigtest::handle_signal", file: "./src/main.rs", line: 23 },
    { fn: "syscall" },
    { fn: "sigtest::main", file: "./src/main.rs", line: 51 },
    { fn: "core::ops::function::FnOnce::call_once", file: "/rustc/f6e511eec7342f59a25f7c0534f1dbea00d01b14/library/core/src/ops/function.rs", line: 250 },
    { fn: "std::sys::backtrace::__rust_begin_short_backtrace", file: "/rustc/f6e511eec7342f59a25f7c0534f1dbea00d01b14/library/std/src/sys/backtrace.rs", line: 154 },
    { fn: "std::rt::lang_start::{{closure}}", file: "/rustc/f6e511eec7342f59a25f7c0534f1dbea00d01b14/library/std/src/rt.rs", line: 164 },
    { fn: "core::ops::function::impls::<impl core::ops::function::FnOnce<A> for &F>::call_once", file: "/rustc/f6e511eec7342f59a25f7c0534f1dbea00d01b14/library/core/src/ops/function.rs", line: 284 },
    { fn: "std::panicking::try::do_call", file: "/rustc/f6e511eec7342f59a25f7c0534f1dbea00d01b14/library/std/src/panicking.rs", line: 554 },
    { fn: "std::panicking::try", file: "/rustc/f6e511eec7342f59a25f7c0534f1dbea00d01b14/library/std/src/panicking.rs", line: 518 },
    { fn: "std::panic::catch_unwind", file: "/rustc/f6e511eec7342f59a25f7c0534f1dbea00d01b14/library/std/src/panic.rs", line: 345 },
    { fn: "std::rt::lang_start_internal::{{closure}}", file: "/rustc/f6e511eec7342f59a25f7c0534f1dbea00d01b14/library/std/src/rt.rs", line: 143 },
    { fn: "std::panicking::try::do_call", file: "/rustc/f6e511eec7342f59a25f7c0534f1dbea00d01b14/library/std/src/panicking.rs", line: 554 },
    { fn: "std::panicking::try", file: "/rustc/f6e511eec7342f59a25f7c0534f1dbea00d01b14/library/std/src/panicking.rs", line: 518 },
    { fn: "std::panic::catch_unwind", file: "/rustc/f6e511eec7342f59a25f7c0534f1dbea00d01b14/library/std/src/panic.rs", line: 345 },
    { fn: "std::rt::lang_start_internal", file: "/rustc/f6e511eec7342f59a25f7c0534f1dbea00d01b14/library/std/src/rt.rs", line: 143 },
    { fn: "std::rt::lang_start", file: "/rustc/f6e511eec7342f59a25f7c0534f1dbea00d01b14/library/std/src/rt.rs", line: 163 },
    { fn: "main" },
    { fn: "__libc_start_main" },
    { fn: "_start" },
]

When built for x86_64-unknown-linux-musl, this is the output:

[src/main.rs:24:5] bt = Backtrace [
    { fn: "sigtest::handle_signal", file: "./src/main.rs", line: 23 },
]

Observe that at one point the backtrace walks through syscall - while in this instance this is because the same thread issuing the syscall receives the signal, it is conceivable - and quite likely - that any arbitrary thread may be caught making a syscall or otherwise inside of libc if some other thread were to send a signal for such purpose.

This is caused by a combination of two similar but technically distinct problems:

  1. musl's signal trampoline does not have CFI annotations - this can be worked around by writing one's own trampoline and making the rt_sigaction syscall directly.
  2. musl, by default, does not include exception handling information in non-debug builds:
#
# Modern GCC wants to put DWARF tables (used for debugging and
# unwinding) in the loaded part of the program where they are
# unstrippable. These options force them back to debug sections (and
# cause them not to get generated at all if debugging is off).
#
tryflag CFLAGS_AUTO -fno-unwind-tables
tryflag CFLAGS_AUTO -fno-asynchronous-unwind-tables

I have not tracked down the build configuration for the musl platform; however, the second issue should be easily addressable in one of two ways:

  1. If musl is already being built, change the build flags for musl such that it always includes unwind tables
  2. Otherwise, begin building the copy of musl which is to be redistributed such that it may include unwind tables

Unwind tables are not included in musl because - as best I can guess - there is concern over memory utilization in extreme environments , and because it is further assumed that unwinding through libc is an unlikely case as this will most likely occur when unwinding from a signal handler - already uncommon - and only when the application is one that does unwinding which, while common, is still a subset of libc's consumers.

As backtraces are a first-class component of Rust's error handling design, and because this functionality works as intended and expected on -gnu - a Tier 1 platform - it seems reasonable to correct the behavior on -musl, particularly if it is indeed as straightforward as building without two flags. For users who require to exclude as much as is necessary from compiled artifacts, including unwind tables and the unwinder, it is still possible to strip the exception handling information at a later time - strip just needs to be configured to remove .eh_frame or .debug_info.

I can do the work if someone could point me in the direction of build configuration.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Assignees

No one assigned

    Labels

    A-backtraceArea: BacktracesC-bugCategory: This is a bug.O-muslTarget: The musl libcT-bootstrapRelevant to the bootstrap subteam: Rust's build system (x.py and src/bootstrap)T-compilerRelevant to the compiler team, which will review and decide on the PR/issue.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions