Skip to content

Commit c8b8b93

Browse files
committed
Using cpython api - still not safe
1 parent 83545b1 commit c8b8b93

File tree

2 files changed

+144
-158
lines changed

2 files changed

+144
-158
lines changed

experimental_debug.json

Lines changed: 0 additions & 93 deletions
This file was deleted.

src/native/crashtracker.rs

Lines changed: 144 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
use anyhow;
22
use std::collections::HashMap;
3-
use std::ffi::c_void;
3+
use std::ffi::{c_void, c_char, c_int, c_long, CStr};
44
use std::sync::atomic::{AtomicU8, Ordering};
55
use std::sync::Once;
66
use std::time::Duration;
7+
use std::ptr;
78

89
use datadog_crashtracker::{
910
register_runtime_stack_callback, CallbackError, CrashtrackerConfiguration,
@@ -12,6 +13,50 @@ use datadog_crashtracker::{
1213
use ddcommon::Endpoint;
1314
use pyo3::prelude::*;
1415

16+
// Python C API structures for direct stack walking
17+
// These are opaque pointers to avoid ABI dependencies
18+
#[repr(C)]
19+
struct PyObject {
20+
_private: [u8; 0],
21+
}
22+
23+
#[repr(C)]
24+
struct PyCodeObject {
25+
_private: [u8; 0],
26+
}
27+
28+
#[repr(C)]
29+
struct PyFrameObject {
30+
_private: [u8; 0],
31+
}
32+
33+
#[repr(C)]
34+
struct PyThreadState {
35+
_private: [u8; 0],
36+
}
37+
38+
#[repr(C)]
39+
struct PyInterpreterState {
40+
_private: [u8; 0],
41+
}
42+
43+
// External C API functions we'll link to
44+
// Using more stable public API functions that are available across Python versions
45+
extern "C" {
46+
// Minimal set of stable Python C API functions available across versions
47+
48+
// Frame access - most stable approach
49+
fn PyEval_GetFrame() -> *mut PyFrameObject;
50+
fn PyFrame_GetBack(frame: *mut PyFrameObject) -> *mut PyFrameObject;
51+
fn PyFrame_GetCode(frame: *mut PyFrameObject) -> *mut PyCodeObject;
52+
fn PyFrame_GetLineNumber(frame: *mut PyFrameObject) -> c_int;
53+
54+
// Object attribute access - very stable API
55+
fn PyObject_GetAttrString(obj: *mut PyObject, attr_name: *const c_char) -> *mut PyObject;
56+
fn PyUnicode_AsUTF8(unicode: *mut PyObject) -> *const c_char;
57+
58+
}
59+
1560
pub trait RustWrapper {
1661
type Inner;
1762
const INNER_TYPE_NAME: &'static str;
@@ -418,100 +463,134 @@ impl StackBuffer {
418463
self.len = copy_len;
419464
}
420465

466+
fn set_from_cstr(&mut self, cstr_ptr: *const c_char) {
467+
if cstr_ptr.is_null() {
468+
self.set_from_str("<null>");
469+
return;
470+
}
471+
472+
unsafe {
473+
let cstr = match CStr::from_ptr(cstr_ptr).to_str() {
474+
Ok(s) => s,
475+
Err(_) => "<invalid_utf8>",
476+
};
477+
self.set_from_str(cstr);
478+
}
479+
}
421480
}
422481

423-
// Improved signal-safer Python frame walker
424-
// This approach reuses the existing Python high-level API but with better bounds checking
425-
// and stack-allocated buffers to minimize allocations
426-
unsafe extern "C" fn native_runtime_stack_callback(
427-
emit_frame: unsafe extern "C" fn(*const RuntimeStackFrame),
428-
_context: *mut c_void,
429-
) {
430-
// For maximum safety, we'll fall back to the Python with_gil approach
431-
// but with improved buffer management and bounds checking
432-
Python::with_gil(|py| {
433-
// Try to get the current frame using Python's sys._getframe()
434-
if let Ok(sys_module) = py.import("sys") {
435-
if let Ok(getframe_fn) = sys_module.getattr("_getframe") {
436-
if let Ok(frame_obj) = getframe_fn.call0() {
437-
emit_python_stack_with_bounds(emit_frame, py, frame_obj, 0);
438-
}
439-
}
482+
483+
// Static string constants for attribute names (must be null-terminated)
484+
static CO_NAME: &[u8] = b"co_name\0";
485+
static CO_FILENAME: &[u8] = b"co_filename\0";
486+
487+
unsafe fn extract_frame_info(
488+
frame: *mut PyFrameObject,
489+
function_buf: &mut StackBuffer,
490+
file_buf: &mut StackBuffer,
491+
) -> u32 {
492+
if frame.is_null() {
493+
function_buf.set_from_str("<null_frame>");
494+
file_buf.set_from_str("<null_frame>");
495+
return 0;
496+
}
497+
498+
// Get code object
499+
let code = PyFrame_GetCode(frame);
500+
if code.is_null() {
501+
function_buf.set_from_str("<no_code>");
502+
file_buf.set_from_str("<no_code>");
503+
return 0;
504+
}
505+
506+
// Extract function name
507+
let name_obj = PyObject_GetAttrString(code as *mut PyObject, CO_NAME.as_ptr() as *const c_char);
508+
if !name_obj.is_null() {
509+
let name_cstr = PyUnicode_AsUTF8(name_obj);
510+
if !name_cstr.is_null() {
511+
function_buf.set_from_cstr(name_cstr);
512+
} else {
513+
function_buf.set_from_str("<invalid_name>");
440514
}
441-
});
515+
} else {
516+
function_buf.set_from_str("<unknown_function>");
517+
}
518+
519+
// Extract filename
520+
let filename_obj = PyObject_GetAttrString(code as *mut PyObject, CO_FILENAME.as_ptr() as *const c_char);
521+
if !filename_obj.is_null() {
522+
let filename_cstr = PyUnicode_AsUTF8(filename_obj);
523+
if !filename_cstr.is_null() {
524+
file_buf.set_from_cstr(filename_cstr);
525+
} else {
526+
file_buf.set_from_str("<invalid_filename>");
527+
}
528+
} else {
529+
file_buf.set_from_str("<unknown_file>");
530+
}
531+
532+
// Get line number
533+
let line_num = PyFrame_GetLineNumber(frame);
534+
if line_num > 0 {
535+
line_num as u32
536+
} else {
537+
0
538+
}
442539
}
443540

444-
// Stack walking with bounds checking and stack-allocated buffers
445-
fn emit_python_stack_with_bounds(
541+
unsafe fn walk_frame_chain(
446542
emit_frame: unsafe extern "C" fn(*const RuntimeStackFrame),
447-
py: Python,
448-
frame_obj: Bound<'_, PyAny>,
543+
mut frame: *mut PyFrameObject,
449544
depth: usize,
450545
) {
451-
if depth >= MAX_FRAMES {
452-
return; // Prevent stack overflow
546+
if depth >= MAX_FRAMES || frame.is_null() {
547+
return;
453548
}
454549

455-
// Get frame information with error handling
456-
let function_name = get_frame_attr_string(py, &frame_obj, "f_code", "co_name");
457-
let file_name = get_frame_attr_string(py, &frame_obj, "f_code", "co_filename");
458-
let line_number = get_frame_line_number(py, &frame_obj);
459-
460-
// Use stack-allocated buffers
550+
// Stack-allocate buffers for this frame
461551
let mut function_buf = StackBuffer::new();
462552
let mut file_buf = StackBuffer::new();
463553

464-
function_buf.set_from_str(&function_name);
465-
file_buf.set_from_str(&file_name);
554+
let line_number = extract_frame_info(frame, &mut function_buf, &mut file_buf);
466555

467556
let c_frame = RuntimeStackFrame {
468557
function_name: function_buf.as_ptr(),
469558
file_name: file_buf.as_ptr(),
470559
line_number,
471560
column_number: 0,
472-
class_name: std::ptr::null(),
473-
module_name: std::ptr::null(),
561+
class_name: ptr::null(),
562+
module_name: ptr::null(),
474563
};
475564

476-
unsafe {
477-
emit_frame(&c_frame);
478-
}
565+
emit_frame(&c_frame);
479566

480-
// Get the back frame and recurse with depth limit
481-
if let Ok(back_frame) = frame_obj.getattr("f_back") {
482-
if !back_frame.is_none() {
483-
emit_python_stack_with_bounds(emit_frame, py, back_frame, depth + 1);
484-
}
567+
// Get the previous frame in the chain
568+
let back_frame = PyFrame_GetBack(frame);
569+
if !back_frame.is_null() {
570+
walk_frame_chain(emit_frame, back_frame, depth + 1);
485571
}
486572
}
487573

488-
// Safe attribute extraction with fallback
489-
fn get_frame_attr_string(_py: Python, frame: &Bound<'_, PyAny>, attr1: &str, attr2: &str) -> String {
490-
if let Ok(code) = frame.getattr(attr1) {
491-
if let Ok(name) = code.getattr(attr2) {
492-
if let Ok(name_str) = name.extract::<String>() {
493-
// Truncate if too long for our buffer
494-
if name_str.len() < MAX_STRING_LEN {
495-
return name_str;
496-
} else {
497-
return name_str.chars().take(MAX_STRING_LEN - 4).collect::<String>() + "...";
498-
}
499-
}
500-
}
574+
unsafe fn walk_current_thread_stack(
575+
emit_frame: unsafe extern "C" fn(*const RuntimeStackFrame),
576+
) {
577+
let frame = PyEval_GetFrame();
578+
if !frame.is_null() {
579+
walk_frame_chain(emit_frame, frame, 0);
501580
}
502-
"<unknown>".to_string()
503581
}
504582

505-
// Safe line number extraction
506-
fn get_frame_line_number(_py: Python, frame: &Bound<'_, PyAny>) -> u32 {
507-
if let Ok(lineno) = frame.getattr("f_lineno") {
508-
if let Ok(line_num) = lineno.extract::<u32>() {
509-
return line_num;
510-
}
511-
}
512-
0
583+
// Native signal-safe Python frame walker
584+
// This implementation uses direct Python C API calls
585+
unsafe extern "C" fn native_runtime_stack_callback(
586+
emit_frame: unsafe extern "C" fn(*const RuntimeStackFrame),
587+
_context: *mut c_void,
588+
) {
589+
// Walk the current thread's stack
590+
walk_current_thread_stack(emit_frame);
513591
}
514592

593+
515594
/// Register the native runtime stack collection callback
516595
///
517596
/// This function registers a native callback that directly collects Python runtime

0 commit comments

Comments
 (0)