diff --git a/bin_tests/src/modes/behavior.rs b/bin_tests/src/modes/behavior.rs index efc7678923..db707dedb6 100644 --- a/bin_tests/src/modes/behavior.rs +++ b/bin_tests/src/modes/behavior.rs @@ -129,6 +129,8 @@ pub fn get_behavior(mode_str: &str) -> Box { "chained" => Box::new(test_007_chaining::Test), "fork" => Box::new(test_008_fork::Test), "prechain_abort" => Box::new(test_009_prechain_with_abort::Test), + "runtime_callback_frame" => Box::new(test_010_runtime_callback_frame::Test), + "runtime_callback_string" => Box::new(test_011_runtime_callback_string::Test), _ => panic!("Unknown mode: {mode_str}"), } } diff --git a/bin_tests/src/modes/unix/mod.rs b/bin_tests/src/modes/unix/mod.rs index 86f21a8519..876beb581e 100644 --- a/bin_tests/src/modes/unix/mod.rs +++ b/bin_tests/src/modes/unix/mod.rs @@ -10,3 +10,5 @@ pub mod test_006_sigchld_sigstack; pub mod test_007_chaining; pub mod test_008_fork; pub mod test_009_prechain_with_abort; +pub mod test_010_runtime_callback_frame; +pub mod test_011_runtime_callback_string; diff --git a/bin_tests/src/modes/unix/test_010_runtime_callback_frame.rs b/bin_tests/src/modes/unix/test_010_runtime_callback_frame.rs new file mode 100644 index 0000000000..00a63d695d --- /dev/null +++ b/bin_tests/src/modes/unix/test_010_runtime_callback_frame.rs @@ -0,0 +1,120 @@ +// Copyright 2024-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 +// +// This test validates the runtime stack collection callback mechanism using frame-by-frame mode. +// It registers a test callback that provides mock runtime stack frames, +// then crashes and verifies that the runtime frames appear in the crash report. +// +// This test uses CallbackType::Frame to emit structured runtime stack data. + +use crate::modes::behavior::Behavior; +use datadog_crashtracker::{ + clear_runtime_callback, register_runtime_stack_callback, CallbackType, + CrashtrackerConfiguration, RuntimeStackFrame, +}; +use std::ffi::c_char; +use std::path::Path; + +pub struct Test; + +impl Behavior for Test { + fn setup( + &self, + _output_dir: &Path, + _config: &mut CrashtrackerConfiguration, + ) -> anyhow::Result<()> { + Ok(()) + } + + fn pre(&self, _output_dir: &Path) -> anyhow::Result<()> { + // Ensure clean state + unsafe { + clear_runtime_callback(); + } + register_runtime_stack_callback(test_runtime_callback_frame, CallbackType::Frame) + .map_err(|e| anyhow::anyhow!("Failed to register runtime callback: {:?}", e))?; + Ok(()) + } + + fn post(&self, _output_dir: &Path) -> anyhow::Result<()> { + Ok(()) + } +} + +// Signal-safe test callback that emits mock runtime stack frames +unsafe extern "C" fn test_runtime_callback_frame( + emit_frame: unsafe extern "C" fn(*const RuntimeStackFrame), + _emit_stacktrace_string: unsafe extern "C" fn(*const c_char), +) { + // Use static null-terminated strings to avoid allocation in signal context + // In a real runtime, these would come from the runtime's managed string pool + // Using fully qualified function names that include module/class hierarchy + static FUNCTION_NAME_1: &[u8] = b"test_module.TestClass.runtime_function_1\0"; + static FUNCTION_NAME_2: &[u8] = b"my_package.submodule.MyModule.runtime_function_2\0"; + static FUNCTION_NAME_3: &[u8] = b"__main__.runtime_main\0"; + static FILE_NAME_1: &[u8] = b"script.py\0"; + static FILE_NAME_2: &[u8] = b"module.py\0"; + static FILE_NAME_3: &[u8] = b"main.py\0"; + + // Frame 1: test_module.TestClass.runtime_function_1 in script.py + let frame1 = RuntimeStackFrame { + function_name: FUNCTION_NAME_1.as_ptr() as *const c_char, + file_name: FILE_NAME_1.as_ptr() as *const c_char, + line_number: 42, + column_number: 15, + }; + emit_frame(&frame1); + + // Frame 2: my_package.submodule.MyModule.runtime_function_2 in module.py + let frame2 = RuntimeStackFrame { + function_name: FUNCTION_NAME_2.as_ptr() as *const c_char, + file_name: FILE_NAME_2.as_ptr() as *const c_char, + line_number: 100, + column_number: 8, + }; + emit_frame(&frame2); + + // Frame 3: __main__.runtime_main in main.py + let frame3 = RuntimeStackFrame { + function_name: FUNCTION_NAME_3.as_ptr() as *const c_char, + file_name: FILE_NAME_3.as_ptr() as *const c_char, + line_number: 10, + column_number: 1, + }; + emit_frame(&frame3); +} + +#[cfg(test)] +mod tests { + use super::*; + use datadog_crashtracker::{clear_runtime_callback, is_runtime_callback_registered}; + + #[test] + fn test_runtime_callback_frame_registration() { + // Ensure clean state + unsafe { + clear_runtime_callback(); + } + + // Test that no callback is initially registered + assert!(!is_runtime_callback_registered()); + + // Test frame mode registration + let result = + register_runtime_stack_callback(test_runtime_callback_frame, CallbackType::Frame); + assert!(result.is_ok(), "Frame callback registration should succeed"); + assert!( + is_runtime_callback_registered(), + "Callback should be registered" + ); + + // Clean up + unsafe { + clear_runtime_callback(); + } + assert!( + !is_runtime_callback_registered(), + "Callback should be cleared" + ); + } +} diff --git a/bin_tests/src/modes/unix/test_011_runtime_callback_string.rs b/bin_tests/src/modes/unix/test_011_runtime_callback_string.rs new file mode 100644 index 0000000000..1fcb86dfde --- /dev/null +++ b/bin_tests/src/modes/unix/test_011_runtime_callback_string.rs @@ -0,0 +1,94 @@ +// Copyright 2024-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 +// +// This test validates the runtime stack collection callback mechanism using string mode. +// It registers a test callback that provides mock runtime stacktrace string, +// then crashes and verifies that the runtime stacktrace string appear in the crash report. +// +// This test uses CallbackType::StacktraceString to emit structured runtime stack data. + +use crate::modes::behavior::Behavior; +use datadog_crashtracker::{ + clear_runtime_callback, register_runtime_stack_callback, CallbackType, + CrashtrackerConfiguration, RuntimeStackFrame, +}; +use std::ffi::c_char; +use std::path::Path; + +pub struct Test; + +impl Behavior for Test { + fn setup( + &self, + _output_dir: &Path, + _config: &mut CrashtrackerConfiguration, + ) -> anyhow::Result<()> { + Ok(()) + } + + fn pre(&self, _output_dir: &Path) -> anyhow::Result<()> { + // Ensure clean state + unsafe { + clear_runtime_callback(); + } + register_runtime_stack_callback( + test_runtime_callback_string, + CallbackType::StacktraceString, + ) + .map_err(|e| anyhow::anyhow!("Failed to register runtime callback: {:?}", e))?; + Ok(()) + } + + fn post(&self, _output_dir: &Path) -> anyhow::Result<()> { + Ok(()) + } +} + +// Signal-safe test callback that emits mock runtime stacktrace string +unsafe extern "C" fn test_runtime_callback_string( + _emit_frame: unsafe extern "C" fn(*const RuntimeStackFrame), + emit_stacktrace_string: unsafe extern "C" fn(*const c_char), +) { + static STACKTRACE_STRING: &[u8] = b"test_stacktrace_string\0"; + emit_stacktrace_string(STACKTRACE_STRING.as_ptr() as *const c_char); +} + +#[cfg(test)] +mod tests { + use super::*; + use datadog_crashtracker::{clear_runtime_callback, is_runtime_callback_registered}; + + #[test] + fn test_runtime_callback_string_registration() { + // Ensure clean state + unsafe { + clear_runtime_callback(); + } + + // Test that no callback is initially registered + assert!(!is_runtime_callback_registered()); + + // Test string mode registration + let result = register_runtime_stack_callback( + test_runtime_callback_string, + CallbackType::StacktraceString, + ); + assert!( + result.is_ok(), + "String callback registration should succeed" + ); + assert!( + is_runtime_callback_registered(), + "Callback should be registered" + ); + + // Clean up + unsafe { + clear_runtime_callback(); + } + assert!( + !is_runtime_callback_registered(), + "Callback should be cleared" + ); + } +} diff --git a/bin_tests/tests/crashtracker_bin_test.rs b/bin_tests/tests/crashtracker_bin_test.rs index f16320157f..618de7a0dc 100644 --- a/bin_tests/tests/crashtracker_bin_test.rs +++ b/bin_tests/tests/crashtracker_bin_test.rs @@ -129,6 +129,36 @@ fn test_crash_tracking_bin_prechain_sigabrt() { test_crash_tracking_bin(BuildProfile::Release, "prechain_abort", "null_deref"); } +#[test] +#[cfg_attr(miri, ignore)] +fn test_crash_tracking_bin_runtime_callback_frame() { + test_crash_tracking_bin_runtime_callback_frame_impl( + BuildProfile::Release, + "runtime_callback_frame", + "null_deref", + ); +} + +#[test] +#[cfg_attr(miri, ignore)] +fn test_crash_tracking_bin_runtime_callback_string() { + test_crash_tracking_bin_runtime_callback_string_impl( + BuildProfile::Release, + "runtime_callback_string", + "null_deref", + ); +} + +#[test] +#[cfg_attr(miri, ignore)] +fn test_crash_tracking_bin_no_runtime_callback() { + test_crash_tracking_bin_no_runtime_callback_impl( + BuildProfile::Release, + "donothing", + "null_deref", + ); +} + #[test] #[cfg_attr(miri, ignore)] fn test_crash_ping_timing_and_content() { @@ -232,6 +262,412 @@ fn test_crash_tracking_callstack() { } } +fn test_crash_tracking_bin_runtime_callback_frame_impl( + crash_tracking_receiver_profile: BuildProfile, + mode: &str, + crash_typ: &str, +) { + let (crashtracker_bin, crashtracker_receiver) = + setup_crashtracking_crates(crash_tracking_receiver_profile); + let fixtures = setup_test_fixtures(&[&crashtracker_receiver, &crashtracker_bin]); + + let mut p = process::Command::new(&fixtures.artifacts[&crashtracker_bin]) + .arg(format!("file://{}", fixtures.crash_profile_path.display())) + .arg(fixtures.artifacts[&crashtracker_receiver].as_os_str()) + .arg(&fixtures.output_dir) + .arg(mode) + .arg(crash_typ) + .spawn() + .unwrap(); + + let exit_status = bin_tests::timeit!("exit after signal", { + eprintln!("Waiting for exit"); + p.wait().unwrap() + }); + + // Runtime callback tests should crash like normal tests + assert!(!exit_status.success()); + + let stderr_path = format!("{0}/out.stderr", fixtures.output_dir.display()); + let stderr = fs::read(stderr_path) + .context("reading crashtracker stderr") + .unwrap(); + let stdout_path = format!("{0}/out.stdout", fixtures.output_dir.display()); + let stdout = fs::read(stdout_path) + .context("reading crashtracker stdout") + .unwrap(); + let s = String::from_utf8(stderr); + assert!( + matches!( + s.as_deref(), + Ok("") | Ok("Failed to fully receive crash. Exit state was: StackTrace([])\n") + | Ok("Failed to fully receive crash. Exit state was: InternalError(\"{\\\"ip\\\": \\\"\")\n"), + ), + "got {s:?}" + ); + assert_eq!(Ok(""), String::from_utf8(stdout).as_deref()); + + // Check the crash data + let crash_profile = fs::read(&fixtures.crash_profile_path) + .context("reading crashtracker profiling payload") + .unwrap(); + let crash_payload = serde_json::from_slice::(&crash_profile) + .context("deserializing crashtracker profiling payload to json") + .unwrap(); + + // Validate normal crash data first + assert_eq!( + serde_json::json!({ + "profiler_collecting_sample": 1, + "profiler_inactive": 0, + "profiler_serializing": 0, + "profiler_unwinding": 0 + }), + crash_payload["counters"], + ); + + let sig_info = &crash_payload["sig_info"]; + assert_siginfo_message(sig_info, crash_typ); + + let error = &crash_payload["error"]; + assert_error_message(&error["message"], sig_info); + + // Validate runtime callback frame data + validate_runtime_callback_frame_data(&crash_payload); + + let crash_telemetry = fs::read(&fixtures.crash_telemetry_path) + .context("reading crashtracker telemetry payload") + .unwrap(); + assert_telemetry_message(&crash_telemetry, crash_typ); +} + +fn validate_runtime_callback_frame_data(crash_payload: &Value) { + // Look for runtime stack frames in the experimental section + let experimental = crash_payload.get("experimental"); + assert!( + experimental.is_some(), + "Experimental section should be present in crash payload for runtime callback test" + ); + + let runtime_stack = experimental.unwrap().get("runtime_stack"); + assert!( + runtime_stack.is_some(), + "Runtime stack should be present in experimental section for frame mode" + ); + + let runtime_stack = runtime_stack.unwrap(); + + // Check the format field + assert_eq!( + runtime_stack["format"].as_str().unwrap(), + "Datadog Runtime Callback 1.0", + "Runtime stack format should be correct" + ); + + // The runtime stack should have a frames array + let frames = runtime_stack.get("frames"); + assert!(frames.is_some(), "Runtime stack should have frames array"); + + let frames = frames.unwrap().as_array(); + assert!(frames.is_some(), "Runtime stack frames should be an array"); + + let frames = frames.unwrap(); + assert!( + frames.len() >= 3, + "Should have at least 3 runtime frames, got {}", + frames.len() + ); + + // Validate the expected test frames + let expected_functions = [ + "test_module.TestClass.runtime_function_1", + "my_package.submodule.MyModule.runtime_function_2", + "__main__.runtime_main", + ]; + let expected_files = ["script.py", "module.py", "main.py"]; + let expected_lines = [42, 100, 10]; + let expected_columns = [15, 8, 1]; + + for (i, frame) in frames.iter().take(3).enumerate() { + if let Some(function) = frame.get("function") { + assert_eq!( + function.as_str().unwrap(), + expected_functions[i], + "Frame {} function mismatch", + i + ); + } + + if let Some(file) = frame.get("file") { + assert_eq!( + file.as_str().unwrap(), + expected_files[i], + "Frame {} file mismatch", + i + ); + } + + if let Some(line) = frame.get("line") { + assert_eq!( + line.as_u64().unwrap() as u32, + expected_lines[i], + "Frame {} line mismatch", + i + ); + } + + if let Some(column) = frame.get("column") { + assert_eq!( + column.as_u64().unwrap() as u32, + expected_columns[i], + "Frame {} column mismatch", + i + ); + } + } + + // Ensure stacktrace_string is null for frame mode + assert!( + runtime_stack.get("stacktrace_string").is_none() + || runtime_stack["stacktrace_string"].is_null(), + "Stacktrace string should be null/absent for frame mode" + ); +} + +fn validate_runtime_callback_string_data(crash_payload: &Value) { + // Look for runtime stacktrace string in the experimental section + let experimental = crash_payload.get("experimental"); + assert!( + experimental.is_some(), + "Experimental section should be present in crash payload for runtime callback test" + ); + + let runtime_stack = experimental.unwrap().get("runtime_stack"); + assert!( + runtime_stack.is_some(), + "{}", + format!( + "Runtime stack should be present in experimental section for string mode. Got: {:?}", + experimental + ) + ); + + let runtime_stack = runtime_stack.unwrap(); + + // Check the format field + assert_eq!( + runtime_stack["format"].as_str().unwrap(), + "Datadog Runtime Callback 1.0", + "Runtime stack format should be correct" + ); + + // The stacktrace_string should be present + let stacktrace_string = runtime_stack.get("stacktrace_string"); + assert!( + stacktrace_string.is_some(), + "Runtime stack should have stacktrace_string field for string mode" + ); + + let stacktrace_str = stacktrace_string.unwrap().as_str(); + assert!( + stacktrace_str.is_some(), + "Runtime stacktrace_string should be a string" + ); + + let stacktrace_str = stacktrace_str.unwrap(); + // Validate that it contains the expected content from our test callback + assert_eq!( + stacktrace_str, "test_stacktrace_string", + "Runtime stacktrace_string should be correct" + ); + + // Ensure frames array is empty for string mode + let frames = runtime_stack.get("frames"); + if let Some(frames_array) = frames { + if let Some(array) = frames_array.as_array() { + assert!( + array.is_empty(), + "Frames array should be empty for string mode" + ); + } + } +} + +fn validate_no_runtime_callback_data(crash_payload: &Value) { + // Check if experimental section exists + let experimental = crash_payload.get("experimental"); + + if let Some(experimental) = experimental { + // If experimental section exists, runtime_stack should not be present + let runtime_stack = experimental.get("runtime_stack"); + assert!( + runtime_stack.is_none(), + "Runtime stack should NOT be present in experimental section when no callback is registered. Got: {:?}", + runtime_stack + ); + } + // If experimental section doesn't exist at all, that's also fine - + // it means no experimental features were added to the crash report +} + +fn test_crash_tracking_bin_runtime_callback_string_impl( + crash_tracking_receiver_profile: BuildProfile, + mode: &str, + crash_typ: &str, +) { + let (crashtracker_bin, crashtracker_receiver) = + setup_crashtracking_crates(crash_tracking_receiver_profile); + let fixtures = setup_test_fixtures(&[&crashtracker_receiver, &crashtracker_bin]); + + let mut p = process::Command::new(&fixtures.artifacts[&crashtracker_bin]) + .arg(format!("file://{}", fixtures.crash_profile_path.display())) + .arg(fixtures.artifacts[&crashtracker_receiver].as_os_str()) + .arg(&fixtures.output_dir) + .arg(mode) + .arg(crash_typ) + .spawn() + .unwrap(); + + let exit_status = bin_tests::timeit!("exit after signal", { + eprintln!("Waiting for exit"); + p.wait().unwrap() + }); + + // Runtime callback tests should crash like normal tests + assert!(!exit_status.success()); + + let stderr_path = format!("{0}/out.stderr", fixtures.output_dir.display()); + let stderr = fs::read(stderr_path) + .context("reading crashtracker stderr") + .unwrap(); + let stdout_path = format!("{0}/out.stdout", fixtures.output_dir.display()); + let stdout = fs::read(stdout_path) + .context("reading crashtracker stdout") + .unwrap(); + let s = String::from_utf8(stderr); + assert!( + matches!( + s.as_deref(), + Ok("") | Ok("Failed to fully receive crash. Exit state was: StackTrace([])\n") + | Ok("Failed to fully receive crash. Exit state was: InternalError(\"{\\\"ip\\\": \\\"\")\n"), + ), + "got {s:?}" + ); + assert_eq!(Ok(""), String::from_utf8(stdout).as_deref()); + + // Check the crash data + let crash_profile = fs::read(&fixtures.crash_profile_path) + .context("reading crashtracker profiling payload") + .unwrap(); + let crash_payload = serde_json::from_slice::(&crash_profile) + .context("deserializing crashtracker profiling payload to json") + .unwrap(); + + // Validate normal crash data first + assert_eq!( + serde_json::json!({ + "profiler_collecting_sample": 1, + "profiler_inactive": 0, + "profiler_serializing": 0, + "profiler_unwinding": 0 + }), + crash_payload["counters"], + ); + + let sig_info = &crash_payload["sig_info"]; + assert_siginfo_message(sig_info, crash_typ); + + let error = &crash_payload["error"]; + assert_error_message(&error["message"], sig_info); + + // Validate runtime callback string data + validate_runtime_callback_string_data(&crash_payload); + + let crash_telemetry = fs::read(&fixtures.crash_telemetry_path) + .context("reading crashtracker telemetry payload") + .unwrap(); + assert_telemetry_message(&crash_telemetry, crash_typ); +} + +fn test_crash_tracking_bin_no_runtime_callback_impl( + crash_tracking_receiver_profile: BuildProfile, + mode: &str, + crash_typ: &str, +) { + let (crashtracker_bin, crashtracker_receiver) = + setup_crashtracking_crates(crash_tracking_receiver_profile); + let fixtures = setup_test_fixtures(&[&crashtracker_receiver, &crashtracker_bin]); + + let mut p = process::Command::new(&fixtures.artifacts[&crashtracker_bin]) + .arg(format!("file://{}", fixtures.crash_profile_path.display())) + .arg(fixtures.artifacts[&crashtracker_receiver].as_os_str()) + .arg(&fixtures.output_dir) + .arg(mode) + .arg(crash_typ) + .spawn() + .unwrap(); + + let exit_status = bin_tests::timeit!("exit after signal", { + eprintln!("Waiting for exit"); + p.wait().unwrap() + }); + + // Should crash like normal tests + assert!(!exit_status.success()); + + let stderr_path = format!("{0}/out.stderr", fixtures.output_dir.display()); + let stderr = fs::read(stderr_path) + .context("reading crashtracker stderr") + .unwrap(); + let stdout_path = format!("{0}/out.stdout", fixtures.output_dir.display()); + let stdout = fs::read(stdout_path) + .context("reading crashtracker stdout") + .unwrap(); + let s = String::from_utf8(stderr); + assert!( + matches!( + s.as_deref(), + Ok("") | Ok("Failed to fully receive crash. Exit state was: StackTrace([])\n") + | Ok("Failed to fully receive crash. Exit state was: InternalError(\"{\\\"ip\\\": \\\"\")\n"), + ), + "got {s:?}" + ); + assert_eq!(Ok(""), String::from_utf8(stdout).as_deref()); + + // Check the crash data + let crash_profile = fs::read(&fixtures.crash_profile_path) + .context("reading crashtracker profiling payload") + .unwrap(); + let crash_payload = serde_json::from_slice::(&crash_profile) + .context("deserializing crashtracker profiling payload to json") + .unwrap(); + + // Validate normal crash data first + assert_eq!( + serde_json::json!({ + "profiler_collecting_sample": 1, + "profiler_inactive": 0, + "profiler_serializing": 0, + "profiler_unwinding": 0 + }), + crash_payload["counters"], + ); + + let sig_info = &crash_payload["sig_info"]; + assert_siginfo_message(sig_info, crash_typ); + + let error = &crash_payload["error"]; + assert_error_message(&error["message"], sig_info); + + // Validate no runtime callback data is present + validate_no_runtime_callback_data(&crash_payload); + + let crash_telemetry = fs::read(&fixtures.crash_telemetry_path) + .context("reading crashtracker telemetry payload") + .unwrap(); + assert_telemetry_message(&crash_telemetry, crash_typ); +} + fn test_crash_tracking_bin( crash_tracking_receiver_profile: BuildProfile, mode: &str, diff --git a/datadog-crashtracker-ffi/src/lib.rs b/datadog-crashtracker-ffi/src/lib.rs index 09e18a5f4b..a4aa8a0e53 100644 --- a/datadog-crashtracker-ffi/src/lib.rs +++ b/datadog-crashtracker-ffi/src/lib.rs @@ -16,6 +16,7 @@ mod crash_info; mod demangler; #[cfg(all(unix, feature = "receiver"))] mod receiver; +mod runtime_callback; #[cfg(all(unix, feature = "collector"))] pub use collector::*; #[cfg(all(windows, feature = "collector_windows"))] @@ -25,3 +26,4 @@ pub use crash_info::*; pub use demangler::*; #[cfg(all(unix, feature = "receiver"))] pub use receiver::*; +pub use runtime_callback::*; diff --git a/datadog-crashtracker-ffi/src/runtime_callback.rs b/datadog-crashtracker-ffi/src/runtime_callback.rs new file mode 100644 index 0000000000..03281b628f --- /dev/null +++ b/datadog-crashtracker-ffi/src/runtime_callback.rs @@ -0,0 +1,235 @@ +// Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//! FFI bindings for runtime callback registration +//! +//! This module provides C-compatible FFI bindings for registering runtime-specific +//! crash callbacks that can provide stack traces for dynamic languages. +use datadog_crashtracker::{ + get_registered_callback_type_ptr, is_runtime_callback_registered, + register_runtime_stack_callback, CallbackError, CallbackType, RuntimeStackCallback, +}; + +// Re-export the enums for C/C++ consumers +pub use datadog_crashtracker::CallbackType as ddog_CallbackType; + +pub use datadog_crashtracker::RuntimeStackFrame as ddog_RuntimeStackFrame; + +/// Result type for runtime callback registration +#[repr(C)] +#[derive(Debug, PartialEq, Eq)] +pub enum CallbackResult { + Ok, + NullCallback, + UnknownError, +} + +impl From for CallbackResult { + fn from(error: CallbackError) -> Self { + match error { + CallbackError::NullCallback => CallbackResult::NullCallback, + } + } +} + +/// Register a runtime stack collection callback +/// +/// This function allows language runtimes to register a callback that will be invoked +/// during crash handling to collect runtime-specific stack traces. +/// +/// # Arguments +/// - `callback`: The callback function to invoke during crashes +/// +/// # Returns +/// - `CallbackResult::Ok` if registration succeeds +/// - `CallbackResult::NullCallback` if the callback function is null +/// +/// # Safety +/// - The callback must be signal-safe +/// - Only one callback can be registered at a time +/// - The callback must be registered once on CrashTracker initialization, before any crash occurs +/// +/// # Example Usage from C +/// ```c +/// static void my_runtime_callback( +/// void (*emit_frame)(const ddog_RuntimeStackFrame*), +/// void (*emit_stacktrace_string)(const char*) +/// ) { +/// // Collect runtime frames and call emit_frame for each one +/// ddog_RuntimeStackFrame frame = { +/// .function_name = "MyModule.MyClass.my_function", +/// .file_name = "script.rb", +/// .line_number = 42, +/// .column_number = 10 +/// }; +/// emit_frame(&frame); +/// } +/// +/// +/// ddog_CallbackResult result = ddog_crasht_register_runtime_stack_callback( +/// my_runtime_callback, +/// CallbackType::Frame, +/// ); +/// ``` +/// Register a runtime stack collection callback using type-safe enums +/// +/// This function provides compile-time safety by using enums instead of strings +/// for runtime and callback types. +/// +/// # Arguments +/// - `callback`: The callback function to invoke during crashes +/// - `callback_type`: Callback type enum (Frame, StacktraceString) +/// +/// # Returns +/// - `CallbackResult::Ok` if registration succeeds (replaces any existing callback) +/// - `CallbackResult::NullCallback` if the callback function is null +/// +/// # Safety +/// - The callback must be signal-safe +/// - Only one callback can be registered at a time (this replaces any existing one) +#[no_mangle] +pub unsafe extern "C" fn ddog_crasht_register_runtime_stack_callback( + callback: RuntimeStackCallback, + callback_type: CallbackType, +) -> CallbackResult { + match register_runtime_stack_callback(callback, callback_type) { + Ok(()) => CallbackResult::Ok, + Err(e) => e.into(), + } +} + +/// Check if a runtime callback is currently registered +/// +/// Returns true if a callback is registered, false otherwise +/// +/// # Safety +/// This function is safe to call at any time +#[no_mangle] +pub extern "C" fn ddog_crasht_is_runtime_callback_registered() -> bool { + is_runtime_callback_registered() +} + +/// Get the callback type from the currently registered callback context +/// +/// Returns the callback type C string pointer if a callback with valid context is registered, +/// null pointer otherwise +/// +/// # Safety +/// - The returned pointer is valid only while the callback remains registered +/// - The caller should not free the returned pointer +/// - The returned string should be copied if it needs to persist beyond callback lifetime +#[no_mangle] +pub unsafe extern "C" fn ddog_crasht_get_registered_callback_type() -> *const std::ffi::c_char { + get_registered_callback_type_ptr() +} + +#[cfg(test)] +mod tests { + use super::*; + use datadog_crashtracker::{clear_runtime_callback, RuntimeStackFrame}; + use std::ffi::{c_char, CString}; + use std::sync::Mutex; + + // Use a mutex to ensure tests run sequentially to avoid race conditions + // with the global static variable + static TEST_MUTEX: Mutex<()> = Mutex::new(()); + + unsafe extern "C" fn test_runtime_callback( + emit_frame: unsafe extern "C" fn(*const RuntimeStackFrame), + _emit_stacktrace_string: unsafe extern "C" fn(*const c_char), + ) { + let function_name = CString::new("TestModule.TestClass.test_function").unwrap(); + let file_name = CString::new("test.rb").unwrap(); + + // Create the internal RuntimeStackFrame directly; no conversion needed + // since both RuntimeStackFrame and ddog_RuntimeStackFrame have identical layouts + let frame = RuntimeStackFrame { + function_name: function_name.as_ptr(), + file_name: file_name.as_ptr(), + line_number: 42, + column_number: 10, + }; + + emit_frame(&frame); + } + + #[test] + fn test_ffi_callback_registration() { + let _guard = TEST_MUTEX.lock().unwrap(); + unsafe { + // Ensure clean state at start + clear_runtime_callback(); + + // Test that no callback is initially registered + assert!(!ddog_crasht_is_runtime_callback_registered()); + + // Test successful registration using type-safe enums + let result = ddog_crasht_register_runtime_stack_callback( + test_runtime_callback, + CallbackType::Frame, + ); + + assert_eq!(result, CallbackResult::Ok); + + // Verify callback is now registered + assert!(ddog_crasht_is_runtime_callback_registered()); + + // Test duplicate registration fails + let result = ddog_crasht_register_runtime_stack_callback( + test_runtime_callback, + CallbackType::Frame, + ); + assert_eq!(result, CallbackResult::Ok); + + // Callback should still be registered after successful re-registration + assert!(ddog_crasht_is_runtime_callback_registered()); + + // Clean up - clear the registered callback for subsequent tests + clear_runtime_callback(); + } + } + + #[test] + fn test_enum_based_registration() { + let _guard = TEST_MUTEX.lock().unwrap(); + unsafe { + clear_runtime_callback(); + + // Test that no callback is initially registered + assert!(!ddog_crasht_is_runtime_callback_registered()); + + // Test registration with enum values - Python + StacktraceString + let result = ddog_crasht_register_runtime_stack_callback( + test_runtime_callback, + CallbackType::StacktraceString, + ); + + assert_eq!(result, CallbackResult::Ok); + assert!(ddog_crasht_is_runtime_callback_registered()); + + // Verify callback type + let callback_type_ptr = ddog_crasht_get_registered_callback_type(); + assert!(!callback_type_ptr.is_null()); + let callback_type_str = std::ffi::CStr::from_ptr(callback_type_ptr) + .to_str() + .unwrap(); + assert_eq!(callback_type_str, "stacktrace_string"); + + // Test re-registration with different values - Ruby + Frame + let result = ddog_crasht_register_runtime_stack_callback( + test_runtime_callback, + CallbackType::Frame, + ); + + assert_eq!(result, CallbackResult::Ok); + + let callback_type_ptr = ddog_crasht_get_registered_callback_type(); + let callback_type_str = std::ffi::CStr::from_ptr(callback_type_ptr) + .to_str() + .unwrap(); + assert_eq!(callback_type_str, "frame"); + + clear_runtime_callback(); + } + } +} diff --git a/datadog-crashtracker/src/collector/emitters.rs b/datadog-crashtracker/src/collector/emitters.rs index 4794a14103..a54fb3306f 100644 --- a/datadog-crashtracker/src/collector/emitters.rs +++ b/datadog-crashtracker/src/collector/emitters.rs @@ -4,6 +4,10 @@ use crate::collector::additional_tags::consume_and_emit_additional_tags; use crate::collector::counters::emit_counters; use crate::collector::spans::{emit_spans, emit_traces}; +use crate::runtime_callback::{ + get_registered_callback_type_enum, invoke_runtime_callback_with_writer, + is_runtime_callback_registered, CallbackType, +}; use crate::shared::constants::*; use crate::{translate_si_code, CrashtrackerConfiguration, SignalNames, StacktraceCollection}; use backtrace::Frame; @@ -148,6 +152,11 @@ pub(crate) fn emit_crashreport( let fault_rsp = extract_rsp(ucontext); unsafe { emit_backtrace_by_frames(pipe, config.resolve_frames(), fault_rsp)? }; } + + if is_runtime_callback_registered() { + emit_runtime_stack(pipe)?; + } + writeln!(pipe, "{DD_CRASHTRACK_DONE}")?; pipe.flush()?; @@ -199,6 +208,60 @@ fn emit_ucontext(w: &mut impl Write, ucontext: *const ucontext_t) -> Result<(), Ok(()) } +/// Emit runtime stack frames collected from registered runtime callback +/// +/// This function invokes any registered runtime callback to collect runtime-specific +/// stack traces +/// +/// If runtime stacks are being emitted frame by frame, this function writes structured JSON. +/// If not, it writes a single line with the stacktrace string. +/// +/// SAFETY: +/// Crash-tracking functions are not reentrant. +/// No other crash-handler functions should be called concurrently. +/// SIGNAL SAFETY: +/// This function attempts to be signal safe by only invoking user-registered +/// callbacks and writing to the provided stream. The runtime callback itself +/// must be signal safe. +fn emit_runtime_stack(w: &mut impl Write) -> Result<(), EmitterError> { + let callback_type = unsafe { get_registered_callback_type_enum() }; + + let callback_type = match callback_type { + Some(ct) => ct, + None => return Ok(()), // No callback registered + }; + + match callback_type { + CallbackType::Frame => emit_runtime_stack_by_frames(w), + CallbackType::StacktraceString => emit_runtime_stack_by_stacktrace_string(w), + } +} + +fn emit_runtime_stack_by_frames(w: &mut impl Write) -> Result<(), EmitterError> { + writeln!(w, "{DD_CRASHTRACK_BEGIN_RUNTIME_STACK_FRAME}")?; + + // JSON array for frames + write!(w, "[")?; + unsafe { invoke_runtime_callback_with_writer(w)? }; + write!(w, "]")?; + + writeln!(w)?; + writeln!(w, "{DD_CRASHTRACK_END_RUNTIME_STACK_FRAME}")?; + w.flush()?; + Ok(()) +} + +fn emit_runtime_stack_by_stacktrace_string(w: &mut impl Write) -> Result<(), EmitterError> { + writeln!(w, "{DD_CRASHTRACK_BEGIN_RUNTIME_STACK_STRING}")?; + + // Emit the stacktrace string + unsafe { invoke_runtime_callback_with_writer(w)? }; + + writeln!(w, "{DD_CRASHTRACK_END_RUNTIME_STACK_STRING}")?; + w.flush()?; + Ok(()) +} + #[cfg(target_os = "macos")] fn emit_ucontext(w: &mut impl Write, ucontext: *const ucontext_t) -> Result<(), EmitterError> { if ucontext.is_null() { diff --git a/datadog-crashtracker/src/crash_info/builder.rs b/datadog-crashtracker/src/crash_info/builder.rs index f39c67e12d..e42dc822f0 100644 --- a/datadog-crashtracker/src/crash_info/builder.rs +++ b/datadog-crashtracker/src/crash_info/builder.rs @@ -1,6 +1,8 @@ // Copyright 2024-Present Datadog, Inc. https://www.datadoghq.com/ // SPDX-License-Identifier: Apache-2.0 +use crate::runtime_callback::RuntimeStack; + use chrono::{DateTime, Utc}; use error_data::ThreadData; use stacktrace::StackTrace; @@ -195,6 +197,18 @@ impl CrashInfoBuilder { Ok(self) } + pub fn with_experimental_runtime_stack( + &mut self, + runtime_stack: RuntimeStack, + ) -> anyhow::Result<&mut Self> { + if let Some(experimental) = &mut self.experimental { + experimental.runtime_stack = Some(runtime_stack); + } else { + self.experimental = Some(Experimental::new().with_runtime_stack(runtime_stack)); + } + Ok(self) + } + pub fn with_kind(&mut self, kind: ErrorKind) -> anyhow::Result<&mut Self> { self.error.with_kind(kind)?; Ok(self) diff --git a/datadog-crashtracker/src/crash_info/experimental.rs b/datadog-crashtracker/src/crash_info/experimental.rs index b7160b9436..cbabc1140b 100644 --- a/datadog-crashtracker/src/crash_info/experimental.rs +++ b/datadog-crashtracker/src/crash_info/experimental.rs @@ -1,5 +1,6 @@ // Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/ // SPDX-License-Identifier: Apache-2.0 +use crate::runtime_callback::RuntimeStack; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -11,6 +12,8 @@ pub struct Experimental { pub additional_tags: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] pub ucontext: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub runtime_stack: Option, } impl Experimental { @@ -27,6 +30,11 @@ impl Experimental { self.ucontext = Some(ucontext); self } + + pub fn with_runtime_stack(mut self, runtime_stack: RuntimeStack) -> Self { + self.runtime_stack = Some(runtime_stack); + self + } } impl UnknownValue for Experimental { @@ -34,6 +42,7 @@ impl UnknownValue for Experimental { Self { additional_tags: vec![], ucontext: None, + runtime_stack: None, } } } diff --git a/datadog-crashtracker/src/crash_info/mod.rs b/datadog-crashtracker/src/crash_info/mod.rs index 9c6e1d5480..86974e8f69 100644 --- a/datadog-crashtracker/src/crash_info/mod.rs +++ b/datadog-crashtracker/src/crash_info/mod.rs @@ -135,7 +135,7 @@ mod tests { fn test_schema_matches_rfc() { let rfc_schema_filename = concat!( env!("CARGO_MANIFEST_DIR"), - "/../docs/RFCs/artifacts/0009-crashtracker-schema.json" + "/../docs/RFCs/artifacts/0011-crashtracker-unified-runtime-stack-schema.json" ); let rfc_schema_json = fs::read_to_string(rfc_schema_filename).expect("File to exist"); let rfc_schema: RootSchema = serde_json::from_str(&rfc_schema_json).expect("Valid json"); diff --git a/datadog-crashtracker/src/lib.rs b/datadog-crashtracker/src/lib.rs index 02e56c5353..8e11da6b0c 100644 --- a/datadog-crashtracker/src/lib.rs +++ b/datadog-crashtracker/src/lib.rs @@ -59,6 +59,7 @@ mod common; mod crash_info; #[cfg(all(unix, feature = "receiver"))] mod receiver; +mod runtime_callback; // Keep this module private to avoid exposing blazesym to users of the crate #[cfg(all(unix, any(feature = "collector", feature = "receiver")))] @@ -82,6 +83,7 @@ pub use collector::{ pub use collector_windows::api::{exception_event_callback, init_crashtracking_windows}; pub use crash_info::*; +pub use runtime_callback::*; #[cfg(all(unix, feature = "receiver"))] pub use receiver::{ diff --git a/datadog-crashtracker/src/receiver/receive_report.rs b/datadog-crashtracker/src/receiver/receive_report.rs index f96e972fa0..7fc8051492 100644 --- a/datadog-crashtracker/src/receiver/receive_report.rs +++ b/datadog-crashtracker/src/receiver/receive_report.rs @@ -3,6 +3,7 @@ use crate::{ crash_info::{CrashInfo, CrashInfoBuilder, ErrorKind, Span, TelemetryCrashUploader}, + runtime_callback::{RuntimeFrame, RuntimeStack}, shared::constants::*, CrashtrackerConfiguration, }; @@ -54,6 +55,10 @@ pub(crate) enum StdinState { TraceIds, Ucontext, Waiting, + // StackFrame is always emitted as one stream of all the frames but StackString + // may have lines that we need to accumulate depending on runtime (e.g. Python) + RuntimeStackFrame, + RuntimeStackString(Vec), } /// A state machine that processes data from the crash-tracker collector line by @@ -130,7 +135,42 @@ fn process_line( builder.with_proc_info(proc_info)?; StdinState::ProcInfo } - + StdinState::RuntimeStackFrame + if line.starts_with(DD_CRASHTRACK_END_RUNTIME_STACK_FRAME) => + { + StdinState::Waiting + } + StdinState::RuntimeStackFrame => { + // Try to parse as frames array + if let Ok(runtime_frames) = serde_json::from_str::>(line) { + if !runtime_frames.is_empty() { + let runtime_stack = RuntimeStack { + format: "Datadog Runtime Callback 1.0".to_string(), + frames: runtime_frames, + stacktrace_string: None, + }; + builder.with_experimental_runtime_stack(runtime_stack)?; + } + } + StdinState::RuntimeStackFrame + } + StdinState::RuntimeStackString(lines) + if line.starts_with(DD_CRASHTRACK_END_RUNTIME_STACK_STRING) => + { + // Join all accumulated lines with newlines to reconstruct the full stack trace + let stacktrace_string = lines.join("\n"); + let runtime_stack = RuntimeStack { + format: "Datadog Runtime Callback 1.0".to_string(), + frames: vec![], + stacktrace_string: Some(stacktrace_string), + }; + builder.with_experimental_runtime_stack(runtime_stack)?; + StdinState::Waiting + } + StdinState::RuntimeStackString(mut lines) => { + lines.push(line.to_string()); + StdinState::RuntimeStackString(lines) + } StdinState::SigInfo if line.starts_with(DD_CRASHTRACK_END_SIGINFO) => StdinState::Waiting, StdinState::SigInfo => { let sig_info: crate::SigInfo = serde_json::from_str(line)?; @@ -203,6 +243,12 @@ fn process_line( StdinState::Waiting if line.starts_with(DD_CRASHTRACK_BEGIN_STACKTRACE) => { StdinState::StackTrace } + StdinState::Waiting if line.starts_with(DD_CRASHTRACK_BEGIN_RUNTIME_STACK_STRING) => { + StdinState::RuntimeStackString(vec![]) + } + StdinState::Waiting if line.starts_with(DD_CRASHTRACK_BEGIN_RUNTIME_STACK_FRAME) => { + StdinState::RuntimeStackFrame + } StdinState::Waiting if line.starts_with(DD_CRASHTRACK_BEGIN_TRACE_IDS) => { StdinState::TraceIds } diff --git a/datadog-crashtracker/src/runtime_callback.rs b/datadog-crashtracker/src/runtime_callback.rs new file mode 100644 index 0000000000..e7a7587dc7 --- /dev/null +++ b/datadog-crashtracker/src/runtime_callback.rs @@ -0,0 +1,613 @@ +// Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//! Runtime callback registration system for enhanced crash tracing +//! +//! This module provides APIs for runtime languages (Ruby, Python, PHP, etc.) to register +//! callbacks that can provide runtime-specific stack traces during crash handling. + +/// Runtime-specific stack frame representation suitable for signal-safe context +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use std::{ + ffi::c_char, + ptr, + sync::atomic::{AtomicPtr, Ordering}, +}; +use thiserror::Error; + +static FRAME_CSTR: &std::ffi::CStr = c"frame"; +static STACKTRACE_STRING_CSTR: &std::ffi::CStr = c"stacktrace_string"; + +/// Callback type identifier for different collection strategies +#[repr(C)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum CallbackType { + Frame, + StacktraceString, +} + +impl CallbackType { + pub fn as_str(&self) -> &'static str { + match self { + CallbackType::Frame => "frame", + CallbackType::StacktraceString => "stacktrace_string", + } + } + + pub fn as_cstr(&self) -> &'static std::ffi::CStr { + match self { + CallbackType::Frame => FRAME_CSTR, + CallbackType::StacktraceString => STACKTRACE_STRING_CSTR, + } + } +} + +impl std::str::FromStr for CallbackType { + type Err = &'static str; + fn from_str(s: &str) -> Result { + match s { + "frame" => Ok(CallbackType::Frame), + "stacktrace_string" => Ok(CallbackType::StacktraceString), + _ => Err("Invalid callback type"), + } + } +} + +/// Global storage for the runtime callback +/// +/// Uses atomic pointer to ensure safe access from signal handlers +static RUNTIME_CALLBACK: AtomicPtr<(RuntimeStackCallback, CallbackType)> = + AtomicPtr::new(ptr::null_mut()); + +#[repr(C)] +#[derive(Debug, Clone)] +pub struct RuntimeStackFrame { + /// Fully qualified function/method name (null-terminated C string) + /// Examples: "my_package.submodule.TestClass.method", "MyClass::method", "namespace::function" + pub function_name: *const c_char, + /// Source file name (null-terminated C string) + pub file_name: *const c_char, + /// Line number in source file (0 if unknown) + pub line_number: u32, + /// Column number in source file (0 if unknown) + pub column_number: u32, +} + +/// Function signature for runtime stack collection callbacks +/// +/// This callback is invoked during crash handling in a signal context, so it must be signal-safe: +/// - No dynamic memory allocation +/// - No mutex operations +/// - No I/O operations +/// - Only async-signal-safe functions +/// +/// # Parameters +/// - `emit_frame`: Function to call for each runtime frame (takes frame pointer) +/// - `emit_stacktrace_string`: Function to call for complete stacktrace string (takes C string) +/// +/// # Safety +/// The callback function is marked unsafe because: +/// - It receives function pointers that take raw pointers as parameters +/// - The callback must ensure any pointers it passes to these functions are valid +/// - All C strings passed must be null-terminated and remain valid for the call duration +pub type RuntimeStackCallback = unsafe extern "C" fn( + emit_frame: unsafe extern "C" fn(*const RuntimeStackFrame), + emit_stacktrace_string: unsafe extern "C" fn(*const c_char), +); + +/// Runtime stack representation for JSON serialization +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct RuntimeStack { + /// Format identifier for this runtime stack + pub format: String, + /// Array of runtime-specific stack frames (optional, mutually exclusive with + /// stacktrace_string) + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub frames: Vec, + /// Raw stacktrace string (optional, mutually exclusive with frames) + #[serde(default, skip_serializing_if = "Option::is_none")] + pub stacktrace_string: Option, +} + +/// JSON-serializable runtime stack frame +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct RuntimeFrame { + /// Fully qualified function/method name + /// Examples: "my_package.submodule.TestClass.method", "MyClass::method", "namespace::function" + #[serde(default, skip_serializing_if = "Option::is_none")] + pub function: Option, + /// Source file name + #[serde(default, skip_serializing_if = "Option::is_none")] + pub file: Option, + /// Line number in source file + #[serde(default, skip_serializing_if = "Option::is_none")] + pub line: Option, + /// Column number in source file + #[serde(default, skip_serializing_if = "Option::is_none")] + pub column: Option, +} + +/// Errors that can occur during callback registration +#[derive(Debug, Error)] +pub enum CallbackError { + #[error("Null callback function provided")] + NullCallback, +} + +/// Register a runtime stack collection callback +pub fn register_runtime_stack_callback( + callback: RuntimeStackCallback, + callback_type: CallbackType, +) -> Result<(), CallbackError> { + if callback as usize == 0 { + return Err(CallbackError::NullCallback); + } + + let callback_data = Box::into_raw(Box::new((callback, callback_type))); + let previous = RUNTIME_CALLBACK.swap(callback_data, Ordering::SeqCst); + + if !previous.is_null() { + // Safety: previous was returned by Box::into_raw() above or in a previous call, + // so it's guaranteed to be a valid Box pointer. We reconstruct the Box to drop it. + let _ = unsafe { Box::from_raw(previous) }; + } + + Ok(()) +} + +/// Check if a runtime callback is currently registered +/// +/// Returns true if a callback is registered, false otherwise +pub fn is_runtime_callback_registered() -> bool { + !RUNTIME_CALLBACK.load(Ordering::SeqCst).is_null() +} + +/// Get the callback type enum from the currently registered callback +/// +/// Returns the callback type enum if a callback is registered, None otherwise +/// +/// # Safety +/// This function loads from an atomic pointer and dereferences it. +/// The caller must ensure that no other thread is calling `clear_runtime_callback` +/// or `register_runtime_stack_callback` concurrently, as those could invalidate +/// the pointer between the null check and dereferencing. +pub unsafe fn get_registered_callback_type_enum() -> Option { + let callback_ptr = RUNTIME_CALLBACK.load(Ordering::SeqCst); + if callback_ptr.is_null() { + return None; + } + + // Safety: callback_ptr was checked to be non-null above, and was created by + // Box::into_raw() in register_runtime_stack_callback(), so it's a valid pointer + // to a properly aligned, initialized tuple. The atomic load with SeqCst ordering + // ensures we see the pointer after it was stored. + let (_, callback_type) = &*callback_ptr; + Some(*callback_type) +} + +/// Get the callback type C string pointer from the currently registered callback +/// +/// # Safety +/// This function loads from an atomic pointer and dereferences it. +/// The caller must ensure that no other thread is calling `clear_runtime_callback` +/// or `register_runtime_stack_callback` concurrently, as those could invalidate +/// the pointer between the null check and dereferencing. +pub unsafe fn get_registered_callback_type_ptr() -> *const std::ffi::c_char { + let callback_ptr = RUNTIME_CALLBACK.load(Ordering::SeqCst); + if callback_ptr.is_null() { + return std::ptr::null(); + } + + // Safety: callback_ptr was checked to be non-null above, and was created by + // Box::into_raw() in register_runtime_stack_callback(), so it's a valid pointer + // to a properly aligned, initialized tuple. The returned C string pointer + // points to static string literals, so it's always valid. + let (_, callback_type) = &*callback_ptr; + callback_type.as_cstr().as_ptr() +} + +/// Clear the registered runtime callback +/// +/// This function is primarily intended for testing purposes to clean up state +/// between tests. In production, callbacks typically remain registered for the +/// lifetime of the process. +/// +/// # Safety +/// This function should only be called when it's safe to clear the callback, +/// such as during testing or application shutdown. The caller must ensure: +/// - No other thread is concurrently calling functions that dereference the callback pointer +/// - No signal handlers are currently executing that might invoke the callback +/// - The callback is not being used in any other way +pub unsafe fn clear_runtime_callback() { + let old_ptr = RUNTIME_CALLBACK.swap(std::ptr::null_mut(), Ordering::SeqCst); + if !old_ptr.is_null() { + // Safety: old_ptr was created by Box::into_raw() in register_runtime_stack_callback(), + // so it's a valid Box pointer. We reconstruct the Box to properly drop the tuple. + let _ = Box::from_raw(old_ptr); + } +} + +/// Internal function to invoke the registered runtime callback with direct pipe writing +/// +/// This is called during crash handling to collect runtime-specific stack frames +/// and write them directly to the provided writer for efficiency. +/// +/// # Safety +/// This function is intended to be called from signal handlers and must maintain +/// signal safety. It does not perform any dynamic allocation. The caller must ensure: +/// - No other thread is calling `clear_runtime_callback` concurrently +/// - The registered callback function is signal-safe +/// - The writer parameter remains valid for the duration of the call +#[cfg(unix)] +pub(crate) unsafe fn invoke_runtime_callback_with_writer( + writer: &mut W, +) -> Result<(), std::io::Error> { + let callback_ptr = RUNTIME_CALLBACK.load(Ordering::SeqCst); + if callback_ptr.is_null() { + return Err(std::io::Error::other("No runtime callback registered")); + } + + // Safety: callback_ptr was checked to be non-null above, and was created by + // Box::into_raw() in register_runtime_stack_callback(), so it's a valid pointer + // to a properly aligned, initialized tuple. + let (callback_fn, _) = &*callback_ptr; + + use std::sync::atomic::{AtomicPtr, Ordering}; + + // Thread-safe storage for the current callback context + // Store as raw data and meta pointers + static CURRENT_WRITER_DATA: AtomicPtr<()> = AtomicPtr::new(ptr::null_mut()); + static CURRENT_WRITER_VTABLE: AtomicPtr<()> = AtomicPtr::new(ptr::null_mut()); + static CURRENT_FRAME_COUNT: AtomicPtr = AtomicPtr::new(ptr::null_mut()); + + let mut frame_count = 0usize; + + let writer_trait_obj: *mut dyn std::io::Write = writer; + let writer_parts: (*mut (), *mut ()) = unsafe { std::mem::transmute(writer_trait_obj) }; + let frame_count_ptr = &mut frame_count as *mut usize; + + // Store components atomically + CURRENT_WRITER_DATA.store(writer_parts.0, Ordering::SeqCst); + CURRENT_WRITER_VTABLE.store(writer_parts.1, Ordering::SeqCst); + CURRENT_FRAME_COUNT.store(frame_count_ptr, Ordering::SeqCst); + + // Define the emit functions that read from the atomic storage + unsafe extern "C" fn emit_frame_collector(frame: *const RuntimeStackFrame) { + if frame.is_null() { + return; + } + + let writer_data = CURRENT_WRITER_DATA.load(Ordering::SeqCst); + let writer_vtable = CURRENT_WRITER_VTABLE.load(Ordering::SeqCst); + let frame_count_ptr = CURRENT_FRAME_COUNT.load(Ordering::SeqCst); + + if writer_data.is_null() || writer_vtable.is_null() || frame_count_ptr.is_null() { + return; + } + + // Reconstruct fat pointer + let writer_trait_obj: *mut dyn std::io::Write = + std::mem::transmute((writer_data, writer_vtable)); + let writer = &mut *writer_trait_obj; + let frame_count = &mut *frame_count_ptr; + + // Add comma separator for frames after the first + if *frame_count > 0 { + let _ = write!(writer, ", "); + } + + // Write the frame as JSON + let _ = emit_frame_as_json(writer, frame); + let _ = writer.flush(); + + *frame_count += 1; + } + + unsafe extern "C" fn emit_stacktrace_string_collector(stacktrace_string: *const c_char) { + if stacktrace_string.is_null() { + return; + } + + let writer_data = CURRENT_WRITER_DATA.load(Ordering::SeqCst); + let writer_vtable = CURRENT_WRITER_VTABLE.load(Ordering::SeqCst); + + if writer_data.is_null() || writer_vtable.is_null() { + return; + } + + // Reconstruct fat pointer + let writer_trait_obj: *mut dyn std::io::Write = + std::mem::transmute((writer_data, writer_vtable)); + let writer = &mut *writer_trait_obj; + + // Safety: stacktrace_string is guaranteed by the runtime callback contract to be + // a valid, null-terminated C string that remains valid for the duration of this call. + let cstr = std::ffi::CStr::from_ptr(stacktrace_string); + let bytes = cstr.to_bytes(); + + let _ = writer.write_all(bytes); + let _ = writeln!(writer); + let _ = writer.flush(); + } + + // Invoke the user callback with the simplified emit functions + // Safety: callback_fn was verified to be non-null during registration, and the + // emit functions are valid for the duration of this call. + callback_fn(emit_frame_collector, emit_stacktrace_string_collector); + + // Clear atomic storage + CURRENT_WRITER_DATA.store(ptr::null_mut(), Ordering::SeqCst); + CURRENT_WRITER_VTABLE.store(ptr::null_mut(), Ordering::SeqCst); + CURRENT_FRAME_COUNT.store(ptr::null_mut(), Ordering::SeqCst); + + Ok(()) +} + +/// Emit a single runtime frame as JSON to the writer +/// +/// This function writes a RuntimeStackFrame directly as JSON without intermediate allocation. +/// It must be signal-safe. +/// +/// # Safety +/// The caller must ensure that `frame` is either null or points to a valid, properly +/// initialized RuntimeStackFrame. All C string pointers within the frame must be either +/// null or point to valid, null-terminated C strings. +#[cfg(unix)] +unsafe fn emit_frame_as_json( + writer: &mut dyn std::io::Write, + frame: *const RuntimeStackFrame, +) -> std::io::Result<()> { + if frame.is_null() { + return Ok(()); + } + + // Safety: frame was checked to be non-null above. The caller guarantees it points + // to a valid RuntimeStackFrame. + let frame_ref = &*frame; + write!(writer, "{{")?; + let mut first = true; + + // Convert C strings to Rust strings and write JSON fields + if !frame_ref.function_name.is_null() { + // Safety: frame_ref.function_name was checked to be non-null. The caller + // guarantees it points to a valid, null-terminated C string. + let c_str = std::ffi::CStr::from_ptr(frame_ref.function_name); + if let Ok(s) = c_str.to_str() { + if !s.is_empty() { + write!(writer, "\"function\": \"{}\"", s)?; + first = false; + } + } + } + + if !frame_ref.file_name.is_null() { + // Safety: frame_ref.file_name was checked to be non-null. The caller + // guarantees it points to a valid, null-terminated C string. + let c_str = std::ffi::CStr::from_ptr(frame_ref.file_name); + if let Ok(s) = c_str.to_str() { + if !s.is_empty() { + if !first { + write!(writer, ", ")?; + } + write!(writer, "\"file\": \"{}\"", s)?; + first = false; + } + } + } + + if frame_ref.line_number != 0 { + if !first { + write!(writer, ", ")?; + } + write!(writer, "\"line\": {}", frame_ref.line_number)?; + first = false; + } + + if frame_ref.column_number != 0 { + if !first { + write!(writer, ", ")?; + } + write!(writer, "\"column\": {}", frame_ref.column_number)?; + } + + write!(writer, "}}")?; + Ok(()) +} + +#[cfg(all(test, unix))] +mod tests { + use super::*; + use std::ffi::CString; + use std::sync::Mutex; + + // Use a mutex to ensure tests run sequentially to avoid race conditions + // with the global static variable + static TEST_MUTEX: Mutex<()> = Mutex::new(()); + + unsafe extern "C" fn test_emit_frame_callback( + emit_frame: unsafe extern "C" fn(*const RuntimeStackFrame), + _emit_stacktrace_string: unsafe extern "C" fn(*const c_char), + ) { + let function_name = CString::new("TestModule.TestClass.test_function").unwrap(); + let file_name = CString::new("test.rb").unwrap(); + + let frame = RuntimeStackFrame { + function_name: function_name.as_ptr(), + file_name: file_name.as_ptr(), + line_number: 42, + column_number: 10, + }; + + // Safety: frame is a valid RuntimeStackFrame with valid C string pointers + emit_frame(&frame); + } + + unsafe extern "C" fn test_emit_stacktrace_string_callback( + _emit_frame: unsafe extern "C" fn(*const RuntimeStackFrame), + emit_stacktrace_string: unsafe extern "C" fn(*const c_char), + ) { + let stacktrace_string = CString::new("test_stacktrace_string").unwrap(); + + // Safety: stacktrace_string.as_ptr() returns a valid null-terminated C string + emit_stacktrace_string(stacktrace_string.as_ptr()); + } + + fn ensure_callback_cleared() { + // Ensure no callback is registered before starting + let old_ptr = RUNTIME_CALLBACK.swap(ptr::null_mut(), Ordering::SeqCst); + if !old_ptr.is_null() { + let _ = unsafe { Box::from_raw(old_ptr) }; + } + } + + #[test] + fn test_callback_registration() { + let _guard = TEST_MUTEX.lock().unwrap(); + ensure_callback_cleared(); + + // Test successful registration + let result = register_runtime_stack_callback(test_emit_frame_callback, CallbackType::Frame); + assert!(result.is_ok(), "Failed to register callback: {:?}", result); + + // Test duplicate registration succeeds (replaces previous) + let result = register_runtime_stack_callback(test_emit_frame_callback, CallbackType::Frame); + assert!( + result.is_ok(), + "Failed to re-register callback: {:?}", + result + ); + + // Clean up + ensure_callback_cleared(); + } + + #[test] + fn test_frame_collection() { + let _guard = TEST_MUTEX.lock().unwrap(); + ensure_callback_cleared(); + + // Register callback + let result = register_runtime_stack_callback(test_emit_frame_callback, CallbackType::Frame); + assert!(result.is_ok(), "Failed to register callback: {:?}", result); + + // Invoke callback and collect frames using writer + let mut buffer = Vec::new(); + let invocation_result = unsafe { invoke_runtime_callback_with_writer(&mut buffer) }; + assert!( + invocation_result.is_ok(), + "Failed to invoke callback with writer" + ); + + let json_output = String::from_utf8(buffer).expect("Invalid UTF-8 in output"); + + // Should contain the frame data as JSON + assert!( + json_output.contains("\"function\""), + "Missing function field" + ); + assert!( + json_output.contains("TestModule.TestClass.test_function"), + "Missing fully qualified function name" + ); + assert!(json_output.contains("\"file\""), "Missing file field"); + assert!(json_output.contains("test.rb"), "Missing file name"); + assert!(json_output.contains("\"line\": 42"), "Missing line number"); + assert!( + json_output.contains("\"column\": 10"), + "Missing column number" + ); + + // Clean up + ensure_callback_cleared(); + } + + #[test] + fn test_stacktrace_string_collection() { + let _guard = TEST_MUTEX.lock().unwrap(); + ensure_callback_cleared(); + + // Register callback + let result = register_runtime_stack_callback( + test_emit_stacktrace_string_callback, + CallbackType::StacktraceString, + ); + assert!(result.is_ok(), "Failed to register callback: {:?}", result); + + let mut buffer = Vec::new(); + let invocation_result = unsafe { invoke_runtime_callback_with_writer(&mut buffer) }; + assert!( + invocation_result.is_ok(), + "Failed to invoke callback with writer" + ); + + let json_output = String::from_utf8(buffer).expect("Invalid UTF-8 in output"); + + // Should contain the stacktrace string + assert!( + json_output.contains("test_stacktrace_string"), + "Missing stacktrace string" + ); + + ensure_callback_cleared(); + } + + #[test] + fn test_no_callback_registered() { + let _guard = TEST_MUTEX.lock().unwrap(); + ensure_callback_cleared(); + + // Test that invoking callback returns 0 frames + let mut buffer = Vec::new(); + let invocation_result = unsafe { invoke_runtime_callback_with_writer(&mut buffer) }; + + assert_eq!( + invocation_result.unwrap_err().kind(), + std::io::ErrorKind::Other, + "Expected Other error when no callback registered" + ); + + assert!( + buffer.is_empty(), + "Expected empty buffer when no callback registered" + ); + } + + #[test] + fn test_direct_pipe_writing() { + let _guard = TEST_MUTEX.lock().unwrap(); + ensure_callback_cleared(); + + let result = register_runtime_stack_callback(test_emit_frame_callback, CallbackType::Frame); + assert!(result.is_ok(), "Failed to register callback: {:?}", result); + + // Test writing directly to a buffer + let mut buffer = Vec::new(); + let invocation_result = unsafe { invoke_runtime_callback_with_writer(&mut buffer) }; + assert!( + invocation_result.is_ok(), + "Failed to invoke callback with writer" + ); + + // Convert buffer to string and check JSON format + let json_output = String::from_utf8(buffer).expect("Invalid UTF-8 in output"); + + assert!( + json_output.contains("\"function\""), + "Missing function field" + ); + assert!( + json_output.contains("TestModule.TestClass.test_function"), + "Missing fully qualified function name" + ); + assert!(json_output.contains("\"file\""), "Missing file field"); + assert!(json_output.contains("test.rb"), "Missing file name"); + assert!(json_output.contains("\"line\": 42"), "Missing line number"); + assert!( + json_output.contains("\"column\": 10"), + "Missing column number" + ); + + ensure_callback_cleared(); + } +} diff --git a/datadog-crashtracker/src/shared/constants.rs b/datadog-crashtracker/src/shared/constants.rs index e2da98c1ad..43b56a95e3 100644 --- a/datadog-crashtracker/src/shared/constants.rs +++ b/datadog-crashtracker/src/shared/constants.rs @@ -9,6 +9,9 @@ pub const DD_CRASHTRACK_BEGIN_COUNTERS: &str = "DD_CRASHTRACK_BEGIN_COUNTERS"; pub const DD_CRASHTRACK_BEGIN_FILE: &str = "DD_CRASHTRACK_BEGIN_FILE"; pub const DD_CRASHTRACK_BEGIN_METADATA: &str = "DD_CRASHTRACK_BEGIN_METADATA"; pub const DD_CRASHTRACK_BEGIN_PROCINFO: &str = "DD_CRASHTRACK_BEGIN_PROCESSINFO"; +pub const DD_CRASHTRACK_BEGIN_RUNTIME_STACK_FRAME: &str = "DD_CRASHTRACK_BEGIN_RUNTIME_STACK_FRAME"; +pub const DD_CRASHTRACK_BEGIN_RUNTIME_STACK_STRING: &str = + "DD_CRASHTRACK_BEGIN_RUNTIME_STACK_STRING"; pub const DD_CRASHTRACK_BEGIN_SIGINFO: &str = "DD_CRASHTRACK_BEGIN_SIGINFO"; pub const DD_CRASHTRACK_BEGIN_SPAN_IDS: &str = "DD_CRASHTRACK_BEGIN_SPAN_IDS"; pub const DD_CRASHTRACK_BEGIN_STACKTRACE: &str = "DD_CRASHTRACK_BEGIN_STACKTRACE"; @@ -21,6 +24,8 @@ pub const DD_CRASHTRACK_END_COUNTERS: &str = "DD_CRASHTRACK_END_COUNTERS"; pub const DD_CRASHTRACK_END_FILE: &str = "DD_CRASHTRACK_END_FILE"; pub const DD_CRASHTRACK_END_METADATA: &str = "DD_CRASHTRACK_END_METADATA"; pub const DD_CRASHTRACK_END_PROCINFO: &str = "DD_CRASHTRACK_END_PROCESSINFO"; +pub const DD_CRASHTRACK_END_RUNTIME_STACK_FRAME: &str = "DD_CRASHTRACK_END_RUNTIME_STACK_FRAME"; +pub const DD_CRASHTRACK_END_RUNTIME_STACK_STRING: &str = "DD_CRASHTRACK_END_RUNTIME_STACK_STRING"; pub const DD_CRASHTRACK_END_SIGINFO: &str = "DD_CRASHTRACK_END_SIGINFO"; pub const DD_CRASHTRACK_END_SPAN_IDS: &str = "DD_CRASHTRACK_END_SPAN_IDS"; pub const DD_CRASHTRACK_END_STACKTRACE: &str = "DD_CRASHTRACK_END_STACKTRACE"; diff --git a/docs/RFCs/artifacts/0011-crashtracker-unified-runtime-stack-schema.json b/docs/RFCs/artifacts/0011-crashtracker-unified-runtime-stack-schema.json new file mode 100644 index 0000000000..5f17eead70 --- /dev/null +++ b/docs/RFCs/artifacts/0011-crashtracker-unified-runtime-stack-schema.json @@ -0,0 +1,602 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CrashInfo", + "type": "object", + "required": [ + "data_schema_version", + "error", + "incomplete", + "metadata", + "os_info", + "timestamp", + "uuid" + ], + "properties": { + "counters": { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int64" + } + }, + "data_schema_version": { + "type": "string" + }, + "error": { + "$ref": "#/definitions/ErrorData" + }, + "experimental": { + "anyOf": [ + { + "$ref": "#/definitions/Experimental" + }, + { + "type": "null" + } + ] + }, + "files": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "fingerprint": { + "type": [ + "string", + "null" + ] + }, + "incomplete": { + "type": "boolean" + }, + "log_messages": { + "type": "array", + "items": { + "type": "string" + } + }, + "metadata": { + "$ref": "#/definitions/Metadata" + }, + "os_info": { + "$ref": "#/definitions/OsInfo" + }, + "proc_info": { + "anyOf": [ + { + "$ref": "#/definitions/ProcInfo" + }, + { + "type": "null" + } + ] + }, + "sig_info": { + "anyOf": [ + { + "$ref": "#/definitions/SigInfo" + }, + { + "type": "null" + } + ] + }, + "span_ids": { + "type": "array", + "items": { + "$ref": "#/definitions/Span" + } + }, + "timestamp": { + "type": "string" + }, + "trace_ids": { + "type": "array", + "items": { + "$ref": "#/definitions/Span" + } + }, + "uuid": { + "type": "string" + } + }, + "definitions": { + "BuildIdType": { + "type": "string", + "enum": [ + "GNU", + "GO", + "PDB", + "SHA1" + ] + }, + "ErrorData": { + "type": "object", + "required": [ + "is_crash", + "kind", + "source_type", + "stack" + ], + "properties": { + "is_crash": { + "type": "boolean" + }, + "kind": { + "$ref": "#/definitions/ErrorKind" + }, + "message": { + "type": [ + "string", + "null" + ] + }, + "source_type": { + "$ref": "#/definitions/SourceType" + }, + "stack": { + "$ref": "#/definitions/StackTrace" + }, + "threads": { + "type": "array", + "items": { + "$ref": "#/definitions/ThreadData" + } + } + } + }, + "ErrorKind": { + "type": "string", + "enum": [ + "Panic", + "UnhandledException", + "UnixSignal" + ] + }, + "Experimental": { + "type": "object", + "properties": { + "additional_tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "runtime_stack": { + "anyOf": [ + { + "$ref": "#/definitions/RuntimeStack" + }, + { + "type": "null" + } + ] + }, + "ucontext": { + "type": [ + "string", + "null" + ] + } + } + }, + "FileType": { + "type": "string", + "enum": [ + "APK", + "ELF", + "PE" + ] + }, + "Metadata": { + "type": "object", + "required": [ + "family", + "library_name", + "library_version" + ], + "properties": { + "family": { + "type": "string" + }, + "library_name": { + "type": "string" + }, + "library_version": { + "type": "string" + }, + "tags": { + "description": "A list of \"key:value\" tuples.", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "OsInfo": { + "type": "object", + "required": [ + "architecture", + "bitness", + "os_type", + "version" + ], + "properties": { + "architecture": { + "type": "string" + }, + "bitness": { + "type": "string" + }, + "os_type": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "ProcInfo": { + "type": "object", + "required": [ + "pid" + ], + "properties": { + "pid": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + } + }, + "RuntimeFrame": { + "description": "JSON-serializable runtime stack frame", + "type": "object", + "properties": { + "column": { + "description": "Column number in source file", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "file": { + "description": "Source file name", + "type": [ + "string", + "null" + ] + }, + "function": { + "description": "Fully qualified function/method name Examples: \"my_package.submodule.TestClass.method\", \"MyClass::method\", \"namespace::function\"", + "type": [ + "string", + "null" + ] + }, + "line": { + "description": "Line number in source file", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + } + }, + "RuntimeStack": { + "description": "Runtime stack representation for JSON serialization", + "type": "object", + "required": [ + "format" + ], + "properties": { + "format": { + "description": "Format identifier for this runtime stack", + "type": "string" + }, + "frames": { + "description": "Array of runtime-specific stack frames (optional, mutually exclusive with stacktrace_string)", + "type": "array", + "items": { + "$ref": "#/definitions/RuntimeFrame" + } + }, + "stacktrace_string": { + "description": "Raw stacktrace string (optional, mutually exclusive with frames)", + "type": [ + "string", + "null" + ] + } + } + }, + "SiCodes": { + "description": "See https://man7.org/linux/man-pages/man2/sigaction.2.html MUST REMAIN IN SYNC WITH THE ENUM IN emit_sigcodes.c", + "type": "string", + "enum": [ + "BUS_ADRALN", + "BUS_ADRERR", + "BUS_MCEERR_AO", + "BUS_MCEERR_AR", + "BUS_OBJERR", + "ILL_BADSTK", + "ILL_COPROC", + "ILL_ILLADR", + "ILL_ILLOPC", + "ILL_ILLOPN", + "ILL_ILLTRP", + "ILL_PRVOPC", + "ILL_PRVREG", + "SEGV_ACCERR", + "SEGV_BNDERR", + "SEGV_MAPERR", + "SEGV_PKUERR", + "SI_ASYNCIO", + "SI_KERNEL", + "SI_MESGQ", + "SI_QUEUE", + "SI_SIGIO", + "SI_TIMER", + "SI_TKILL", + "SI_USER", + "SYS_SECCOMP", + "UNKNOWN" + ] + }, + "SigInfo": { + "type": "object", + "required": [ + "si_code", + "si_code_human_readable", + "si_signo", + "si_signo_human_readable" + ], + "properties": { + "si_addr": { + "type": [ + "string", + "null" + ] + }, + "si_code": { + "type": "integer", + "format": "int32" + }, + "si_code_human_readable": { + "$ref": "#/definitions/SiCodes" + }, + "si_signo": { + "type": "integer", + "format": "int32" + }, + "si_signo_human_readable": { + "$ref": "#/definitions/SignalNames" + } + } + }, + "SignalNames": { + "description": "See https://man7.org/linux/man-pages/man7/signal.7.html", + "type": "string", + "enum": [ + "SIGHUP", + "SIGINT", + "SIGQUIT", + "SIGILL", + "SIGTRAP", + "SIGABRT", + "SIGBUS", + "SIGFPE", + "SIGKILL", + "SIGUSR1", + "SIGSEGV", + "SIGUSR2", + "SIGPIPE", + "SIGALRM", + "SIGTERM", + "SIGCHLD", + "SIGCONT", + "SIGSTOP", + "SIGTSTP", + "SIGTTIN", + "SIGTTOU", + "SIGURG", + "SIGXCPU", + "SIGXFSZ", + "SIGVTALRM", + "SIGPROF", + "SIGWINCH", + "SIGIO", + "SIGSYS", + "SIGEMT", + "SIGINFO", + "UNKNOWN" + ] + }, + "SourceType": { + "type": "string", + "enum": [ + "Crashtracking" + ] + }, + "Span": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "string" + }, + "thread_name": { + "type": [ + "string", + "null" + ] + } + } + }, + "StackFrame": { + "type": "object", + "properties": { + "build_id": { + "type": [ + "string", + "null" + ] + }, + "build_id_type": { + "anyOf": [ + { + "$ref": "#/definitions/BuildIdType" + }, + { + "type": "null" + } + ] + }, + "column": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "comments": { + "type": "array", + "items": { + "type": "string" + } + }, + "file": { + "type": [ + "string", + "null" + ] + }, + "file_type": { + "anyOf": [ + { + "$ref": "#/definitions/FileType" + }, + { + "type": "null" + } + ] + }, + "function": { + "type": [ + "string", + "null" + ] + }, + "ip": { + "type": [ + "string", + "null" + ] + }, + "line": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "mangled_name": { + "type": [ + "string", + "null" + ] + }, + "module_base_address": { + "type": [ + "string", + "null" + ] + }, + "path": { + "type": [ + "string", + "null" + ] + }, + "relative_address": { + "type": [ + "string", + "null" + ] + }, + "sp": { + "type": [ + "string", + "null" + ] + }, + "symbol_address": { + "type": [ + "string", + "null" + ] + } + } + }, + "StackTrace": { + "type": "object", + "required": [ + "format", + "frames", + "incomplete" + ], + "properties": { + "format": { + "type": "string" + }, + "frames": { + "type": "array", + "items": { + "$ref": "#/definitions/StackFrame" + } + }, + "incomplete": { + "type": "boolean" + } + } + }, + "ThreadData": { + "type": "object", + "required": [ + "crashed", + "name", + "stack" + ], + "properties": { + "crashed": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "stack": { + "$ref": "#/definitions/StackTrace" + }, + "state": { + "type": [ + "string", + "null" + ] + } + } + } + } +}