Skip to main content

libdd_crashtracker/collector/
crash_handler.rs

1// Copyright 2023-Present Datadog, Inc. https://www.datadoghq.com/
2// SPDX-License-Identifier: Apache-2.0
3
4#![cfg(unix)]
5
6use super::collector_manager::Collector;
7use super::receiver_manager::Receiver;
8use super::saguard::{SaGuard, SuppressionMode};
9use super::signal_handler_manager::chain_signal_handler;
10use crate::crash_info::Metadata;
11use crate::shared::configuration::CrashtrackerConfiguration;
12use crate::StackTrace;
13use core::ptr;
14use core::sync::atomic::Ordering::{Acquire, Relaxed, SeqCst};
15use core::sync::atomic::{AtomicBool, AtomicI32, AtomicPtr, AtomicU64};
16use errno::{errno, set_errno};
17use libc::{c_void, pid_t, siginfo_t, ucontext_t};
18use libdd_common::timeout::TimeoutManager;
19use std::os::fd::OwnedFd;
20use std::os::unix::io::{AsRawFd, FromRawFd};
21use std::os::unix::net::UnixStream;
22use std::panic;
23use std::panic::PanicHookInfo;
24
25// Note that this file makes use the following async-signal safe functions in a signal handler.
26// <https://man7.org/linux/man-pages/man7/signal-safety.7.html>
27// - clock_gettime
28// - close (although Rust may call `free` because we call the higher-level nix interface)
29// - dup2
30// - fork (on MacOS; Linux calls `fork()` directly as syscall)
31// - kill
32// - poll
33// - raise
34// - read
35// - sigaction
36// - write
37
38// These represent data used by the crashtracker.
39// Using mutexes inside a signal handler is not allowed, so use `AtomicPtr`
40// instead to get atomicity.
41// These should always be either: null_mut, or `Box::into_raw()`
42// This means that we can always clean up the memory inside one of these using
43// `Box::from_raw` to recreate the box, then dropping it.
44static METADATA: AtomicPtr<(Metadata, String)> = AtomicPtr::new(ptr::null_mut());
45static CONFIG: AtomicPtr<(CrashtrackerConfiguration, String)> = AtomicPtr::new(ptr::null_mut());
46static PANIC_MESSAGE: AtomicPtr<String> = AtomicPtr::new(ptr::null_mut());
47
48type PanicHook = Box<dyn Fn(&PanicHookInfo<'_>) + Send + Sync>;
49static PREVIOUS_PANIC_HOOK: AtomicPtr<PanicHook> = AtomicPtr::new(ptr::null_mut());
50
51/// Expected PID of the socket-based receiver (sidecar), set during trusted
52/// initialization. A value of 0 means "not set" and will cause the signal handler
53/// to skip granting ptrace permission
54static EXPECTED_RECEIVER_PID: AtomicI32 = AtomicI32::new(0);
55
56/// Register the expected receiver PID for socket-based crash receivers.
57///
58/// When `collect_all_threads` is enabled and the receiver is reached via a Unix
59/// socket (not a forked child), the signal handler will only grant ptrace
60/// permission (`PR_SET_PTRACER`) if the socket peer's PID (via `SO_PEERCRED`)
61/// matches this value.
62///
63/// Call this during trusted initialization (after connecting to or spawning
64/// the sidecar) with the sidecar's PID
65///
66/// SAFETY:
67///     This function is safe to call from any context, its a single atomic store.
68pub fn set_expected_receiver_pid(pid: pid_t) {
69    EXPECTED_RECEIVER_PID.store(pid, Relaxed);
70}
71
72/// Returns the currently registered expected receiver PID, or 0 if unset.
73pub fn get_expected_receiver_pid() -> pid_t {
74    EXPECTED_RECEIVER_PID.load(Relaxed)
75}
76
77#[derive(Debug, thiserror::Error)]
78pub enum CrashHandlerError {
79    #[error("No crashtracking config available")]
80    NoConfig,
81    #[error("No crashtracking metadata available")]
82    NoMetadata,
83    #[error("Failed to spawn receiver: {0}")]
84    ReceiverSpawnError(#[from] super::receiver_manager::ReceiverError),
85    #[error("Failed to spawn collector: {0}")]
86    CollectorSpawnError(#[from] super::collector_manager::CollectorSpawnError),
87}
88
89/// Updates the crashtracker metadata for this process
90/// Metadata is stored in a global variable and sent to the crashtracking
91/// receiver when a crash occurs.
92///
93/// PRECONDITIONS:
94///     None
95/// SAFETY:
96///     Crash-tracking functions are not guaranteed to be reentrant.
97///     No other crash-handler functions should be called concurrently.
98/// ATOMICITY:
99///     This function uses a swap on an atomic pointer.
100pub fn update_metadata(metadata: Metadata) -> anyhow::Result<()> {
101    let metadata_string = serde_json::to_string(&metadata)?;
102    let box_ptr = Box::into_raw(Box::new((metadata, metadata_string)));
103    let old = METADATA.swap(box_ptr, SeqCst);
104    if !old.is_null() {
105        // Safety: This can only come from a box above.
106        unsafe {
107            core::mem::drop(Box::from_raw(old));
108        }
109    }
110    Ok(())
111}
112
113/// Format a panic message with optional location information.
114fn format_message(
115    category: &str,
116    panic_message: &str,
117    location: Option<&panic::Location>,
118) -> String {
119    let base = if panic_message.is_empty() {
120        format!("Process panicked with {}", category)
121    } else {
122        format!("Process panicked with {} \"{}\"", category, panic_message)
123    };
124
125    match location {
126        Some(loc) => format!("{} ({}:{}:{})", base, loc.file(), loc.line(), loc.column()),
127        None => base,
128    }
129}
130
131/// Register the panic hook.
132///
133/// This function is used to register the panic hook and store the previous hook.
134/// PRECONDITIONS:
135///     None
136/// SAFETY:
137///     Crash-tracking functions are not guaranteed to be reentrant.
138///     No other crash-handler functions should be called concurrently.
139/// ATOMICITY:
140///     This function uses a swap on an atomic pointer.
141pub fn register_panic_hook() -> anyhow::Result<()> {
142    // register only once, if it is already registered, do nothing
143    if !PREVIOUS_PANIC_HOOK.load(SeqCst).is_null() {
144        return Ok(());
145    }
146
147    let old_hook = panic::take_hook();
148    let old_hook_ptr = Box::into_raw(Box::new(old_hook));
149    PREVIOUS_PANIC_HOOK.swap(old_hook_ptr, SeqCst);
150    panic::set_hook(Box::new(|panic_info| {
151        // Extract panic message from payload (supports &str and String)
152        let message = if let Some(&s) = panic_info.payload().downcast_ref::<&str>() {
153            format_message("message", s, panic_info.location())
154        } else if let Some(s) = panic_info.payload().downcast_ref::<String>() {
155            format_message("message", s.as_str(), panic_info.location())
156        } else {
157            // For non-string types, use a generic message
158            format_message("unknown type", "", panic_info.location())
159        };
160
161        // Store the message, cleaning up any old message
162        let message_ptr = PANIC_MESSAGE.swap(Box::into_raw(Box::new(message)), SeqCst);
163        // message_ptr should be null, but just in case.
164        if !message_ptr.is_null() {
165            unsafe {
166                core::mem::drop(Box::from_raw(message_ptr));
167            }
168        }
169
170        call_previous_panic_hook(panic_info);
171    }));
172    Ok(())
173}
174
175/// Call the previous panic hook.
176///
177/// This function is used to call the previous panic hook.
178/// PRECONDITIONS:
179///     None
180/// SAFETY:
181///     Crash-tracking functions are not guaranteed to be reentrant.
182///     No other crash-handler functions should be called concurrently.
183fn call_previous_panic_hook(panic_info: &PanicHookInfo<'_>) {
184    let old_hook_ptr = PREVIOUS_PANIC_HOOK.load(SeqCst);
185    if !old_hook_ptr.is_null() {
186        // Safety: This pointer can only come from Box::into_raw above in register_panic_hook.
187        // We borrow it here without taking ownership so it remains valid for future calls.
188        unsafe {
189            let old_hook = &*old_hook_ptr;
190            old_hook(panic_info);
191        }
192    }
193}
194
195/// Updates the crashtracker config for this process
196/// Config is stored in a global variable and sent to the crashtracking
197/// receiver when a crash occurs.
198///
199/// PRECONDITIONS:
200///     None
201/// SAFETY:
202///     Crash-tracking functions are not guaranteed to be reentrant.
203///     No other crash-handler functions should be called concurrently.
204/// ATOMICITY:
205///     This function uses a swap on an atomic pointer.
206pub fn update_config(config: CrashtrackerConfiguration) -> anyhow::Result<()> {
207    let config_string = serde_json::to_string(&config)?;
208    let box_ptr = Box::into_raw(Box::new((config, config_string)));
209    let old = CONFIG.swap(box_ptr, SeqCst);
210    if !old.is_null() {
211        // Safety: This can only come from a box above.
212        unsafe {
213            core::mem::drop(Box::from_raw(old));
214        }
215    }
216    Ok(())
217}
218
219pub(crate) extern "C" fn handle_posix_sigaction(
220    signum: i32,
221    sig_info: *mut siginfo_t,
222    ucontext: *mut c_void,
223) {
224    // Save errno
225    let errno = errno();
226
227    // Handle the signal.  Note this has a guard to ensure that we only generate
228    // one crash report per process.
229    let _ = handle_posix_signal_impl(sig_info, ucontext as *mut ucontext_t);
230
231    // Restore errno
232    set_errno(errno);
233    // SAFETY: No preconditions.
234
235    unsafe { chain_signal_handler(signum, sig_info, ucontext) };
236}
237
238static ENABLED: AtomicBool = AtomicBool::new(true);
239
240/// Disables the crashtracker.
241/// Note that this does not restore the old signal handlers, but rather turns crash-tracking into a
242/// no-op, and then chains the old handlers.  This means that handlers registered after the
243/// crashtracker will continue to work as expected.
244///
245/// # Preconditions
246///   None
247/// # Safety
248///   None
249/// # Atomicity
250///   This function is atomic and idempotent.  Calling it multiple times is allowed.
251pub fn disable() {
252    ENABLED.store(false, SeqCst);
253}
254
255/// Enables the crashtracker, if had been previously disabled.
256/// If crashtracking has not been initialized, this function will have no effect.
257///
258/// # Preconditions
259///   None
260/// # Safety
261///   None
262/// # Atomicity
263///   This function is atomic and idempotent.  Calling it multiple times is allowed.
264pub fn enable() {
265    ENABLED.store(true, SeqCst);
266}
267
268fn handle_posix_signal_impl(
269    sig_info: *const siginfo_t,
270    ucontext: *const ucontext_t,
271) -> Result<(), CrashHandlerError> {
272    if !ENABLED.load(SeqCst) {
273        return Ok(());
274    }
275
276    // If this code hits a stack overflow, then it will result in a segfault.  That situation is
277    // protected by the one-time guard.
278
279    // One-time guard to guarantee at most one crash per process
280    static NUM_TIMES_CALLED: AtomicU64 = AtomicU64::new(0);
281    if NUM_TIMES_CALLED.fetch_add(1, SeqCst) > 0 {
282        // In the case where some lower-level signal handler recovered the error
283        // we don't want to spam the system with calls.  Make this one shot.
284        return Ok(());
285    }
286
287    #[cfg(target_os = "linux")]
288    {
289        super::api::mark_preload_logger_collector();
290    }
291
292    // Suppress SIGPIPE and defer SIGCHLD during crash handling.
293    // SIGCHLD is block-only because SIG_IGN changes child reaping semantics (waitpid/ECHILD),
294    // which can interfere with receiver/collector process cleanup.
295    let _sa_guard = SaGuard::new_with_modes(&[
296        (
297            nix::sys::signal::Signal::SIGCHLD,
298            SuppressionMode::BlockOnly,
299        ),
300        (
301            nix::sys::signal::Signal::SIGPIPE,
302            SuppressionMode::IgnoreAndBlock,
303        ),
304    ]);
305
306    // Take config and metadata out of global storage.
307    // We borrow via raw pointer and intentionally leak (do not reconstruct the Box) to avoid
308    // calling `drop`, and therefore `free`, inside a signal handler, which is not
309    // async-signal-safe.  Once the one-time guard is passed, this storage is never updated again.
310    let config_ptr = take_config_ptr();
311    if config_ptr.is_null() {
312        return Err(CrashHandlerError::NoConfig);
313    }
314    let (config, config_str) = unsafe { &*config_ptr };
315
316    let metadata_ptr = take_metadata_ptr();
317    if metadata_ptr.is_null() {
318        return Err(CrashHandlerError::NoMetadata);
319    }
320    let (_metadata, metadata_string) = unsafe { &*metadata_ptr };
321
322    // Take the panic message pointer. We borrow via raw pointer and
323    // intentionally leak (do not reconstruct the Box) to avoid calling
324    // `free` in the signal handler.
325    let panic_message_ptr = PANIC_MESSAGE.swap(ptr::null_mut(), Acquire);
326
327    // Prefer the panic message; fall back to a stored assert-failure
328    // message (captured by our __assert_fail GOT hook on SIGABRT).
329    let message: Option<&str> = if !panic_message_ptr.is_null() {
330        // SAFETY: the pointer was created by `Box::into_raw(Box::new(String))`
331        // in the panic hook and has not been freed.
332        Some(unsafe { &*panic_message_ptr })
333    } else {
334        #[cfg(all(target_os = "linux", target_pointer_width = "64"))]
335        {
336            super::assert_interceptor::take_assert_message()
337        }
338        #[cfg(not(all(target_os = "linux", target_pointer_width = "64")))]
339        {
340            None
341        }
342    };
343
344    let timeout_manager = TimeoutManager::new(config.timeout());
345
346    let receiver = Receiver::from_crashtracker_config(config)?;
347
348    // Enable ptrace permissions for receiver if multi-thread collection is enabled.
349    // For fork/exec receivers, we have the child PID directly (trusted: we spawned it).
350    // For socket-based receivers (PHP sidecar), verify the peer PID matches the
351    // expected receiver PID that was registered during trusted initialization
352    #[cfg(target_os = "linux")]
353    if config.collect_all_threads() {
354        grant_ptracer_permission(&receiver);
355    }
356
357    let collector = Collector::spawn(
358        &receiver,
359        config,
360        config_str,
361        metadata_string,
362        message,
363        sig_info,
364        ucontext,
365    )?;
366
367    // We're done. Wrap up our interaction with the receiver.
368    collector.finish(&timeout_manager);
369    receiver.finish(&timeout_manager);
370
371    Ok(())
372}
373
374/// Atomically swaps the metadata pointer to null and returns the old raw pointer.
375/// Async-signal-safe (only performs an atomic swap).
376///
377/// Callers are responsible for the returned memory:
378/// - Signal handlers: borrow via `&*ptr` and intentionally leak (avoids signal-unsafe `free`).
379fn take_metadata_ptr() -> *mut (crate::crash_info::Metadata, String) {
380    METADATA.swap(ptr::null_mut(), SeqCst)
381}
382
383/// Atomically swaps the config pointer to null and returns the old raw pointer.
384/// Async-signal-safe (only performs an atomic swap).
385///
386/// Callers are responsible for the returned memory:
387/// - Signal handlers: borrow via `&*ptr` and intentionally leak (avoids signal-unsafe `free`).
388fn take_config_ptr() -> *mut (
389    crate::shared::configuration::CrashtrackerConfiguration,
390    String,
391) {
392    CONFIG.swap(ptr::null_mut(), SeqCst)
393}
394
395/// Takes the current metadata out of global storage, leaving it unset.
396/// The returned value is properly owned and will be dropped by the caller.
397/// Do NOT call from a signal handler; use `take_metadata_ptr` instead.
398fn take_metadata() -> Option<(crate::crash_info::Metadata, String)> {
399    let ptr = take_metadata_ptr();
400    if ptr.is_null() {
401        None
402    } else {
403        // Safety: ptr was created by Box::into_raw in update_metadata
404        Some(*unsafe { Box::from_raw(ptr) })
405    }
406}
407
408/// Takes the current config out of global storage, leaving it unset.
409/// The returned value is properly owned and will be dropped by the caller.
410/// Do NOT call from a signal handler; use `take_config_ptr` instead.
411fn take_config() -> Option<(
412    crate::shared::configuration::CrashtrackerConfiguration,
413    String,
414)> {
415    let ptr = take_config_ptr();
416    if ptr.is_null() {
417        None
418    } else {
419        // Safety: ptr was created by Box::into_raw in update_config
420        Some(*unsafe { Box::from_raw(ptr) })
421    }
422}
423
424/// Grant the receiver process permission to ptrace this process via `PR_SET_PTRACER`.
425///
426/// For fork/exec receivers we have the child PID directly (trusted: we spawned it).
427/// For socket-based receivers (e.g. PHP sidecar), we verify the peer PID via
428/// `SO_PEERCRED` matches the expected receiver PID registered during initialization.
429///
430/// This is async-signal-safe: only calls `getsockopt` and `prctl`.
431#[cfg(target_os = "linux")]
432fn grant_ptracer_permission(receiver: &Receiver) {
433    let ptracer_pid = match receiver.handle.pid {
434        Some(pid) => pid,
435        None => {
436            let expected_pid = get_expected_receiver_pid();
437            if expected_pid <= 0 {
438                0
439            } else {
440                let mut cred: libc::ucred = unsafe { core::mem::zeroed() };
441                let mut len = core::mem::size_of::<libc::ucred>() as libc::socklen_t;
442                // SAFETY: getsockopt is async-signal-safe
443                let ret = unsafe {
444                    libc::getsockopt(
445                        receiver.handle.uds_fd,
446                        libc::SOL_SOCKET,
447                        libc::SO_PEERCRED,
448                        &mut cred as *mut _ as *mut libc::c_void,
449                        &mut len,
450                    )
451                };
452                if ret == 0 && cred.pid == expected_pid {
453                    cred.pid
454                } else {
455                    0
456                }
457            }
458        }
459    };
460    if ptracer_pid > 0 {
461        // SAFETY: prctl is async-signal-safe
462        unsafe {
463            libc::prctl(libc::PR_SET_PTRACER, ptracer_pid as libc::c_ulong);
464        }
465    }
466}
467
468/// This function is designed to be when a program is at a terminal state
469/// and the application wants to report an unhandled exception to the crashtracker
470/// If this crashes, then the application will also crash. Ensure that this API is
471/// called when the application is at a terminal state and exit quickly after.
472///
473/// This API handles reporting both the crash ping and the crash report for the
474/// unhandled exception.
475///
476/// Preconditions:
477/// - The crashtracker must be started
478/// - The stacktrace must be valid
479///
480///  This function will spawn the receiver process and call an emit function to pipe over
481///  the crash data. We don't use the collector process because we are not in a signal handler
482///  Rather, we call emit_crashreport directly and pipe over data to the receiver
483pub fn report_unhandled_exception(
484    exception_type: Option<&str>,
485    exception_message: Option<&str>,
486    stacktrace: StackTrace,
487) -> Result<(), CrashHandlerError> {
488    // Although both report_unhandled_exception and handle_posix_signal_impl do similar things of
489    //   1. Getting config and metadata
490    //   2. Spawn receiver
491    //   3. Set timeout
492    //   4. Emit report
493    //   5. Finish logic
494    // It is not worth going out of the way to combine these because:
495    //   1. The signal handler borrows and leaks (async-signal-safe); unifying them would require a
496    //      generic or trait just to paper over a deliberate constraint, making the split harder to
497    //      see.
498    //   2. The emit + finish: completely different mechanisms (fork vs. direct IO, Collector vs.
499    //      raw ProcessHandle).
500    //   3. TimeoutManager::new(config.timeout()); one line, not worth extracting.
501
502    // Turn crashtracker off to prevent a recursive crash report emission
503    // We do not turn it back on because this function is not intended to be used as
504    // a recurring mechanism to report exceptions. We expect the application to exit
505    // after
506    disable();
507
508    let (config, config_str) = take_config().ok_or(CrashHandlerError::NoConfig)?;
509    let (_metadata, metadata_str) = take_metadata().ok_or(CrashHandlerError::NoMetadata)?;
510
511    let receiver = Receiver::from_crashtracker_config(&config)?;
512
513    #[cfg(target_os = "linux")]
514    if config.collect_all_threads() {
515        grant_ptracer_permission(&receiver);
516    }
517
518    let timeout_manager = TimeoutManager::new(config.timeout());
519
520    let pid = unsafe { libc::getpid() };
521    let tid = libdd_common::threading::get_current_thread_id() as libc::pid_t;
522
523    // This allocates but that is okay because we are not in the signal handling path
524    // Both error type and error message are user-controlled and may contain newlines or protocol
525    // sentinel strings (DD_CRASHTRACK_*). We need to escape newlines here, as the receiver treats
526    // new lines as separate sections in the crash report, and this allows consumers to
527    // potentially inject artitrary configuration and other sections into the crash report.
528    // emit_message adds a second sanitization pass as defense-in-depth at the protocol
529    // boundary.
530    let error_type_str = exception_type
531        .unwrap_or("<unknown>")
532        .replace('\n', "\\n")
533        .replace('\r', "\\r");
534    let error_message_str = exception_message
535        .unwrap_or("<no message>")
536        .replace('\n', "\\n")
537        .replace('\r', "\\r");
538    let message = format!(
539        "Process was terminated due to an unhandled exception of type '{error_type_str}'. \
540         Message: {error_message_str}"
541    );
542
543    // Duplicate the socket fd before handing it to UnixStream so we retain an fd to poll on after
544    // the write end is closed.  OwnedFd is the scope guard: it closes poll_fd on any exit path.
545    //
546    // SAFETY: dup() returns a fresh fd; we are its sole owner.  ProcessHandle only polls it
547    // (wait_for_pollhup) and has no Drop impl, so it never closes the fd. Closing it here
548    // after finish() returns is the first and only close
549    let poll_fd = unsafe { OwnedFd::from_raw_fd(libc::dup(receiver.handle.uds_fd)) };
550    let receiver_pid = receiver.handle.pid;
551
552    {
553        let mut unix_stream = unsafe { UnixStream::from_raw_fd(receiver.handle.uds_fd) };
554        let _ = super::emitters::emit_crashreport(
555            &mut unix_stream,
556            &config,
557            &config_str,
558            &metadata_str,
559            Some(message.as_str()),
560            super::emitters::CrashKindData::UnhandledException { stacktrace },
561            pid,
562            tid,
563        );
564        // unix_stream is dropped here, closing the write end of the socket.
565        // This signals EOF to the receiver so it can finish writing the crash report.
566    }
567
568    // Wait for the receiver to signal it is done (POLLHUP on the dup'd fd), then reap it.
569    // poll_fd is dropped at the end of this function, closing the fd.
570    let finish_handle =
571        super::process_handle::ProcessHandle::new(poll_fd.as_raw_fd(), receiver_pid);
572    finish_handle.finish(&timeout_manager);
573
574    Ok(())
575}
576#[cfg(test)]
577mod tests {
578    use super::*;
579    use core::time::Duration;
580
581    fn make_test_metadata() -> Metadata {
582        Metadata {
583            library_name: "test-lib".to_string(),
584            library_version: "1.0.0".to_string(),
585            family: "test-family".to_string(),
586            tags: vec![],
587        }
588    }
589
590    fn make_test_config() -> CrashtrackerConfiguration {
591        let builder = CrashtrackerConfiguration::builder();
592        builder.timeout(Duration::from_secs(1)).build().unwrap()
593    }
594
595    /// Clears METADATA global, properly freeing any existing Box
596    fn clear_metadata() {
597        let ptr = METADATA.swap(ptr::null_mut(), SeqCst);
598        if !ptr.is_null() {
599            unsafe { drop(Box::from_raw(ptr)) };
600        }
601    }
602
603    /// Clears CONFIG global, properly freeing any existing Box
604    fn clear_config() {
605        let ptr = CONFIG.swap(ptr::null_mut(), SeqCst);
606        if !ptr.is_null() {
607            unsafe { drop(Box::from_raw(ptr)) };
608        }
609    }
610
611    #[test]
612    fn test_register_panic_hook() {
613        assert!(PREVIOUS_PANIC_HOOK.load(SeqCst).is_null());
614
615        let result = register_panic_hook();
616        assert!(result.is_ok());
617
618        assert!(!PREVIOUS_PANIC_HOOK.load(SeqCst).is_null());
619    }
620
621    #[test]
622    fn test_panic_message_storage_and_retrieval() {
623        // Test that panic messages can be stored and retrieved via atomic pointer
624        let test_message = "test panic message".to_string();
625        let message_ptr = Box::into_raw(Box::new(test_message.clone()));
626
627        // Store the message
628        let old_ptr = PANIC_MESSAGE.swap(message_ptr, SeqCst);
629        assert!(old_ptr.is_null()); // Should be null initially
630
631        // Retrieve and verify
632        let retrieved_ptr = PANIC_MESSAGE.swap(ptr::null_mut(), SeqCst);
633        assert!(!retrieved_ptr.is_null());
634
635        unsafe {
636            let retrieved_message = *Box::from_raw(retrieved_ptr);
637            assert_eq!(retrieved_message, test_message);
638        }
639    }
640
641    #[test]
642    fn test_panic_message_null_handling() {
643        // Test that null message pointers are handled correctly
644        PANIC_MESSAGE.store(ptr::null_mut(), SeqCst);
645
646        let message_ptr = PANIC_MESSAGE.load(SeqCst);
647        assert!(message_ptr.is_null());
648
649        // Swapping null with null should be safe
650        let old_ptr = PANIC_MESSAGE.swap(ptr::null_mut(), SeqCst);
651        assert!(old_ptr.is_null());
652    }
653
654    #[test]
655    fn test_panic_message_replacement() {
656        // Test that replacing an existing message cleans up the old one
657        let message1 = "first message".to_string();
658        let message2 = "second message".to_string();
659
660        let ptr1 = Box::into_raw(Box::new(message1));
661        let ptr2 = Box::into_raw(Box::new(message2.clone()));
662
663        PANIC_MESSAGE.store(ptr1, SeqCst);
664        let old_ptr = PANIC_MESSAGE.swap(ptr2, SeqCst);
665
666        // Old pointer should be the first one
667        assert_eq!(old_ptr, ptr1);
668
669        // Clean up both
670        unsafe {
671            drop(Box::from_raw(old_ptr));
672            let final_ptr = PANIC_MESSAGE.swap(ptr::null_mut(), SeqCst);
673            let final_message = *Box::from_raw(final_ptr);
674            assert_eq!(final_message, message2);
675        }
676    }
677
678    #[test]
679    fn test_metadata_update_atomic() {
680        // Test that metadata updates are atomic
681        let metadata = Metadata {
682            library_name: "test".to_string(),
683            library_version: "1.0.0".to_string(),
684            family: "test_family".to_string(),
685            tags: vec![],
686        };
687
688        let result = update_metadata(metadata.clone());
689        assert!(result.is_ok());
690
691        // Verify metadata was stored
692        let metadata_ptr = METADATA.load(SeqCst);
693        assert!(!metadata_ptr.is_null());
694
695        unsafe {
696            let (stored_metadata, _) = &*metadata_ptr;
697            assert_eq!(stored_metadata.library_name, "test");
698        }
699    }
700
701    #[test]
702    fn test_format_message_with_message_and_location() {
703        let location = panic::Location::caller();
704        let result = format_message("message", "test panic", Some(location));
705
706        assert!(result.starts_with("Process panicked with message \"test panic\" ("));
707        assert!(result.contains(&format!("{}:", location.file())));
708        assert!(result.contains(&format!(":{}", location.line())));
709        assert!(result.ends_with(&format!("{})", location.column())));
710    }
711
712    #[test]
713    fn test_format_message_with_message_no_location() {
714        let result = format_message("message", "test panic", None);
715        assert_eq!(result, "Process panicked with message \"test panic\"");
716    }
717
718    #[test]
719    fn test_format_message_empty_message_with_location() {
720        let location = panic::Location::caller();
721        let result = format_message("unknown type", "", Some(location));
722
723        assert!(result.starts_with("Process panicked with unknown type ("));
724        assert!(result.contains(&format!("{}:", location.file())));
725        assert!(result.ends_with(&format!("{})", location.column())));
726    }
727
728    #[test]
729    fn test_format_message_empty_message_no_location() {
730        let result = format_message("unknown type", "", None);
731        assert_eq!(result, "Process panicked with unknown type");
732    }
733
734    #[test]
735    fn test_format_message_different_categories() {
736        let result1 = format_message("message", "test", None);
737        assert_eq!(result1, "Process panicked with message \"test\"");
738
739        let result2 = format_message("unknown type", "", None);
740        assert_eq!(result2, "Process panicked with unknown type");
741
742        let result3 = format_message("custom category", "content", None);
743        assert_eq!(result3, "Process panicked with custom category \"content\"");
744    }
745
746    #[test]
747    fn test_format_message_with_special_characters() {
748        let result = format_message("message", "test \"quoted\" 'text'", None);
749        assert_eq!(
750            result,
751            "Process panicked with message \"test \"quoted\" 'text'\""
752        );
753    }
754
755    // take_metadata_ptr
756
757    #[test]
758    fn test_take_metadata_ptr_returns_null_when_unset() {
759        clear_metadata();
760        assert!(take_metadata_ptr().is_null());
761    }
762
763    #[test]
764    fn test_take_metadata_ptr_takes_value_and_leaves_null() {
765        clear_metadata();
766        update_metadata(make_test_metadata()).unwrap();
767
768        let ptr = take_metadata_ptr();
769        assert!(!ptr.is_null());
770
771        // Storage is now null; a second take returns null.
772        assert!(take_metadata_ptr().is_null());
773
774        // Reconstruct the Box to avoid a leak.
775        unsafe { drop(Box::from_raw(ptr)) };
776    }
777
778    #[test]
779    fn test_take_metadata_ptr_preserves_data() {
780        clear_metadata();
781        let metadata = make_test_metadata();
782        update_metadata(metadata.clone()).unwrap();
783
784        let ptr = take_metadata_ptr();
785        assert!(!ptr.is_null());
786
787        let (stored_metadata, stored_json) = unsafe { &*ptr };
788        assert_eq!(stored_metadata.library_name, metadata.library_name);
789        assert_eq!(stored_metadata.library_version, metadata.library_version);
790        assert_eq!(stored_metadata.family, metadata.family);
791        // The serialised string must be valid non-empty JSON.
792        assert!(!stored_json.is_empty());
793        assert!(serde_json::from_str::<serde_json::Value>(stored_json).is_ok());
794
795        unsafe { drop(Box::from_raw(ptr)) };
796    }
797
798    // take_config_ptr
799
800    #[test]
801    fn test_take_config_ptr_returns_null_when_unset() {
802        clear_config();
803        assert!(take_config_ptr().is_null());
804    }
805
806    #[test]
807    fn test_take_config_ptr_takes_value_and_leaves_null() {
808        clear_config();
809        update_config(make_test_config()).unwrap();
810
811        let ptr = take_config_ptr();
812        assert!(!ptr.is_null());
813
814        // Storage is now null; a second take returns null.
815        assert!(take_config_ptr().is_null());
816
817        unsafe { drop(Box::from_raw(ptr)) };
818    }
819
820    // take_metadata
821
822    #[test]
823    fn test_take_metadata_returns_none_when_unset() {
824        clear_metadata();
825        assert!(take_metadata().is_none());
826    }
827
828    #[test]
829    fn test_take_metadata_returns_value_and_leaves_none() {
830        clear_metadata();
831        let metadata = make_test_metadata();
832        update_metadata(metadata.clone()).unwrap();
833
834        let (taken_metadata, taken_json) = take_metadata().expect("should return Some");
835        assert_eq!(taken_metadata.library_name, metadata.library_name);
836        assert_eq!(taken_metadata.library_version, metadata.library_version);
837        assert_eq!(taken_metadata.family, metadata.family);
838        assert!(!taken_json.is_empty());
839
840        // Second take: storage is empty.
841        assert!(take_metadata().is_none());
842    }
843
844    // take_config
845
846    #[test]
847    fn test_take_config_returns_none_when_unset() {
848        clear_config();
849        assert!(take_config().is_none());
850    }
851
852    #[test]
853    fn test_take_config_returns_value_and_leaves_none() {
854        clear_config();
855        let config = make_test_config();
856        update_config(config.clone()).unwrap();
857
858        let (taken_config, taken_json) = take_config().expect("should return Some");
859        assert_eq!(taken_config, config);
860        assert!(!taken_json.is_empty());
861        assert!(serde_json::from_str::<serde_json::Value>(&taken_json).is_ok());
862
863        // Second take: storage is empty.
864        assert!(take_config().is_none());
865    }
866}