Skip to content

Commit 83545b1

Browse files
committed
try to make it a bit safer
1 parent a762885 commit 83545b1

File tree

4 files changed

+99
-164
lines changed

4 files changed

+99
-164
lines changed

experimental_debug.json

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,6 @@
22
"runtime_stack": {
33
"format": "Datadog Runtime Callback 1.0",
44
"frames": [
5-
{
6-
"function": "<module>",
7-
"file": "<string>",
8-
"line": 1
9-
},
105
{
116
"function": "string_at",
127
"file": "/home/bits/.pyenv/versions/3.13.5/lib/python3.13/ctypes/__init__.py",
@@ -95,4 +90,4 @@
9590
],
9691
"runtime_type": "unknown"
9792
}
98-
}
93+
}

src/native/crashtracker.rs

Lines changed: 95 additions & 156 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
use anyhow;
22
use std::collections::HashMap;
3-
use std::ffi::{c_void, CString};
4-
use pyo3::ffi;
5-
use std::sync::atomic::{AtomicU8, AtomicPtr, Ordering};
3+
use std::ffi::c_void;
4+
use std::sync::atomic::{AtomicU8, Ordering};
65
use std::sync::Once;
76
use std::time::Duration;
8-
use std::ptr;
97

108
use datadog_crashtracker::{
119
register_runtime_stack_callback, CallbackError, CrashtrackerConfiguration,
@@ -322,8 +320,6 @@ impl From<CallbackError> for CallbackResult {
322320
}
323321

324322

325-
// Global storage for the Python callback - using atomic pointer for signal safety
326-
static PYTHON_CALLBACK: AtomicPtr<PyObject> = AtomicPtr::new(ptr::null_mut());
327323

328324
/// Runtime-specific stack frame representation for FFI
329325
///
@@ -392,185 +388,128 @@ impl RuntimeStackFramePy {
392388
}
393389
}
394390

395-
// Thread-local storage for the current emit_frame function (signal context)
396-
thread_local! {
397-
static EMIT_FRAME_FN: std::cell::Cell<Option<unsafe extern "C" fn(*const RuntimeStackFrame)>> =
398-
const { std::cell::Cell::new(None) };
399-
}
391+
// Constants for signal-safe operation
392+
const MAX_FRAMES: usize = 64;
393+
const MAX_STRING_LEN: usize = 256;
400394

401-
// Helper function to emit a frame from Python during crash context
402-
#[pyfunction(name = "emit_python_frame")]
403-
pub fn emit_python_frame(frame: &RuntimeStackFramePy) -> PyResult<()> {
404-
eprintln!("DEBUG: emit_python_frame called with function: {:?}", frame.function_name);
405-
406-
EMIT_FRAME_FN.with(|emit_fn_cell| {
407-
if let Some(emit_frame_fn) = emit_fn_cell.get() {
408-
eprintln!("DEBUG: Found emit_frame_fn, creating C frame");
409-
410-
// Create CStrings that will live for the duration of this function call
411-
let function_c_str = frame.function_name.as_ref().and_then(|s| CString::new(s.as_str()).ok());
412-
let file_c_str = frame.file_name.as_ref().and_then(|s| CString::new(s.as_str()).ok());
413-
let class_c_str = frame.class_name.as_ref().and_then(|s| CString::new(s.as_str()).ok());
414-
let module_c_str = frame.module_name.as_ref().and_then(|s| CString::new(s.as_str()).ok());
415-
416-
let c_frame = RuntimeStackFrame {
417-
function_name: function_c_str.as_ref().map_or(std::ptr::null(), |s| s.as_ptr()),
418-
file_name: file_c_str.as_ref().map_or(std::ptr::null(), |s| s.as_ptr()),
419-
line_number: frame.line_number,
420-
column_number: frame.column_number,
421-
class_name: class_c_str.as_ref().map_or(std::ptr::null(), |s| s.as_ptr()),
422-
module_name: module_c_str.as_ref().map_or(std::ptr::null(), |s| s.as_ptr()),
423-
};
424-
425-
eprintln!("DEBUG: About to call emit_frame_fn");
426-
unsafe {
427-
emit_frame_fn(&c_frame);
428-
}
429-
eprintln!("DEBUG: emit_frame_fn called successfully");
430-
} else {
431-
eprintln!("DEBUG: emit_python_frame called but no emit function available");
432-
}
433-
});
434-
Ok(())
395+
// Stack-allocated buffer for signal-safe string handling
396+
struct StackBuffer {
397+
data: [u8; MAX_STRING_LEN],
398+
len: usize,
435399
}
436400

437-
// C callback function that bridges to Python
438-
unsafe extern "C" fn python_callback_bridge(
439-
emit_frame: unsafe extern "C" fn(*const RuntimeStackFrame),
440-
_context: *mut c_void,
441-
) {
442-
eprintln!("DEBUG: Python callback bridge invoked");
443-
444-
// Store the emit_frame function in thread-local storage for this callback execution
445-
EMIT_FRAME_FN.with(|emit_fn_cell| {
446-
emit_fn_cell.set(Some(emit_frame));
447-
});
448-
449-
let callback_ptr = PYTHON_CALLBACK.load(Ordering::SeqCst);
450-
if !callback_ptr.is_null() {
451-
eprintln!("DEBUG: Found Python callback, calling it");
452-
453-
// This is unsafe, but necessary for runtime callbacks
454-
Python::with_gil(|py| {
455-
let py_callback = &*callback_ptr;
456-
// Call the Python callback - it should call emit_python_frame for each frame
457-
if let Err(_e) = py_callback.call0(py) {
458-
eprintln!("DEBUG: Error calling Python callback");
459-
} else {
460-
eprintln!("DEBUG: Python callback executed successfully");
461-
}
462-
});
463-
} else {
464-
eprintln!("DEBUG: No Python callback found");
401+
impl StackBuffer {
402+
const fn new() -> Self {
403+
Self {
404+
data: [0u8; MAX_STRING_LEN],
405+
len: 0,
406+
}
465407
}
466408

467-
// Clear the emit_frame function after callback execution
468-
EMIT_FRAME_FN.with(|emit_fn_cell| {
469-
emit_fn_cell.set(None);
470-
});
471-
}
409+
fn as_ptr(&self) -> *const i8 {
410+
self.data.as_ptr() as *const i8
411+
}
472412

473-
/// Register a runtime stack collection callback
474-
///
475-
/// This function allows language runtimes to register a callback that will be invoked
476-
/// during crash handling to collect runtime-specific stack traces.
477-
///
478-
/// # Arguments
479-
/// - `callback`: The Python callback function to invoke during crashes
480-
///
481-
/// # Returns
482-
/// - `CallbackResult::Ok` if registration succeeds
483-
/// - `CallbackResult::AlreadyRegistered` if a callback is already registered
484-
/// - `CallbackResult::NullCallback` if the callback function is null
485-
///
486-
/// # Safety
487-
/// - The callback must be signal-safe
488-
/// - Only one callback can be registered at a time
489-
#[pyfunction(name = "crashtracker_register_runtime_callback")]
490-
pub fn crashtracker_register_runtime_callback(
491-
py: Python,
492-
callback: PyObject,
493-
) -> CallbackResult {
494-
// Create a boxed PyObject on the heap so it has a stable address
495-
let callback_box = Box::new(callback.clone_ref(py));
496-
let callback_ptr = Box::into_raw(callback_box);
497-
498-
// Try to store the callback pointer atomically
499-
let previous = PYTHON_CALLBACK.compare_exchange(
500-
std::ptr::null_mut(),
501-
callback_ptr,
502-
Ordering::SeqCst,
503-
Ordering::SeqCst,
504-
);
505-
506-
match previous {
507-
Ok(_) => {
508-
match register_runtime_stack_callback(python_callback_bridge, std::ptr::null_mut()) {
509-
Ok(()) => CallbackResult::Ok,
510-
Err(e) => e.into(),
511-
}
512-
}
513-
Err(_) => {
514-
let _ = unsafe { Box::from_raw(callback_ptr) };
515-
CallbackResult::AlreadyRegistered
516-
}
413+
fn set_from_str(&mut self, s: &str) {
414+
let bytes = s.as_bytes();
415+
let copy_len = bytes.len().min(MAX_STRING_LEN - 1);
416+
self.data[..copy_len].copy_from_slice(&bytes[..copy_len]);
417+
self.data[copy_len] = 0; // null terminator
418+
self.len = copy_len;
517419
}
420+
518421
}
519422

520-
// Native C callback function that directly emits runtime stack frames
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
521426
unsafe extern "C" fn native_runtime_stack_callback(
522427
emit_frame: unsafe extern "C" fn(*const RuntimeStackFrame),
523428
_context: *mut c_void,
524429
) {
430+
// For maximum safety, we'll fall back to the Python with_gil approach
431+
// but with improved buffer management and bounds checking
525432
Python::with_gil(|py| {
526-
let frame = py.eval(ffi::c_str!("__import__('sys')._getframe()"), None, None);
527-
if let Ok(frame_obj) = frame {
528-
emit_python_stack_from_frame(emit_frame, py, frame_obj);
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+
}
529440
}
530441
});
531442
}
532443

533-
fn emit_python_stack_from_frame(
444+
// Stack walking with bounds checking and stack-allocated buffers
445+
fn emit_python_stack_with_bounds(
534446
emit_frame: unsafe extern "C" fn(*const RuntimeStackFrame),
535447
py: Python,
536448
frame_obj: Bound<'_, PyAny>,
449+
depth: usize,
537450
) {
538-
let code = frame_obj.getattr("f_code");
539-
let back = frame_obj.getattr("f_back");
540-
541-
if let (Ok(code_obj), Ok(back_obj)) = (code, back) {
542-
let function_name = code_obj.getattr("co_name")
543-
.and_then(|n| n.extract::<String>())
544-
.unwrap_or_else(|_| "<unknown>".to_string());
545-
546-
let file_name = code_obj.getattr("co_filename")
547-
.and_then(|f| f.extract::<String>())
548-
.unwrap_or_else(|_| "<unknown>".to_string());
549-
550-
let line_number = frame_obj.getattr("f_lineno")
551-
.and_then(|l| l.extract::<u32>())
552-
.unwrap_or(0);
451+
if depth >= MAX_FRAMES {
452+
return; // Prevent stack overflow
453+
}
553454

554-
let function_c_str = CString::new(function_name).unwrap_or_else(|_| CString::new("<invalid>").unwrap());
555-
let file_c_str = CString::new(file_name).unwrap_or_else(|_| CString::new("<invalid>").unwrap());
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
461+
let mut function_buf = StackBuffer::new();
462+
let mut file_buf = StackBuffer::new();
463+
464+
function_buf.set_from_str(&function_name);
465+
file_buf.set_from_str(&file_name);
466+
467+
let c_frame = RuntimeStackFrame {
468+
function_name: function_buf.as_ptr(),
469+
file_name: file_buf.as_ptr(),
470+
line_number,
471+
column_number: 0,
472+
class_name: std::ptr::null(),
473+
module_name: std::ptr::null(),
474+
};
475+
476+
unsafe {
477+
emit_frame(&c_frame);
478+
}
556479

557-
let c_frame = RuntimeStackFrame {
558-
function_name: function_c_str.as_ptr(),
559-
file_name: file_c_str.as_ptr(),
560-
line_number,
561-
column_number: 0,
562-
class_name: std::ptr::null(),
563-
module_name: std::ptr::null(),
564-
};
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+
}
485+
}
486+
}
565487

566-
unsafe {
567-
emit_frame(&c_frame);
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+
}
568500
}
501+
}
502+
"<unknown>".to_string()
503+
}
569504

570-
if !back_obj.is_none() {
571-
emit_python_stack_from_frame(emit_frame, py, back_obj);
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;
572510
}
573511
}
512+
0
574513
}
575514

576515
/// Register the native runtime stack collection callback

src/native/lib.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,7 @@ fn _native(m: &Bound<'_, PyModule>) -> PyResult<()> {
3131
m.add_function(wrap_pyfunction!(crashtracker::crashtracker_on_fork, m)?)?;
3232
m.add_function(wrap_pyfunction!(crashtracker::crashtracker_status, m)?)?;
3333
m.add_function(wrap_pyfunction!(crashtracker::crashtracker_receiver, m)?)?;
34-
m.add_function(wrap_pyfunction!(crashtracker::crashtracker_register_runtime_callback, m)?)?;
3534
m.add_function(wrap_pyfunction!(crashtracker::crashtracker_register_native_runtime_callback, m)?)?;
36-
m.add_function(wrap_pyfunction!(crashtracker::emit_python_frame, m)?)?;
3735
}
3836
m.add_class::<library_config::PyTracerMetadata>()?;
3937
m.add_class::<library_config::PyAnonymousFileHandle>()?;

tests/internal/crashtracker/test_crashtracker.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -673,6 +673,9 @@ def func14():
673673
return func15()
674674

675675
def func15():
676+
return func16()
677+
678+
def func16():
676679
ctypes.string_at(0)
677680
sys.exit(-1)
678681

0 commit comments

Comments
 (0)