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
//! Backtrace capture slot for the SIGUSR2 handler.
use std::sync::atomic::{AtomicUsize, Ordering};
use std::{cell::UnsafeCell, mem::MaybeUninit};
pub(crate) static CAPTURE: Capture = Capture::new();
/// Single backtrace slot shared between the ping loop and the SIGUSR2 handler.
///
/// The handler only uses atomics and writes into a preallocated slot, it never
/// locks or allocates. State is `tid << 3 | tag`, so a late signal cannot fill
/// a slot armed for another thread.
pub(crate) struct Capture {
state: AtomicUsize,
slot: UnsafeCell<MaybeUninit<ntex_error::BacktraceRaw>>,
}
// SAFETY: Atomic states grant the handler exclusive writes and publish them to the single arming watchdog.
unsafe impl Sync for Capture {}
impl Capture {
const IDLE: usize = 0;
const ARMED: usize = 1;
const WRITING: usize = 2;
const READY: usize = 3;
const ABANDONED: usize = 4;
const fn new() -> Self {
Self {
state: AtomicUsize::new(Self::IDLE),
slot: UnsafeCell::new(MaybeUninit::uninit()),
}
}
#[allow(clippy::cast_sign_loss)]
fn state(tid: i32, tag: usize) -> usize {
((tid as u32 as usize) << 3) | tag
}
/// Arm the slot for `tid`, fails if another capture is in progress.
pub(crate) fn arm(&self, tid: i32) -> bool {
self.state
.compare_exchange(
Self::IDLE,
Self::state(tid, Self::ARMED),
Ordering::Acquire,
Ordering::Relaxed,
)
.is_ok()
}
/// Called from the signal handler.
pub(crate) fn capture(&self, tid: i32, location: &'static str) {
if self
.state
.compare_exchange(
Self::state(tid, Self::ARMED),
Self::state(tid, Self::WRITING),
Ordering::Acquire,
Ordering::Relaxed,
)
.is_err()
{
return;
}
// SAFETY: `WRITING` serializes handler captures; the platform unwinder is assumed signal-safe.
let bt = unsafe { ntex_error::BacktraceRaw::with_filename_unsynchronized(location) };
// SAFETY: Winning `ARMED` to `WRITING` grants this handler exclusive access to the slot.
unsafe { (*self.slot.get()).write(bt) };
if self
.state
.compare_exchange(
Self::state(tid, Self::WRITING),
Self::state(tid, Self::READY),
Ordering::Release,
Ordering::Relaxed,
)
.is_err()
{
// abandoned by the ping loop
// SAFETY: This handler initialized the slot; abandonment leaves cleanup exclusively to it.
unsafe { (*self.slot.get()).assume_init_drop() };
self.state.store(Self::IDLE, Ordering::Release);
}
}
pub(crate) fn take(&self, tid: i32) -> Option<ntex_error::BacktraceRaw> {
if self.state.load(Ordering::Acquire) == Self::state(tid, Self::READY) {
// SAFETY: The arming watchdog alone consumes `READY`; acquire observes the handler's initialized slot.
let bt = unsafe { (*self.slot.get()).assume_init_read() };
self.state.store(Self::IDLE, Ordering::Release);
Some(bt)
} else {
None
}
}
pub(crate) fn disarm(&self, tid: i32) {
loop {
let cur = self.state.load(Ordering::Acquire);
let next = if cur == Self::state(tid, Self::ARMED) {
Self::IDLE
} else if cur == Self::state(tid, Self::WRITING) {
// handler is still running, it releases the slot when done
Self::state(tid, Self::ABANDONED)
} else if cur == Self::state(tid, Self::READY) {
let _ = self.take(tid);
return;
} else {
return;
};
if self
.state
.compare_exchange(cur, next, Ordering::AcqRel, Ordering::Acquire)
.is_ok()
{
return;
}
}
}
}
#[cfg(test)]
#[path = "../tests/unit/capture.rs"]
mod tests;