Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
features:
- |
Crashtracker: This introduces a fallback to capture runtime stack frames when
Python's ``_Py_DumpTracebackThreads`` function is not available.
1 change: 1 addition & 0 deletions src/native/build.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
fn main() {
pyo3_build_config::use_pyo3_cfgs();
//NOTE(@dmehala): PyO3 doesn't link to `libpython` on MacOS.
// This set the correct linker arguments for the platform.
// Source: <https://pyo3.rs/main/building-and-distribution.html#macos>
Expand Down
158 changes: 20 additions & 138 deletions src/native/crashtracker.rs
Original file line number Diff line number Diff line change
@@ -1,37 +1,21 @@
use anyhow;
use std::collections::HashMap;
use std::ffi::{c_char, c_int, c_void};
use std::ptr;
use std::sync::atomic::{AtomicU8, Ordering};
use std::sync::Once;
use std::time::Duration;

use libdd_common::Endpoint;
use libdd_crashtracker::{
register_runtime_stacktrace_string_callback, CrashtrackerConfiguration,
CrashtrackerReceiverConfig, Metadata, StacktraceCollection,
register_runtime_frame_callback, register_runtime_stacktrace_string_callback,
CrashtrackerConfiguration, CrashtrackerReceiverConfig, Metadata, StacktraceCollection,
};
use pyo3::prelude::*;

// Function pointer type for _Py_DumpTracebackThreads
type PyDumpTracebackThreadsFn = unsafe extern "C" fn(
fd: c_int,
interp: *mut pyo3_ffi::PyInterpreterState,
current_tstate: *mut pyo3_ffi::PyThreadState,
) -> *const c_char;

// Cached function pointer to avoid dlsym during crash
static mut DUMP_TRACEBACK_FN: Option<PyDumpTracebackThreadsFn> = None;
static DUMP_TRACEBACK_INIT: std::sync::Once = std::sync::Once::new();

// We define these raw system calls here to be used within signal handler context.
// These direct C functions are preferred over going through Rust wrappers
extern "C" {
fn pipe(pipefd: *mut [c_int; 2]) -> c_int;
fn read(fd: c_int, buf: *mut c_void, count: usize) -> isize;
fn close(fd: c_int) -> c_int;
fn fcntl(fd: c_int, cmd: c_int, arg: c_int) -> c_int;
}
mod crashtracker_runtime_stacks;
use crashtracker_runtime_stacks::{
get_cached_dump_traceback_fn, init_dump_traceback_fn, native_runtime_stack_frame_callback,
native_runtime_stack_string_callback,
};

pub trait RustWrapper {
type Inner;
Expand Down Expand Up @@ -273,8 +257,19 @@ pub fn crashtracker_init<'py>(
unsafe {
init_dump_traceback_fn();
}
if let Err(e) = register_runtime_stacktrace_string_callback(native_runtime_stack_callback) {
eprintln!("Failed to register runtime callback: {}", e);
let dump_fn_available = unsafe { get_cached_dump_traceback_fn().is_some() };
if dump_fn_available {
if let Err(e) =
register_runtime_stacktrace_string_callback(
native_runtime_stack_string_callback,
)
{
eprintln!("Failed to register runtime stacktrace callback: {}", e);
}
} else if let Err(e) =
register_runtime_frame_callback(native_runtime_stack_frame_callback)
{
eprintln!("Failed to register runtime frame callback: {}", e);
}
}
match libdd_crashtracker::init(config, receiver_config, metadata) {
Expand Down Expand Up @@ -330,116 +325,3 @@ pub fn crashtracker_status() -> anyhow::Result<CrashtrackerStatus> {
pub fn crashtracker_receiver() -> anyhow::Result<()> {
libdd_crashtracker::receiver_entry_point_stdin()
}

const MAX_TRACEBACK_SIZE: usize = 8 * 1024; // 8KB

// Attempt to resolve _Py_DumpTracebackThreads at runtime
// Try to link once during registration
unsafe fn init_dump_traceback_fn() {
DUMP_TRACEBACK_INIT.call_once(|| {
#[cfg(unix)]
{
extern "C" {
fn dlsym(
handle: *mut std::ffi::c_void,
symbol: *const std::ffi::c_char,
) -> *mut std::ffi::c_void;
}

const RTLD_DEFAULT: *mut std::ffi::c_void = ptr::null_mut();

let symbol_ptr = dlsym(
RTLD_DEFAULT,
b"_Py_DumpTracebackThreads\0".as_ptr() as *const std::ffi::c_char,
);

if !symbol_ptr.is_null() {
DUMP_TRACEBACK_FN = Some(std::mem::transmute(symbol_ptr));
}
}

#[cfg(not(unix))]
{
// DUMP_TRACEBACK_FN remains None on non-Unix platforms
}
});
}

// Get the cached function pointer; should only be called after init_dump_traceback_fn
unsafe fn get_cached_dump_traceback_fn() -> Option<PyDumpTracebackThreadsFn> {
DUMP_TRACEBACK_FN
}

unsafe fn dump_python_traceback_as_string(
emit_stacktrace_string: unsafe extern "C" fn(*const c_char),
) {
// Use function linked during registration
let dump_fn = match get_cached_dump_traceback_fn() {
Some(func) => func,
None => {
emit_stacktrace_string(
"<python_runtime_stacktrace_unavailable>\0".as_ptr() as *const c_char
);
return;
}
};

// Create a pipe to capture CPython internal traceback dump. _Py_DumpTracebackThreads writes to
// a fd. Reading and writing to pipe is signal-safe. We stack allocate a buffer in the beginning,
// and use it to read the output
let mut pipefd: [c_int; 2] = [0, 0];
if pipe(&mut pipefd as *mut [c_int; 2]) != 0 {
emit_stacktrace_string("<pipe_creation_failed>\0".as_ptr() as *const c_char);
return;
}

let read_fd = pipefd[0];
let write_fd = pipefd[1];

fcntl(read_fd, libc::F_SETFL as c_int, libc::O_NONBLOCK as c_int);

// Use null thread state for signal-safety; CPython will dump all threads.
let error_msg = dump_fn(write_fd, ptr::null_mut(), ptr::null_mut());

close(write_fd);

if !error_msg.is_null() {
close(read_fd);
emit_stacktrace_string(error_msg as *const c_char);
return;
}

let mut buffer = [0u8; MAX_TRACEBACK_SIZE];
let bytes_read = read(
read_fd,
buffer.as_mut_ptr() as *mut c_void,
MAX_TRACEBACK_SIZE,
);

close(read_fd);

if bytes_read > 0 {
let bytes_read = bytes_read as usize;
if bytes_read < MAX_TRACEBACK_SIZE {
buffer[bytes_read] = 0;
} else {
// Buffer is full; add truncation indicator
let truncation_msg = b"\n[TRUNCATED]\0";
let msg_len = truncation_msg.len();
if MAX_TRACEBACK_SIZE >= msg_len {
let start_pos = MAX_TRACEBACK_SIZE - msg_len;
buffer[start_pos..].copy_from_slice(truncation_msg);
}
}
emit_stacktrace_string(buffer.as_ptr() as *const c_char);
return;
}

emit_stacktrace_string("<traceback_read_failed>\0".as_ptr() as *const c_char);
}

unsafe extern "C" fn native_runtime_stack_callback(
emit_stacktrace_string: unsafe extern "C" fn(*const c_char),
) {
dump_python_traceback_as_string(emit_stacktrace_string);
}
Loading
Loading