1#![allow(unsafe_code)]
11
12pub mod spool;
13
14use std::cell::UnsafeCell;
15use std::collections::BTreeMap;
16use std::fs::File;
17use std::io;
18use std::path::PathBuf;
19use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, AtomicUsize, Ordering};
20use std::sync::{Arc, Mutex, OnceLock, Weak};
21use std::thread::JoinHandle;
22use std::time::Duration;
23
24use crash_handler::{CrashContext, CrashEventResult, CrashHandler};
25
26use self::spool::{CrashFrame, CrashMetadata, CrashModule, CrashThread, RECORD_SIZE};
27
28#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
30pub enum CrashPolicy {
31 #[default]
33 On,
34 Off,
36}
37
38pub const NO_CRASH_HANDLER_ENV: &str = "RUNNING_PROCESS_PROBE_NO_CRASH_HANDLER";
40
41#[derive(Debug, thiserror::Error)]
43pub enum InstallError {
44 #[error("cannot prepare crash spool: {0}")]
46 Spool(#[source] io::Error),
47 #[error("cannot attach native crash handler: {0}")]
49 Handler(#[source] crash_handler::Error),
50 #[error("cannot start crash snapshot sampler: {0}")]
52 Sampler(#[source] io::Error),
53 #[cfg(any(windows, target_os = "macos"))]
55 #[error("cannot chain the platform abort handler: {0}")]
56 AbortChain(#[source] io::Error),
57 #[error("crash capture was inherited across fork; exec before reinstalling")]
59 ForkedProcess,
60}
61
62pub struct CrashGuard {
64 runtime: Option<Arc<Runtime>>,
65 registration_id: Option<u64>,
66}
67
68impl CrashGuard {
69 pub fn inert() -> Self {
71 Self {
72 runtime: None,
73 registration_id: None,
74 }
75 }
76
77 pub fn is_armed(&self) -> bool {
79 self.runtime.as_ref().is_some_and(|runtime| {
80 runtime.pid == std::process::id() && runtime.handler_armed.load(Ordering::Acquire)
81 })
82 }
83
84 pub fn sample_ready(&self) -> bool {
86 self.runtime
87 .as_ref()
88 .is_some_and(|runtime| runtime.shared.sample_ready.load(Ordering::Acquire))
89 }
90
91 pub fn sample_thread_count(&self) -> usize {
93 self.runtime.as_ref().map_or(0, |runtime| {
94 runtime.shared.sample_thread_count.load(Ordering::Acquire)
95 })
96 }
97
98 pub fn spool_path(&self) -> Option<&std::path::Path> {
100 self.runtime.as_ref().map(|runtime| runtime.path.as_path())
101 }
102}
103
104impl Drop for CrashGuard {
105 fn drop(&mut self) {
106 if let (Some(runtime), Some(id)) = (&self.runtime, self.registration_id.take()) {
107 runtime.remove_registration(id);
108 }
109 }
110}
111
112static RUNTIME: OnceLock<Mutex<Weak<Runtime>>> = OnceLock::new();
113static OWNER_PID: AtomicU32 = AtomicU32::new(0);
114static HANDLER_TRANSITION: Mutex<()> = Mutex::new(());
115#[cfg(test)]
116static TEST_PAUSE_RESUME: AtomicBool = AtomicBool::new(false);
117#[cfg(test)]
118static TEST_RESUME_ENTERED: AtomicBool = AtomicBool::new(false);
119#[cfg(test)]
120static TEST_RELEASE_RESUME: AtomicBool = AtomicBool::new(false);
121
122pub fn install(policy: CrashPolicy, metadata: CrashMetadata) -> Result<CrashGuard, InstallError> {
124 if policy == CrashPolicy::Off || env_opted_out() {
125 return Ok(CrashGuard::inert());
126 }
127
128 let pid = std::process::id();
129 match OWNER_PID.compare_exchange(0, pid, Ordering::AcqRel, Ordering::Acquire) {
130 Ok(_) => {}
131 Err(owner) if owner == pid => {}
132 Err(_) => {
133 return Err(InstallError::ForkedProcess);
136 }
137 }
138
139 let _transition = match HANDLER_TRANSITION.lock() {
140 Ok(transition) => transition,
141 Err(poisoned) => poisoned.into_inner(),
142 };
143 let slot = RUNTIME.get_or_init(|| Mutex::new(Weak::new()));
144 let mut weak = match slot.lock() {
145 Ok(guard) => guard,
146 Err(poisoned) => poisoned.into_inner(),
147 };
148 if let Some(runtime) = weak.upgrade() {
149 if let Err(error) = runtime.resume_handler() {
150 drop(weak);
155 drop(_transition);
156 drop(runtime);
157 return Err(error);
158 }
159 let registration_id = runtime.add_registration(metadata);
160 return Ok(CrashGuard {
161 runtime: Some(runtime),
162 registration_id: Some(registration_id),
163 });
164 }
165
166 let (runtime, registration_id) = Runtime::new(metadata)?;
167 *weak = Arc::downgrade(&runtime);
168 Ok(CrashGuard {
169 runtime: Some(runtime),
170 registration_id: Some(registration_id),
171 })
172}
173
174fn env_opted_out() -> bool {
175 std::env::var_os(NO_CRASH_HANDLER_ENV).is_some_and(|value| {
176 let text = value.to_string_lossy();
177 text == "1" || text.eq_ignore_ascii_case("true") || text.eq_ignore_ascii_case("yes")
178 })
179}
180
181struct Runtime {
182 shared: Arc<Shared>,
183 handler: Mutex<Option<CrashHandler>>,
184 handler_armed: AtomicBool,
185 sampler: Option<JoinHandle<()>>,
186 path: PathBuf,
187 pid: u32,
188 registrations: Mutex<RegistrationState>,
189}
190
191impl Runtime {
192 fn new(metadata: CrashMetadata) -> Result<(Arc<Self>, u64), InstallError> {
193 let (file, path, template) = spool::create_sink(&metadata).map_err(InstallError::Spool)?;
194 let shared = Arc::new(Shared::new(file, template));
195
196 let handler = match attach_handler(&shared) {
197 Ok(handler) => handler,
198 Err(error) => {
199 drop(shared);
204 let _ = std::fs::remove_file(&path);
205 return Err(error);
206 }
207 };
208
209 let sampler_state = Arc::clone(&shared);
210 let sampler = match std::thread::Builder::new()
211 .name("rp-crash-sampler".into())
212 .spawn(move || sampler_loop(sampler_state))
213 {
214 Ok(sampler) => sampler,
215 Err(error) => {
216 #[cfg(windows)]
217 uninstall_windows_abort_chain();
218 #[cfg(target_os = "macos")]
219 uninstall_macos_abort_chain();
220 drop(handler);
221 drop(shared);
222 let _ = std::fs::remove_file(&path);
223 return Err(InstallError::Sampler(error));
224 }
225 };
226
227 let registration_id = 1;
228 let mut entries = BTreeMap::new();
229 entries.insert(registration_id, metadata);
230 Ok((
231 Arc::new(Self {
232 shared,
233 handler: Mutex::new(Some(handler)),
234 handler_armed: AtomicBool::new(true),
235 sampler: Some(sampler),
236 path,
237 pid: std::process::id(),
238 registrations: Mutex::new(RegistrationState {
239 next_id: registration_id + 1,
240 generation: 1,
241 entries,
242 }),
243 }),
244 registration_id,
245 ))
246 }
247
248 fn add_registration(&self, metadata: CrashMetadata) -> u64 {
249 let mut state = match self.registrations.lock() {
250 Ok(state) => state,
251 Err(poisoned) => poisoned.into_inner(),
252 };
253 let id = state.next_id;
254 state.next_id = state.next_id.wrapping_add(1).max(1);
255 state.entries.insert(id, metadata);
256 id
257 }
258
259 fn remove_registration(&self, id: u64) {
260 let selected = {
261 let mut state = match self.registrations.lock() {
262 Ok(state) => state,
263 Err(poisoned) => poisoned.into_inner(),
264 };
265 let was_selected = state.entries.first_key_value().map(|(key, _)| *key) == Some(id);
266 state.entries.remove(&id);
267 if was_selected {
268 state.generation = state.generation.wrapping_add(1).max(1);
269 state
270 .entries
271 .first_key_value()
272 .map(|(_, value)| (state.generation, value.clone()))
273 } else {
274 None
275 }
276 };
277 if let Some((generation, metadata)) = selected {
278 self.shared.set_metadata(generation, &metadata);
279 }
280 }
281
282 fn suspend_handler(&self) -> bool {
283 let mut handler = match self.handler.lock() {
284 Ok(handler) => handler,
285 Err(poisoned) => poisoned.into_inner(),
286 };
287 if handler.is_none() {
288 return false;
289 }
290 self.handler_armed.store(false, Ordering::Release);
291 #[cfg(windows)]
292 uninstall_windows_abort_chain();
293 #[cfg(target_os = "macos")]
294 uninstall_macos_abort_chain();
295 handler.take();
296 true
297 }
298
299 fn resume_handler(&self) -> Result<(), InstallError> {
300 let mut handler = match self.handler.lock() {
301 Ok(handler) => handler,
302 Err(poisoned) => poisoned.into_inner(),
303 };
304 #[cfg(test)]
305 if TEST_PAUSE_RESUME.load(Ordering::Acquire) {
306 TEST_RESUME_ENTERED.store(true, Ordering::Release);
307 while !TEST_RELEASE_RESUME.load(Ordering::Acquire) {
308 std::thread::yield_now();
309 }
310 TEST_PAUSE_RESUME.store(false, Ordering::Release);
311 }
312 if handler.is_none() {
313 *handler = Some(attach_handler(&self.shared)?);
314 self.handler_armed.store(true, Ordering::Release);
315 }
316 Ok(())
317 }
318}
319
320struct RegistrationState {
321 next_id: u64,
322 generation: u64,
323 entries: BTreeMap<u64, CrashMetadata>,
324}
325
326impl Drop for Runtime {
327 fn drop(&mut self) {
328 let handler = match self.handler.get_mut() {
329 Ok(handler) => handler,
330 Err(poisoned) => poisoned.into_inner(),
331 };
332 if self.pid != std::process::id() {
333 if let Some(handler) = handler.take() {
338 std::mem::forget(handler);
339 }
340 if let Some(sampler) = self.sampler.take() {
341 std::mem::forget(sampler);
342 }
343 return;
344 }
345 let _transition = match HANDLER_TRANSITION.lock() {
346 Ok(transition) => transition,
347 Err(poisoned) => poisoned.into_inner(),
348 };
349 #[cfg(windows)]
352 uninstall_windows_abort_chain();
353 #[cfg(target_os = "macos")]
354 uninstall_macos_abort_chain();
355 self.handler_armed.store(false, Ordering::Release);
356 handler.take();
357 self.shared.stop.store(true, Ordering::Release);
358 if let Some(handle) = self.sampler.take() {
359 let _ = handle.join();
360 }
361 let _ = std::fs::remove_file(&self.path);
363 }
364}
365
366fn attach_handler(shared: &Arc<Shared>) -> Result<CrashHandler, InstallError> {
367 #[cfg(windows)]
368 let previous_abort = windows_previous_abort_handler().map_err(InstallError::AbortChain)?;
369 #[cfg(target_os = "macos")]
370 let previous_abort = macos_previous_abort_action().map_err(InstallError::AbortChain)?;
371
372 let callback_state = Arc::clone(shared);
373 let event = unsafe {
377 crash_handler::make_crash_event(move |context| {
378 callback_state.handle_crash(context);
379 CrashEventResult::Handled(false)
380 })
381 };
382 let handler = CrashHandler::attach(event).map_err(InstallError::Handler)?;
383
384 #[cfg(windows)]
385 if let Err(error) = install_windows_abort_chain(shared, previous_abort) {
386 drop(handler);
387 return Err(InstallError::AbortChain(error));
388 }
389 #[cfg(target_os = "macos")]
390 if let Err(error) = install_macos_abort_chain(shared, previous_abort) {
391 drop(handler);
392 return Err(InstallError::AbortChain(error));
393 }
394
395 Ok(handler)
396}
397
398pub fn with_handler_suspended<R>(install: impl FnOnce() -> R) -> Result<R, InstallError> {
406 let pid = std::process::id();
407 let owner_before_lock = OWNER_PID.load(Ordering::Acquire);
408 if owner_before_lock != 0 && owner_before_lock != pid {
409 return Err(InstallError::ForkedProcess);
410 }
411
412 let runtime: Option<Arc<Runtime>>;
416 let transition = match HANDLER_TRANSITION.lock() {
417 Ok(transition) => transition,
418 Err(poisoned) => poisoned.into_inner(),
419 };
420 let owner = OWNER_PID.load(Ordering::Acquire);
421 if owner != 0 && owner != pid {
422 return Err(InstallError::ForkedProcess);
423 }
424 runtime = if owner == 0 {
425 None
426 } else {
427 let slot = RUNTIME.get_or_init(|| Mutex::new(Weak::new()));
428 let weak = match slot.lock() {
429 Ok(weak) => weak,
430 Err(poisoned) => poisoned.into_inner(),
431 };
432 weak.upgrade()
433 };
434
435 let Some(runtime) = runtime.as_ref() else {
436 return Ok(install());
437 };
438 runtime.resume_handler()?;
439 let suspended = runtime.suspend_handler();
440 let mut resume = ResumeHandler {
441 runtime: Arc::clone(runtime),
442 suspended,
443 };
444 let result = install();
445 let resumed = resume.finish();
446 drop(resume);
447 drop(transition);
448 resumed?;
449 Ok(result)
450}
451
452struct ResumeHandler {
453 runtime: Arc<Runtime>,
454 suspended: bool,
455}
456
457impl ResumeHandler {
458 fn finish(&mut self) -> Result<(), InstallError> {
459 if self.suspended {
460 self.runtime.resume_handler()?;
461 self.suspended = false;
462 }
463 Ok(())
464 }
465}
466
467impl Drop for ResumeHandler {
468 fn drop(&mut self) {
469 if self.suspended {
470 let _ = self.runtime.resume_handler();
471 }
472 }
473}
474
475struct Shared {
476 buffers: UnsafeCell<[[u8; RECORD_SIZE]; 2]>,
477 template: Mutex<[u8; RECORD_SIZE]>,
478 publish: Mutex<()>,
479 metadata_generation: AtomicU64,
480 active: AtomicUsize,
481 reading: AtomicBool,
482 in_handler: AtomicBool,
483 stop: AtomicBool,
484 sample_ready: AtomicBool,
485 sample_thread_count: AtomicUsize,
486 file: File,
487 pid: u32,
488}
489
490unsafe impl Sync for Shared {}
494
495impl Shared {
496 fn new(file: File, template: [u8; RECORD_SIZE]) -> Self {
497 Self {
498 buffers: UnsafeCell::new([template, template]),
499 template: Mutex::new(template),
500 publish: Mutex::new(()),
501 metadata_generation: AtomicU64::new(1),
502 active: AtomicUsize::new(0),
503 reading: AtomicBool::new(false),
504 in_handler: AtomicBool::new(false),
505 stop: AtomicBool::new(false),
506 sample_ready: AtomicBool::new(false),
507 sample_thread_count: AtomicUsize::new(0),
508 file,
509 pid: std::process::id(),
510 }
511 }
512
513 fn set_metadata(&self, generation: u64, metadata: &CrashMetadata) {
514 let _publish = match self.publish.lock() {
515 Ok(publish) => publish,
516 Err(poisoned) => poisoned.into_inner(),
517 };
518 if generation <= self.metadata_generation.load(Ordering::Acquire) {
519 return;
520 }
521 let mut template = match self.template.lock() {
522 Ok(template) => template,
523 Err(poisoned) => poisoned.into_inner(),
524 };
525 spool::put_metadata(&mut template, metadata);
526 let inactive = 1 - self.active.load(Ordering::Acquire);
527 let target = unsafe { &mut (*self.buffers.get())[inactive] };
531 *target = *template;
532 self.metadata_generation
533 .store(generation, Ordering::Release);
534 self.active.store(inactive, Ordering::Release);
535 }
536
537 fn handle_crash(&self, context: &CrashContext) {
538 if self.pid != std::process::id() {
539 return;
540 }
541 if self
542 .in_handler
543 .compare_exchange(false, true, Ordering::AcqRel, Ordering::Relaxed)
544 .is_err()
545 {
546 return;
547 }
548 self.reading.store(true, Ordering::SeqCst);
549 let index = self.active.load(Ordering::Acquire);
550 let record = unsafe { (*self.buffers.get())[index].as_mut_ptr() };
552 with_platform_fields(context, |fields| {
553 unsafe {
556 spool::put_crash(
557 record,
558 fields.tid,
559 fields.code,
560 fields.address,
561 fields.raw,
562 fields.raw_len,
563 );
564 write_once(&self.file, record, RECORD_SIZE);
565 }
566 });
567 self.reading.store(false, Ordering::Release);
568 self.in_handler.store(false, Ordering::Release);
569 }
570
571 #[cfg(any(windows, target_os = "macos"))]
572 fn handle_abort(&self, tid: u64, code: i64) {
573 if self.pid != std::process::id() {
574 return;
575 }
576 if self
577 .in_handler
578 .compare_exchange(false, true, Ordering::AcqRel, Ordering::Relaxed)
579 .is_err()
580 {
581 return;
582 }
583 self.reading.store(true, Ordering::SeqCst);
584 let index = self.active.load(Ordering::Acquire);
585 let record = unsafe { (*self.buffers.get())[index].as_mut_ptr() };
587 unsafe {
591 spool::put_crash(record, tid, code, 0, std::ptr::null(), 0);
592 write_once(&self.file, record, RECORD_SIZE);
593 }
594 self.reading.store(false, Ordering::Release);
595 self.in_handler.store(false, Ordering::Release);
596 }
597}
598
599#[cfg(windows)]
600static WINDOWS_ABORT_SHARED: std::sync::atomic::AtomicPtr<Shared> =
601 std::sync::atomic::AtomicPtr::new(std::ptr::null_mut());
602#[cfg(windows)]
603static WINDOWS_PREVIOUS_ABORT: AtomicUsize = AtomicUsize::new(0);
604#[cfg(windows)]
605static WINDOWS_ABORT_READERS: AtomicUsize = AtomicUsize::new(0);
606
607#[cfg(windows)]
608fn windows_previous_abort_handler() -> io::Result<usize> {
609 let previous = unsafe { libc::signal(libc::SIGABRT, libc::SIG_DFL) };
612 if previous == usize::MAX {
613 return Err(io::Error::last_os_error());
614 }
615 unsafe {
616 libc::signal(libc::SIGABRT, previous);
617 }
618 Ok(previous)
619}
620
621#[cfg(windows)]
622fn install_windows_abort_chain(shared: &Arc<Shared>, previous: usize) -> io::Result<()> {
623 WINDOWS_PREVIOUS_ABORT.store(previous, Ordering::Release);
628 WINDOWS_ABORT_SHARED.store(Arc::as_ptr(shared).cast_mut(), Ordering::Release);
629 let replaced =
630 unsafe { libc::signal(libc::SIGABRT, windows_abort_handler as *const () as usize) };
631 if replaced == usize::MAX {
632 WINDOWS_ABORT_SHARED.store(std::ptr::null_mut(), Ordering::Release);
633 return Err(io::Error::last_os_error());
634 }
635 Ok(())
636}
637
638#[cfg(windows)]
639fn uninstall_windows_abort_chain() {
640 WINDOWS_ABORT_SHARED.store(std::ptr::null_mut(), Ordering::SeqCst);
641 while WINDOWS_ABORT_READERS.load(Ordering::SeqCst) != 0 {
645 std::hint::spin_loop();
646 }
647 let previous = WINDOWS_PREVIOUS_ABORT.swap(0, Ordering::AcqRel);
648 if previous != 0 {
649 unsafe {
650 libc::signal(libc::SIGABRT, previous);
651 }
652 }
653}
654
655#[cfg(windows)]
656unsafe extern "C" fn windows_abort_handler(signal: i32, _subcode: i32) {
657 use windows_sys::Win32::System::Threading::GetCurrentThreadId;
658
659 WINDOWS_ABORT_READERS.fetch_add(1, Ordering::SeqCst);
660 let shared = WINDOWS_ABORT_SHARED.load(Ordering::SeqCst);
661 if let Some(shared) = unsafe { shared.as_ref() } {
662 shared.handle_abort(
663 u64::from(GetCurrentThreadId()),
664 i64::from(crash_handler::ExceptionCode::Abort as i32),
665 );
666 }
667 WINDOWS_ABORT_READERS.fetch_sub(1, Ordering::SeqCst);
668
669 let previous = WINDOWS_PREVIOUS_ABORT.load(Ordering::Acquire);
670 unsafe {
674 libc::signal(libc::SIGABRT, previous);
675 libc::raise(signal);
676 libc::signal(libc::SIGABRT, windows_abort_handler as *const () as usize);
677 }
678}
679
680#[cfg(target_os = "macos")]
681static MACOS_PREVIOUS_ABORT: std::sync::atomic::AtomicPtr<libc::sigaction> =
682 std::sync::atomic::AtomicPtr::new(std::ptr::null_mut());
683#[cfg(target_os = "macos")]
684static MACOS_ABORT_SHARED: std::sync::atomic::AtomicPtr<Shared> =
685 std::sync::atomic::AtomicPtr::new(std::ptr::null_mut());
686#[cfg(target_os = "macos")]
687static MACOS_ABORT_READERS: AtomicUsize = AtomicUsize::new(0);
688
689#[cfg(target_os = "macos")]
690fn macos_previous_abort_action() -> io::Result<libc::sigaction> {
691 let mut previous = std::mem::MaybeUninit::uninit();
692 let result = unsafe { libc::sigaction(libc::SIGABRT, std::ptr::null(), previous.as_mut_ptr()) };
693 if result == 0 {
694 Ok(unsafe { previous.assume_init() })
695 } else {
696 Err(io::Error::last_os_error())
697 }
698}
699
700#[cfg(target_os = "macos")]
701fn install_macos_abort_chain(shared: &Arc<Shared>, previous: libc::sigaction) -> io::Result<()> {
702 let previous = Box::into_raw(Box::new(previous));
714 let retired = MACOS_PREVIOUS_ABORT.swap(previous, Ordering::SeqCst);
715 while MACOS_ABORT_READERS.load(Ordering::SeqCst) != 0 {
716 std::hint::spin_loop();
717 }
718 if !retired.is_null() {
719 unsafe {
720 drop(Box::from_raw(retired));
721 }
722 }
723 MACOS_ABORT_SHARED.store(Arc::as_ptr(shared).cast_mut(), Ordering::Release);
724
725 let mut action: libc::sigaction = unsafe { std::mem::zeroed() };
726 unsafe {
727 libc::sigemptyset(&mut action.sa_mask);
728 libc::sigaddset(&mut action.sa_mask, libc::SIGABRT);
729 }
730 action.sa_sigaction = macos_abort_handler as *const () as usize;
731 action.sa_flags = libc::SA_SIGINFO;
732 if unsafe { libc::sigaction(libc::SIGABRT, &action, std::ptr::null_mut()) } != 0 {
733 let error = io::Error::last_os_error();
734 MACOS_ABORT_SHARED.store(std::ptr::null_mut(), Ordering::SeqCst);
738 while MACOS_ABORT_READERS.load(Ordering::SeqCst) != 0 {
739 std::hint::spin_loop();
740 }
741 return Err(error);
742 }
743 Ok(())
744}
745
746#[cfg(target_os = "macos")]
747fn uninstall_macos_abort_chain() {
748 unsafe {
752 let previous = MACOS_PREVIOUS_ABORT.load(Ordering::Acquire);
753 debug_assert!(!previous.is_null());
754 libc::sigaction(libc::SIGABRT, previous, std::ptr::null_mut());
755 }
756 MACOS_ABORT_SHARED.store(std::ptr::null_mut(), Ordering::SeqCst);
757 while MACOS_ABORT_READERS.load(Ordering::SeqCst) != 0 {
758 std::hint::spin_loop();
759 }
760}
761
762#[cfg(target_os = "macos")]
763unsafe extern "C" fn macos_abort_handler(
764 signal: i32,
765 _info: *mut libc::siginfo_t,
766 _context: *mut std::ffi::c_void,
767) {
768 MACOS_ABORT_READERS.fetch_add(1, Ordering::SeqCst);
769 let previous = MACOS_PREVIOUS_ABORT.load(Ordering::SeqCst);
770 let shared = MACOS_ABORT_SHARED.load(Ordering::SeqCst);
771 if let Some(shared) = unsafe { shared.as_ref() } {
772 shared.handle_abort(0, i64::from(signal));
775 }
776
777 unsafe {
781 libc::sigaction(signal, previous, std::ptr::null_mut());
782 }
783 MACOS_ABORT_READERS.fetch_sub(1, Ordering::SeqCst);
784 unsafe {
785 libc::raise(signal);
786 }
787}
788
789struct PlatformFields {
790 tid: u64,
791 code: i64,
792 address: u64,
793 raw: *const u8,
794 raw_len: usize,
795}
796
797#[cfg(any(target_os = "linux", target_os = "android"))]
798fn with_platform_fields<R>(
799 context: &CrashContext,
800 callback: impl FnOnce(PlatformFields) -> R,
801) -> R {
802 callback(PlatformFields {
803 tid: context.tid as u64,
804 code: i64::from(context.siginfo.ssi_signo),
805 address: context.siginfo.ssi_addr,
806 raw: context.as_bytes().as_ptr(),
807 raw_len: context.as_bytes().len(),
808 })
809}
810
811#[cfg(windows)]
812fn with_platform_fields<R>(
813 context: &CrashContext,
814 callback: impl FnOnce(PlatformFields) -> R,
815) -> R {
816 let mut address = 0u64;
817 let mut raw = (context as *const CrashContext).cast::<u8>();
818 let mut raw_len = std::mem::size_of::<CrashContext>();
819 unsafe {
823 if let Some(pointers) = context.exception_pointers.as_ref() {
824 if let Some(exception) = pointers.ExceptionRecord.as_ref() {
825 address = exception.ExceptionAddress as usize as u64;
826 }
827 if let Some(registers) = pointers.ContextRecord.as_ref() {
828 raw = std::ptr::from_ref(registers).cast::<u8>();
829 raw_len = std::mem::size_of_val(registers);
830 }
831 }
832 }
833 callback(PlatformFields {
834 tid: u64::from(context.thread_id),
835 code: i64::from(context.exception_code),
836 address,
837 raw,
838 raw_len,
839 })
840}
841
842#[cfg(target_os = "macos")]
843fn with_platform_fields<R>(
844 context: &CrashContext,
845 callback: impl FnOnce(PlatformFields) -> R,
846) -> R {
847 use mach2::kern_return::KERN_SUCCESS;
848 use mach2::thread_act::thread_get_state;
849
850 let mut identifier: libc::thread_identifier_info_data_t = unsafe { std::mem::zeroed() };
851 let mut identifier_count = libc::THREAD_IDENTIFIER_INFO_COUNT;
852 let identifier_result = unsafe {
855 libc::thread_info(
856 context.thread,
857 libc::THREAD_IDENTIFIER_INFO as libc::thread_flavor_t,
858 (&raw mut identifier).cast(),
859 &raw mut identifier_count,
860 )
861 };
862 let tid = if identifier_result == KERN_SUCCESS {
863 identifier.thread_id
864 } else {
865 u64::from(context.thread)
866 };
867 let code = context
868 .exception
869 .map(|exception| i64::from(exception.kind))
870 .unwrap_or(0);
871 let address = context
872 .exception
873 .and_then(|exception| exception.subcode)
874 .unwrap_or(0);
875
876 #[cfg(target_arch = "x86_64")]
877 {
878 use mach2::structs::x86_thread_state64_t;
879 use mach2::thread_status::x86_THREAD_STATE64;
880 let mut state = x86_thread_state64_t::new();
881 let mut count = x86_thread_state64_t::count();
882 let result = unsafe {
885 thread_get_state(
886 context.thread,
887 x86_THREAD_STATE64,
888 (&raw mut state).cast(),
889 &raw mut count,
890 )
891 };
892 if result == KERN_SUCCESS && count >= x86_thread_state64_t::count() {
893 return callback(PlatformFields {
894 tid,
895 code,
896 address,
897 raw: std::ptr::from_ref(&state).cast(),
898 raw_len: std::mem::size_of_val(&state),
899 });
900 }
901 }
902 #[cfg(target_arch = "aarch64")]
903 {
904 use mach2::structs::arm_thread_state64_t;
905 use mach2::thread_status::ARM_THREAD_STATE64;
906 let mut state = arm_thread_state64_t::new();
907 let mut count = arm_thread_state64_t::count();
908 let result = unsafe {
911 thread_get_state(
912 context.thread,
913 ARM_THREAD_STATE64,
914 (&raw mut state).cast(),
915 &raw mut count,
916 )
917 };
918 if result == KERN_SUCCESS && count >= arm_thread_state64_t::count() {
919 return callback(PlatformFields {
920 tid,
921 code,
922 address,
923 raw: std::ptr::from_ref(&state).cast(),
924 raw_len: std::mem::size_of_val(&state),
925 });
926 }
927 }
928
929 callback(PlatformFields {
933 tid,
934 code,
935 address,
936 raw: std::ptr::null(),
937 raw_len: 0,
938 })
939}
940
941fn sampler_loop(shared: Arc<Shared>) {
942 while !shared.stop.load(Ordering::Acquire) {
943 if !shared.reading.load(Ordering::Acquire) {
944 let sample = capture_sample();
945 if let Some(sample) = sample {
948 if !shared.reading.load(Ordering::Acquire) {
949 let _publish = match shared.publish.lock() {
950 Ok(publish) => publish,
951 Err(poisoned) => poisoned.into_inner(),
952 };
953 let mut template = match shared.template.lock() {
954 Ok(template) => template,
955 Err(poisoned) => poisoned.into_inner(),
956 };
957 spool::put_sample(&mut template, &sample.modules, &sample.threads);
958 let inactive = 1 - shared.active.load(Ordering::Acquire);
959 let target = unsafe { &mut (*shared.buffers.get())[inactive] };
962 *target = *template;
963 shared.active.store(inactive, Ordering::Release);
964 shared
965 .sample_thread_count
966 .store(sample.threads.len(), Ordering::Release);
967 shared.sample_ready.store(true, Ordering::Release);
968 }
969 }
970 }
971 std::thread::sleep(Duration::from_millis(50));
972 }
973}
974
975#[cfg(all(
976 any(windows, target_os = "linux", target_os = "macos"),
977 any(target_arch = "x86_64", target_arch = "aarch64")
978))]
979fn capture_sample() -> Option<CrashSample> {
980 use crate::snapshot::attribute::attribute;
981 use crate::snapshot::modules::enumerate_modules;
982 use crate::snapshot::{capture_and_resolve, SnapshotConfig};
983
984 let Ok(snapshot) = capture_and_resolve(&SnapshotConfig::default()) else {
985 return None;
986 };
987 let Ok(loaded) = enumerate_modules() else {
988 return None;
989 };
990 let attributed = attribute(&snapshot, &loaded);
991 Some(CrashSample {
992 modules: attributed
993 .modules
994 .into_iter()
995 .map(|module| CrashModule {
996 identity: module.path.unwrap_or(module.name),
997 })
998 .collect(),
999 threads: attributed
1000 .threads
1001 .into_iter()
1002 .map(|thread| CrashThread {
1003 os_tid: thread.os_tid,
1004 frames: thread
1005 .frames
1006 .into_iter()
1007 .map(|frame| CrashFrame {
1008 module_index: frame.module_index,
1009 relative_address: frame.relative_address,
1010 })
1011 .collect(),
1012 })
1013 .collect(),
1014 })
1015}
1016
1017#[cfg(not(all(
1018 any(windows, target_os = "linux", target_os = "macos"),
1019 any(target_arch = "x86_64", target_arch = "aarch64")
1020)))]
1021fn capture_sample() -> Option<CrashSample> {
1022 None
1023}
1024
1025#[derive(Default)]
1026struct CrashSample {
1027 modules: Vec<CrashModule>,
1028 threads: Vec<CrashThread>,
1029}
1030
1031#[cfg(unix)]
1038unsafe fn write_once(file: &File, record: *const u8, length: usize) {
1039 use std::os::fd::AsRawFd as _;
1040 let _ = unsafe { libc::write(file.as_raw_fd(), record.cast(), length) };
1042}
1043
1044#[cfg(windows)]
1050unsafe fn write_once(file: &File, record: *const u8, length: usize) {
1051 use std::os::windows::io::AsRawHandle as _;
1052 use windows_sys::Win32::Storage::FileSystem::WriteFile;
1053 let mut written = 0u32;
1054 let _ = unsafe {
1056 WriteFile(
1057 file.as_raw_handle() as _,
1058 record.cast(),
1059 length as u32,
1060 &raw mut written,
1061 std::ptr::null_mut(),
1062 )
1063 };
1064}
1065
1066#[cfg(test)]
1067mod tests {
1068 use super::*;
1069
1070 static PROCESS_STATE: Mutex<()> = Mutex::new(());
1071
1072 fn process_state() -> std::sync::MutexGuard<'static, ()> {
1073 match PROCESS_STATE.lock() {
1074 Ok(guard) => guard,
1075 Err(poisoned) => poisoned.into_inner(),
1076 }
1077 }
1078
1079 #[test]
1080 fn off_policy_is_inert() {
1081 let _state = process_state();
1082 let guard = install(
1083 CrashPolicy::Off,
1084 CrashMetadata {
1085 app_class: "a".into(),
1086 app_name: "a".into(),
1087 app_version: "1".into(),
1088 instance_name: String::new(),
1089 creation_time_ms: 1,
1090 cwd: "/test".into(),
1091 },
1092 )
1093 .unwrap();
1094 assert!(!guard.is_armed());
1095 assert!(guard.spool_path().is_none());
1096 }
1097
1098 #[test]
1099 fn opt_out_values_are_strict_and_documented() {
1100 let _state = process_state();
1101 assert!(!matches!(CrashPolicy::default(), CrashPolicy::Off));
1102 }
1103
1104 fn metadata(name: &str) -> CrashMetadata {
1105 CrashMetadata {
1106 app_class: name.into(),
1107 app_name: name.into(),
1108 app_version: "1".into(),
1109 instance_name: String::new(),
1110 creation_time_ms: 1,
1111 cwd: "/test".into(),
1112 }
1113 }
1114
1115 #[test]
1116 fn independent_guards_reselect_oldest_live_metadata() {
1117 let _state = process_state();
1118 let first = install(CrashPolicy::On, metadata("first")).unwrap();
1119 let second = install(CrashPolicy::On, metadata("second")).unwrap();
1120 assert!(first.is_armed());
1121 assert!(second.is_armed());
1122 drop(first);
1123 let template = match second.runtime.as_ref().unwrap().shared.template.lock() {
1124 Ok(template) => *template,
1125 Err(poisoned) => *poisoned.into_inner(),
1126 };
1127 assert_eq!(
1128 spool::parse(&template).unwrap().metadata.app_class,
1129 "second"
1130 );
1131 drop(second);
1132 }
1133
1134 #[test]
1135 fn external_handler_transitions_are_serialized_and_rearm() {
1136 let _state = process_state();
1137 let guard = install(CrashPolicy::On, metadata("transition-owner")).unwrap();
1138 let start = Arc::new(std::sync::Barrier::new(3));
1139 let inside = Arc::new(AtomicUsize::new(0));
1140 let overlapped = Arc::new(AtomicBool::new(false));
1141 let mut threads = Vec::new();
1142 for _ in 0..2 {
1143 let start = Arc::clone(&start);
1144 let inside = Arc::clone(&inside);
1145 let overlapped = Arc::clone(&overlapped);
1146 threads.push(std::thread::spawn(move || {
1147 start.wait();
1148 with_handler_suspended(|| {
1149 if inside.fetch_add(1, Ordering::AcqRel) != 0 {
1150 overlapped.store(true, Ordering::Release);
1151 }
1152 std::thread::sleep(Duration::from_millis(20));
1153 inside.fetch_sub(1, Ordering::AcqRel);
1154 })
1155 .unwrap();
1156 }));
1157 }
1158 start.wait();
1159 for thread in threads {
1160 thread.join().unwrap();
1161 }
1162 assert!(!overlapped.load(Ordering::Acquire));
1163 assert!(guard.is_armed());
1164 }
1165
1166 #[test]
1167 fn install_waits_for_external_handler_transition() {
1168 let _state = process_state();
1169 let live_runtime = RUNTIME
1170 .get()
1171 .and_then(|slot| slot.lock().ok())
1172 .and_then(|weak| weak.upgrade());
1173 assert!(
1174 live_runtime.is_none(),
1175 "the first-attach race requires no existing runtime"
1176 );
1177 let (entered_tx, entered_rx) = std::sync::mpsc::channel();
1178 let (release_tx, release_rx) = std::sync::mpsc::channel();
1179 let transition = std::thread::spawn(move || {
1180 with_handler_suspended(|| {
1181 entered_tx.send(()).unwrap();
1182 release_rx.recv().unwrap();
1183 })
1184 .unwrap();
1185 });
1186 entered_rx.recv().unwrap();
1187
1188 let (installed_tx, installed_rx) = std::sync::mpsc::channel();
1189 let installer = std::thread::spawn(move || {
1190 let guard = install(CrashPolicy::On, metadata("transition-first")).unwrap();
1191 installed_tx.send(guard).unwrap();
1192 });
1193 assert!(
1194 installed_rx
1195 .recv_timeout(Duration::from_millis(30))
1196 .is_err(),
1197 "install returned while native interception was suspended"
1198 );
1199 release_tx.send(()).unwrap();
1200 transition.join().unwrap();
1201 let first = installed_rx.recv_timeout(Duration::from_secs(2)).unwrap();
1202 installer.join().unwrap();
1203 assert!(first.is_armed());
1204 }
1205
1206 #[test]
1207 fn failed_external_handler_rearm_is_reported_unarmed() {
1208 let _state = process_state();
1209 let guard = install(CrashPolicy::On, metadata("failed-rearm")).unwrap();
1210 let foreign = Arc::new(Mutex::new(None));
1211 let foreign_slot = Arc::clone(&foreign);
1212 let result = with_handler_suspended(|| {
1213 let event =
1217 unsafe { crash_handler::make_crash_event(|_| CrashEventResult::Handled(false)) };
1218 let handler = CrashHandler::attach(event).unwrap();
1219 match foreign_slot.lock() {
1220 Ok(mut slot) => *slot = Some(handler),
1221 Err(poisoned) => *poisoned.into_inner() = Some(handler),
1222 }
1223 });
1224 assert!(matches!(result, Err(InstallError::Handler(_))));
1225 assert!(!guard.is_armed());
1226
1227 match foreign.lock() {
1230 Ok(mut slot) => slot.take(),
1231 Err(poisoned) => poisoned.into_inner().take(),
1232 };
1233
1234 with_handler_suspended(|| {}).unwrap();
1235 assert!(
1236 guard.is_armed(),
1237 "the next transition must retry a failed reattach"
1238 );
1239
1240 let foreign_slot = Arc::clone(&foreign);
1241 let second_failure = with_handler_suspended(|| {
1242 let event =
1244 unsafe { crash_handler::make_crash_event(|_| CrashEventResult::Handled(false)) };
1245 let handler = CrashHandler::attach(event).unwrap();
1246 match foreign_slot.lock() {
1247 Ok(mut slot) => *slot = Some(handler),
1248 Err(poisoned) => *poisoned.into_inner() = Some(handler),
1249 }
1250 });
1251 assert!(matches!(second_failure, Err(InstallError::Handler(_))));
1252 match foreign.lock() {
1253 Ok(mut slot) => slot.take(),
1254 Err(poisoned) => poisoned.into_inner().take(),
1255 };
1256 let second = install(CrashPolicy::On, metadata("retried-install")).unwrap();
1257 assert!(
1258 guard.is_armed() && second.is_armed(),
1259 "an ordinary install must retry a failed reattach"
1260 );
1261 drop(second);
1262 drop(guard);
1263 }
1264
1265 #[test]
1266 fn failed_install_rearm_drops_last_runtime_after_transition_gate() {
1267 let _state = process_state();
1268 let guard = install(CrashPolicy::On, metadata("last-guard")).unwrap();
1269 let foreign = Arc::new(Mutex::new(None));
1270 let foreign_slot = Arc::clone(&foreign);
1271 let failure = with_handler_suspended(|| {
1272 let event =
1274 unsafe { crash_handler::make_crash_event(|_| CrashEventResult::Handled(false)) };
1275 let handler = CrashHandler::attach(event).unwrap();
1276 match foreign_slot.lock() {
1277 Ok(mut slot) => *slot = Some(handler),
1278 Err(poisoned) => *poisoned.into_inner() = Some(handler),
1279 }
1280 });
1281 assert!(matches!(failure, Err(InstallError::Handler(_))));
1282 assert!(!guard.is_armed());
1283
1284 TEST_RESUME_ENTERED.store(false, Ordering::Release);
1285 TEST_RELEASE_RESUME.store(false, Ordering::Release);
1286 TEST_PAUSE_RESUME.store(true, Ordering::Release);
1287 let (result_tx, result_rx) = std::sync::mpsc::channel();
1288 let installer = std::thread::spawn(move || {
1289 result_tx
1290 .send(install(CrashPolicy::On, metadata("racer")))
1291 .unwrap();
1292 });
1293 let deadline = std::time::Instant::now() + Duration::from_secs(2);
1294 while !TEST_RESUME_ENTERED.load(Ordering::Acquire) {
1295 if std::time::Instant::now() >= deadline {
1296 TEST_RELEASE_RESUME.store(true, Ordering::Release);
1297 panic!("install never reached the forced reattach point");
1298 }
1299 std::thread::yield_now();
1300 }
1301
1302 drop(guard);
1305 TEST_RELEASE_RESUME.store(true, Ordering::Release);
1306 let result = result_rx
1307 .recv_timeout(Duration::from_secs(2))
1308 .expect("failed reattach deadlocked while dropping the final Runtime Arc");
1309 assert!(matches!(result, Err(InstallError::Handler(_))));
1310 installer.join().unwrap();
1311
1312 match foreign.lock() {
1313 Ok(mut slot) => slot.take(),
1314 Err(poisoned) => poisoned.into_inner().take(),
1315 };
1316 }
1317
1318 #[cfg(unix)]
1319 #[test]
1320 fn post_fork_child_is_detected_before_touching_inherited_locks() {
1321 let _state = process_state();
1322 let guard = install(CrashPolicy::On, metadata("fork-owner")).unwrap();
1323 let child = unsafe { libc::fork() };
1326 assert!(child >= 0, "fork failed");
1327 if child == 0 {
1328 let rejected = matches!(
1329 install(CrashPolicy::On, metadata("fork-owner")),
1330 Err(InstallError::ForkedProcess)
1331 );
1332 unsafe { libc::_exit(i32::from(!rejected)) };
1333 }
1334 let mut status = 0;
1335 assert_eq!(unsafe { libc::waitpid(child, &raw mut status, 0) }, child);
1337 assert_eq!(status, 0, "child touched inherited crash state");
1338 drop(guard);
1339 }
1340}