|
| 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 | +} |
0 commit comments