use std::collections::HashMap;
use std::ffi::CString;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Condvar, Mutex, MutexGuard, OnceLock};
use std::thread;
use std::time::{Duration, Instant};
use thiserror::Error;
use windows::Win32::Foundation::{E_INVALIDARG, E_NOINTERFACE, S_FALSE, S_OK};
use windows::core::{HRESULT, IUnknown, Interface, PCSTR, PCWSTR, PWSTR};
use windows::Win32::System::Diagnostics::Debug::Extensions::{
DEBUG_ANY_ID, DEBUG_ATTACH_KERNEL_CONNECTION, DEBUG_ATTACH_LOCAL_KERNEL, DEBUG_BREAKPOINT_CODE,
DEBUG_BREAKPOINT_DATA, DEBUG_BREAKPOINT_DEFERRED, DEBUG_BREAKPOINT_ENABLED,
DEBUG_BREAKPOINT_ONE_SHOT, DEBUG_CLASS_KERNEL, DEBUG_ENGOPT_INITIAL_BREAK,
DEBUG_EVENT_BREAKPOINT, DEBUG_EXECUTE_ECHO, DEBUG_INTERRUPT_ACTIVE, DEBUG_KERNEL_SMALL_DUMP,
DEBUG_MODNAME_SYMBOL_FILE, DEBUG_MODULE_PARAMETERS, DEBUG_MODULE_USER_MODE,
DEBUG_OUTCTL_THIS_CLIENT, DEBUG_OUTPUT_NORMAL, DEBUG_REGISTER_DESCRIPTION,
DEBUG_REGISTER_SUB_REGISTER, DEBUG_STACK_FRAME, DEBUG_STATUS_GO, DEBUG_STATUS_GO_HANDLED,
DEBUG_STATUS_GO_NOT_HANDLED, DEBUG_STATUS_MASK, DEBUG_STATUS_NO_DEBUGGEE,
DEBUG_STATUS_REVERSE_GO, DEBUG_STATUS_REVERSE_STEP_BRANCH, DEBUG_STATUS_REVERSE_STEP_INTO,
DEBUG_STATUS_REVERSE_STEP_OVER, DEBUG_STATUS_STEP_BRANCH, DEBUG_STATUS_STEP_INTO,
DEBUG_STATUS_STEP_OVER, DEBUG_SYMINFO_IMAGEHLP_MODULEW64, DEBUG_SYMTYPE_CODEVIEW,
DEBUG_SYMTYPE_COFF, DEBUG_SYMTYPE_DEFERRED, DEBUG_SYMTYPE_DIA, DEBUG_SYMTYPE_EXPORT,
DEBUG_SYMTYPE_NONE, DEBUG_SYMTYPE_PDB, DEBUG_SYMTYPE_SYM, DEBUG_VALUE, DEBUG_VALUE_FLOAT32,
DEBUG_VALUE_FLOAT64, DEBUG_VALUE_FLOAT80, DEBUG_VALUE_FLOAT82, DEBUG_VALUE_FLOAT128,
DEBUG_VALUE_INT8, DEBUG_VALUE_INT16, DEBUG_VALUE_INT32, DEBUG_VALUE_INT64,
DEBUG_VALUE_VECTOR64, DEBUG_VALUE_VECTOR128, DebugConnectWide, IDebugAdvanced2,
IDebugBreakpoint, IDebugBreakpoint2, IDebugClient6, IDebugControl4, IDebugDataSpaces4,
IDebugEventContextCallbacks, IDebugOutputCallbacks, IDebugRegisters, IDebugSymbols3,
IDebugSystemObjects,
};
use windows::Win32::System::Diagnostics::Debug::IMAGEHLP_MODULEW64;
pub type BreakpointCallback =
Box<dyn Fn(&IDebugBreakpoint2, *const std::ffi::c_void, u32) -> windows::core::Result<()>>;
#[derive(Debug, Error)]
pub enum DbgEngError {
#[error("Failed to initialize COM: {0}")]
ComInitFailed(#[from] windows::core::Error),
#[error("Failed to create debug client: {0}")]
CreateClientFailed(windows::core::Error),
#[error("Failed to get debug control: {0}")]
GetControlFailed(windows::core::Error),
#[error("Failed to get debug symbols: {0}")]
GetSymbolsFailed(windows::core::Error),
#[error("Failed to attach to kernel: {0}")]
AttachFailed(windows::core::Error),
#[error("Debug command failed: {0}")]
CommandFailed(windows::core::Error),
#[error("Symbol path operation failed: {0}")]
SymbolPathFailed(windows::core::Error),
#[error("Breakpoint failed: {0}")]
BreakpointFailed(windows::core::Error),
#[error("Invalid command string (contains interior NUL)")]
InvalidCommand,
#[error(
"No active debuggee — attach to a target, launch a process, or open a dump/trace first"
)]
NoDebuggee,
#[error(
"kernel target did not break in within the attach timeout — is it reachable and in debug mode?"
)]
KernelBreakTimeout,
#[error("Operation failed: {0}")]
OperationFailed(windows::core::Error),
#[error("{operation} failed: {source}")]
Context {
operation: String,
#[source]
source: windows::core::Error,
},
#[error("short virtual read at {address:#x}: requested {requested} bytes, read {actual}")]
ShortRead {
address: u64,
requested: usize,
actual: usize,
},
#[error("requested debugger buffer is too large: {0} bytes")]
BufferTooLarge(usize),
#[error("debugger text contains an interior NUL")]
InvalidOutput,
#[error("this scope was read from a target the engine no longer holds")]
ScopeFromAnotherTarget,
}
const EPROCESS_IMAGE_NAME_LEN: u32 = 15;
const MODULE_NAME_FALLBACK: usize = 260;
const DEBUG_MODULE_UNLOADED: u32 = 0x0000_0001;
const DEBUG_ONLY_THIS_PROCESS: u32 = 0x0000_0002;
const CREATE_NEW_CONSOLE: u32 = 0x0000_0010;
const DEBUG_ATTACH_DEFAULT: u32 = 0x0000_0000;
const DEBUG_END_PASSIVE: u32 = 0x0000_0000;
const DEBUG_END_ACTIVE_DETACH: u32 = 0x0000_0002;
const LIVE_WAIT_MS: u32 = 30_000;
const WAIT_INFINITE: u32 = u32::MAX;
const KERNEL_ATTACH_WAIT_MS: u32 = 60_000;
const SCOPE_CONTEXT_SIZES: &[u32] = &[716, 912, 1232, 2048, 4096, 8192, 16384, 32768, 65536];
pub struct InterruptHandle {
control: IDebugControl4,
raised: Arc<AtomicBool>,
}
unsafe impl Send for InterruptHandle {}
unsafe impl Sync for InterruptHandle {}
impl InterruptHandle {
pub fn interrupt(&self) -> Result<(), DbgEngError> {
self.raised.store(true, Ordering::SeqCst);
unsafe { self.control.SetInterrupt(DEBUG_INTERRUPT_ACTIVE) }.map_err(|source| {
DbgEngError::Context {
operation: "requesting a debugger interrupt".into(),
source,
}
})
}
}
const WATCHDOG_REPEAT: Duration = Duration::from_millis(200);
struct Watchdog {
disarmed: Arc<(Mutex<bool>, Condvar)>,
fired: Arc<AtomicBool>,
thread: Option<thread::JoinHandle<()>>,
}
impl Watchdog {
fn arm(deadline: Duration, on_deadline: impl Fn() + Send + 'static) -> Self {
let disarmed = Arc::new((Mutex::new(false), Condvar::new()));
let fired = Arc::new(AtomicBool::new(false));
let woken = Arc::clone(&disarmed);
let raised = Arc::clone(&fired);
let thread = thread::spawn(move || {
let (lock, wake) = &*woken;
let start = Instant::now();
loop {
let nap = if raised.load(Ordering::SeqCst) {
WATCHDOG_REPEAT
} else {
deadline.saturating_sub(start.elapsed())
};
{
let stop = lock.lock().unwrap_or_else(|e| e.into_inner());
if *stop {
return;
}
let (stop, _) = wake
.wait_timeout(stop, nap)
.unwrap_or_else(|e| e.into_inner());
if *stop {
return;
}
}
if start.elapsed() >= deadline {
on_deadline();
raised.store(true, Ordering::SeqCst);
}
}
});
Self {
disarmed,
fired,
thread: Some(thread),
}
}
fn disarm(mut self) -> bool {
self.stop();
self.fired.load(Ordering::SeqCst)
}
fn stop(&mut self) {
{
let (lock, wake) = &*self.disarmed;
let mut stop = lock.lock().unwrap_or_else(|e| e.into_inner());
*stop = true;
wake.notify_all();
}
if let Some(thread) = self.thread.take() {
let _ = thread.join();
}
}
}
impl Drop for Watchdog {
fn drop(&mut self) {
self.stop();
}
}
fn is_running_status(status: u32) -> bool {
matches!(
status & DEBUG_STATUS_MASK,
DEBUG_STATUS_GO
| DEBUG_STATUS_GO_HANDLED
| DEBUG_STATUS_GO_NOT_HANDLED
| DEBUG_STATUS_STEP_OVER
| DEBUG_STATUS_STEP_INTO
| DEBUG_STATUS_STEP_BRANCH
| DEBUG_STATUS_REVERSE_GO
| DEBUG_STATUS_REVERSE_STEP_BRANCH
| DEBUG_STATUS_REVERSE_STEP_OVER
| DEBUG_STATUS_REVERSE_STEP_INTO
)
}
fn to_wide(s: &str) -> Vec<u16> {
s.encode_utf16().chain(std::iter::once(0)).collect()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RunToOutcome {
Hit,
StoppedElsewhere { stopped_at: u64 },
Timeout,
TargetGone,
}
#[derive(Debug, Clone)]
pub struct RunToResult {
pub outcome: RunToOutcome,
pub output: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Interruption {
Deadline { after_ms: u32 },
OnRequest,
}
#[derive(Debug, Clone)]
pub struct CommandRun {
pub output: String,
pub cut_short: Option<Interruption>,
pub target_gone: bool,
}
impl CommandRun {
pub fn into_output(self) -> String {
self.output
}
}
const DEBUG_INVALID_OFFSET: u64 = u64::MAX;
#[derive(Debug, Clone, PartialEq)]
pub enum RegisterValue {
Int(u64),
Float(f64),
Bytes(Vec<u8>),
Unavailable,
}
impl RegisterValue {
fn decode(value: &DEBUG_VALUE) -> Self {
unsafe {
match value.Type {
DEBUG_VALUE_INT8 => Self::Int(u64::from(value.Anonymous.I8)),
DEBUG_VALUE_INT16 => Self::Int(u64::from(value.Anonymous.I16)),
DEBUG_VALUE_INT32 => Self::Int(u64::from(value.Anonymous.I32)),
DEBUG_VALUE_INT64 => Self::Int(value.Anonymous.Anonymous.I64),
DEBUG_VALUE_FLOAT32 => Self::Float(f64::from(value.Anonymous.F32)),
DEBUG_VALUE_FLOAT64 => Self::Float(value.Anonymous.F64),
DEBUG_VALUE_FLOAT80 => Self::Bytes(value.Anonymous.F80Bytes.to_vec()),
DEBUG_VALUE_FLOAT82 => Self::Bytes(value.Anonymous.F82Bytes.to_vec()),
DEBUG_VALUE_FLOAT128 => Self::Bytes(value.Anonymous.F128Bytes.to_vec()),
DEBUG_VALUE_VECTOR64 => Self::Bytes(value.Anonymous.VI8[..8].to_vec()),
DEBUG_VALUE_VECTOR128 => Self::Bytes(value.Anonymous.VI8.to_vec()),
_ => Self::Unavailable,
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RegisterDescription {
pub name: String,
pub kind: u32,
pub flags: u32,
pub subreg_master: u32,
pub subreg_length: u32,
pub subreg_mask: u64,
pub subreg_shift: u32,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Register {
pub name: String,
pub value: RegisterValue,
pub subregister: bool,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub enum SymbolKind {
#[default]
None,
Deferred,
Coff,
CodeView,
Pdb,
Export,
Sym,
Dia,
Other(u32),
}
impl SymbolKind {
fn from_engine(code: u32) -> Self {
match code {
DEBUG_SYMTYPE_NONE => Self::None,
DEBUG_SYMTYPE_COFF => Self::Coff,
DEBUG_SYMTYPE_CODEVIEW => Self::CodeView,
DEBUG_SYMTYPE_PDB => Self::Pdb,
DEBUG_SYMTYPE_EXPORT => Self::Export,
DEBUG_SYMTYPE_DEFERRED => Self::Deferred,
DEBUG_SYMTYPE_SYM => Self::Sym,
DEBUG_SYMTYPE_DIA => Self::Dia,
other => Self::Other(other),
}
}
pub fn has_type_info(self) -> bool {
matches!(self, Self::Pdb | Self::Dia)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Module {
pub base: u64,
pub size: u32,
pub name: String,
pub image_name: String,
pub loaded_image_name: String,
pub timestamp: u32,
pub checksum: u32,
pub symbols: SymbolKind,
pub user_mode: bool,
pub unloaded: bool,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
pub struct ModuleIdentity {
pub name: String,
pub image_name: String,
pub loaded_image_name: String,
pub symbol_file: String,
pub symbols: SymbolKind,
pub base: u64,
pub size: u32,
pub timestamp: u32,
pub checksum: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PdbIdentity {
pub guid: String,
pub age: u32,
pub unmatched: bool,
pub file: String,
}
impl Module {
pub fn end(&self) -> u64 {
self.base.saturating_add(u64::from(self.size))
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub struct KernelImage {
pub base: u64,
pub size: u32,
pub timestamp: u32,
pub checksum: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BugCheck {
pub code: u32,
pub parameters: [u64; 4],
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StackFrame {
pub index: u32,
pub instruction_offset: u64,
pub return_offset: u64,
pub frame_offset: u64,
pub stack_offset: u64,
pub symbol: Option<String>,
pub displacement: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Instruction {
pub address: u64,
pub bytes: String,
pub text: String,
}
#[derive(Clone, PartialEq)]
pub struct Scope {
instruction: u64,
frame: DEBUG_STACK_FRAME,
context: Vec<u8>,
target: u64,
}
impl Scope {
pub fn instruction_offset(&self) -> u64 {
self.instruction
}
pub fn frame(&self) -> &DEBUG_STACK_FRAME {
&self.frame
}
pub fn has_context(&self) -> bool {
!self.context.is_empty()
}
}
impl std::fmt::Debug for Scope {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Scope")
.field("instruction", &format_args!("{:#x}", self.instruction))
.field("frame", &self.frame.FrameNumber)
.field(
"frame_offset",
&format_args!("{:#x}", self.frame.FrameOffset),
)
.field("context", &format_args!("{} bytes", self.context.len()))
.field("target", &self.target)
.finish()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BreakpointKind {
Code,
Data,
Other(u32),
}
impl BreakpointKind {
fn from_engine(code: u32) -> Self {
match code {
DEBUG_BREAKPOINT_CODE => Self::Code,
DEBUG_BREAKPOINT_DATA => Self::Data,
other => Self::Other(other),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BreakpointInfo {
pub id: u32,
pub kind: BreakpointKind,
pub address: Option<u64>,
pub expression: Option<String>,
pub command: Option<String>,
pub thread: Option<u32>,
pub enabled: bool,
pub deferred: bool,
pub one_shot: bool,
pub pass_count: u32,
pub passes_remaining: u32,
}
fn read_engine_string(
mut get: impl FnMut(Option<&mut [u8]>, Option<*mut u32>) -> windows::core::Result<()>,
) -> windows::core::Result<String> {
let mut needed = 0u32;
get(None, Some(&mut needed))?;
if needed <= 1 {
return Ok(String::new());
}
let mut buffer = vec![0u8; needed as usize];
get(Some(&mut buffer), None)?;
Ok(nul_terminated(&buffer))
}
fn split_instruction(address: u64, line: &str) -> Instruction {
let mut columns = line.trim().splitn(3, char::is_whitespace);
let (_address, bytes, rest) = (columns.next(), columns.next(), columns.next());
match (bytes, rest) {
(Some(bytes), Some(rest)) => Instruction {
address,
bytes: bytes.to_string(),
text: collapse_spaces(rest),
},
(Some(only), None) => Instruction {
address,
bytes: String::new(),
text: collapse_spaces(only),
},
_ => Instruction {
address,
bytes: String::new(),
text: collapse_spaces(line),
},
}
}
fn collapse_spaces(text: &str) -> String {
text.split_whitespace().collect::<Vec<_>>().join(" ")
}
fn format_pdb_guid(guid: &windows::core::GUID) -> String {
let mut out = format!("{:08X}{:04X}{:04X}", guid.data1, guid.data2, guid.data3);
for byte in guid.data4 {
out.push_str(&format!("{byte:02X}"));
}
out
}
fn wide_to_string(buffer: &[u16]) -> String {
let end = buffer.iter().position(|&c| c == 0).unwrap_or(buffer.len());
String::from_utf16_lossy(&buffer[..end])
}
fn nul_terminated(buffer: &[u8]) -> String {
let end = buffer.iter().position(|&b| b == 0).unwrap_or(buffer.len());
String::from_utf8_lossy(&buffer[..end]).into_owned()
}
static NEXT_TARGET_IDENTITY: AtomicU64 = AtomicU64::new(1);
fn next_target_identity() -> u64 {
NEXT_TARGET_IDENTITY.fetch_add(1, Ordering::Relaxed)
}
fn client_identities() -> &'static Mutex<HashMap<usize, u64>> {
static IDENTITIES: OnceLock<Mutex<HashMap<usize, u64>>> = OnceLock::new();
IDENTITIES.get_or_init(Mutex::default)
}
const MAX_REMEMBERED_CLIENTS: usize = 64;
fn client_key(client: &IDebugClient6) -> usize {
client.as_raw() as usize
}
fn locked_identities() -> MutexGuard<'static, HashMap<usize, u64>> {
client_identities()
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
fn identity_of(client: &IDebugClient6) -> u64 {
identity_for(client_key(client))
}
fn reissue_identity(client: &IDebugClient6) -> u64 {
reissue_for(client_key(client))
}
fn identity_for(key: usize) -> u64 {
let mut identities = locked_identities();
if !identities.contains_key(&key) && identities.len() >= MAX_REMEMBERED_CLIENTS {
identities.clear();
}
*identities.entry(key).or_insert_with(next_target_identity)
}
fn reissue_for(key: usize) -> u64 {
let identity = next_target_identity();
locked_identities().insert(key, identity);
identity
}
pub struct DebugEngine {
client: IDebugClient6,
control: IDebugControl4,
dataspaces: IDebugDataSpaces4,
symbols: IDebugSymbols3,
owns_session: bool,
deferred_inputs: Mutex<Vec<TargetInput>>,
interrupt_raised: Arc<AtomicBool>,
attached_processes: Mutex<std::collections::HashSet<u32>>,
}
impl Default for DebugEngine {
fn default() -> Self {
Self::new()
}
}
unsafe impl Sync for DebugEngine {}
unsafe impl Send for DebugEngine {}
impl DebugEngine {
pub fn new() -> Self {
let client: IDebugClient6 =
unsafe { windows::Win32::System::Diagnostics::Debug::Extensions::DebugCreate() }
.expect("[-] Failed to create debug client");
let mut engine = Self::from_client_interface(client);
engine.owns_session = true;
reissue_identity(&engine.client);
engine
}
pub fn connect(remote_options: &str) -> Result<Self, DbgEngError> {
let wide: Vec<u16> = remote_options
.encode_utf16()
.chain(std::iter::once(0))
.collect();
let mut raw: *mut std::ffi::c_void = std::ptr::null_mut();
unsafe {
DebugConnectWide(
PCWSTR::from_raw(wide.as_ptr()),
&IDebugClient6::IID,
&raw mut raw,
)
}
.map_err(|source| DbgEngError::Context {
operation: format!("connecting to the debugging server at `{remote_options}`"),
source,
})?;
let client: IDebugClient6 = unsafe { IDebugClient6::from_raw(raw) };
let engine = Self::try_from_client_interface(client)?;
reissue_identity(&engine.client);
Ok(engine)
}
pub fn from_windbg_client(client: &IUnknown) -> Self {
let client: IDebugClient6 = client.cast().expect("[-] Failed to cast debug client");
Self::from_client_interface(client)
}
pub fn try_from_windbg_client(client: &IUnknown) -> Result<Self, DbgEngError> {
let client: IDebugClient6 = client.cast().map_err(|source| DbgEngError::Context {
operation: "querying IDebugClient6".into(),
source,
})?;
Self::try_from_client_interface(client)
}
pub fn create_from_windbg_client(client: &IUnknown) -> Self {
let client: IDebugClient6 = client.cast().expect("[-] Failed to cast debug client");
let new_client = unsafe {
client
.CreateClient()
.expect("[-] Failed to create debug client")
}
.cast::<IDebugClient6>()
.expect("[-] Failed to cast debug client");
let engine = Self::from_client_interface(new_client);
reissue_identity(&engine.client);
engine
}
pub fn from_client_interface(client: IDebugClient6) -> Self {
let control: IDebugControl4 = client
.cast::<IDebugControl4>()
.expect("[-] Failed to get debug control interface");
let dataspaces: IDebugDataSpaces4 = client
.cast::<IDebugDataSpaces4>()
.expect("[-] Failed to get debug data spaces interface");
let symbols: IDebugSymbols3 = client
.cast::<IDebugSymbols3>()
.expect("[-] Failed to get debug symbols interface");
Self {
client,
control,
dataspaces,
symbols,
owns_session: false,
deferred_inputs: Mutex::new(Vec::new()),
interrupt_raised: Arc::new(AtomicBool::new(false)),
attached_processes: Mutex::new(std::collections::HashSet::new()),
}
}
pub fn try_from_client_interface(client: IDebugClient6) -> Result<Self, DbgEngError> {
let control = client
.cast::<IDebugControl4>()
.map_err(|source| DbgEngError::Context {
operation: "querying IDebugControl4".into(),
source,
})?;
let dataspaces =
client
.cast::<IDebugDataSpaces4>()
.map_err(|source| DbgEngError::Context {
operation: "querying IDebugDataSpaces4".into(),
source,
})?;
let symbols = client
.cast::<IDebugSymbols3>()
.map_err(|source| DbgEngError::Context {
operation: "querying IDebugSymbols3".into(),
source,
})?;
Ok(Self {
client,
control,
dataspaces,
symbols,
owns_session: false,
deferred_inputs: Mutex::new(Vec::new()),
interrupt_raised: Arc::new(AtomicBool::new(false)),
attached_processes: Mutex::new(std::collections::HashSet::new()),
})
}
pub fn interrupt_handle(&self) -> InterruptHandle {
InterruptHandle {
control: self.control.clone(),
raised: Arc::clone(&self.interrupt_raised),
}
}
pub fn target_identity(&self) -> u64 {
identity_of(&self.client)
}
pub fn read_memory(&self, address: u64, size: usize) -> Result<Vec<u8>, DbgEngError> {
let size_u32 = u32::try_from(size).map_err(|_| DbgEngError::BufferTooLarge(size))?;
let mut buffer = vec![0; size];
let mut read = 0u32;
unsafe {
self.dataspaces.ReadVirtual(
address,
buffer.as_mut_ptr().cast(),
size_u32,
Some(&mut read),
)
}
.map_err(|source| DbgEngError::Context {
operation: format!("reading {size} bytes of virtual memory at {address:#x}"),
source,
})?;
if read as usize != size {
return Err(DbgEngError::ShortRead {
address,
requested: size,
actual: read as usize,
});
}
Ok(buffer)
}
pub fn kernel_base(&self) -> Result<u64, DbgEngError> {
let name = CString::new("nt").unwrap();
let mut base = 0u64;
unsafe {
self.symbols.GetModuleByModuleName(
PCSTR::from_raw(name.as_ptr().cast()),
0,
None,
Some(&mut base),
)
}
.map_err(|source| DbgEngError::Context {
operation: "discovering the nt kernel base".into(),
source,
})?;
Ok(base)
}
pub fn kernel_image(&self) -> Result<KernelImage, DbgEngError> {
let base = self.kernel_base()?;
let mut params = DEBUG_MODULE_PARAMETERS::default();
unsafe {
self.symbols
.GetModuleParameters(1, Some(&base), 0, &mut params)
}
.map_err(|source| DbgEngError::Context {
operation: format!("reading the parameters of the kernel image at {base:#x}"),
source,
})?;
Ok(KernelImage {
base,
size: params.Size,
timestamp: params.TimeDateStamp,
checksum: params.Checksum,
})
}
pub fn symbol_offset(&self, name: &str) -> Result<u64, DbgEngError> {
let name = CString::new(name).map_err(|_| DbgEngError::InvalidCommand)?;
unsafe {
self.symbols
.GetOffsetByName(PCSTR::from_raw(name.as_ptr().cast()))
}
.map_err(|source| DbgEngError::Context {
operation: format!("resolving symbol {}", name.to_string_lossy()),
source,
})
}
pub fn type_id(&self, module: u64, name: &str) -> Result<u32, DbgEngError> {
let name = CString::new(name).map_err(|_| DbgEngError::InvalidCommand)?;
unsafe {
self.symbols
.GetTypeId(module, PCSTR::from_raw(name.as_ptr().cast()))
}
.map_err(|source| DbgEngError::Context {
operation: format!("resolving type {}", name.to_string_lossy()),
source,
})
}
pub fn type_size(&self, module: u64, type_id: u32) -> Result<u32, DbgEngError> {
unsafe { self.symbols.GetTypeSize(module, type_id) }.map_err(|source| {
DbgEngError::Context {
operation: format!("resolving size of type id {type_id}"),
source,
}
})
}
pub fn field_offset(&self, module: u64, type_id: u32, field: &str) -> Result<u32, DbgEngError> {
let field = CString::new(field).map_err(|_| DbgEngError::InvalidCommand)?;
unsafe {
self.symbols
.GetFieldOffset(module, type_id, PCSTR::from_raw(field.as_ptr().cast()))
}
.map_err(|source| DbgEngError::Context {
operation: format!("resolving field {}", field.to_string_lossy()),
source,
})
}
pub fn field_type_and_offset(
&self,
module: u64,
type_id: u32,
field: &str,
) -> Result<(u32, u32), DbgEngError> {
let field = CString::new(field).map_err(|_| DbgEngError::InvalidCommand)?;
let mut field_type = 0u32;
let mut offset = 0u32;
unsafe {
self.symbols.GetFieldTypeAndOffset(
module,
type_id,
PCSTR::from_raw(field.as_ptr().cast()),
Some(&mut field_type),
Some(&mut offset),
)
}
.map_err(|source| DbgEngError::Context {
operation: format!(
"resolving type and offset of field {}",
field.to_string_lossy()
),
source,
})?;
Ok((field_type, offset))
}
pub fn field_names(&self, module: u64, type_id: u32) -> Vec<String> {
const MAX_FIELDS: u32 = 4096;
let mut fields = Vec::new();
for index in 0..MAX_FIELDS {
let name = read_engine_string(|buffer, size| unsafe {
self.symbols
.GetFieldName(module, type_id, index, buffer, size)
});
match name {
Ok(name) if !name.is_empty() => fields.push(name),
Ok(_) | Err(_) => break,
}
}
fields
}
pub fn current_process_peb(&self) -> Result<u64, DbgEngError> {
let objects: IDebugSystemObjects =
self.client.cast().map_err(|source| DbgEngError::Context {
operation: "obtaining the system-objects interface".into(),
source,
})?;
unsafe { objects.GetCurrentProcessPeb() }.map_err(|source| DbgEngError::Context {
operation: "reading the current process PEB".into(),
source,
})
}
pub fn current_process_system_id(&self) -> Result<u32, DbgEngError> {
let objects: IDebugSystemObjects =
self.client.cast().map_err(|source| DbgEngError::Context {
operation: "obtaining the system-objects interface".into(),
source,
})?;
unsafe { objects.GetCurrentProcessSystemId() }.map_err(|source| DbgEngError::Context {
operation: "reading the current process id".into(),
source,
})
}
pub fn valid_virtual_region(
&self,
base: u64,
size: usize,
) -> Result<(u64, usize), DbgEngError> {
let size_u32 = u32::try_from(size).map_err(|_| DbgEngError::BufferTooLarge(size))?;
let mut valid_base = 0;
let mut valid_size = 0;
unsafe {
self.dataspaces
.GetValidRegionVirtual(base, size_u32, &mut valid_base, &mut valid_size)
}
.map_err(|source| DbgEngError::Context {
operation: format!("querying valid virtual region at {base:#x}"),
source,
})?;
Ok((valid_base, valid_size as usize))
}
pub fn interrupted(&self) -> Result<bool, DbgEngError> {
let result = unsafe {
(Interface::vtable(&self.control).GetInterrupt)(Interface::as_raw(&self.control))
};
match result {
S_OK => Ok(true),
S_FALSE => Ok(false),
result => Err(DbgEngError::Context {
operation: "polling debugger interrupt".into(),
source: windows::core::Error::from_hresult(HRESULT(result.0)),
}),
}
}
fn output_inner(&self, text: &str, dml: bool) -> Result<(), DbgEngError> {
let escaped = text.replace('%', "%%");
let message = CString::new(escaped).map_err(|_| DbgEngError::InvalidOutput)?;
let outctl = if dml {
DEBUG_OUTCTL_THIS_CLIENT | 0x20
} else {
DEBUG_OUTCTL_THIS_CLIENT
};
unsafe {
self.control.ControlledOutput(
outctl,
DEBUG_OUTPUT_NORMAL,
PCSTR::from_raw(message.as_ptr().cast()),
)
}
.map_err(|source| DbgEngError::Context {
operation: "writing debugger output".into(),
source,
})
}
pub fn output(&self, text: &str) -> Result<(), DbgEngError> {
self.output_inner(text, false)
}
pub fn output_dml(&self, text: &str) -> Result<(), DbgEngError> {
self.output_inner(text, true)
}
pub fn execution_status(&self) -> Result<u32, DbgEngError> {
unsafe { self.control.GetExecutionStatus() }.map_err(|source| DbgEngError::Context {
operation: "querying target execution status".into(),
source,
})
}
pub fn processor_type(&self) -> Result<u32, DbgEngError> {
unsafe { self.control.GetActualProcessorType() }.map_err(|source| DbgEngError::Context {
operation: "querying target processor type".into(),
source,
})
}
pub fn is_kernel_target(&self) -> Result<bool, DbgEngError> {
let mut class = 0;
let mut qualifier = 0;
unsafe { self.control.GetDebuggeeType(&mut class, &mut qualifier) }.map_err(|source| {
DbgEngError::Context {
operation: "querying target type".into(),
source,
}
})?;
Ok(class == DEBUG_CLASS_KERNEL)
}
fn request_initial_break(&self) -> Result<(), DbgEngError> {
unsafe { self.control.AddEngineOptions(DEBUG_ENGOPT_INITIAL_BREAK) }
.map_err(DbgEngError::OperationFailed)
}
fn clear_initial_break(&self) {
unsafe {
let _ = self.control.RemoveEngineOptions(DEBUG_ENGOPT_INITIAL_BREAK);
}
}
fn absorb_initial_break_artifact(&self) {
let _ = self.execute_and_wait("g", 5_000);
}
fn is_live_kernel(&self) -> bool {
let mut class = 0u32;
let mut qualifier = 0u32;
if unsafe { self.control.GetDebuggeeType(&mut class, &mut qualifier) }.is_err() {
return false;
}
class == DEBUG_CLASS_KERNEL && qualifier < DEBUG_KERNEL_SMALL_DUMP
}
pub fn attach_local_kernel(&self) -> Result<(), DbgEngError> {
self.attach_local_kernel_begin()?.wait()
}
pub fn attach_local_kernel_begin(&self) -> Result<PendingTarget<'_>, DbgEngError> {
self.request_initial_break()?;
unsafe {
self.client
.AttachKernel(DEBUG_ATTACH_LOCAL_KERNEL, None)
.map_err(DbgEngError::AttachFailed)?;
}
self.forget_attachments();
Ok(PendingTarget::new(self, WaitKind::KernelBreakIn))
}
pub fn attach_kernel(&self, connection_string: &str) -> Result<(), DbgEngError> {
self.attach_kernel_begin(connection_string)?.wait()
}
pub fn attach_kernel_begin(
&self,
connection_string: &str,
) -> Result<PendingTarget<'_>, DbgEngError> {
let connection =
CString::new(connection_string).map_err(|_| DbgEngError::InvalidCommand)?;
self.request_initial_break()?;
unsafe {
self.client
.AttachKernel(
DEBUG_ATTACH_KERNEL_CONNECTION,
PCSTR::from_raw(connection.as_ptr() as *const u8),
)
.map_err(DbgEngError::AttachFailed)?;
}
self.retain_deferred_input(TargetInput::Ansi(connection));
self.forget_attachments();
Ok(PendingTarget::new(self, WaitKind::KernelBreakIn))
}
fn wait_for_kernel_break_in(&self) -> Result<(), DbgEngError> {
let (waited, timed_out) = self.wait_for_event_bounded(KERNEL_ATTACH_WAIT_MS);
self.clear_initial_break();
waited.map_err(DbgEngError::CommandFailed)?;
let status =
unsafe { self.control.GetExecutionStatus() }.map_err(DbgEngError::CommandFailed)?;
if timed_out || status == DEBUG_STATUS_NO_DEBUGGEE {
return Err(DbgEngError::KernelBreakTimeout);
}
self.absorb_initial_break_artifact();
Ok(())
}
pub fn set_symbol_path(&self, symbol_path: &str) -> Result<(), DbgEngError> {
let path = CString::new(symbol_path).map_err(|_| DbgEngError::InvalidCommand)?;
unsafe {
self.symbols
.SetSymbolPath(PCSTR::from_raw(path.as_ptr() as *const u8))
.map_err(DbgEngError::SymbolPathFailed)
}
}
pub fn append_symbol_path(&self, symbol_path: &str) -> Result<(), DbgEngError> {
let path = CString::new(symbol_path).map_err(|_| DbgEngError::InvalidCommand)?;
unsafe {
self.symbols
.AppendSymbolPath(PCSTR::from_raw(path.as_ptr() as *const u8))
.map_err(DbgEngError::SymbolPathFailed)
}
}
pub fn execute_command(&self, command: &str) -> Result<String, DbgEngError> {
self.refuse_without_a_debuggee()?;
self.execute_fixed_command(command)
}
fn execute_fixed_command(&self, command: &str) -> Result<String, DbgEngError> {
let cmd_c = CString::new(command).map_err(|_| DbgEngError::InvalidCommand)?;
let cmd = PCSTR::from_raw(cmd_c.as_ptr() as *const u8);
let mut output_buffer = Vec::<u8>::with_capacity(4096);
let output_callbacks = OutputCallbacks::new(&mut output_buffer);
let output_interface: IDebugOutputCallbacks = output_callbacks.into();
unsafe {
self.client
.SetOutputCallbacks(Some(&output_interface))
.map_err(DbgEngError::CommandFailed)?;
}
let result = unsafe {
self.control
.Execute(DEBUG_OUTCTL_THIS_CLIENT, cmd, DEBUG_EXECUTE_ECHO)
};
unsafe {
let _ = self.client.SetOutputCallbacks(None);
}
result.map_err(DbgEngError::CommandFailed)?;
Ok(String::from_utf8_lossy(&output_buffer).to_string())
}
pub fn execute_command_bounded(
&self,
command: &str,
timeout_ms: u32,
) -> Result<CommandRun, DbgEngError> {
self.refuse_without_a_debuggee()?;
let cmd_c = CString::new(command).map_err(|_| DbgEngError::InvalidCommand)?;
let cmd = PCSTR::from_raw(cmd_c.as_ptr() as *const u8);
self.interrupt_raised.store(false, Ordering::SeqCst);
let mut output_buffer = Vec::<u8>::with_capacity(4096);
let output_callbacks = OutputCallbacks::new(&mut output_buffer);
let output_interface: IDebugOutputCallbacks = output_callbacks.into();
unsafe {
self.client
.SetOutputCallbacks(Some(&output_interface))
.map_err(DbgEngError::CommandFailed)?;
}
let watchdog = (timeout_ms > 0).then(|| {
let handle = self.interrupt_handle();
Watchdog::arm(Duration::from_millis(u64::from(timeout_ms)), move || {
let _ = handle.interrupt();
})
});
let result = unsafe {
self.control
.Execute(DEBUG_OUTCTL_THIS_CLIENT, cmd, DEBUG_EXECUTE_ECHO)
};
let by_watchdog = watchdog.is_some_and(Watchdog::disarm);
unsafe {
let _ = self.client.SetOutputCallbacks(None);
}
let interrupted = by_watchdog | self.interrupt_raised.swap(false, Ordering::SeqCst);
if interrupted {
let _ = self.interrupted();
}
if !interrupted {
result.map_err(DbgEngError::CommandFailed)?;
}
Ok(CommandRun {
output: String::from_utf8_lossy(&output_buffer).to_string(),
cut_short: match (by_watchdog, interrupted) {
(true, _) => Some(Interruption::Deadline {
after_ms: timeout_ms,
}),
(false, true) => Some(Interruption::OnRequest),
(false, false) => None,
},
target_gone: self.lost_its_target(),
})
}
pub fn wait_for_event(&self, timeout_ms: u32) -> Result<(), DbgEngError> {
let result = unsafe { self.control.WaitForEvent(0, timeout_ms) };
if result.is_err() {
return Err(DbgEngError::CommandFailed(result.err().unwrap()));
}
Ok(())
}
fn wait_for_event_bounded(&self, timeout_ms: u32) -> (windows::core::Result<()>, bool) {
let handle = self.interrupt_handle();
let watchdog = Watchdog::arm(Duration::from_millis(u64::from(timeout_ms)), move || {
let _ = handle.interrupt();
});
let result = unsafe { self.control.WaitForEvent(0, WAIT_INFINITE) };
(result, watchdog.disarm())
}
pub fn execute_and_wait(
&self,
command: &str,
timeout_ms: u32,
) -> Result<CommandRun, DbgEngError> {
self.interrupt_raised.store(false, Ordering::SeqCst);
self.refuse_without_a_debuggee()?;
let cmd_c = CString::new(command).map_err(|_| DbgEngError::InvalidCommand)?;
let cmd = PCSTR::from_raw(cmd_c.as_ptr() as *const u8);
let mut output_buffer = Vec::<u8>::with_capacity(4096);
let output_callbacks = OutputCallbacks::new(&mut output_buffer);
let output_interface: IDebugOutputCallbacks = output_callbacks.into();
unsafe {
self.client
.SetOutputCallbacks(Some(&output_interface))
.map_err(DbgEngError::CommandFailed)?;
}
let exec = unsafe {
self.control
.Execute(DEBUG_OUTCTL_THIS_CLIENT, cmd, DEBUG_EXECUTE_ECHO)
};
let (waited, by_watchdog) = if exec.is_ok() {
self.wait_for_event_bounded(timeout_ms)
} else {
(Ok(()), false)
};
unsafe {
let _ = self.client.SetOutputCallbacks(None);
}
let interrupted = by_watchdog | self.interrupt_raised.swap(false, Ordering::SeqCst);
let target_gone = self.lost_its_target();
if interrupted {
let _ = self.interrupted();
} else if !target_gone {
exec.map_err(DbgEngError::CommandFailed)?;
waited.map_err(DbgEngError::CommandFailed)?;
}
Ok(CommandRun {
output: String::from_utf8_lossy(&output_buffer).to_string(),
cut_short: match (by_watchdog, interrupted) {
(true, _) => Some(Interruption::Deadline {
after_ms: timeout_ms,
}),
(false, true) => Some(Interruption::OnRequest),
(false, false) => None,
},
target_gone,
})
}
pub fn is_running(&self) -> Result<bool, DbgEngError> {
Ok(is_running_status(self.execution_status()?))
}
pub fn has_target(&self) -> Result<bool, DbgEngError> {
Ok(self.execution_status()? != DEBUG_STATUS_NO_DEBUGGEE)
}
fn refuse_without_a_debuggee(&self) -> Result<(), DbgEngError> {
match self.has_target()? {
true => Ok(()),
false => Err(DbgEngError::NoDebuggee),
}
}
fn lost_its_target(&self) -> bool {
!self.has_target().unwrap_or(true)
}
pub fn settle(&self, timeout_ms: u32) -> Result<Option<CommandRun>, DbgEngError> {
if !self.is_running()? {
return Ok(None);
}
self.interrupt_raised.store(false, Ordering::SeqCst);
let mut output_buffer = Vec::<u8>::with_capacity(4096);
let output_callbacks = OutputCallbacks::new(&mut output_buffer);
let output_interface: IDebugOutputCallbacks = output_callbacks.into();
unsafe {
self.client
.SetOutputCallbacks(Some(&output_interface))
.map_err(DbgEngError::CommandFailed)?;
}
let (waited, by_watchdog) = self.wait_for_event_bounded(timeout_ms);
unsafe {
let _ = self.client.SetOutputCallbacks(None);
}
let interrupted = by_watchdog | self.interrupt_raised.swap(false, Ordering::SeqCst);
let target_gone = self.lost_its_target();
if interrupted {
let _ = self.interrupted();
} else if !target_gone {
waited.map_err(DbgEngError::CommandFailed)?;
}
Ok(Some(CommandRun {
output: String::from_utf8_lossy(&output_buffer).to_string(),
cut_short: match (by_watchdog, interrupted) {
(true, _) => Some(Interruption::Deadline {
after_ms: timeout_ms,
}),
(false, true) => Some(Interruption::OnRequest),
(false, false) => None,
},
target_gone,
}))
}
pub fn run_to_address(
&self,
address: u64,
timeout_ms: u32,
) -> Result<RunToResult, DbgEngError> {
self.refuse_without_a_debuggee()?;
let _breakpoint = ScopedBreakpoint::at(self, address)?;
let cmd_c = CString::new("g").map_err(|_| DbgEngError::InvalidCommand)?;
let cmd = PCSTR::from_raw(cmd_c.as_ptr() as *const u8);
let mut output_buffer = Vec::<u8>::with_capacity(4096);
let output_callbacks = OutputCallbacks::new(&mut output_buffer);
let output_interface: IDebugOutputCallbacks = output_callbacks.into();
unsafe {
self.client
.SetOutputCallbacks(Some(&output_interface))
.map_err(DbgEngError::CommandFailed)?;
}
let exec = unsafe {
self.control
.Execute(DEBUG_OUTCTL_THIS_CLIENT, cmd, DEBUG_EXECUTE_ECHO)
};
let (waited, expired) = if exec.is_ok() {
let (waited, forced) = self.wait_for_event_bounded(timeout_ms);
let waited = if forced {
Ok(())
} else {
waited.map_err(DbgEngError::CommandFailed)
};
(waited, forced)
} else {
(Ok(()), false)
};
unsafe {
let _ = self.client.SetOutputCallbacks(None);
}
let output = String::from_utf8_lossy(&output_buffer).to_string();
if self.lost_its_target() {
return Ok(RunToResult {
outcome: RunToOutcome::TargetGone,
output,
});
}
exec.map_err(DbgEngError::CommandFailed)?;
waited?;
if expired {
if self.instruction_pointer().ok() == Some(address) {
return Ok(RunToResult {
outcome: RunToOutcome::Hit,
output,
});
}
return Ok(RunToResult {
outcome: RunToOutcome::Timeout,
output,
});
}
let rip = self.instruction_pointer()?;
let outcome = if rip == address {
RunToOutcome::Hit
} else {
RunToOutcome::StoppedElsewhere { stopped_at: rip }
};
Ok(RunToResult { outcome, output })
}
pub fn create_debug_event_context_callbacks(
callback: Option<BreakpointCallback>,
) -> IDebugEventContextCallbacks {
let callbacks = DebugEventContextCallbacks::new(callback);
callbacks.into()
}
pub fn set_breakpoint_event_callbacks(&self, event_callbacks: IDebugEventContextCallbacks) {
unsafe {
self.client
.SetEventContextCallbacks(Some(&event_callbacks))
.expect("[-] Failed to set event callbacks");
};
}
pub fn log(&self, message: &str) {
let message = CString::new(message).expect("Failed to create CString");
let message = PCSTR::from_raw(message.as_ptr() as *const u8);
unsafe { self.control.Output(DEBUG_OUTPUT_NORMAL, message) }
.expect("[-] Failed to log message");
}
pub fn reload_symbols(&self, args: &str) -> Result<(), DbgEngError> {
let args = CString::new(args).map_err(|_| DbgEngError::InvalidCommand)?;
unsafe {
self.symbols
.Reload(PCSTR::from_raw(args.as_ptr() as *const u8))
.map_err(DbgEngError::OperationFailed)
}
}
pub fn registers(&self) -> Result<String, DbgEngError> {
self.execute_command("r")
}
pub fn scope(&self) -> Result<Scope, DbgEngError> {
let mut refusal = None;
for &size in SCOPE_CONTEXT_SIZES {
let mut instruction = 0u64;
let mut frame = DEBUG_STACK_FRAME::default();
let mut context = vec![0u8; size as usize];
match unsafe {
self.symbols.GetScope(
Some(&mut instruction),
Some(&mut frame),
Some(context.as_mut_ptr().cast()),
size,
)
} {
Ok(()) => {
return Ok(Scope {
instruction,
frame,
context,
target: self.target_identity(),
});
}
Err(why) if why.code() == E_INVALIDARG => refusal = Some(why),
Err(why) => {
refusal = Some(why);
break;
}
}
}
self.contextless_scope(refusal)
}
fn contextless_scope(
&self,
refusal: Option<windows::core::Error>,
) -> Result<Scope, DbgEngError> {
let mut instruction = 0u64;
let mut frame = DEBUG_STACK_FRAME::default();
unsafe {
self.symbols
.GetScope(Some(&mut instruction), Some(&mut frame), None, 0)
}
.map_err(|source| DbgEngError::Context {
operation: "reading the debugger's scope".into(),
source: refusal.unwrap_or(source),
})?;
Ok(Scope {
instruction,
frame,
context: Vec::new(),
target: self.target_identity(),
})
}
pub fn set_scope(&self, scope: &Scope) -> Result<(), DbgEngError> {
if scope.target != self.target_identity() {
return Err(DbgEngError::ScopeFromAnotherTarget);
}
unsafe {
self.symbols.SetScope(
scope.instruction,
Some(&scope.frame),
if scope.context.is_empty() {
None
} else {
Some(scope.context.as_ptr().cast())
},
scope.context.len() as u32,
)
}
.map_err(|source| DbgEngError::Context {
operation: "restoring the debugger's scope".into(),
source,
})
}
pub fn scope_guard(&self) -> Result<ScopeGuard<'_>, DbgEngError> {
Ok(ScopeGuard {
engine: self,
saved: self.scope()?,
})
}
pub fn register_values(&self) -> Result<Vec<Register>, DbgEngError> {
let registers: IDebugRegisters =
self.client.cast().map_err(|source| DbgEngError::Context {
operation: "obtaining the register interface".into(),
source,
})?;
let count =
unsafe { registers.GetNumberRegisters() }.map_err(|source| DbgEngError::Context {
operation: "counting the target's registers".into(),
source,
})?;
let mut out = Vec::with_capacity(count as usize);
for index in 0..count {
let mut description = DEBUG_REGISTER_DESCRIPTION::default();
let name = read_engine_string(|buffer, size| unsafe {
registers.GetDescription(index, buffer, size, Some(&mut description))
})
.map_err(|source| DbgEngError::Context {
operation: format!("describing register {index}"),
source,
})?;
let mut value = DEBUG_VALUE::default();
let value = match unsafe { registers.GetValue(index, &mut value) } {
Ok(()) => RegisterValue::decode(&value),
Err(_) => RegisterValue::Unavailable,
};
out.push(Register {
name,
value,
subregister: description.Flags & DEBUG_REGISTER_SUB_REGISTER != 0,
});
}
Ok(out)
}
pub fn register_descriptions(&self) -> Result<Vec<RegisterDescription>, DbgEngError> {
let registers: IDebugRegisters =
self.client.cast().map_err(|source| DbgEngError::Context {
operation: "obtaining the register interface".into(),
source,
})?;
let count =
unsafe { registers.GetNumberRegisters() }.map_err(|source| DbgEngError::Context {
operation: "counting the target's registers".into(),
source,
})?;
let mut out = Vec::with_capacity(count as usize);
for index in 0..count {
let mut description = DEBUG_REGISTER_DESCRIPTION::default();
let name = read_engine_string(|buffer, size| unsafe {
registers.GetDescription(index, buffer, size, Some(&mut description))
})
.map_err(|source| DbgEngError::Context {
operation: format!("describing register {index}"),
source,
})?;
out.push(RegisterDescription {
name,
kind: description.Type,
flags: description.Flags,
subreg_master: description.SubregMaster,
subreg_length: description.SubregLength,
subreg_mask: description.SubregMask,
subreg_shift: description.SubregShift,
});
}
Ok(out)
}
pub fn instruction_pointer(&self) -> Result<u64, DbgEngError> {
let registers: IDebugRegisters =
self.client.cast().map_err(|source| DbgEngError::Context {
operation: "obtaining the register interface".into(),
source,
})?;
unsafe { registers.GetInstructionOffset() }.map_err(|source| DbgEngError::Context {
operation: "reading the instruction pointer".into(),
source,
})
}
pub fn modules(&self) -> Result<Vec<Module>, DbgEngError> {
let (loaded, _) = self.module_counts()?;
self.module_range(0, loaded)
}
pub fn module(&self, name: &str) -> Result<Module, DbgEngError> {
let name = CString::new(name).map_err(|_| DbgEngError::InvalidCommand)?;
let mut index = 0u32;
let mut base = 0u64;
unsafe {
self.symbols.GetModuleByModuleName(
PCSTR::from_raw(name.as_ptr().cast()),
0,
Some(&mut index),
Some(&mut base),
)
}
.map_err(|source| DbgEngError::Context {
operation: format!("locating module {}", name.to_string_lossy()),
source,
})?;
let mut params = DEBUG_MODULE_PARAMETERS::default();
unsafe {
self.symbols
.GetModuleParameters(1, Some(&base), 0, &mut params)
}
.map_err(|source| DbgEngError::Context {
operation: format!("reading parameters of module at {base:#x}"),
source,
})?;
Ok(self.named_module(index, ¶ms))
}
pub fn module_symbol_file(&self, module_base: u64) -> Result<String, DbgEngError> {
read_engine_string(|buffer, size| unsafe {
self.symbols.GetModuleNameString(
DEBUG_MODNAME_SYMBOL_FILE,
DEBUG_ANY_ID,
module_base,
buffer,
size,
)
})
.map_err(|source| DbgEngError::Context {
operation: format!("reading the symbol file for module at {module_base:#x}"),
source,
})
}
pub fn module_pdb(&self, base: u64) -> Result<Option<PdbIdentity>, DbgEngError> {
let advanced =
self.client
.cast::<IDebugAdvanced2>()
.map_err(|source| DbgEngError::Context {
operation: "querying IDebugAdvanced2".into(),
source,
})?;
let mut info = IMAGEHLP_MODULEW64 {
SizeOfStruct: std::mem::size_of::<IMAGEHLP_MODULEW64>() as u32,
..Default::default()
};
let filled = unsafe {
advanced.GetSymbolInformation(
DEBUG_SYMINFO_IMAGEHLP_MODULEW64,
base,
0,
Some((&raw mut info).cast()),
std::mem::size_of::<IMAGEHLP_MODULEW64>() as u32,
None,
None,
None,
)
};
filled.map_err(|source| DbgEngError::Context {
operation: format!("reading the PDB identity of the module at {base:#x}"),
source,
})?;
let guid = info.PdbSig70;
if guid.data1 == 0 && guid.data2 == 0 && guid.data3 == 0 && guid.data4 == [0; 8] {
return Ok(None);
}
Ok(Some(PdbIdentity {
guid: format_pdb_guid(&guid),
age: info.PdbAge,
unmatched: info.PdbUnmatched.as_bool(),
file: wide_to_string(&info.LoadedPdbName),
}))
}
pub fn module_identity(&self, name: &str) -> Result<ModuleIdentity, DbgEngError> {
let module = self.module(name)?;
let symbol_file = self.module_symbol_file(module.base)?;
Ok(ModuleIdentity {
name: module.name,
image_name: module.image_name,
loaded_image_name: module.loaded_image_name,
symbol_file,
symbols: module.symbols,
base: module.base,
size: module.size,
timestamp: module.timestamp,
checksum: module.checksum,
})
}
pub fn unloaded_modules(&self) -> Result<Vec<Module>, DbgEngError> {
let (loaded, unloaded) = self.module_counts()?;
self.module_range(loaded, unloaded)
}
fn module_counts(&self) -> Result<(u32, u32), DbgEngError> {
let mut loaded = 0u32;
let mut unloaded = 0u32;
unsafe { self.symbols.GetNumberModules(&mut loaded, &mut unloaded) }.map_err(|source| {
DbgEngError::Context {
operation: "counting the target's modules".into(),
source,
}
})?;
Ok((loaded, unloaded))
}
fn module_range(&self, start: u32, count: u32) -> Result<Vec<Module>, DbgEngError> {
if count == 0 {
return Ok(Vec::new());
}
let mut params = vec![DEBUG_MODULE_PARAMETERS::default(); count as usize];
unsafe {
self.symbols
.GetModuleParameters(count, None, start, params.as_mut_ptr())
}
.map_err(|source| DbgEngError::Context {
operation: "reading module parameters".into(),
source,
})?;
let mut out = Vec::with_capacity(count as usize);
for (offset, params) in params.iter().enumerate() {
out.push(self.named_module(start + offset as u32, params));
}
Ok(out)
}
pub fn module_at(&self, address: u64) -> Result<Option<Module>, DbgEngError> {
let mut index = 0u32;
match unsafe {
self.symbols
.GetModuleByOffset(address, 0, Some(&mut index), None)
} {
Ok(()) => {}
Err(why) if matches!(why.code(), E_INVALIDARG | E_NOINTERFACE) => return Ok(None),
Err(source) => {
return Err(DbgEngError::Context {
operation: format!("locating the module holding {address:#x}"),
source,
});
}
}
let mut params = DEBUG_MODULE_PARAMETERS::default();
unsafe {
self.symbols
.GetModuleParameters(1, None, index, &mut params)
}
.map_err(|source| DbgEngError::Context {
operation: format!("reading the parameters of the module at {address:#x}"),
source,
})?;
Ok(Some(self.named_module(index, ¶ms)))
}
fn named_module(&self, index: u32, params: &DEBUG_MODULE_PARAMETERS) -> Module {
let mut name = String::new();
let mut image_name = String::new();
let mut loaded_image_name = String::new();
let sized = |reported: u32| {
vec![
0u8;
if reported == 0 {
MODULE_NAME_FALLBACK
} else {
reported as usize
}
]
};
let mut name_buffer = sized(params.ModuleNameSize);
let mut image_buffer = sized(params.ImageNameSize);
let mut loaded_buffer = sized(params.LoadedImageNameSize);
let named = unsafe {
self.symbols.GetModuleNames(
index,
0,
Some(&mut image_buffer),
None,
Some(&mut name_buffer),
None,
Some(&mut loaded_buffer),
None,
)
};
if named.is_ok() {
name = nul_terminated(&name_buffer);
image_name = nul_terminated(&image_buffer);
loaded_image_name = nul_terminated(&loaded_buffer);
}
Module {
base: params.Base,
size: params.Size,
name,
image_name,
loaded_image_name,
timestamp: params.TimeDateStamp,
checksum: params.Checksum,
symbols: SymbolKind::from_engine(params.SymbolType),
user_mode: params.Flags & DEBUG_MODULE_USER_MODE != 0,
unloaded: params.Flags & DEBUG_MODULE_UNLOADED != 0,
}
}
pub fn bug_check(&self) -> Result<Option<BugCheck>, DbgEngError> {
let mut code = 0u32;
let mut parameters = [0u64; 4];
let [arg1, arg2, arg3, arg4] = &mut parameters;
unsafe {
self.control
.ReadBugCheckData(&mut code, arg1, arg2, arg3, arg4)
}
.map_err(|source| DbgEngError::Context {
operation: "reading the target's bug check data".into(),
source,
})?;
if code == 0 {
return Ok(None);
}
Ok(Some(BugCheck { code, parameters }))
}
pub fn stack_frames(&self, max_frames: usize) -> Result<Vec<StackFrame>, DbgEngError> {
if max_frames == 0 {
return Ok(Vec::new());
}
let mut raw = vec![DEBUG_STACK_FRAME::default(); max_frames];
let mut filled = 0u32;
unsafe {
self.control
.GetStackTrace(0, 0, 0, &mut raw, Some(&mut filled))
}
.map_err(|source| DbgEngError::Context {
operation: "walking the current thread's stack".into(),
source,
})?;
raw.truncate((filled as usize).min(max_frames));
Ok(raw
.iter()
.enumerate()
.map(|(index, frame)| {
let (symbol, displacement) = self.symbol_at(frame.InstructionOffset);
StackFrame {
index: index as u32,
instruction_offset: frame.InstructionOffset,
return_offset: frame.ReturnOffset,
frame_offset: frame.FrameOffset,
stack_offset: frame.StackOffset,
symbol,
displacement,
}
})
.collect())
}
pub fn disassemble(&self, address: u64, count: usize) -> Result<Vec<Instruction>, DbgEngError> {
let mut out = Vec::with_capacity(count.min(64));
let mut at = address;
for _ in 0..count {
let mut next = 0u64;
let line = read_engine_string(|buffer, size| unsafe {
self.control.Disassemble(at, 0, buffer, size, &mut next)
});
let line = match line {
Ok(line) if !line.trim().is_empty() => line,
Ok(_) | Err(_) if !out.is_empty() => break,
Ok(_) => {
return Err(DbgEngError::Context {
operation: format!("disassembling {at:#x}"),
source: S_FALSE.into(),
});
}
Err(source) => {
return Err(DbgEngError::Context {
operation: format!("disassembling {at:#x}"),
source,
});
}
};
out.push(split_instruction(at, &line));
if next <= at {
break;
}
at = next;
}
Ok(out)
}
fn symbol_at(&self, address: u64) -> (Option<String>, u64) {
let mut displacement = 0u64;
let name = read_engine_string(|buffer, size| unsafe {
self.symbols
.GetNameByOffset(address, buffer, size, Some(&mut displacement))
});
match name {
Ok(name) if !name.is_empty() => (Some(name), displacement),
_ => (None, 0),
}
}
pub fn current_process_name(&self) -> Result<String, DbgEngError> {
let system: IDebugSystemObjects =
self.client.cast().map_err(|source| DbgEngError::Context {
operation: "querying IDebugSystemObjects".into(),
source,
})?;
if !self.is_kernel_target()? {
return read_engine_string(|buffer, size| unsafe {
system.GetCurrentProcessExecutableName(buffer, size)
})
.map_err(|source| DbgEngError::Context {
operation: "reading the current process's image name".into(),
source,
});
}
let process = unsafe { system.GetCurrentProcessDataOffset() }.map_err(|source| {
DbgEngError::Context {
operation: "locating the current process's EPROCESS".into(),
source,
}
})?;
let nt = self.kernel_base()?;
let eprocess = self.type_id(nt, "_EPROCESS")?;
if let Some(full) = self.audit_image_name(nt, eprocess, process) {
return Ok(full);
}
let offset = self.field_offset(nt, eprocess, "ImageFileName")?;
let size = self
.field_size(nt, eprocess, "ImageFileName")
.unwrap_or(EPROCESS_IMAGE_NAME_LEN) as usize;
let raw = self.read_memory(process.saturating_add(u64::from(offset)), size)?;
Ok(nul_terminated(&raw))
}
fn audit_image_name(&self, nt: u64, eprocess: u32, process: u64) -> Option<String> {
let offset = self
.field_offset(nt, eprocess, "SeAuditProcessCreationInfo")
.ok()?;
let name_info = u64::from_le_bytes(
self.read_memory(process.checked_add(u64::from(offset))?, 8)
.ok()?
.try_into()
.ok()?,
);
if name_info == 0 {
return None;
}
let unicode_string = self.read_memory(name_info, 16).ok()?;
let length = u16::from_le_bytes(unicode_string[0..2].try_into().ok()?) as usize;
let buffer = u64::from_le_bytes(unicode_string[8..16].try_into().ok()?);
if buffer == 0 || length == 0 || length > 2 * 1024 {
return None;
}
let raw = self.read_memory(buffer, length).ok()?;
let wide: Vec<u16> = raw
.chunks_exact(2)
.map(|pair| u16::from_le_bytes([pair[0], pair[1]]))
.collect();
let path = String::from_utf16_lossy(&wide);
let leaf = path.rsplit(['\\', '/']).next().unwrap_or(&path).trim();
(!leaf.is_empty()).then(|| leaf.to_string())
}
fn field_size(&self, module: u64, type_id: u32, field: &str) -> Option<u32> {
let name = CString::new(field).ok()?;
let mut field_type = 0u32;
let mut offset = 0u32;
unsafe {
self.symbols.GetFieldTypeAndOffset(
module,
type_id,
PCSTR::from_raw(name.as_ptr().cast()),
Some(&mut field_type),
Some(&mut offset),
)
}
.ok()?;
self.type_size(module, field_type).ok()
}
pub fn breakpoints(&self) -> Result<Vec<BreakpointInfo>, DbgEngError> {
let count = unsafe { self.control.GetNumberBreakpoints() }.map_err(|source| {
DbgEngError::Context {
operation: "counting breakpoints".into(),
source,
}
})?;
let mut out = Vec::with_capacity(count as usize);
for index in 0..count {
let breakpoint =
unsafe { self.control.GetBreakpointByIndex(index) }.map_err(|source| {
DbgEngError::Context {
operation: format!("reading breakpoint at index {index}"),
source,
}
})?;
let breakpoint = std::mem::ManuallyDrop::new(breakpoint);
let id = unsafe { breakpoint.GetId() }.map_err(|source| DbgEngError::Context {
operation: format!("reading the id of breakpoint {index}"),
source,
})?;
let mut kind = 0u32;
let mut _processor = 0u32;
let kind = match unsafe { breakpoint.GetType(&mut kind, &mut _processor) } {
Ok(()) => BreakpointKind::from_engine(kind),
Err(_) => BreakpointKind::Other(DEBUG_ANY_ID),
};
let flags = unsafe { breakpoint.GetFlags() }.unwrap_or(0);
let address = match unsafe { breakpoint.GetOffset() } {
Ok(offset) if offset != DEBUG_INVALID_OFFSET => Some(offset),
_ => None,
};
let expression = read_engine_string(|buffer, size| unsafe {
breakpoint.GetOffsetExpression(buffer, size)
})
.ok()
.filter(|text| !text.is_empty());
let command =
read_engine_string(|buffer, size| unsafe { breakpoint.GetCommand(buffer, size) })
.ok()
.filter(|text| !text.is_empty());
let thread = unsafe { breakpoint.GetMatchThreadId() }
.ok()
.filter(|id| *id != DEBUG_ANY_ID);
out.push(BreakpointInfo {
id,
kind,
address,
expression,
command,
thread,
enabled: flags & DEBUG_BREAKPOINT_ENABLED != 0,
deferred: flags & DEBUG_BREAKPOINT_DEFERRED != 0,
one_shot: flags & DEBUG_BREAKPOINT_ONE_SHOT != 0,
pass_count: unsafe { breakpoint.GetPassCount() }.unwrap_or(0),
passes_remaining: unsafe { breakpoint.GetCurrentPassCount() }.unwrap_or(0),
});
}
Ok(out)
}
fn enable_initial_break(&self) -> Result<(), DbgEngError> {
self.execute_fixed_command("sxe ibp").map(|_| ())
}
pub fn launch_process(&self, command_line: &str) -> Result<(), DbgEngError> {
self.launch_process_begin(command_line)?.wait()
}
pub fn launch_process_begin(
&self,
command_line: &str,
) -> Result<PendingTarget<'_>, DbgEngError> {
self.prune_dead_attachments();
self.enable_initial_break()?;
let mut wide = to_wide(command_line);
unsafe {
self.client.CreateProcessWide(
0,
PWSTR::from_raw(wide.as_mut_ptr()),
DEBUG_ONLY_THIS_PROCESS | CREATE_NEW_CONSOLE,
)
}
.map_err(DbgEngError::OperationFailed)?;
self.retain_deferred_input(TargetInput::Wide(wide));
Ok(PendingTarget::new(self, WaitKind::Live))
}
pub fn attach_process(&self, pid: u32) -> Result<(), DbgEngError> {
self.attach_process_begin(pid)?.wait()
}
pub fn attach_process_begin(&self, pid: u32) -> Result<PendingTarget<'_>, DbgEngError> {
self.enable_initial_break()?;
unsafe { self.client.AttachProcess(0, pid, DEBUG_ATTACH_DEFAULT) }
.map_err(DbgEngError::OperationFailed)?;
self.prune_dead_attachments();
self.claim_attached(pid);
Ok(PendingTarget::new(self, WaitKind::Live))
}
pub fn open_dump(&self, path: &str) -> Result<(), DbgEngError> {
let wide = to_wide(path);
unsafe {
self.client
.OpenDumpFileWide(PCWSTR::from_raw(wide.as_ptr()), 0)
}
.map_err(DbgEngError::OperationFailed)?;
self.forget_attachments();
Ok(())
}
pub fn open_trace(&self, path: &str) -> Result<(), DbgEngError> {
self.open_dump(path)
}
fn retain_deferred_input(&self, input: TargetInput) {
self.deferred_inputs
.lock()
.unwrap_or_else(|e| e.into_inner())
.push(input);
}
fn release_deferred_inputs(&self) {
self.deferred_inputs
.lock()
.unwrap_or_else(|e| e.into_inner())
.clear();
}
fn claim_attached(&self, pid: u32) {
self.attached_processes
.lock()
.unwrap_or_else(|e| e.into_inner())
.insert(pid);
}
fn prune_dead_attachments(&self) {
let Ok(held) = self.session_processes() else {
return;
};
self.attached_processes
.lock()
.unwrap_or_else(|e| e.into_inner())
.retain(|pid| held.iter().any(|(_, held)| held == pid));
}
fn forget_attachments(&self) {
self.attached_processes
.lock()
.unwrap_or_else(|e| e.into_inner())
.clear();
}
pub fn attached_to_a_live_process(&self) -> bool {
let attached = self
.attached_processes
.lock()
.unwrap_or_else(|e| e.into_inner());
!attached.is_empty()
&& self
.session_processes()
.is_ok_and(|held| held.iter().any(|(_, pid)| attached.contains(pid)))
}
pub fn end_session(&self) -> Result<(), DbgEngError> {
reissue_identity(&self.client);
let ended = if self.is_live_kernel() {
self.resume_and_detach_live_kernel()
} else {
let detached = self.detach_attached_processes();
unsafe { self.client.EndSession(DEBUG_END_PASSIVE) }
.map_err(DbgEngError::OperationFailed)
.and(detached)
};
if ended.is_ok() {
self.release_deferred_inputs();
}
ended
}
fn resume_and_detach_live_kernel(&self) -> Result<(), DbgEngError> {
let _ = self.execute_command("bc *");
unsafe {
let _ = self.control.SetExecutionStatus(DEBUG_STATUS_GO);
self.client.EndSession(DEBUG_END_ACTIVE_DETACH)
}
.map_err(DbgEngError::OperationFailed)
}
fn detach_attached_processes(&self) -> Result<(), DbgEngError> {
let attached = std::mem::take(
&mut *self
.attached_processes
.lock()
.unwrap_or_else(|e| e.into_inner()),
);
if attached.is_empty() {
return Ok(());
}
let _ = self.execute_command("bc *");
let mut failure = None;
for (id, pid) in self.session_processes()? {
if !attached.contains(&pid) {
continue;
}
if let Err(e) = unsafe {
self.system_objects()?
.SetCurrentProcessId(id)
.and_then(|()| self.client.DetachCurrentProcess())
} {
failure.get_or_insert(DbgEngError::OperationFailed(e));
}
}
failure.map_or(Ok(()), Err)
}
fn session_processes(&self) -> Result<Vec<(u32, u32)>, DbgEngError> {
if !self.has_target()? {
return Ok(Vec::new());
}
let system = self.system_objects()?;
let count =
unsafe { system.GetNumberProcesses() }.map_err(|source| DbgEngError::Context {
operation: "counting the processes in this session".into(),
source,
})? as usize;
let mut ids = vec![0u32; count];
let mut pids = vec![0u32; count];
unsafe {
system.GetProcessIdsByIndex(
0,
count as u32,
Some(ids.as_mut_ptr()),
Some(pids.as_mut_ptr()),
)
}
.map_err(|source| DbgEngError::Context {
operation: "listing the processes in this session".into(),
source,
})?;
Ok(ids.into_iter().zip(pids).collect())
}
fn system_objects(&self) -> Result<IDebugSystemObjects, DbgEngError> {
self.client.cast().map_err(|source| DbgEngError::Context {
operation: "querying IDebugSystemObjects".into(),
source,
})
}
}
impl Drop for DebugEngine {
fn drop(&mut self) {
if !self.owns_session {
std::mem::forget(std::mem::take(
&mut *self
.deferred_inputs
.lock()
.unwrap_or_else(|e| e.into_inner()),
));
return;
}
if self.is_live_kernel() {
let _ = self.resume_and_detach_live_kernel();
return;
}
let _ = self.detach_attached_processes();
unsafe {
let _ = self.client.EndSession(DEBUG_END_PASSIVE);
}
}
}
#[derive(Clone, Copy)]
enum WaitKind {
Live,
KernelBreakIn,
}
#[allow(dead_code)]
enum TargetInput {
Wide(Vec<u16>),
Ansi(CString),
}
#[must_use = "the target was created but never waited for; call `wait()` to reach the initial break"]
pub struct PendingTarget<'a> {
engine: &'a DebugEngine,
kind: WaitKind,
}
impl<'a> PendingTarget<'a> {
fn new(engine: &'a DebugEngine, kind: WaitKind) -> Self {
Self { engine, kind }
}
pub fn wait(self) -> Result<(), DbgEngError> {
match self.kind {
WaitKind::Live => self.engine.wait_for_event(LIVE_WAIT_MS),
WaitKind::KernelBreakIn => self.engine.wait_for_kernel_break_in(),
}
}
}
#[must_use = "the scope is restored when this is dropped, so dropping it immediately restores nothing later"]
pub struct ScopeGuard<'a> {
engine: &'a DebugEngine,
saved: Scope,
}
impl ScopeGuard<'_> {
pub fn saved(&self) -> &Scope {
&self.saved
}
pub fn restore(&self) -> Result<(), DbgEngError> {
self.engine.set_scope(&self.saved)
}
}
impl Drop for ScopeGuard<'_> {
fn drop(&mut self) {
let _ = self.engine.set_scope(&self.saved);
}
}
#[windows::core::implement(
windows::Win32::System::Diagnostics::Debug::Extensions::IDebugOutputCallbacks
)]
#[derive(Debug)]
pub struct OutputCallbacks {
buffer: *mut Vec<u8>,
}
impl OutputCallbacks {
fn new(buffer: &mut Vec<u8>) -> Self {
Self {
buffer: buffer as *mut Vec<u8>,
}
}
}
#[allow(non_snake_case)]
impl windows::Win32::System::Diagnostics::Debug::Extensions::IDebugOutputCallbacks_Impl
for OutputCallbacks_Impl
{
fn Output(&self, _mask: u32, text: &PCSTR) -> windows::core::Result<()> {
if text.is_null() {
return Ok(());
}
let c_str = unsafe { std::ffi::CStr::from_ptr(text.0 as *const i8) };
if let Ok(str_slice) = c_str.to_str() {
unsafe {
(*self.buffer).extend_from_slice(str_slice.as_bytes());
}
}
Ok(())
}
}
struct ScopedBreakpoint<'a> {
control: &'a IDebugControl4,
breakpoint: std::mem::ManuallyDrop<IDebugBreakpoint2>,
}
impl<'a> ScopedBreakpoint<'a> {
fn at(engine: &'a DebugEngine, address: u64) -> Result<Self, DbgEngError> {
let breakpoint = unsafe {
engine
.control
.AddBreakpoint2(DEBUG_BREAKPOINT_CODE, DEBUG_ANY_ID)
}
.map_err(DbgEngError::BreakpointFailed)?;
let scoped = Self {
control: &engine.control,
breakpoint: std::mem::ManuallyDrop::new(breakpoint),
};
unsafe {
scoped
.breakpoint
.SetOffset(address)
.map_err(DbgEngError::BreakpointFailed)?;
scoped
.breakpoint
.AddFlags(DEBUG_BREAKPOINT_ENABLED)
.map_err(DbgEngError::BreakpointFailed)?;
}
Ok(scoped)
}
}
impl Drop for ScopedBreakpoint<'_> {
fn drop(&mut self) {
unsafe {
let _ = self.control.RemoveBreakpoint2(&*self.breakpoint);
}
}
}
pub struct Breakpoint<'a> {
control: &'a IDebugControl4,
breakpoint: std::mem::ManuallyDrop<IDebugBreakpoint>,
}
impl<'a> Breakpoint<'a> {
pub fn new(engine: &'a DebugEngine) -> Result<Self, DbgEngError> {
let breakpoint = unsafe {
engine
.control
.AddBreakpoint(DEBUG_BREAKPOINT_CODE, DEBUG_ANY_ID)
};
if breakpoint.is_err() {
return Err(DbgEngError::BreakpointFailed(breakpoint.err().unwrap()));
}
Ok(Self {
breakpoint: std::mem::ManuallyDrop::new(breakpoint.unwrap()),
control: &engine.control,
})
}
pub fn set_offset_expression(&self, expression: &str) -> Result<(), DbgEngError> {
let expr = CString::new(expression).map_err(|_| DbgEngError::InvalidCommand)?;
unsafe {
self.breakpoint
.SetOffsetExpression(PCSTR::from_raw(expr.as_ptr() as *const u8))
.map_err(DbgEngError::BreakpointFailed)?;
}
Ok(())
}
pub fn enable(&self) {
unsafe {
self.breakpoint
.AddFlags(DEBUG_BREAKPOINT_ENABLED)
.expect("[-] Failed to set breakpoint offset");
}
}
pub fn disable(&self) {
unsafe {
self.breakpoint
.RemoveFlags(DEBUG_BREAKPOINT_ENABLED)
.expect("[-] Failed to remove breakpoint offset");
}
}
pub fn remove(&self) {
unsafe {
self.control
.RemoveBreakpoint(&*self.breakpoint)
.expect("[-] Failed to remove breakpoint");
}
}
}
#[cfg(test)]
mod tests {
use windows::Win32::System::Diagnostics::Debug::Extensions::{
DEBUG_STATUS_BREAK, DEBUG_STATUS_IGNORE_EVENT, DEBUG_STATUS_NO_CHANGE,
DEBUG_STATUS_OUT_OF_SYNC, DEBUG_STATUS_RESTART_REQUESTED, DEBUG_STATUS_TIMEOUT,
DEBUG_STATUS_WAIT_INPUT, DEBUG_VALUE_0, DEBUG_VALUE_INVALID, DEBUG_VALUE_TYPES,
};
use super::*;
#[test]
fn test_a_pdb_guid_is_spelled_the_way_a_symbol_server_path_is() {
let guid = windows::core::GUID {
data1: 0xFE3F_58BD,
data2: 0xA39D,
data3: 0x2FC1,
data4: [0x3C, 0x37, 0x06, 0x18, 0xD1, 0xDB, 0xDF, 0x22],
};
assert_eq!(format_pdb_guid(&guid), "FE3F58BDA39D2FC13C370618D1DBDF22");
}
#[test]
fn test_an_instruction_splits_into_its_encoding_and_its_mnemonic() {
let x64 = split_instruction(
0xfffff803_89201234,
"fffff803`89201234 48895c2408 mov qword ptr [rsp+8],rbx\n",
);
assert_eq!(x64.address, 0xfffff803_89201234);
assert_eq!(x64.bytes, "48895c2408");
assert_eq!(x64.text, "mov qword ptr [rsp+8],rbx");
let arm64 = split_instruction(
0xfffff803_89201234,
"fffff803`89201234 a9bf7bfd stp fp,lr,[sp,#-0x10]!\n",
);
assert_eq!(arm64.bytes, "a9bf7bfd");
assert_eq!(arm64.text, "stp fp,lr,[sp,#-0x10]!");
}
#[test]
fn test_an_instructions_address_comes_from_the_walk_not_the_rendering() {
let one = split_instruction(0x1000, "deadbeef`deadbeef 90 nop");
assert_eq!(one.address, 0x1000);
assert_eq!(one.text, "nop");
}
#[test]
fn test_an_unrecognised_line_keeps_its_text_rather_than_inventing_an_encoding() {
let two_columns = split_instruction(0x1000, "fffff803`89201234 ????");
assert!(two_columns.bytes.is_empty(), "{two_columns:?}");
assert_eq!(two_columns.text, "????");
let one_column = split_instruction(0x1000, "???");
assert!(one_column.bytes.is_empty(), "{one_column:?}");
assert_eq!(one_column.text, "???");
}
#[test]
fn test_a_clients_identity_outlives_the_wrapper_it_was_issued_to() {
let (client, other) = (0x11, 0x22);
let first = identity_for(client);
assert_eq!(
identity_for(client),
first,
"a rebuilt wrapper keeps its caches"
);
assert_ne!(identity_for(other), first, "two clients are two targets");
let after_release = reissue_for(client);
assert_ne!(after_release, first);
assert_eq!(identity_for(client), after_release);
for filler in 0..MAX_REMEMBERED_CLIENTS {
identity_for(0x1000 + filler);
}
assert!(locked_identities().len() <= MAX_REMEMBERED_CLIENTS);
assert!(
identity_for(client) >= after_release,
"a forgotten client is issued a later identity, never an earlier one"
);
let mut identities = locked_identities();
identities.clear();
identities.insert(client, after_release);
for filler in 1..MAX_REMEMBERED_CLIENTS {
identities.insert(0x2000 + filler, next_target_identity());
}
assert_eq!(identities.len(), MAX_REMEMBERED_CLIENTS);
drop(identities);
assert_eq!(
identity_for(client),
after_release,
"a client at the cap keeps the caches it is in the middle of using"
);
assert_eq!(locked_identities().len(), MAX_REMEMBERED_CLIENTS);
identity_for(0xbeef);
assert!(locked_identities().len() < MAX_REMEMBERED_CLIENTS);
}
fn tagged(type_code: u32, fill: impl FnOnce(&mut DEBUG_VALUE_0)) -> DEBUG_VALUE {
let mut anonymous = DEBUG_VALUE_0::default();
fill(&mut anonymous);
DEBUG_VALUE {
Anonymous: anonymous,
TailOfRawBytes: 0,
Type: type_code,
}
}
#[test]
fn a_register_value_is_read_by_the_arm_its_tag_names() {
let int32 = tagged(DEBUG_VALUE_INT32, |v| v.I32 = 0xdead_beef);
assert_eq!(
RegisterValue::decode(&int32),
RegisterValue::Int(0xdead_beef)
);
let int64 = tagged(DEBUG_VALUE_INT64, |v| {
v.Anonymous.I64 = 0xffff_8000_dead_beef
});
assert_eq!(
RegisterValue::decode(&int64),
RegisterValue::Int(0xffff_8000_dead_beef)
);
let byte = tagged(DEBUG_VALUE_INT8, |v| v.I8 = 0xff);
assert_eq!(RegisterValue::decode(&byte), RegisterValue::Int(0xff));
let float = tagged(DEBUG_VALUE_FLOAT64, |v| v.F64 = 1.5);
assert_eq!(RegisterValue::decode(&float), RegisterValue::Float(1.5));
}
#[test]
fn a_wide_register_keeps_every_byte() {
let mut bytes = [0u8; 16];
for (i, b) in bytes.iter_mut().enumerate() {
*b = i as u8;
}
let vector = tagged(DEBUG_VALUE_VECTOR128, |v| v.VI8 = bytes);
assert_eq!(
RegisterValue::decode(&vector),
RegisterValue::Bytes(bytes.to_vec())
);
let half = tagged(DEBUG_VALUE_VECTOR64, |v| v.VI8 = bytes);
assert_eq!(
RegisterValue::decode(&half),
RegisterValue::Bytes(bytes[..8].to_vec())
);
let x87 = tagged(DEBUG_VALUE_FLOAT80, |v| v.F80Bytes = [7u8; 10]);
assert_eq!(
RegisterValue::decode(&x87),
RegisterValue::Bytes(vec![7u8; 10])
);
}
#[test]
fn an_undecodable_register_is_unavailable_rather_than_zero() {
let unknown = tagged(DEBUG_VALUE_TYPES + 1, |v| {
v.Anonymous.I64 = 0xdead_beef_dead_beef
});
assert_eq!(RegisterValue::decode(&unknown), RegisterValue::Unavailable);
let invalid = tagged(DEBUG_VALUE_INVALID, |v| v.Anonymous.I64 = 1);
assert_eq!(RegisterValue::decode(&invalid), RegisterValue::Unavailable);
}
#[test]
fn an_unknown_symbol_type_is_not_reported_as_having_no_symbols() {
assert_eq!(SymbolKind::from_engine(DEBUG_SYMTYPE_PDB), SymbolKind::Pdb);
assert_eq!(
SymbolKind::from_engine(DEBUG_SYMTYPE_DEFERRED),
SymbolKind::Deferred
);
assert_eq!(
SymbolKind::from_engine(DEBUG_SYMTYPE_NONE),
SymbolKind::None
);
assert_eq!(SymbolKind::from_engine(4242), SymbolKind::Other(4242));
assert!(SymbolKind::Pdb.has_type_info());
assert!(SymbolKind::Dia.has_type_info());
assert!(!SymbolKind::Export.has_type_info());
assert!(!SymbolKind::Deferred.has_type_info());
}
#[test]
fn a_breakpoint_type_keeps_an_unknown_code() {
assert_eq!(
BreakpointKind::from_engine(DEBUG_BREAKPOINT_CODE),
BreakpointKind::Code
);
assert_eq!(
BreakpointKind::from_engine(DEBUG_BREAKPOINT_DATA),
BreakpointKind::Data
);
assert_eq!(BreakpointKind::from_engine(9), BreakpointKind::Other(9));
}
#[test]
fn a_name_stops_at_the_nul_the_engine_wrote() {
assert_eq!(nul_terminated(b"nt\0junkjunk"), "nt");
assert_eq!(nul_terminated(b"\0"), "");
assert_eq!(nul_terminated(b"no terminator"), "no terminator");
}
#[cfg(not(miri))]
#[test]
fn test_connecting_to_a_server_that_is_not_there_is_an_error() {
let options = "npipe:pipe=dbgscope-no-such-server-2f9c41d8,server=localhost";
let Err(err) = DebugEngine::connect(options) else {
panic!("nothing is serving `{options}`, so connecting to it must fail");
};
assert!(err.to_string().contains(options), "{err}");
}
#[cfg(not(miri))]
#[test]
fn test_create_debug_engine() {
let _debuggee = one_debuggee();
let _ = DebugEngine::new();
println!("Debug engine created successfully");
}
#[cfg(not(miri))]
#[test]
fn test_every_live_wrapper_sees_a_release_through_any_of_them() {
let _debuggee = one_debuggee();
let owner = DebugEngine::new();
let borrowed = DebugEngine::from_client_interface(owner.client.clone());
let before = owner.target_identity();
assert_eq!(borrowed.target_identity(), before);
let _ = owner.end_session();
assert_ne!(
owner.target_identity(),
before,
"a release moves the identity"
);
assert_eq!(
borrowed.target_identity(),
owner.target_identity(),
"a wrapper that did not perform the release still has to observe it"
);
}
#[cfg(not(miri))]
fn read_pseudo_register_opt(e: &DebugEngine, expr: &str) -> Option<u64> {
eval_expression(e, &format!("@{expr}"))
}
#[cfg(not(miri))]
fn eval_expression(e: &DebugEngine, expr: &str) -> Option<u64> {
let out = e.execute_command(&format!("? {expr}")).ok()?;
let tail = out.split("Evaluate expression: ").nth(1)?;
let digits: String = tail.chars().take_while(char::is_ascii_digit).collect();
digits.parse().ok()
}
#[cfg(not(miri))]
fn breakpoints(e: &DebugEngine) -> Option<Vec<String>> {
let out = e.execute_command("bl").ok()?;
Some(
out.lines()
.map(str::trim)
.filter(|line| !line.is_empty() && *line != "bl")
.map(str::to_string)
.collect(),
)
}
#[cfg(not(miri))]
fn read_pseudo_register(e: &DebugEngine, expr: &str) -> u64 {
read_pseudo_register_opt(e, expr)
.unwrap_or_else(|| panic!("could not read {expr} from the engine"))
}
#[cfg(not(miri))]
fn command_took_effect(e: &DebugEngine, sentinel: u64) -> bool {
if e.execute_command("r $t1 = 0").is_err() || read_pseudo_register_opt(e, "$t1") != Some(0)
{
return false;
}
if e.execute_command(&format!("r $t1 = 0x{sentinel:x}"))
.is_err()
{
return false;
}
read_pseudo_register_opt(e, "$t1") == Some(sentinel)
}
#[test]
fn every_go_and_step_status_is_a_running_one_and_nothing_else_is() {
for status in [
DEBUG_STATUS_GO,
DEBUG_STATUS_GO_HANDLED,
DEBUG_STATUS_GO_NOT_HANDLED,
DEBUG_STATUS_STEP_OVER,
DEBUG_STATUS_STEP_INTO,
DEBUG_STATUS_STEP_BRANCH,
DEBUG_STATUS_REVERSE_GO,
DEBUG_STATUS_REVERSE_STEP_BRANCH,
DEBUG_STATUS_REVERSE_STEP_OVER,
DEBUG_STATUS_REVERSE_STEP_INTO,
] {
assert!(
is_running_status(status),
"status {status} reads as stopped"
);
}
for status in [
DEBUG_STATUS_NO_CHANGE,
DEBUG_STATUS_BREAK,
DEBUG_STATUS_NO_DEBUGGEE,
DEBUG_STATUS_IGNORE_EVENT,
DEBUG_STATUS_RESTART_REQUESTED,
DEBUG_STATUS_OUT_OF_SYNC,
DEBUG_STATUS_WAIT_INPUT,
DEBUG_STATUS_TIMEOUT,
] {
assert!(
!is_running_status(status),
"status {status} reads as running"
);
}
}
#[cfg(not(miri))]
#[test]
fn a_watchdog_disarmed_before_its_deadline_costs_nothing() {
let fires = Arc::new(AtomicU64::new(0));
let counted = Arc::clone(&fires);
let watchdog = Watchdog::arm(Duration::from_secs(30), move || {
counted.fetch_add(1, Ordering::SeqCst);
});
let started = Instant::now();
let fired = watchdog.disarm();
let took = started.elapsed();
assert!(!fired, "a watchdog 30s from its deadline reported firing");
assert_eq!(fires.load(Ordering::SeqCst), 0);
assert!(
took < Duration::from_millis(50),
"disarming took {took:?}; it waits for a wake-up, not for a poll interval"
);
}
#[cfg(not(miri))]
#[test]
fn a_watchdog_past_its_deadline_keeps_raising_the_break() {
let fires = Arc::new(AtomicU64::new(0));
let counted = Arc::clone(&fires);
let watchdog = Watchdog::arm(Duration::ZERO, move || {
counted.fetch_add(1, Ordering::SeqCst);
});
thread::sleep(WATCHDOG_REPEAT * 3);
assert!(
watchdog.disarm(),
"a watchdog past its deadline reported not firing"
);
let fires = fires.load(Ordering::SeqCst);
assert!(
fires >= 2,
"raised the break {fires} time(s) over three repeat intervals; it must repeat"
);
}
#[cfg(not(miri))]
#[test]
#[ignore = "needs a live debuggee; run manually with --ignored"]
fn a_go_that_never_stops_is_reported_and_leaves_the_engine_usable() {
let e = DebugEngine::new();
e.launch_process("cmd.exe /c ping -n 30 127.0.0.1")
.expect("launch failed");
let run = e
.execute_and_wait("g", 2_000)
.expect("execute_and_wait errored");
assert_eq!(
run.cut_short,
Some(Interruption::Deadline { after_ms: 2_000 }),
"a `g` broken in at its own bound must say so rather than pass for a stop: {}",
run.output
);
assert!(
command_took_effect(&e, 0x67),
"the engine is unusable after a `g` that did not stop — the target was left running \
with no current process/thread"
);
let _ = e.end_session();
}
#[cfg(not(miri))]
#[test]
#[ignore = "needs a live debuggee; run manually with --ignored"]
fn stepping_leaves_the_engine_usable_whether_or_not_it_reaches_a_stop() {
let e = DebugEngine::new();
e.launch_process("cmd.exe /c ping -n 30 127.0.0.1")
.expect("launch failed");
for command in ["p", "t"] {
let run = e
.execute_and_wait(command, 2_000)
.unwrap_or_else(|why| panic!("`{command}` errored: {why}"));
assert!(
!matches!(run.cut_short, Some(Interruption::OnRequest)),
"`{command}` reported a break somebody asked for; nobody did"
);
assert!(
command_took_effect(&e, 0x68),
"the engine is unusable after `{command}`: {}",
run.output
);
}
let _ = e.end_session();
}
#[cfg(not(miri))]
#[test]
#[ignore = "needs a live debuggee; run manually with --ignored"]
fn settle_pumps_the_run_state_a_raw_command_left_behind() {
let e = DebugEngine::new();
e.launch_process("cmd.exe /c ping -n 30 127.0.0.1")
.expect("launch failed");
assert!(
!e.is_running().expect("could not read the execution status"),
"a freshly launched target is stopped at its initial breakpoint"
);
assert!(
e.settle(2_000).expect("settle errored").is_none(),
"settle pumped a target that was already stopped"
);
for (command, sentinel) in [("g", 0x67u64), ("p", 0x68), ("t", 0x69)] {
e.execute_command_bounded(command, 0).unwrap_or_else(|why| {
panic!("the raw `{command}` itself should succeed — it is the pump that is missing: {why}")
});
assert!(
e.is_running().expect("could not read the execution status"),
"a raw `{command}` left the engine reading as stopped, so there is nothing here to settle"
);
let settled = e
.settle(2_000)
.expect("settle errored")
.unwrap_or_else(|| panic!("settle found nothing to pump after a raw `{command}`"));
assert!(
!e.is_running().expect("could not read the execution status"),
"settle returned with the engine still running after `{command}`: {}",
settled.output
);
let run = e.execute_and_wait("g", 2_000).unwrap_or_else(|why| {
panic!("execution control is still refused after settling `{command}`: {why}")
});
assert!(
command_took_effect(&e, sentinel),
"the engine is unusable after settling `{command}` and running `g`: {}",
run.output
);
}
let _ = e.end_session();
}
#[cfg(not(miri))]
#[test]
#[ignore = "needs a live debuggee; run manually with --ignored"]
fn test_run_to_address_hit_removes_its_breakpoint() {
let e = DebugEngine::new();
e.launch_process("cmd.exe /c ping -n 30 127.0.0.1")
.expect("launch failed");
assert_eq!(
breakpoints(&e).expect("bl failed"),
Vec::<String>::new(),
"the target should start with no breakpoints"
);
let addr = eval_expression(&e, "ntdll!NtCreateFile").expect("could not resolve symbol");
let res = e
.run_to_address(addr, 20_000)
.expect("run_to_address errored");
assert_eq!(res.outcome, RunToOutcome::Hit, "output: {}", res.output);
assert_eq!(
breakpoints(&e).expect("bl failed"),
Vec::<String>::new(),
"run_to_address left its breakpoint armed after a hit"
);
let _ = e.end_session();
}
#[cfg(not(miri))]
#[test]
#[ignore = "needs a live debuggee; run manually with --ignored"]
fn test_run_to_address_timeout_removes_its_breakpoint() {
let e = DebugEngine::new();
e.launch_process("cmd.exe /c ping -n 30 127.0.0.1")
.expect("launch failed");
let addr = eval_expression(&e, "ntdll!NtShutdownSystem").expect("could not resolve symbol");
let res = e
.run_to_address(addr, 2_000)
.expect("run_to_address errored");
assert_eq!(res.outcome, RunToOutcome::Timeout, "output: {}", res.output);
assert_eq!(
breakpoints(&e).expect("bl failed"),
Vec::<String>::new(),
"run_to_address left its breakpoint armed after a timeout — a later `g` passing that address would stop there spuriously"
);
assert!(
command_took_effect(&e, 0x63),
"the engine is unusable after a timeout — the target was left running, or the current process/thread was never restored"
);
let _ = e.end_session();
}
#[cfg(not(miri))]
#[test]
#[ignore = "needs a live debuggee; run manually with --ignored"]
fn test_get_interrupt_drain_semantics() {
let e = DebugEngine::new();
e.launch_process("cmd.exe /c exit").expect("launch failed");
const DRAINS_ON_FIRST_POLL: [bool; 5] = [true, false, false, false, false];
unsafe { e.control.SetInterrupt(DEBUG_INTERRUPT_ACTIVE) }.expect("SetInterrupt failed");
let polls: Vec<bool> = (0..5).map(|_| e.interrupted().unwrap()).collect();
println!("after 1x SetInterrupt, five GetInterrupt polls: {polls:?}");
assert_eq!(
polls, DRAINS_ON_FIRST_POLL,
"GetInterrupt no longer clears the pending request on this engine"
);
for _ in 0..3 {
unsafe { e.control.SetInterrupt(DEBUG_INTERRUPT_ACTIVE) }.expect("SetInterrupt failed");
}
let polls: Vec<bool> = (0..5).map(|_| e.interrupted().unwrap()).collect();
println!("after 3x SetInterrupt, five GetInterrupt polls: {polls:?}");
assert_eq!(
polls, DRAINS_ON_FIRST_POLL,
"repeated SetInterrupt now accumulates; one drain no longer suffices"
);
let _ = e.end_session();
}
#[cfg(not(miri))]
#[test]
#[ignore = "needs a live debuggee; run manually with --ignored"]
fn test_stale_interrupt_effect_on_the_next_command() {
let e = DebugEngine::new();
e.launch_process("cmd.exe /c exit").expect("launch failed");
assert!(
command_took_effect(&e, 0xBA5E),
"baseline command did not take effect; the probe is broken, not the engine"
);
unsafe { e.control.SetInterrupt(DEBUG_INTERRUPT_ACTIVE) }.expect("SetInterrupt failed");
let undrained = command_took_effect(&e, 0xA11);
println!("undrained next command took effect: {undrained}");
unsafe { e.control.SetInterrupt(DEBUG_INTERRUPT_ACTIVE) }.expect("SetInterrupt failed");
assert!(
e.interrupted().expect("GetInterrupt failed"),
"staged interrupt was not pending — nothing was drained, so the case below is not \
the drained one it claims to be"
);
let drained = command_took_effect(&e, 0xB22);
println!("drained next command took effect: {drained}");
const LONG_ITERS: u64 = 0x4_0000;
let long = format!(".for (r $t0 = 0; @$t0 < 0x{LONG_ITERS:x}; r $t0 = @$t0 + 1) {{ }}");
const UNSTARTED: u64 = 0xDEAD_BEEF;
let seed = format!("r $t0 = 0x{UNSTARTED:x}");
e.execute_command(&seed).expect("seeding $t0 failed");
let clean_start = Instant::now();
e.execute_command(&long).expect("long command failed");
let clean = clean_start.elapsed();
let clean_t0 = read_pseudo_register(&e, "$t0");
assert_eq!(
clean_t0, LONG_ITERS,
"the uninterrupted run did not complete — the probe is broken, not the engine"
);
e.execute_command(&seed).expect("seeding $t0 failed");
unsafe { e.control.SetInterrupt(DEBUG_INTERRUPT_ACTIVE) }.expect("SetInterrupt failed");
let stale_start = Instant::now();
let stale = e.execute_command(&long);
let stale_elapsed = stale_start.elapsed();
let stale_t0 = read_pseudo_register_opt(&e, "$t0");
let stale_result = if stale.is_ok() { "Ok" } else { "Err" };
println!("long command, clean: {clean:?} (t0={clean_t0} of {LONG_ITERS})");
println!(
"long command, stale interrupt: {stale_elapsed:?} (t0={stale_t0:?} of {LONG_ITERS}, {stale_result})"
);
println!(
" -> stale interrupt {} the long command",
match stale_t0 {
None => "gave no readable $t0 after",
Some(UNSTARTED) => "ABORTED, before the loop even started,",
Some(t0) if t0 < LONG_ITERS => "ABORTED mid-loop",
_ => "did NOT abort",
}
);
let _ = e.interrupted();
assert!(
drained,
"draining should leave the next command fully usable"
);
let _ = e.end_session();
}
#[cfg(not(miri))]
#[test]
#[ignore = "needs a live debuggee; run manually with --ignored"]
fn test_next_command_survives_a_bounded_timeout() {
let e = DebugEngine::new();
e.launch_process("cmd.exe /c exit").expect("launch failed");
const ITERATIONS: u64 = 0x100_0000;
const TIMEOUT_MS: u32 = 1_500;
let started = Instant::now();
let out = e
.execute_command_bounded(
&format!(".for (r $t0 = 0; @$t0 < 0x{ITERATIONS:x}; r $t0 = @$t0 + 1) {{ }}"),
TIMEOUT_MS,
)
.expect("bounded command should return, not error");
let elapsed = started.elapsed();
let t0 = read_pseudo_register(&e, "$t0");
println!("bounded command returned after {elapsed:?}, $t0 = {t0} of {ITERATIONS}");
assert!(t0 > 0, "loop never started; $t0 = {t0}");
assert!(
t0 < ITERATIONS,
"loop ran to completion ($t0 = {t0}) — the watchdog did not cut it short, so the \
rest of this test would prove nothing"
);
assert_eq!(
out.cut_short,
Some(Interruption::Deadline {
after_ms: TIMEOUT_MS
}),
"a loop that stopped short has to say a deadline stopped it"
);
assert!(
command_took_effect(&e, 0x5A5E),
"next command did not take effect — a stale interrupt aborted it"
);
let _ = e.end_session();
}
#[cfg(not(miri))]
#[test]
#[ignore = "needs a live debuggee; run manually with --ignored"]
fn test_command_interrupted_on_request_keeps_its_output() {
let e = DebugEngine::new();
e.launch_process("cmd.exe /c exit").expect("launch failed");
const ITERATIONS: u64 = 0x100_0000;
let long = format!(".for (r $t0 = 0; @$t0 < 0x{ITERATIONS:x}; r $t0 = @$t0 + 1) {{ }}");
let handle = e.interrupt_handle();
let asker = thread::spawn(move || {
thread::sleep(Duration::from_millis(1_500));
handle.interrupt().expect("SetInterrupt failed");
});
let out = e
.execute_command_bounded(&long, 0)
.expect("an interrupted command must return its partial output, not an error");
asker.join().expect("the interrupting thread panicked");
let t0 = read_pseudo_register(&e, "$t0");
println!("command interrupted on request, $t0 = {t0} of {ITERATIONS}");
assert!(t0 > 0, "loop never started; $t0 = {t0}");
assert!(
t0 < ITERATIONS,
"loop ran to completion ($t0 = {t0}) — the interrupt never reached it, so the rest \
of this test would prove nothing"
);
assert_eq!(
out.cut_short,
Some(Interruption::OnRequest),
"the break came from the handle, not from a deadline — and which it was is what a \
caller renders its advice from"
);
assert!(
command_took_effect(&e, 0x1234),
"next command did not take effect — the requested interrupt was left pending"
);
let _ = e.end_session();
}
#[cfg(not(miri))]
static ONE_DEBUGGEE: Mutex<()> = Mutex::new(());
#[cfg(not(miri))]
fn one_debuggee() -> std::sync::MutexGuard<'static, ()> {
ONE_DEBUGGEE.lock().unwrap_or_else(|e| e.into_inner())
}
#[cfg(not(miri))]
fn move_the_scope(e: &DebugEngine) -> Option<&'static str> {
let before = e.scope().ok()?;
for command in [".frame 1", ".frame 2", ".ecxr"] {
let _ = e.execute_command(command);
if e.scope().ok()? != before {
return Some(command);
}
}
None
}
#[test]
#[cfg(not(miri))]
fn a_scope_needs_a_target_to_be_read_from() {
let _debuggee = one_debuggee();
let e = DebugEngine::new();
let err = e
.scope()
.expect_err("an engine holding no target reported a scope");
println!("scope() with no target: {err}");
}
#[test]
#[cfg(not(miri))]
fn a_saved_scope_is_the_one_restored() {
let _debuggee = one_debuggee();
let e = DebugEngine::new();
e.launch_process("cmd.exe /c exit").expect("launch failed");
let moved_by = move_the_scope(&e).expect("nothing moved the scope; the rest is vacuous");
let saved = e.scope().expect("scope() failed");
println!("scope moved by `{moved_by}`: {saved:?}");
assert!(
saved.has_context(),
"no size in SCOPE_CONTEXT_SIZES covered this target's CONTEXT"
);
e.execute_command(".frame 0").expect(".frame 0 failed");
assert_ne!(
e.scope().expect("scope() failed"),
saved,
"the second move did not move anything"
);
e.set_scope(&saved).expect("set_scope failed");
assert_eq!(
e.scope().expect("scope() failed"),
saved,
"the scope that came back is not the one that was saved"
);
let _ = e.end_session();
}
#[test]
#[cfg(not(miri))]
fn a_guard_restores_the_scope_even_when_the_caller_panics() {
let _debuggee = one_debuggee();
let e = DebugEngine::new();
e.launch_process("cmd.exe /c exit").expect("launch failed");
move_the_scope(&e).expect("nothing moved the scope; the rest is vacuous");
let before = e.scope().expect("scope() failed");
let panicked = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _guard = e.scope_guard().expect("scope_guard() failed");
e.execute_command(".frame 0").expect(".frame 0 failed");
panic!("the guarded call gave up");
}))
.is_err();
assert!(panicked, "the closure was supposed to panic");
assert_eq!(
e.scope().expect("scope() failed"),
before,
"the guard did not restore the scope while unwinding"
);
let _ = e.end_session();
}
#[test]
#[cfg(not(miri))]
fn a_scope_is_not_restored_onto_a_later_target() {
let _debuggee = one_debuggee();
let e = DebugEngine::new();
e.launch_process("cmd.exe /c exit").expect("launch failed");
let stale = e.scope().expect("scope() failed");
let _ = e.end_session();
e.launch_process("cmd.exe /c exit")
.expect("second launch failed");
let err = e
.set_scope(&stale)
.expect_err("a scope from the previous target was applied to this one");
assert!(
matches!(err, DbgEngError::ScopeFromAnotherTarget),
"wrong error for a stale scope: {err}"
);
let fresh = e.scope().expect("scope() failed");
e.set_scope(&fresh)
.expect("set_scope failed on a fresh scope");
let _ = e.end_session();
}
#[test]
#[cfg(not(miri))]
fn test_a_target_that_exits_during_a_go_is_an_ending_rather_than_a_catastrophe() {
let _debuggee = one_debuggee();
let e = DebugEngine::new();
e.launch_process("cmd.exe /c exit").expect("launch failed");
let run = e
.execute_and_wait("g", 30_000)
.expect("a target running to completion was reported as a failure");
assert!(
run.target_gone,
"the target is gone and the run does not say so: {run:?}"
);
assert!(
run.cut_short.is_none(),
"nothing interrupted this run — the target simply ended: {run:?}"
);
assert!(
!run.output.is_empty(),
"the run captured nothing, so the ending discarded it after all"
);
println!("captured across the ending: {:?}", run.output);
for command in ["k 3", "r", "lm", ".echo alive"] {
assert!(
matches!(
e.execute_command_bounded(command, 5_000),
Err(DbgEngError::NoDebuggee)
),
"`{command}` did not answer that there is no debuggee"
);
}
assert!(
matches!(e.execute_and_wait("g", 5_000), Err(DbgEngError::NoDebuggee)),
"a second resume was not refused"
);
e.end_session()
.expect("end_session failed after the ending");
}
#[test]
#[cfg(not(miri))]
fn test_a_target_that_exits_during_the_settle_pump_reports_the_ending_with_its_output() {
let _debuggee = one_debuggee();
let e = DebugEngine::new();
e.launch_process("cmd.exe /c exit").expect("launch failed");
e.execute_command_bounded("g", 0)
.expect("the raw `g` itself should succeed — it is the pump that follows it");
assert!(
e.is_running().expect("could not read the execution status"),
"a raw `g` left the engine reading as stopped, so there is nothing here to settle"
);
let settled = e
.settle(30_000)
.expect("the pump reported the target's ending as a failure")
.expect("settle found nothing to pump after a raw `g`");
assert!(
settled.target_gone,
"the pump ended because the target did, and did not say so: {settled:?}"
);
println!("captured by the pump: {:?}", settled.output);
assert!(
e.settle(5_000).expect("a second settle errored").is_none(),
"settle pumped an engine that holds no target"
);
e.end_session()
.expect("end_session failed after the ending");
}
#[test]
#[cfg(not(miri))]
fn test_a_command_that_takes_the_target_away_says_so_and_kill_is_not_one_of_them() {
let _debuggee = one_debuggee();
let e = DebugEngine::new();
e.launch_process("cmd.exe /c ping -n 30 127.0.0.1")
.expect("launch failed");
let killed = e
.execute_command_bounded(".kill", 10_000)
.expect("`.kill` failed");
assert!(
!killed.target_gone,
"`.kill` reported the target gone, but the exit events have not been pumped yet: \
{killed:?}"
);
assert!(
e.execute_command_bounded("k 3", 5_000).is_ok(),
"the target should still be readable after `.kill`"
);
let resumed = e
.execute_and_wait("g", 30_000)
.expect("the resume after `.kill` was reported as a failure");
assert!(
resumed.target_gone,
"the resume after `.kill` is where the target goes away: {resumed:?}"
);
e.end_session().expect("end_session failed after `.kill`");
let e = DebugEngine::new();
e.launch_process("cmd.exe /c ping -n 30 127.0.0.1")
.expect("launch failed");
let detached = e
.execute_command_bounded(".detach", 10_000)
.expect("`.detach` failed");
assert!(
detached.target_gone,
"`.detach` did not report that it took the target away: {detached:?}"
);
assert!(
e.settle(5_000).expect("settle errored").is_none(),
"there is nothing to pump after a detach"
);
assert!(
matches!(
e.execute_command_bounded("k 3", 5_000),
Err(DbgEngError::NoDebuggee)
),
"a command after a detach was not refused"
);
e.end_session().expect("end_session failed after `.detach`");
}
#[cfg(not(miri))]
fn a_process_to_attach_to() -> std::process::Child {
std::process::Command::new("ping")
.args(["-n", "30", "127.0.0.1"])
.stdout(std::process::Stdio::null())
.spawn()
.expect("could not start a process to attach to")
}
#[cfg(not(miri))]
const STILL_RUNNING: u32 = 259;
#[cfg(not(miri))]
fn exit_code_of(pid: u32) -> Option<u32> {
use windows::Win32::Foundation::CloseHandle;
use windows::Win32::System::Threading::{
GetExitCodeProcess, OpenProcess, PROCESS_QUERY_INFORMATION,
};
unsafe {
let handle = OpenProcess(PROCESS_QUERY_INFORMATION, false, pid).ok()?;
let mut code = 0u32;
let read = GetExitCodeProcess(handle, &mut code).is_ok();
let _ = CloseHandle(handle);
read.then_some(code)
}
}
#[test]
#[cfg(not(miri))]
fn test_ending_a_session_detaches_from_a_process_it_attached_to_rather_than_killing_it() {
let _debuggee = one_debuggee();
let mut target = a_process_to_attach_to();
let pid = target.id();
let e = DebugEngine::new();
e.attach_process(pid).expect("attach failed");
assert!(
e.attached_to_a_live_process(),
"the engine attached to a process and does not know it"
);
assert!(
e.execute_command_bounded("k 3", 5_000).is_ok(),
"the attached process should be readable before the session ends"
);
e.end_session().expect("end_session failed after an attach");
assert!(
!e.attached_to_a_live_process(),
"the engine still believes it holds the process it just let go of"
);
assert_eq!(
exit_code_of(pid),
Some(STILL_RUNNING),
"`end_session` did not leave the process it attached to running"
);
target.kill().expect("could not clean up the target");
let _ = target.wait();
}
#[test]
#[cfg(not(miri))]
fn test_a_process_the_engine_launched_still_goes_with_its_session() {
let _debuggee = one_debuggee();
let e = DebugEngine::new();
e.launch_process("cmd.exe /c ping -n 30 127.0.0.1")
.expect("launch failed");
assert!(
!e.attached_to_a_live_process(),
"a launched process is not one this engine attached to"
);
let pid = eval_expression(&e, "@$tpid").expect("could not read the launched process id");
e.end_session().expect("end_session failed after a launch");
assert_ne!(
exit_code_of(pid as u32),
Some(STILL_RUNNING),
"the process this engine launched (pid {pid}) outlived its session"
);
}
#[test]
#[cfg(not(miri))]
fn test_a_launch_after_a_lost_attach_is_still_a_launch() {
let _debuggee = one_debuggee();
let mut target = a_process_to_attach_to();
let e = DebugEngine::new();
e.attach_process(target.id()).expect("attach failed");
let detached = e
.execute_command_bounded(".detach", 10_000)
.expect("`.detach` failed");
assert!(
detached.target_gone,
"`.detach` left a target behind, so this is not the state under test: {detached:?}"
);
e.launch_process("cmd.exe /c ping -n 30 127.0.0.1")
.expect("launch after a lost attach failed");
assert!(
!e.attached_to_a_live_process(),
"the engine still believes it holds an attached process after launching one"
);
assert!(
e.attached_processes
.lock()
.unwrap_or_else(|e| e.into_inner())
.is_empty(),
"the launch left a dead process's pid in the record, where a reused pid can alias it"
);
let launched = eval_expression(&e, "@$tpid").expect("could not read the launched pid");
e.end_session().expect("end_session failed");
assert_ne!(
exit_code_of(launched as u32),
Some(STILL_RUNNING),
"the process this engine launched (pid {launched}) outlived its session, because the \
engine was still carrying the previous attach"
);
let _ = target.kill();
let _ = target.wait();
}
#[test]
#[cfg(not(miri))]
fn test_ending_a_session_whose_attached_target_already_left_is_not_an_error() {
let _debuggee = one_debuggee();
let mut target = a_process_to_attach_to();
let e = DebugEngine::new();
e.attach_process(target.id()).expect("attach failed");
assert!(
e.execute_command_bounded(".detach", 10_000)
.expect("`.detach` failed")
.target_gone,
"`.detach` left a target behind, so this is not the state under test"
);
assert!(
!e.attached_to_a_live_process(),
"the engine reports holding an attached process after that process has gone"
);
e.end_session()
.expect("end_session failed on a session whose attached target had already gone");
let _ = target.kill();
let _ = target.wait();
}
#[cfg(not(miri))]
fn session_pids(e: &DebugEngine, count: usize) -> Vec<u64> {
(0..count)
.filter_map(|index| {
e.execute_command(&format!("|{index}s")).ok()?;
eval_expression(e, "@$tpid")
})
.collect()
}
#[test]
#[cfg(not(miri))]
fn test_a_mixed_session_comes_apart_by_where_each_process_came_from() {
let _debuggee = one_debuggee();
for attach_first in [true, false] {
let mut theirs = a_process_to_attach_to();
let e = DebugEngine::new();
if attach_first {
e.attach_process(theirs.id()).expect("attach failed");
e.launch_process("cmd.exe /c ping -n 30 127.0.0.1")
.expect("launch failed");
} else {
e.launch_process("cmd.exe /c ping -n 30 127.0.0.1")
.expect("launch failed");
e.attach_process(theirs.id()).expect("attach failed");
}
let listed = e.execute_command("|").expect("`|` failed");
let pids = session_pids(&e, 2);
assert_eq!(
pids.len(),
2,
"attach_first={attach_first}: this is not a two-process session:\n{listed}"
);
let ours = *pids
.iter()
.find(|pid| **pid != u64::from(theirs.id()))
.unwrap_or_else(|| {
panic!(
"attach_first={attach_first}: the launched process is not here: {pids:?}"
)
});
e.end_session()
.expect("end_session failed on a mixed session");
assert_eq!(
exit_code_of(theirs.id()),
Some(STILL_RUNNING),
"attach_first={attach_first}: the session's end killed the process it had only \
attached to"
);
assert_ne!(
exit_code_of(ours as u32),
Some(STILL_RUNNING),
"attach_first={attach_first}: the process this engine launched (pid {ours}) \
outlived its session"
);
let _ = theirs.kill();
let _ = theirs.wait();
}
}
#[test]
#[cfg(not(miri))]
fn test_execution_control_with_no_debuggee_is_refused_rather_than_faulting_the_process() {
let _debuggee = one_debuggee();
let e = DebugEngine::new();
assert!(
matches!(e.has_target(), Ok(false)),
"a fresh engine is supposed to be holding no target"
);
for command in ["g", "p", "t", ".if (1) { g }"] {
assert!(
matches!(
e.execute_command_bounded(command, 5_000),
Err(DbgEngError::NoDebuggee)
),
"`{command}` was not refused on the bounded path"
);
assert!(
matches!(e.execute_command(command), Err(DbgEngError::NoDebuggee)),
"`{command}` was not refused on the unbounded path"
);
}
assert!(
matches!(e.execute_and_wait("g", 5_000), Err(DbgEngError::NoDebuggee)),
"execute_and_wait was not refused"
);
assert!(
matches!(
e.run_to_address(0x1000, 5_000),
Err(DbgEngError::NoDebuggee)
),
"run_to_address was not refused"
);
}
}
#[windows::core::implement(
windows::Win32::System::Diagnostics::Debug::Extensions::IDebugEventContextCallbacks
)]
pub struct DebugEventContextCallbacks {
callback: Option<BreakpointCallback>,
}
impl DebugEventContextCallbacks {
pub fn new(callback: Option<BreakpointCallback>) -> Self {
Self { callback }
}
}
#[allow(non_snake_case)]
impl windows::Win32::System::Diagnostics::Debug::Extensions::IDebugEventContextCallbacks_Impl
for DebugEventContextCallbacks_Impl
{
fn GetInterestMask(&self) -> windows::core::Result<u32> {
Ok(DEBUG_EVENT_BREAKPOINT)
}
fn Breakpoint(
&self,
bp: windows::core::Ref<'_, IDebugBreakpoint2>,
_context: *const std::ffi::c_void,
_flags: u32,
) -> windows::core::Result<()> {
if let Some(callback) = &self.callback {
let _ = callback(bp.as_ref().unwrap(), _context, _flags);
}
Ok(())
}
fn Exception(
&self,
_exception: *const windows::Win32::System::Diagnostics::Debug::EXCEPTION_RECORD64,
_first_chance: u32,
_context: *const std::ffi::c_void,
_flags: u32,
) -> windows::core::Result<()> {
Ok(())
}
fn CreateThread(
&self,
_handle: u64,
_data_offset: u64,
_start_offset: u64,
_context: *const std::ffi::c_void,
_flags: u32,
) -> windows::core::Result<()> {
Ok(())
}
fn ExitThread(
&self,
_exit_code: u32,
_context: *const std::ffi::c_void,
_flags: u32,
) -> windows::core::Result<()> {
Ok(())
}
fn CreateProcessA(
&self,
_image_file_handle: u64,
_handle: u64,
_base_offset: u64,
_module_size: u32,
_module_name: &PCWSTR,
_image_name: &PCWSTR,
_checksum: u32,
_timestamp: u32,
_initial_thread_handle: u64,
_thread_data_offset: u64,
_start_offset: u64,
_context: *const std::ffi::c_void,
_flags: u32,
) -> windows::core::Result<()> {
Ok(())
}
fn ExitProcess(
&self,
_exit_code: u32,
_context: *const std::ffi::c_void,
_flags: u32,
) -> windows::core::Result<()> {
Ok(())
}
fn LoadModule(
&self,
_image_file_handle: u64,
_base_offset: u64,
_module_size: u32,
_module_name: &PCWSTR,
_image_name: &PCWSTR,
_checksum: u32,
_timestamp: u32,
_context: *const std::ffi::c_void,
_flags: u32,
) -> windows::core::Result<()> {
Ok(())
}
fn UnloadModule(
&self,
_image_base_name: &PCWSTR,
_base_offset: u64,
_context: *const std::ffi::c_void,
_flags: u32,
) -> windows::core::Result<()> {
Ok(())
}
fn SystemError(
&self,
_error: u32,
_level: u32,
_context: *const std::ffi::c_void,
_flags: u32,
) -> windows::core::Result<()> {
Ok(())
}
fn SessionStatus(&self, _status: u32) -> windows::core::Result<()> {
Ok(())
}
fn ChangeDebuggeeState(
&self,
_flags: u32,
_argument: u64,
_context: *const std::ffi::c_void,
_flags2: u32,
) -> windows::core::Result<()> {
Ok(())
}
fn ChangeEngineState(
&self,
_flags: u32,
_argument: u64,
_context: *const std::ffi::c_void,
_flags2: u32,
) -> windows::core::Result<()> {
Ok(())
}
fn ChangeSymbolState(&self, _flags: u32, _argument: u64) -> windows::core::Result<()> {
Ok(())
}
}