Skip to content

Commit aa11ce8

Browse files
committed
Safety comments
1 parent 00f1122 commit aa11ce8

File tree

2 files changed

+88
-6
lines changed

2 files changed

+88
-6
lines changed

datadog-crashtracker-ffi/src/runtime_callback.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,11 @@ mod tests {
158158
use datadog_crashtracker::RuntimeStackFrame;
159159
use std::ffi::CString;
160160
use std::ptr;
161+
use std::sync::Mutex;
162+
163+
// Use a mutex to ensure tests run sequentially to avoid race conditions
164+
// with the global static variable
165+
static TEST_MUTEX: Mutex<()> = Mutex::new(());
161166

162167
unsafe extern "C" fn test_runtime_callback(
163168
emit_frame: unsafe extern "C" fn(*const RuntimeStackFrame),
@@ -183,6 +188,7 @@ mod tests {
183188

184189
#[test]
185190
fn test_ffi_callback_registration() {
191+
let _guard = TEST_MUTEX.lock().unwrap();
186192
unsafe {
187193
// Ensure clean state at start
188194
clear_runtime_callback();
@@ -227,6 +233,7 @@ mod tests {
227233

228234
#[test]
229235
fn test_enum_based_registration() {
236+
let _guard = TEST_MUTEX.lock().unwrap();
230237
unsafe {
231238
clear_runtime_callback();
232239

datadog-crashtracker/src/runtime_callback.rs

Lines changed: 81 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,6 @@ impl std::str::FromStr for CallbackType {
111111
/// Global storage for the runtime callback
112112
///
113113
/// Uses atomic pointer to ensure safe access from signal handlers
114-
/// Stores enums directly - much simpler and signal-safe
115114
static RUNTIME_CALLBACK: AtomicPtr<(RuntimeStackCallback, RuntimeType, CallbackType)> =
116115
AtomicPtr::new(ptr::null_mut());
117116

@@ -146,8 +145,14 @@ pub struct RuntimeStackFrame {
146145
/// - Only async-signal-safe functions
147146
///
148147
/// # Parameters
149-
/// - `emit_frame`: Function to call for each runtime frame
150-
/// - `emit_stacktrace_string`: Function to call for complete stacktrace string
148+
/// - `emit_frame`: Function to call for each runtime frame (marked unsafe because it takes raw pointers)
149+
/// - `emit_stacktrace_string`: Function to call for complete stacktrace string (marked unsafe because it takes raw C string pointers)
150+
///
151+
/// # Safety
152+
/// The callback function is marked unsafe because:
153+
/// - It receives function pointers that take raw pointers as parameters
154+
/// - The callback must ensure any pointers it passes to these functions are valid
155+
/// - All C strings passed must be null-terminated and remain valid for the call duration
151156
pub type RuntimeStackCallback = unsafe extern "C" fn(
152157
emit_frame: unsafe extern "C" fn(*const RuntimeStackFrame),
153158
emit_stacktrace_string: unsafe extern "C" fn(*const c_char),
@@ -213,6 +218,8 @@ pub fn register_runtime_stack_callback(
213218
let previous = RUNTIME_CALLBACK.swap(callback_data, Ordering::SeqCst);
214219

215220
if !previous.is_null() {
221+
// Safety: previous was returned by Box::into_raw() above or in a previous call,
222+
// so it's guaranteed to be a valid Box pointer. We reconstruct the Box to drop it.
216223
let _ = unsafe { Box::from_raw(previous) };
217224
}
218225

@@ -232,12 +239,19 @@ pub fn is_runtime_callback_registered() -> bool {
232239
///
233240
/// # Safety
234241
/// This function loads from an atomic pointer and dereferences it.
242+
/// The caller must ensure that no other thread is calling `clear_runtime_callback`
243+
/// or `register_runtime_stack_callback` concurrently, as those could invalidate
244+
/// the pointer between the null check and dereferencing.
235245
pub unsafe fn get_registered_runtime_type_enum() -> Option<RuntimeType> {
236246
let callback_ptr = RUNTIME_CALLBACK.load(Ordering::SeqCst);
237247
if callback_ptr.is_null() {
238248
return None;
239249
}
240250

251+
// Safety: callback_ptr was checked to be non-null above, and was created by
252+
// Box::into_raw() in register_runtime_stack_callback(), so it's a valid pointer
253+
// to a properly aligned, initialized tuple. The atomic load with SeqCst ordering
254+
// ensures we see the pointer after it was stored.
241255
let (_, runtime_type, _) = &*callback_ptr;
242256
Some(*runtime_type)
243257
}
@@ -248,12 +262,19 @@ pub unsafe fn get_registered_runtime_type_enum() -> Option<RuntimeType> {
248262
///
249263
/// # Safety
250264
/// This function loads from an atomic pointer and dereferences it.
265+
/// The caller must ensure that no other thread is calling `clear_runtime_callback`
266+
/// or `register_runtime_stack_callback` concurrently, as those could invalidate
267+
/// the pointer between the null check and dereferencing.
251268
pub unsafe fn get_registered_callback_type_enum() -> Option<CallbackType> {
252269
let callback_ptr = RUNTIME_CALLBACK.load(Ordering::SeqCst);
253270
if callback_ptr.is_null() {
254271
return None;
255272
}
256273

274+
// Safety: callback_ptr was checked to be non-null above, and was created by
275+
// Box::into_raw() in register_runtime_stack_callback(), so it's a valid pointer
276+
// to a properly aligned, initialized tuple. The atomic load with SeqCst ordering
277+
// ensures we see the pointer after it was stored.
257278
let (_, _, callback_type) = &*callback_ptr;
258279
Some(*callback_type)
259280
}
@@ -262,12 +283,19 @@ pub unsafe fn get_registered_callback_type_enum() -> Option<CallbackType> {
262283
///
263284
/// # Safety
264285
/// This function loads from an atomic pointer and dereferences it.
286+
/// The caller must ensure that no other thread is calling `clear_runtime_callback`
287+
/// or `register_runtime_stack_callback` concurrently, as those could invalidate
288+
/// the pointer between the null check and dereferencing.
265289
pub unsafe fn get_registered_runtime_type_ptr() -> *const std::ffi::c_char {
266290
let callback_ptr = RUNTIME_CALLBACK.load(Ordering::SeqCst);
267291
if callback_ptr.is_null() {
268292
return std::ptr::null();
269293
}
270294

295+
// Safety: callback_ptr was checked to be non-null above, and was created by
296+
// Box::into_raw() in register_runtime_stack_callback(), so it's a valid pointer
297+
// to a properly aligned, initialized tuple. The returned C string pointer
298+
// points to static string literals, so it's always valid.
271299
let (_, runtime_type, _) = &*callback_ptr;
272300
runtime_type.as_cstr().as_ptr()
273301
}
@@ -276,12 +304,19 @@ pub unsafe fn get_registered_runtime_type_ptr() -> *const std::ffi::c_char {
276304
///
277305
/// # Safety
278306
/// This function loads from an atomic pointer and dereferences it.
307+
/// The caller must ensure that no other thread is calling `clear_runtime_callback`
308+
/// or `register_runtime_stack_callback` concurrently, as those could invalidate
309+
/// the pointer between the null check and dereferencing.
279310
pub unsafe fn get_registered_callback_type_ptr() -> *const std::ffi::c_char {
280311
let callback_ptr = RUNTIME_CALLBACK.load(Ordering::SeqCst);
281312
if callback_ptr.is_null() {
282313
return std::ptr::null();
283314
}
284315

316+
// Safety: callback_ptr was checked to be non-null above, and was created by
317+
// Box::into_raw() in register_runtime_stack_callback(), so it's a valid pointer
318+
// to a properly aligned, initialized tuple. The returned C string pointer
319+
// points to static string literals, so it's always valid.
285320
let (_, _, callback_type) = &*callback_ptr;
286321
callback_type.as_cstr().as_ptr()
287322
}
@@ -294,11 +329,15 @@ pub unsafe fn get_registered_callback_type_ptr() -> *const std::ffi::c_char {
294329
///
295330
/// # Safety
296331
/// This function should only be called when it's safe to clear the callback,
297-
/// such as during testing or application shutdown.
332+
/// such as during testing or application shutdown. The caller must ensure:
333+
/// - No other thread is concurrently calling functions that dereference the callback pointer
334+
/// - No signal handlers are currently executing that might invoke the callback
335+
/// - The callback is not being used in any other way
298336
pub unsafe fn clear_runtime_callback() {
299337
let old_ptr = RUNTIME_CALLBACK.swap(std::ptr::null_mut(), Ordering::SeqCst);
300338
if !old_ptr.is_null() {
301-
// Drop the tuple to clean up
339+
// Safety: old_ptr was created by Box::into_raw() in register_runtime_stack_callback(),
340+
// so it's a valid Box pointer. We reconstruct the Box to properly drop the tuple.
302341
let _ = Box::from_raw(old_ptr);
303342
}
304343
}
@@ -310,7 +349,10 @@ pub unsafe fn clear_runtime_callback() {
310349
///
311350
/// # Safety
312351
/// This function is intended to be called from signal handlers and must maintain
313-
/// signal safety. It does not perform any dynamic allocation.
352+
/// signal safety. It does not perform any dynamic allocation. The caller must ensure:
353+
/// - No other thread is calling `clear_runtime_callback` concurrently
354+
/// - The registered callback function is signal-safe
355+
/// - The writer parameter remains valid for the duration of the call
314356
#[cfg(unix)]
315357
pub(crate) unsafe fn invoke_runtime_callback_with_writer<W: std::io::Write>(
316358
writer: &mut W,
@@ -320,16 +362,25 @@ pub(crate) unsafe fn invoke_runtime_callback_with_writer<W: std::io::Write>(
320362
return Err(std::io::Error::other("No runtime callback registered"));
321363
}
322364

365+
// Safety: callback_ptr was checked to be non-null above, and was created by
366+
// Box::into_raw() in register_runtime_stack_callback(), so it's a valid pointer
367+
// to a properly aligned, initialized tuple.
323368
let (callback_fn, _, _) = &*callback_ptr;
324369

325370
// Reset frame counter
326371
FRAME_COUNT.with(|count| count.set(0));
327372

328373
// Define the emit_frame function that writes directly to the pipe
374+
// Safety: This function is called by the user's runtime callback. It's marked unsafe
375+
// because it dereferences raw pointers, but the safety is guaranteed by the contract
376+
// with the runtime callback system.
329377
unsafe extern "C" fn emit_frame_collector(frame: *const RuntimeStackFrame) {
330378
// We need access to the writer, so we'll use thread-local storage for it
331379
FRAME_WRITER.with(|writer_cell| {
332380
if let Some(writer_ptr) = *writer_cell.borrow() {
381+
// Safety: writer_ptr was stored in thread-local storage before
382+
// calling the user callback, so it's guaranteed to be valid and point
383+
// to the same writer object we passed in.
333384
let writer = &mut *writer_ptr;
334385

335386
FRAME_COUNT.with(|count| {
@@ -341,6 +392,7 @@ pub(crate) unsafe fn invoke_runtime_callback_with_writer<W: std::io::Write>(
341392
}
342393

343394
// Write the frame as JSON
395+
// Safety: frame pointer is passed from the runtime callback, writer is valid
344396
let _ = emit_frame_as_json(writer, frame);
345397
let _ = writer.flush();
346398

@@ -350,16 +402,22 @@ pub(crate) unsafe fn invoke_runtime_callback_with_writer<W: std::io::Write>(
350402
});
351403
}
352404

405+
// Safety: This function is called by the user's runtime callback with a C string pointer.
406+
// The safety requirements are documented in the RuntimeStackCallback type.
353407
unsafe extern "C" fn emit_stacktrace_string_collector(stacktrace_string: *const c_char) {
354408
if stacktrace_string.is_null() {
355409
return;
356410
}
357411

412+
// Safety: stacktrace_string is guaranteed by the runtime callback contract to be
413+
// a valid, null-terminated C string that remains valid for the duration of this call.
358414
let cstr = std::ffi::CStr::from_ptr(stacktrace_string);
359415
let bytes = cstr.to_bytes();
360416

361417
FRAME_WRITER.with(|writer_cell| {
362418
if let Some(writer_ptr) = *writer_cell.borrow() {
419+
// Safety: writer_ptr was stored in thread-local storage just before
420+
// calling the user callback, so it's guaranteed to be valid.
363421
let writer = &mut *writer_ptr;
364422
let _ = writer.write_all(bytes);
365423
let _ = writer.flush();
@@ -373,6 +431,8 @@ pub(crate) unsafe fn invoke_runtime_callback_with_writer<W: std::io::Write>(
373431
});
374432

375433
// Invoke the user callback
434+
// Safety: callback_fn was verified to be non-null during registration, and the
435+
// emit functions we're passing are compatible with the expected function signatures.
376436
callback_fn(emit_frame_collector, emit_stacktrace_string_collector);
377437

378438
// Clear the writer reference
@@ -387,6 +447,11 @@ pub(crate) unsafe fn invoke_runtime_callback_with_writer<W: std::io::Write>(
387447
///
388448
/// This function writes a RuntimeStackFrame directly as JSON without intermediate allocation.
389449
/// It must be signal-safe.
450+
///
451+
/// # Safety
452+
/// The caller must ensure that `frame` is either null or points to a valid, properly
453+
/// initialized RuntimeStackFrame. All C string pointers within the frame must be either
454+
/// null or point to valid, null-terminated C strings.
390455
#[cfg(unix)]
391456
unsafe fn emit_frame_as_json(
392457
writer: &mut dyn std::io::Write,
@@ -396,12 +461,16 @@ unsafe fn emit_frame_as_json(
396461
return Ok(());
397462
}
398463

464+
// Safety: frame was checked to be non-null above. The caller guarantees it points
465+
// to a valid RuntimeStackFrame.
399466
let frame_ref = &*frame;
400467
write!(writer, "{{")?;
401468
let mut first = true;
402469

403470
// Convert C strings to Rust strings and write JSON fields
404471
if !frame_ref.function_name.is_null() {
472+
// Safety: frame_ref.function_name was checked to be non-null. The caller
473+
// guarantees it points to a valid, null-terminated C string.
405474
let c_str = std::ffi::CStr::from_ptr(frame_ref.function_name);
406475
if let Ok(s) = c_str.to_str() {
407476
if !s.is_empty() {
@@ -412,6 +481,8 @@ unsafe fn emit_frame_as_json(
412481
}
413482

414483
if !frame_ref.file_name.is_null() {
484+
// Safety: frame_ref.file_name was checked to be non-null. The caller
485+
// guarantees it points to a valid, null-terminated C string.
415486
let c_str = std::ffi::CStr::from_ptr(frame_ref.file_name);
416487
if let Ok(s) = c_str.to_str() {
417488
if !s.is_empty() {
@@ -441,6 +512,8 @@ unsafe fn emit_frame_as_json(
441512
}
442513

443514
if !frame_ref.class_name.is_null() {
515+
// Safety: frame_ref.class_name was checked to be non-null. The caller
516+
// guarantees it points to a valid, null-terminated C string.
444517
let c_str = std::ffi::CStr::from_ptr(frame_ref.class_name);
445518
if let Ok(s) = c_str.to_str() {
446519
if !s.is_empty() {
@@ -454,6 +527,8 @@ unsafe fn emit_frame_as_json(
454527
}
455528

456529
if !frame_ref.module_name.is_null() {
530+
// Safety: frame_ref.module_name was checked to be non-null. The caller
531+
// guarantees it points to a valid, null-terminated C string.
457532
let c_str = std::ffi::CStr::from_ptr(frame_ref.module_name);
458533
if let Ok(s) = c_str.to_str() {
459534
if !s.is_empty() {

0 commit comments

Comments
 (0)