Skip to content

Commit 678165e

Browse files
committed
Break into the debugger (if attached) on panics (Windows, macOS, Linux, FreeBSD)
The developer experience for panics is to provide the backtrace and exit the program. When running under debugger, that might be improved by breaking into the debugger once the code panics thus enabling the developer to examine the program state at the exact time when the code panicked. Let the developer catch the panic in the debugger if it is attached. If the debugger is not attached, nothing changes. Providing this feature inside the standard library facilitates better debugging experience. Validated under Windows, Linux, macOS 14.6, and FreeBSD 13.3..14.1.
1 parent 24ed1c1 commit 678165e

File tree

3 files changed

+247
-1
lines changed

3 files changed

+247
-1
lines changed

std/src/panicking.rs

+17-1
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ use crate::mem::{self, ManuallyDrop};
2323
use crate::panic::{BacktraceStyle, PanicHookInfo};
2424
use crate::sync::atomic::{AtomicBool, Ordering};
2525
use crate::sync::{PoisonError, RwLock};
26-
use crate::sys::backtrace;
2726
use crate::sys::stdio::panic_output;
27+
use crate::sys::{backtrace, dbg};
2828
use crate::{fmt, intrinsics, process, thread};
2929

3030
// Binary interface to the panic runtime that the standard library depends on.
@@ -855,13 +855,29 @@ pub fn rust_panic_without_hook(payload: Box<dyn Any + Send>) -> ! {
855855
#[cfg_attr(not(test), rustc_std_internal_symbol)]
856856
#[cfg(not(feature = "panic_immediate_abort"))]
857857
fn rust_panic(msg: &mut dyn PanicPayload) -> ! {
858+
// Break into the debugger if it is attached.
859+
// The return value is not currently used.
860+
//
861+
// This function isn't used anywhere else, and
862+
// using inside `#[panic_handler]` doesn't seem
863+
// to count, so a warning is issued.
864+
let _ = dbg::breakpoint_if_debugging();
865+
858866
let code = unsafe { __rust_start_panic(msg) };
859867
rtabort!("failed to initiate panic, error {code}")
860868
}
861869

862870
#[cfg_attr(not(test), rustc_std_internal_symbol)]
863871
#[cfg(feature = "panic_immediate_abort")]
864872
fn rust_panic(_: &mut dyn PanicPayload) -> ! {
873+
// Break into the debugger if it is attached.
874+
// The return value is not currently used.
875+
//
876+
// This function isn't used anywhere else, and
877+
// using inside `#[panic_handler]` doesn't seem
878+
// to count, so a warning is issued.
879+
let _ = dbg::breakpoint_if_debugging();
880+
865881
unsafe {
866882
crate::intrinsics::abort();
867883
}

std/src/sys/dbg.rs

+229
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
//! Debugging aids.
2+
3+
/// Presence of a debugger. The debugger being concerned
4+
/// is expected to use the OS API to debug this process.
5+
#[derive(Copy, Clone, Debug)]
6+
#[allow(unused)]
7+
pub(crate) enum DebuggerPresence {
8+
/// The debugger is attached to this process.
9+
Detected,
10+
/// The debugger is not attached to this process.
11+
NotDetected,
12+
}
13+
14+
#[cfg(target_os = "windows")]
15+
mod os {
16+
use super::DebuggerPresence;
17+
18+
#[link(name = "kernel32")]
19+
extern "system" {
20+
fn IsDebuggerPresent() -> i32;
21+
}
22+
23+
pub(super) fn is_debugger_present() -> Option<DebuggerPresence> {
24+
// SAFETY: No state is shared between threads. The call reads
25+
// a field from the Thread Environment Block using the OS API
26+
// as required by the documentation.
27+
if unsafe { IsDebuggerPresent() } != 0 {
28+
Some(DebuggerPresence::Detected)
29+
} else {
30+
Some(DebuggerPresence::NotDetected)
31+
}
32+
}
33+
}
34+
35+
#[cfg(any(target_vendor = "apple", target_os = "freebsd"))]
36+
mod os {
37+
use libc::{c_int, sysctl, CTL_KERN, KERN_PROC, KERN_PROC_PID};
38+
39+
use super::DebuggerPresence;
40+
use crate::io::{Cursor, Read, Seek, SeekFrom};
41+
use crate::process;
42+
43+
const P_TRACED: i32 = 0x00000800;
44+
45+
// The assumption is that the kernel structures available to the
46+
// user space may not shrink or repurpose the existing fields over
47+
// time. The kernels normally adhere to that for the backward
48+
// compatibility of the user space.
49+
50+
// The macOS 14.5 SDK comes with a header `MacOSX14.5.sdk/usr/include/sys/sysctl.h`
51+
// that defines `struct kinfo_proc` be of `648` bytes on the 64-bit system. That has
52+
// not changed since macOS 10.13 (released in 2017) at least, validated by building
53+
// a C program in XCode while changing the build target. Apple provides this example
54+
// for reference: https://developer.apple.com/library/archive/qa/qa1361/_index.html.
55+
#[cfg(target_vendor = "apple")]
56+
const KINFO_PROC_SIZE: usize = if cfg!(target_pointer_width = "64") { 648 } else { 492 };
57+
#[cfg(target_vendor = "apple")]
58+
const KINFO_PROC_FLAGS_OFFSET: u64 = if cfg!(target_pointer_width = "64") { 32 } else { 16 };
59+
60+
// Works for FreeBSD stable (13.3, 13.4) and current (14.0, 14.1).
61+
// The size of the structure has stayed the same for a long time,
62+
// at least since 2005:
63+
// https://lists.freebsd.org/pipermail/freebsd-stable/2005-November/019899.html
64+
#[cfg(target_os = "freebsd")]
65+
const KINFO_PROC_SIZE: usize = if cfg!(target_pointer_width = "64") { 1088 } else { 768 };
66+
#[cfg(target_os = "freebsd")]
67+
const KINFO_PROC_FLAGS_OFFSET: u64 = if cfg!(target_pointer_width = "64") { 368 } else { 296 };
68+
69+
pub(super) fn is_debugger_present() -> Option<DebuggerPresence> {
70+
debug_assert_ne!(KINFO_PROC_SIZE, 0);
71+
72+
let mut flags = [0u8; 4]; // `ki_flag` under FreeBSD and `p_flag` under macOS.
73+
let mut mib = [CTL_KERN, KERN_PROC, KERN_PROC_PID, process::id() as c_int];
74+
let mut info_size = KINFO_PROC_SIZE;
75+
let mut kinfo_proc = [0u8; KINFO_PROC_SIZE];
76+
77+
// SAFETY: No state is shared with other threads. The sysctl call
78+
// is safe according to the documentation.
79+
if unsafe {
80+
sysctl(
81+
mib.as_mut_ptr(),
82+
mib.len() as u32,
83+
kinfo_proc.as_mut_ptr().cast(),
84+
&mut info_size,
85+
core::ptr::null_mut(),
86+
0,
87+
)
88+
} != 0
89+
{
90+
return None;
91+
}
92+
debug_assert_eq!(info_size, KINFO_PROC_SIZE);
93+
94+
let mut reader = Cursor::new(kinfo_proc);
95+
reader.seek(SeekFrom::Start(KINFO_PROC_FLAGS_OFFSET)).ok()?;
96+
reader.read_exact(&mut flags).ok()?;
97+
// Just in case, not limiting this to the little-endian systems.
98+
let flags = i32::from_ne_bytes(flags);
99+
100+
if flags & P_TRACED != 0 {
101+
Some(DebuggerPresence::Detected)
102+
} else {
103+
Some(DebuggerPresence::NotDetected)
104+
}
105+
}
106+
}
107+
108+
#[cfg(target_os = "linux")]
109+
mod os {
110+
use super::DebuggerPresence;
111+
use crate::fs::File;
112+
use crate::io::Read;
113+
114+
pub(super) fn is_debugger_present() -> Option<DebuggerPresence> {
115+
// This function is crafted with the following goals:
116+
// * Memory efficiency: It avoids crashing the panicking process due to
117+
// out-of-memory (OOM) conditions by not using large heap buffers or
118+
// allocating significant stack space, which could lead to stack overflow.
119+
// * Minimal binary size: The function uses a minimal set of facilities
120+
// from the standard library to avoid increasing the resulting binary size.
121+
//
122+
// To achieve these goals, the function does not use `[std::io::BufReader]`
123+
// and instead reads the file byte by byte using a sliding window approach.
124+
// It's important to note that the "/proc/self/status" pseudo-file is synthesized
125+
// by the Virtual File System (VFS), meaning it is not read from a slow or
126+
// non-volatile storage medium so buffering might not be as beneficial because
127+
// all data is read from memory, though this approach does incur a syscall for
128+
// each byte read.
129+
//
130+
// We cannot make assumptions about the file size or the position of the
131+
// target prefix ("TracerPid:"), so the function does not use
132+
// `[std::fs::read_to_string]` thus not employing UTF-8 to ASCII checking,
133+
// conversion, or parsing as we're looking for an ASCII prefix.
134+
//
135+
// These condiderations make the function deviate from the familiar concise pattern
136+
// of searching for a string in a text file.
137+
138+
fn read_byte(file: &mut File) -> Option<u8> {
139+
let mut buffer = [0];
140+
file.read_exact(&mut buffer).ok()?;
141+
Some(buffer[0])
142+
}
143+
144+
// The ASCII prefix of the datum we're interested in.
145+
const TRACER_PID: &[u8] = b"TracerPid:\t";
146+
147+
let mut file = File::open("/proc/self/status").ok()?;
148+
let mut matched = 0;
149+
150+
// Look for the `TRACER_PID` prefix.
151+
while let Some(byte) = read_byte(&mut file) {
152+
if byte == TRACER_PID[matched] {
153+
matched += 1;
154+
if matched == TRACER_PID.len() {
155+
break;
156+
}
157+
} else {
158+
matched = 0;
159+
}
160+
}
161+
162+
// Was the prefix found?
163+
if matched != TRACER_PID.len() {
164+
return None;
165+
}
166+
167+
// It was; get the ASCII representation of the first digit
168+
// of the PID. That is enough to see if there is a debugger
169+
// attached as the kernel does not pad the PID on the left
170+
// with the leading zeroes.
171+
let byte = read_byte(&mut file)?;
172+
if byte.is_ascii_digit() && byte != b'0' {
173+
Some(DebuggerPresence::Detected)
174+
} else {
175+
Some(DebuggerPresence::NotDetected)
176+
}
177+
}
178+
}
179+
180+
#[cfg(not(any(
181+
target_os = "windows",
182+
target_vendor = "apple",
183+
target_os = "freebsd",
184+
target_os = "linux"
185+
)))]
186+
mod os {
187+
pub(super) fn is_debugger_present() -> Option<super::DebuggerPresence> {
188+
None
189+
}
190+
}
191+
192+
/// Detect the debugger presence.
193+
///
194+
/// The code does not try to detect the debugger at all costs (e.g., when anti-debugger
195+
/// tricks are at play), it relies on the interfaces provided by the OS.
196+
///
197+
/// Return value:
198+
/// * `None`: it's not possible to conclude whether the debugger is attached to this
199+
/// process or not. When checking for the presence of the debugger, the detection logic
200+
/// encountered an issue, such as the OS API throwing an error or the feature not being
201+
/// implemented.
202+
/// * `Some(DebuggerPresence::Detected)`: yes, the debugger is attached
203+
/// to this process.
204+
/// * `Some(DebuggerPresence::NotDetected)`: no, the debugger is not
205+
/// attached to this process.
206+
pub(crate) fn is_debugger_present() -> Option<DebuggerPresence> {
207+
if cfg!(miri) { None } else { os::is_debugger_present() }
208+
}
209+
210+
/// Execute the breakpoint instruction if the debugger presence is detected.
211+
/// Useful for breaking into the debugger without the need to set a breakpoint
212+
/// in the debugger.
213+
///
214+
/// Note that there is a race between attaching or detaching the debugger, and running the
215+
/// breakpoint instruction. This is nonetheless memory-safe, like [`crate::process::abort`]
216+
/// is. In case the debugger is attached and the function is about
217+
/// to run the breakpoint instruction yet right before that the debugger detaches, the
218+
/// process will crash due to running the breakpoint instruction and the debugger not
219+
/// handling the trap exception.
220+
pub(crate) fn breakpoint_if_debugging() -> Option<DebuggerPresence> {
221+
let debugger_present = is_debugger_present();
222+
if let Some(DebuggerPresence::Detected) = debugger_present {
223+
// SAFETY: Executing the breakpoint instruction. No state is shared
224+
// or modified by this code.
225+
unsafe { core::intrinsics::breakpoint() };
226+
}
227+
228+
debugger_present
229+
}

std/src/sys/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ mod personality;
1111
pub mod anonymous_pipe;
1212
pub mod backtrace;
1313
pub mod cmath;
14+
pub mod dbg;
1415
pub mod exit_guard;
1516
pub mod os_str;
1617
pub mod path;

0 commit comments

Comments
 (0)