From 3006d3b0ffb6532bbc07bea532eb6ebfb1b33b77 Mon Sep 17 00:00:00 2001 From: Artem Grinev Date: Sat, 11 Jan 2025 16:09:16 +0000 Subject: [PATCH] feat(daemon): add stack tracing with cpptrace Adds pretty stack trace printing on segfault and terminate() using cpptrace library. --- .vscode/launch.json | 3 +- CMakePresets.json | 2 +- Dockerfile | 2 + cmake/deps.cmake | 3 ++ conanfile.py | 1 + daemon/CMakeLists.txt | 7 +++- daemon/application.cpp | 4 ++ daemon/signal_tracer.cpp | 21 ++++++++++ daemon/sigtrace.h | 84 ++++++++++++++++++++++++++++++++++++++++ 9 files changed, 123 insertions(+), 4 deletions(-) create mode 100644 daemon/signal_tracer.cpp create mode 100644 daemon/sigtrace.h diff --git a/.vscode/launch.json b/.vscode/launch.json index ca17cc8..c40345c 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,7 +10,8 @@ "cwd": "${command:cmake.getLaunchTargetDirectory}", "env": [ "LD_LIBRARY_PATH=./libs/", - "PATH=${env:PATH}:${command:cmake.getLaunchTargetDirectory}" + "PATH=${env:PATH}:${command:cmake.getLaunchTargetDirectory}", + "LLVM_SYMBOLIZER_PATH=/usr/bin/llvm-symbolizer-18" ], "disableASLR": false, "initCommands": ["settings set target.disable-aslr false"] diff --git a/CMakePresets.json b/CMakePresets.json index 3e2d6b4..c65f95e 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -37,7 +37,7 @@ "name": "release", "inherits": "base", "cacheVariables": { - "CMAKE_BUILD_TYPE": "Release" + "CMAKE_BUILD_TYPE": "RelWithDebInfo" } } ], diff --git a/Dockerfile b/Dockerfile index 0bbd2ec..18705b7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -55,6 +55,7 @@ ENV PATH="/root/.bun/bin:/root/.local/bin:${PATH}" RUN pipx install conan==2.7.0 RUN conan profile detect --force --name default && \ + conan remote update conancenter --url https://center2.conan.io && \ cat <> /root/.conan2/profiles/default compiler=clang compiler.version=18 @@ -120,6 +121,7 @@ rm -rf /var/lib/apt/lists/* EOF COPY --from=production-build /build/bin /app +COPY --from=production-build /src /src RUN < +#include + +int main() { + cpptrace::object_trace trace; + while (true) { + cpptrace::safe_object_frame frame; + // fread used over read because a read() from a pipe might not read the full frame + std::size_t res = fread(&frame, sizeof(frame), 1, stdin); + if (res == 0) { + break; + } else if (res != 1) { + std::cerr << "Something went wrong while reading from the pipe" << res << " " + << std::endl; + break; + } else { + trace.frames.push_back(frame.resolve()); + } + } + trace.resolve().print(); +} diff --git a/daemon/sigtrace.h b/daemon/sigtrace.h new file mode 100644 index 0000000..e94d928 --- /dev/null +++ b/daemon/sigtrace.h @@ -0,0 +1,84 @@ +#pragma once + +// Based on https://github.com/jeremy-rifkin/cpptrace/blob/main/docs/signal-safe-tracing.md + +#include +#include +#include +#include +#include + +// This is just a utility I like, it makes the pipe API more expressive. +struct pipe_t { + union { + struct { + int read_end; + int write_end; + }; + int data[2]; + }; +}; + +void do_signal_safe_trace(cpptrace::frame_ptr* buffer, std::size_t count) { + // Setup pipe and spawn child + pipe_t input_pipe; + pipe(input_pipe.data); + pid_t const pid = fork(); + if (pid == -1) { + char const* fork_failure_message = "fork() failed\n"; + write(STDERR_FILENO, fork_failure_message, strlen(fork_failure_message)); + return; + } + if (pid == 0) { // child + dup2(input_pipe.read_end, STDIN_FILENO); + close(input_pipe.read_end); + close(input_pipe.write_end); + execl("signal_tracer", "signal_tracer", nullptr); + char const* exec_failure_message = + "exec(signal_tracer) failed: Make sure the signal_tracer executable is in " + "the current working directory and the binary's permissions are correct.\n"; + write(STDERR_FILENO, exec_failure_message, strlen(exec_failure_message)); + _exit(1); + } + // Resolve to safe_object_frames and write those to the pipe + for (std::size_t i = 0; i < count; i++) { + cpptrace::safe_object_frame frame; + cpptrace::get_safe_object_frame(buffer[i], &frame); + write(input_pipe.write_end, &frame, sizeof(frame)); + } + close(input_pipe.read_end); + close(input_pipe.write_end); + // Wait for child + waitpid(pid, nullptr, 0); +} + +void handler(int signo, siginfo_t* info, void* context) { + // Print basic message + char const* message = "SIGSEGV occurred:\n"; + write(STDERR_FILENO, message, strlen(message)); + // Generate trace + constexpr std::size_t N = 100; + cpptrace::frame_ptr buffer[N]; + std::size_t count = cpptrace::safe_generate_raw_trace(buffer, N); + do_signal_safe_trace(buffer, count); + // Up to you if you want to exit or continue or whatever + _exit(1); +} + +void warmup_cpptrace() { + // This is done for any dynamic-loading shenanigans + cpptrace::frame_ptr buffer[10]; + std::size_t count = cpptrace::safe_generate_raw_trace(buffer, 10); + cpptrace::safe_object_frame frame; + cpptrace::get_safe_object_frame(buffer[0], &frame); +} + +void register_signal_handler() { + warmup_cpptrace(); + struct sigaction action {}; + action.sa_flags = 0; + action.sa_sigaction = &handler; + action.sa_mask = {}; + + sigaction(SIGSEGV, &action, nullptr); +}