libdd-crashtracker 1.0.0

Detects program crashes and reports them to datadog backend.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
// Copyright 2023-Present Datadog, Inc. https://www.datadoghq.com/
// SPDX-License-Identifier: Apache-2.0

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, invoke_runtime_callback_with_writer, is_runtime_callback_registered,
    CallbackData,
};
use crate::shared::constants::*;
use crate::{translate_si_code, CrashtrackerConfiguration, SignalNames, StacktraceCollection};
use backtrace::Frame;
use libc::{siginfo_t, ucontext_t};
use std::{
    fs::File,
    io::{Read, Write},
};
use thiserror::Error;

#[derive(Debug, Error)]
pub enum EmitterError {
    #[error("Failed to write to output: {0}")]
    WriteError(#[from] std::io::Error),
    #[error("Failed to open file: {0}")]
    FileOpenError(std::io::Error),
    #[error("Null pointer provided for ucontext")]
    NullUcontext,
    #[error("Null pointer provided for siginfo")]
    NullSiginfo,
    #[error("Counter error: {0}")]
    CounterError(#[from] crate::collector::counters::CounterError),
    #[error("Atomic set error: {0}")]
    AtomicSetError(#[from] crate::collector::atomic_set::AtomicSetError),
}

/// Emit a stacktrace onto the given handle as formatted json.
/// SAFETY:
///     Crash-tracking functions are not reentrant.
///     No other crash-handler functions should be called concurrently.
/// ATOMICITY:
///     This function is not atomic. A crash during its execution may lead to
///     unexpected crash-handling behaviour.
/// SIGNAL SAFETY:
///     Getting a backtrace on rust is not guaranteed to be signal safe.
///     https://github.com/rust-lang/backtrace-rs/issues/414
///     Calculating the `ip` of the frames seems safe, but resolving the frames
///     sometimes crashes.
unsafe fn emit_backtrace_by_frames(
    w: &mut impl Write,
    resolve_frames: StacktraceCollection,
    fault_ip: usize,
) -> Result<(), EmitterError> {
    // https://docs.rs/backtrace/latest/backtrace/index.html
    writeln!(w, "{DD_CRASHTRACK_BEGIN_STACKTRACE}")?;

    // Absolute addresses appear to be safer to collect during a crash than debug info.
    fn emit_absolute_addresses(w: &mut impl Write, frame: &Frame) -> Result<(), EmitterError> {
        write!(w, "\"ip\": \"{:?}\"", frame.ip())?;
        if let Some(module_base_address) = frame.module_base_address() {
            write!(w, ", \"module_base_address\": \"{module_base_address:?}\"",)?;
        }
        write!(w, ", \"sp\": \"{:?}\"", frame.sp())?;
        write!(w, ", \"symbol_address\": \"{:?}\"", frame.symbol_address())?;
        Ok(())
    }

    let mut ip_found = false;
    loop {
        backtrace::trace_unsynchronized(|frame| {
            // Skip all stack frames until we encounter the determined crash instruction pointer
            // (fault_ip). These initial frames belong exclusively to the crash tracker and the
            // backtrace functionality and are therefore not relevant for troubleshooting.
            let ip = frame.ip();
            if ip as usize == fault_ip {
                ip_found = true;
            }
            if !ip_found {
                return true;
            }
            if resolve_frames == StacktraceCollection::EnabledWithInprocessSymbols {
                backtrace::resolve_frame_unsynchronized(frame, |symbol| {
                    #[allow(clippy::unwrap_used)]
                    write!(w, "{{").unwrap();
                    #[allow(clippy::unwrap_used)]
                    emit_absolute_addresses(w, frame).unwrap();
                    if let Some(column) = symbol.colno() {
                        #[allow(clippy::unwrap_used)]
                        write!(w, ", \"column\": {column}").unwrap();
                    }
                    if let Some(file) = symbol.filename() {
                        // The debug printer for path already wraps it in `"` marks.
                        #[allow(clippy::unwrap_used)]
                        write!(w, ", \"file\": {file:?}").unwrap();
                    }
                    if let Some(function) = symbol.name() {
                        #[allow(clippy::unwrap_used)]
                        write!(w, ", \"function\": \"{function}\"").unwrap();
                    }
                    if let Some(line) = symbol.lineno() {
                        #[allow(clippy::unwrap_used)]
                        write!(w, ", \"line\": {line}").unwrap();
                    }
                    #[allow(clippy::unwrap_used)]
                    writeln!(w, "}}").unwrap();
                    // Flush eagerly to ensure that each frame gets emitted even if the next one
                    // fails
                    #[allow(clippy::unwrap_used)]
                    w.flush().unwrap();
                });
            } else {
                #[allow(clippy::unwrap_used)]
                write!(w, "{{").unwrap();
                #[allow(clippy::unwrap_used)]
                emit_absolute_addresses(w, frame).unwrap();
                #[allow(clippy::unwrap_used)]
                writeln!(w, "}}").unwrap();
                // Flush eagerly to ensure that each frame gets emitted even if the next one fails
                #[allow(clippy::unwrap_used)]
                w.flush().unwrap();
            }
            true // keep going to the next frame
        });
        if ip_found {
            break;
        }
        // emit anything at all, if the crashing frame is not found for some reason
        ip_found = true;
    }
    writeln!(w, "{DD_CRASHTRACK_END_STACKTRACE}")?;
    w.flush()?;
    Ok(())
}

pub(crate) fn emit_crashreport(
    pipe: &mut impl Write,
    config: &CrashtrackerConfiguration,
    config_str: &str,
    metadata_string: &str,
    sig_info: *const siginfo_t,
    ucontext: *const ucontext_t,
    ppid: i32,
) -> Result<(), EmitterError> {
    emit_metadata(pipe, metadata_string)?;
    emit_config(pipe, config_str)?;
    emit_siginfo(pipe, sig_info)?;
    emit_ucontext(pipe, ucontext)?;
    emit_procinfo(pipe, ppid)?;
    emit_counters(pipe)?;
    emit_spans(pipe)?;
    consume_and_emit_additional_tags(pipe)?;
    emit_traces(pipe)?;

    #[cfg(target_os = "linux")]
    emit_proc_self_maps(pipe)?;

    // Getting a backtrace on rust is not guaranteed to be signal safe
    // https://github.com/rust-lang/backtrace-rs/issues/414
    // let current_backtrace = backtrace::Backtrace::new();
    // In fact, if we look into the code here, we see mallocs.
    // https://doc.rust-lang.org/src/std/backtrace.rs.html#332
    // Do this last, so even if it crashes, we still get the other info.
    if config.resolve_frames() != StacktraceCollection::Disabled {
        let fault_ip = extract_ip(ucontext);
        unsafe { emit_backtrace_by_frames(pipe, config.resolve_frames(), fault_ip)? };
    }

    if is_runtime_callback_registered() {
        emit_runtime_stack(pipe)?;
    }

    writeln!(pipe, "{DD_CRASHTRACK_DONE}")?;
    pipe.flush()?;

    Ok(())
}

fn emit_config(w: &mut impl Write, config_str: &str) -> Result<(), EmitterError> {
    writeln!(w, "{DD_CRASHTRACK_BEGIN_CONFIG}")?;
    writeln!(w, "{config_str}")?;
    writeln!(w, "{DD_CRASHTRACK_END_CONFIG}")?;
    w.flush()?;
    Ok(())
}

fn emit_metadata(w: &mut impl Write, metadata_str: &str) -> Result<(), EmitterError> {
    writeln!(w, "{DD_CRASHTRACK_BEGIN_METADATA}")?;
    writeln!(w, "{metadata_str}")?;
    writeln!(w, "{DD_CRASHTRACK_END_METADATA}")?;
    w.flush()?;
    Ok(())
}

fn emit_procinfo(w: &mut impl Write, pid: i32) -> Result<(), EmitterError> {
    writeln!(w, "{DD_CRASHTRACK_BEGIN_PROCINFO}")?;
    writeln!(w, "{{\"pid\": {pid} }}")?;
    writeln!(w, "{DD_CRASHTRACK_END_PROCINFO}")?;
    w.flush()?;
    Ok(())
}

#[cfg(target_os = "linux")]
/// Assumes that the memory layout of the current process (child) is identical to
/// the layout of the target process (parent), which should always be true.
fn emit_proc_self_maps(w: &mut impl Write) -> Result<(), EmitterError> {
    emit_text_file(w, "/proc/self/maps")?;
    Ok(())
}

#[cfg(target_os = "linux")]
fn emit_ucontext(w: &mut impl Write, ucontext: *const ucontext_t) -> Result<(), EmitterError> {
    if ucontext.is_null() {
        return Err(EmitterError::NullUcontext);
    }
    writeln!(w, "{DD_CRASHTRACK_BEGIN_UCONTEXT}")?;
    // SAFETY: the pointer is given to us by the signal handler, and is non-null.
    writeln!(w, "{:?}", unsafe { *ucontext })?;
    writeln!(w, "{DD_CRASHTRACK_END_UCONTEXT}")?;
    w.flush()?;
    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 = unsafe { get_registered_callback() };

    let callback = match callback {
        Some(callback) => callback,
        None => return Ok(()),
    };

    match callback {
        CallbackData::Frame(_) => emit_runtime_stack_by_frames(w),
        CallbackData::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}")?;
    unsafe { invoke_runtime_callback_with_writer(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}")?;
    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() {
        return Err(EmitterError::NullUcontext);
    }
    // On MacOS, the actual machine context is behind a second pointer.
    // SAFETY: the pointer is given to us by the signal handler, and is non-null.
    let mcontext = unsafe { *ucontext }.uc_mcontext;
    writeln!(w, "{DD_CRASHTRACK_BEGIN_UCONTEXT}")?;
    // SAFETY: the pointer is given to us by the signal handler, and is non-null.
    write!(w, "{:?}", unsafe { *ucontext })?;
    if !mcontext.is_null() {
        // SAFETY: the pointer is given to us by the signal handler, and is non-null.
        write!(w, ", {:?}", unsafe { *mcontext })?;
    }
    writeln!(w)?;
    writeln!(w, "{DD_CRASHTRACK_END_UCONTEXT}")?;
    w.flush()?;
    Ok(())
}

fn emit_siginfo(w: &mut impl Write, sig_info: *const siginfo_t) -> Result<(), EmitterError> {
    if sig_info.is_null() {
        return Err(EmitterError::NullSiginfo);
    }

    let si_signo = unsafe { (*sig_info).si_signo };
    let si_signo_human_readable: SignalNames = si_signo.into();

    // Derive the faulting address from `sig_info`
    // https://man7.org/linux/man-pages/man2/sigaction.2.html
    // SIGILL, SIGFPE, SIGSEGV, SIGBUS, and SIGTRAP fill in si_addr with the address of the fault.
    let si_addr: Option<usize> = match si_signo {
        libc::SIGILL | libc::SIGFPE | libc::SIGSEGV | libc::SIGBUS | libc::SIGTRAP => {
            Some(unsafe { (*sig_info).si_addr() as usize })
        }
        _ => None,
    };

    let si_code = unsafe { (*sig_info).si_code };
    let si_code_human_readable = translate_si_code(si_signo, si_code);

    writeln!(w, "{DD_CRASHTRACK_BEGIN_SIGINFO}")?;
    write!(w, "{{")?;
    write!(w, "\"si_code\": {si_code}")?;
    write!(
        w,
        ", \"si_code_human_readable\": \"{si_code_human_readable:?}\""
    )?;
    write!(w, ", \"si_signo\": {si_signo}")?;
    write!(
        w,
        ", \"si_signo_human_readable\": \"{si_signo_human_readable:?}\""
    )?;
    if let Some(si_addr) = si_addr {
        write!(w, ", \"si_addr\": \"{si_addr:#018x}\"")?;
    }
    writeln!(w, "}}")?;
    writeln!(w, "{DD_CRASHTRACK_END_SIGINFO}")?;
    w.flush()?;
    Ok(())
}

/// Emit a file onto the given handle.
/// The file will be emitted in the format
///
/// DD_CRASHTRACK_BEGIN_FILE
/// <FILE BYTES>
/// DD_CRASHTRACK_END_FILE
///
/// PRECONDITIONS:
///     This function assumes that the crash-tracker is initialized.
///     The receiver expects the file to contain valid UTF-8 compatible text.
/// SAFETY:
///     Crash-tracking functions are not reentrant.
///     No other crash-handler functions should be called concurrently.
/// ATOMICITY:
///     This function is not atomic. A crash during its execution may lead to
///     unexpected crash-handling behaviour.
/// SIGNAL SAFETY:
///     This function is careful to only write to the handle, without doing any
///     unnecessary mutexes or memory allocation.
#[allow(dead_code)]
fn emit_text_file(w: &mut impl Write, path: &str) -> Result<(), EmitterError> {
    // open is signal safe
    // https://man7.org/linux/man-pages/man7/signal-safety.7.html
    let mut file = File::open(path).map_err(EmitterError::FileOpenError)?;

    // Reading the file into a fixed buffer is signal safe.
    // Doing anything more complicated may involve allocation which is not.
    // So, just read it in, and then immediately push it out to the pipe.
    const BUFFER_LEN: usize = 512;
    let mut buffer = [0u8; BUFFER_LEN];

    writeln!(w, "{DD_CRASHTRACK_BEGIN_FILE} {path}")?;

    loop {
        let read_count = file.read(&mut buffer)?;
        w.write_all(&buffer[..read_count])?;
        if read_count == 0 {
            break;
        }
    }
    writeln!(w, "\n{DD_CRASHTRACK_END_FILE} \"{path}\"")?;
    w.flush()?;
    Ok(())
}

fn extract_ip(ucontext: *const ucontext_t) -> usize {
    unsafe {
        #[cfg(all(target_os = "macos", target_arch = "x86_64"))]
        return (*(*ucontext).uc_mcontext).__ss.__rip as usize;
        #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
        return (*(*ucontext).uc_mcontext).__ss.__pc as usize;

        #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
        return (*ucontext).uc_mcontext.gregs[libc::REG_RIP as usize] as usize;
        #[cfg(all(target_os = "linux", target_arch = "aarch64"))]
        return (*ucontext).uc_mcontext.pc as usize;
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::str;

    #[inline(never)]
    fn inner_test_emit_backtrace_with_symbols(collection: StacktraceCollection) -> Vec<u8> {
        let mut ip_of_test_fn = 0;
        let mut skip = 3;
        unsafe {
            backtrace::trace_unsynchronized(|frame| {
                ip_of_test_fn = frame.ip() as usize;
                skip -= 1;
                skip > 0
            })
        };
        let mut buf = Vec::new();
        unsafe {
            emit_backtrace_by_frames(&mut buf, collection, ip_of_test_fn).expect("to work ;-)");
        }
        buf
    }

    #[test]
    #[cfg_attr(miri, ignore)]
    fn test_emit_backtrace_disabled() {
        let buf = inner_test_emit_backtrace_with_symbols(StacktraceCollection::Disabled);
        let out = str::from_utf8(&buf).expect("to be valid UTF8");
        assert!(out.contains("BEGIN_STACKTRACE"));
        assert!(out.contains("END_STACKTRACE"));
        assert!(out.contains("\"ip\":"));
        assert!(
            !out.contains("\"column\":"),
            "'column' key must not be emitted"
        );
        assert!(!out.contains("\"file\":"), "'file' key must not be emitted");
        assert!(
            !out.contains("\"function\":"),
            "'function' key must not be emitted"
        );
        assert!(!out.contains("\"line\":"), "'line' key must not be emitted");
    }

    #[test]
    #[cfg_attr(miri, ignore)]
    fn test_emit_backtrace_with_symbols() {
        let buf = inner_test_emit_backtrace_with_symbols(
            StacktraceCollection::EnabledWithInprocessSymbols,
        );
        // retrieve stack pointer for this function
        let out = str::from_utf8(&buf).expect("to be valid UTF8");
        assert!(out.contains("BEGIN_STACKTRACE"));
        assert!(out.contains("END_STACKTRACE"));
        // basic structure assertions
        assert!(out.contains("\"column\":"), "'column' key missing");
        assert!(out.contains("\"file\":"), "'file' key missing");
        assert!(out.contains("\"function\":"), "'function' key missing");
        assert!(out.contains("\"line\":"), "'line' key missing");
        // filter assertions
        assert!(
            !out.contains("emitters::emit_backtrace_by_frames"),
            "crashtracker itself must be filtered, found 'backtrace::backtrace::libunwind'"
        );
        assert!(
            !out.contains("backtrace::backtrace"),
            "crashtracker itself must be filtered away, found 'backtrace::backtrace'"
        );
    }
}