use std::cell::RefCell;
use std::collections::{HashMap, HashSet};
use std::ffi::CString;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Condvar, Mutex, MutexGuard, OnceLock, Weak};
use std::thread;
use std::time::{Duration, Instant};
mod kernel_attach;
use thiserror::Error;
use windows::Win32::Foundation::{
E_FAIL, E_INVALIDARG, E_NOINTERFACE, E_UNEXPECTED, 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_BREAK_EXECUTE,
DEBUG_BREAK_IO, DEBUG_BREAK_READ, DEBUG_BREAK_WRITE, 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_EVENT_EXCEPTION, DEBUG_EXECUTE_ECHO, DEBUG_INTERRUPT_ACTIVE,
DEBUG_KERNEL_SMALL_DUMP, DEBUG_LAST_EVENT_INFO_EXCEPTION, 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_BREAK,
DEBUG_STATUS_GO, DEBUG_STATUS_GO_HANDLED, DEBUG_STATUS_GO_NOT_HANDLED, DEBUG_STATUS_MASK,
DEBUG_STATUS_NO_CHANGE, 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,
IDebugBreakpoint2, IDebugClient6, IDebugControl4, IDebugDataSpaces4,
IDebugEventContextCallbacks, IDebugOutputCallbacks, IDebugRegisters, IDebugSymbols3,
IDebugSystemObjects,
};
use windows::Win32::System::Diagnostics::Debug::{EXCEPTION_RECORD64, IMAGEHLP_MODULEW64};
use windows::Win32::System::Memory::{
MEM_COMMIT, MEM_FREE, MEM_RESERVE, MEMORY_BASIC_INFORMATION64,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BreakpointAction {
Default,
Go,
Break,
}
impl BreakpointAction {
pub fn status(self) -> u32 {
match self {
Self::Default => DEBUG_STATUS_NO_CHANGE,
Self::Go => DEBUG_STATUS_GO,
Self::Break => DEBUG_STATUS_BREAK,
}
}
}
pub type BreakpointCallback = Box<dyn Fn(&BreakpointHit<'_>) -> BreakpointAction>;
pub struct BreakpointHit<'a> {
breakpoint: &'a IDebugBreakpoint2,
engine: DebugEngine,
}
impl<'a> BreakpointHit<'a> {
fn new(breakpoint: &'a IDebugBreakpoint2, client: IDebugClient6) -> Result<Self, DbgEngError> {
Ok(Self {
breakpoint,
engine: DebugEngine::try_from_client_interface(client)?,
})
}
pub fn id(&self) -> Result<u32, DbgEngError> {
unsafe { self.breakpoint.GetId() }.map_err(|source| DbgEngError::Context {
operation: "reading a hit breakpoint's id".into(),
source,
})
}
pub fn address(&self) -> Result<u64, DbgEngError> {
unsafe { self.breakpoint.GetOffset() }.map_err(|source| DbgEngError::Context {
operation: "reading a hit breakpoint's address".into(),
source,
})
}
pub fn engine(&self) -> &DebugEngine {
&self.engine
}
}
#[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("Debug command failed: {source}")]
CommandFailedWithOutput {
source: windows::core::Error,
output: String,
},
#[error("Symbol path operation failed: {0}")]
SymbolPathFailed(windows::core::Error),
#[error("Breakpoint failed: {0}")]
BreakpointFailed(windows::core::Error),
#[error("Invalid breakpoint: {0}")]
InvalidBreakpoint(String),
#[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("quit-and-detach returned without releasing the target; resume is unconfirmed")]
IncompleteKernelDetach,
#[error(
"kernel target did not break in within the attach timeout — is it reachable and in debug mode?"
)]
KernelBreakTimeout,
#[error("experimental kernel attach failed: {0}; target state is unconfirmed")]
ExperimentalKernelAttach(&'static str),
#[error("the process did not join the session within the open timeout")]
LiveTargetTimeout,
#[error("the open was interrupted before the process joined the session")]
LiveTargetInterrupted,
#[error("Operation failed: {0}")]
OperationFailed(windows::core::Error),
#[error("register `{name}`: {reason}")]
Register { name: String, reason: &'static str },
#[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(
"ran out of memory reading {requested} bytes at {address:#x}: {read} bytes were read \
before the host could not hold any more"
)]
OutOfMemory {
address: u64,
requested: usize,
read: usize,
},
#[error("this build does not decode instructions for machine {machine:#x}")]
UndecodedInstructionSet { machine: u32 },
#[error("debugger text contains an interior NUL")]
InvalidOutput,
#[error("this scope was read from a target the engine no longer holds")]
ScopeFromAnotherTarget,
#[error("this register context was read from a target the engine no longer holds")]
ContextFromAnotherTarget,
}
impl DbgEngError {
pub fn output(&self) -> Option<&str> {
match self {
Self::CommandFailedWithOutput { output, .. } => Some(output),
_ => None,
}
}
}
fn printed(error: DbgEngError, output: &[u8]) -> DbgEngError {
match error {
DbgEngError::CommandFailed(source) => DbgEngError::CommandFailedWithOutput {
source,
output: String::from_utf8_lossy(output).to_string(),
},
other => other,
}
}
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_NO_WINDOW: u32 = 0x0800_0000;
const DEBUG_ATTACH_DEFAULT: u32 = 0x0000_0000;
const DEBUG_END_PASSIVE: u32 = 0x0000_0000;
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];
const STORED_CONTEXT_SIZES: &[u32] = &[4096, 8192, 16384, 32768, 65536];
const NO_EVENT_KIND: u32 = 0;
pub struct InterruptHandle {
control: IDebugControl4,
state: Arc<ClientState>,
}
unsafe impl Send for InterruptHandle {}
unsafe impl Sync for InterruptHandle {}
impl InterruptHandle {
pub fn interrupt(&self) -> Result<BreakRequest, DbgEngError> {
let mut scope = self.state.breaks.lock().unwrap_or_else(|e| e.into_inner());
let against = scope.innermost();
unsafe { self.control.SetInterrupt(DEBUG_INTERRUPT_ACTIVE) }.map_err(|source| {
DbgEngError::Context {
operation: "requesting a debugger interrupt".into(),
source,
}
})?;
match against {
Some(operation) => {
scope.record(operation);
Ok(BreakRequest::Raised { operation })
}
None => Ok(BreakRequest::NothingRunning),
}
}
fn break_in_only(&self) -> Result<(), DbgEngError> {
unsafe { self.control.SetInterrupt(DEBUG_INTERRUPT_ACTIVE) }.map_err(|source| {
DbgEngError::Context {
operation: "requesting a debugger interrupt".into(),
source,
}
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct OperationId(u64);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BreakRequest {
Raised { operation: OperationId },
NothingRunning,
}
#[derive(Debug, Default)]
struct ClientState {
arrivals: Mutex<Arrivals>,
breaks: Mutex<BreakScope>,
attached_processes: Mutex<HashMap<u32, Attachment>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Attachment {
Deferred,
Joined,
}
fn client_states() -> &'static Mutex<HashMap<usize, Weak<ClientState>>> {
static STATES: OnceLock<Mutex<HashMap<usize, Weak<ClientState>>>> = OnceLock::new();
STATES.get_or_init(Mutex::default)
}
fn state_for(client: &IDebugClient6) -> Arc<ClientState> {
let key = client.as_raw() as usize;
let mut states = client_states()
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
states.retain(|_, state| state.strong_count() > 0);
if let Some(live) = states.get(&key).and_then(Weak::upgrade) {
return live;
}
let state = Arc::new(ClientState::default());
states.insert(key, Arc::downgrade(&state));
state
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
struct ArrivalId(u64);
#[derive(Debug, Default)]
struct Arrivals {
minted: u64,
pending: Vec<Pending>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Claim {
Waiting,
Arrived((u32, u32)),
Departed,
}
impl Claim {
fn held(self) -> Option<(u32, u32)> {
match self {
Self::Arrived(entry) => Some(entry),
Self::Waiting | Self::Departed => None,
}
}
}
#[derive(Debug)]
struct Pending {
id: ArrivalId,
what: Arrival,
claim: Claim,
inherited: Vec<(u32, u32)>,
abandoned: bool,
}
impl Pending {
fn wants(&self, entry: (u32, u32), others: &[&Pending], attached: &HashSet<u32>) -> bool {
if self.inherited.contains(&entry)
|| others.iter().any(|other| other.claim.held() == Some(entry))
{
return false;
}
match &self.what {
Arrival::Attached(pid) => entry.1 == *pid,
Arrival::Launched(None) => false,
Arrival::Launched(Some(before)) => {
!before.contains(&entry)
&& !attached.contains(&entry.1)
&& !others.iter().any(|other| {
matches!(other.claim, Claim::Waiting)
&& matches!(other.what, Arrival::Attached(pid) if pid == entry.1)
})
}
}
}
}
impl Arrivals {
fn register(&mut self, what: Arrival) -> ArrivalId {
self.minted += 1;
let id = ArrivalId(self.minted);
self.pending.push(Pending {
id,
what,
claim: Claim::Waiting,
inherited: Vec::new(),
abandoned: false,
});
id
}
fn attached_pid(&self, id: ArrivalId) -> Option<u32> {
self.pending
.iter()
.find(|pending| pending.id == id)
.and_then(|pending| match pending.what {
Arrival::Attached(pid) => Some(pid),
Arrival::Launched(_) => None,
})
}
fn discard(&mut self, id: ArrivalId) {
let Some(index) = self.pending.iter().position(|pending| pending.id == id) else {
return;
};
let claimed = self.pending[index].claim.held();
self.pending.remove(index);
if let Some(entry) = claimed {
for pending in &mut self.pending {
pending.inherited.push(entry);
}
}
}
fn forget(&mut self, id: ArrivalId) {
let Some(index) = self.pending.iter().position(|pending| pending.id == id) else {
return;
};
if self.pending[index].claim == Claim::Waiting
&& matches!(self.pending[index].what, Arrival::Launched(Some(_)))
{
self.pending[index].abandoned = true;
return;
}
self.discard(id);
}
fn forget_departed(&mut self, held: &[(u32, u32)]) {
let still_here = |entry: &(u32, u32)| held.contains(entry);
for pending in &mut self.pending {
if pending
.claim
.held()
.is_some_and(|entry| !still_here(&entry))
{
pending.claim = Claim::Departed;
}
pending.inherited.retain(still_here);
}
}
fn forget_all(&mut self) {
self.pending.clear();
}
fn deliver(&mut self, entry: (u32, u32), attached: &HashSet<u32>) {
let Some(index) = self.pending.iter().position(|pending| {
let others: Vec<&Pending> = self
.pending
.iter()
.filter(|other| other.id != pending.id)
.collect();
matches!(pending.claim, Claim::Waiting) && pending.wants(entry, &others, attached)
}) else {
return;
};
self.pending[index].claim = Claim::Arrived(entry);
}
fn presence(&self, id: ArrivalId, held: &[(u32, u32)], attached: &HashSet<u32>) -> Presence {
let Some(pending) = self.pending.iter().find(|pending| pending.id == id) else {
return Presence::Absent;
};
match pending.claim {
Claim::Arrived(entry) if held.contains(&entry) => return Presence::Arrived,
Claim::Arrived(_) | Claim::Departed => return Presence::Absent,
Claim::Waiting => {}
}
if matches!(pending.what, Arrival::Launched(None)) {
return Presence::Unknown;
}
let others: Vec<&Pending> = self
.pending
.iter()
.filter(|other| other.id != pending.id)
.collect();
if held
.iter()
.any(|entry| pending.wants(*entry, &others, attached))
{
Presence::Listed
} else {
Presence::Absent
}
}
}
#[derive(Debug, Default)]
struct BreakScope {
minted: u64,
running: Vec<OperationId>,
asked: std::collections::BTreeSet<OperationId>,
}
impl BreakScope {
fn begin(&mut self) -> OperationId {
self.minted += 1;
let id = OperationId(self.minted);
self.running.push(id);
id
}
fn innermost(&self) -> Option<OperationId> {
self.running.last().copied()
}
fn record(&mut self, operation: OperationId) {
self.asked.insert(operation);
}
fn take(&mut self, operation: OperationId) -> bool {
self.asked.remove(&operation)
}
fn end(&mut self, operation: OperationId) -> bool {
self.running.retain(|running| *running != operation);
self.asked.remove(&operation)
}
}
struct Operation<'a> {
engine: &'a DebugEngine,
id: OperationId,
}
impl Operation<'_> {
fn took_break_request(&self) -> bool {
self.engine
.state
.breaks
.lock()
.unwrap_or_else(|e| e.into_inner())
.take(self.id)
}
fn cut_short_by(&self, by_watchdog: bool, after_ms: u32) -> Option<Interruption> {
let asked = self.took_break_request();
match (by_watchdog, asked) {
(true, _) => Some(Interruption::Deadline { after_ms }),
(false, true) => Some(Interruption::OnRequest),
(false, false) => None,
}
}
}
impl Drop for Operation<'_> {
fn drop(&mut self) {
let unread = self
.engine
.state
.breaks
.lock()
.unwrap_or_else(|e| e.into_inner())
.end(self.id);
if unread {
let _ = self.engine.interrupted();
}
}
}
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, Copy, PartialEq, Eq)]
pub enum WaitOutcome {
Stopped { process: Option<(u32, u32)> },
Expired,
Deadline,
OnRequest,
}
impl WaitOutcome {
pub fn broke_in(self) -> bool {
matches!(self, Self::Deadline | Self::OnRequest)
}
pub fn cut_short(self, after_ms: u32) -> Option<Interruption> {
match self {
Self::Deadline => Some(Interruption::Deadline { after_ms }),
Self::OnRequest => Some(Interruption::OnRequest),
Self::Stopped { .. } | Self::Expired => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Bound {
Finite(u32),
Watchdog(u32),
WatchdogExit(u32),
}
#[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
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Resumed {
OnABreak,
Freely,
OnRequest,
Gone,
}
impl Resumed {
fn of(run: &CommandRun) -> Self {
match (run.target_gone, run.cut_short) {
(true, _) => Self::Gone,
(false, Some(Interruption::Deadline { .. })) => Self::Freely,
(false, Some(Interruption::OnRequest)) => Self::OnRequest,
(false, None) => Self::OnABreak,
}
}
}
#[derive(Debug, PartialEq, Eq)]
struct Drain {
attempts: usize,
spent: usize,
delivered: usize,
free_runs: usize,
gone: bool,
}
impl Default for Drain {
fn default() -> Self {
Self {
attempts: DebugEngine::DRAIN_ATTEMPTS,
spent: 0,
delivered: 0,
free_runs: 0,
gone: false,
}
}
}
impl Drain {
fn for_processors(processors: u32) -> Self {
let owed = usize::try_from(processors).unwrap_or(usize::MAX);
Self {
attempts: owed
.saturating_add(DebugEngine::DRAIN_FREE_RUNS)
.max(DebugEngine::DRAIN_ATTEMPTS),
..Self::default()
}
}
fn resume_again(&self) -> bool {
!self.gone && self.spent < self.attempts && self.free_runs < DebugEngine::DRAIN_FREE_RUNS
}
fn saw(&mut self, resumed: Resumed) {
self.spent += 1;
match resumed {
Resumed::OnABreak => {
self.delivered += 1;
self.free_runs = 0;
}
Resumed::Freely => self.free_runs += 1,
Resumed::OnRequest => {}
Resumed::Gone => self.gone = true,
}
}
}
#[derive(Debug, Clone)]
pub struct MemoryRead {
pub bytes: Vec<u8>,
pub cut_short: Option<Interruption>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SymbolReload {
pub cut_short: Option<Interruption>,
}
#[repr(C, align(16))]
#[derive(Default)]
struct AlignedMemoryInfo(MEMORY_BASIC_INFORMATION64);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct VirtualRegion {
pub base: u64,
pub size: u64,
pub state: VirtualState,
pub protect: u32,
}
impl VirtualRegion {
pub fn contains(&self, address: u64) -> bool {
address >= self.base && address - self.base < self.size
}
pub fn end(&self) -> Option<u64> {
(self.size != 0).then(|| self.base.saturating_add(self.size))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VirtualState {
Free,
Reserved,
Committed,
Unknown(u32),
}
impl VirtualState {
fn from_mem_state(state: u32) -> Self {
match state {
_ if state & MEM_COMMIT.0 != 0 => Self::Committed,
_ if state & MEM_RESERVE.0 != 0 => Self::Reserved,
_ if state & MEM_FREE.0 != 0 => Self::Free,
other => Self::Unknown(other),
}
}
pub fn committed(self) -> Option<bool> {
match self {
Self::Committed => Some(true),
Self::Reserved | Self::Free => Some(false),
Self::Unknown(_) => None,
}
}
}
const READ_CHUNK_BYTES: usize = 4096;
fn read_chunk_len(address: u64, done: usize, size: usize) -> usize {
let at = address.wrapping_add(done as u64);
let to_page_end = READ_CHUNK_BYTES - (at as usize % READ_CHUNK_BYTES);
size.saturating_sub(done).min(to_page_end)
}
fn try_buffer(len: usize, address: u64, requested: usize) -> Result<Vec<u8>, DbgEngError> {
let mut buffer = Vec::new();
buffer
.try_reserve_exact(len)
.map_err(|_| DbgEngError::OutOfMemory {
address,
requested,
read: 0,
})?;
buffer.resize(len, 0);
Ok(buffer)
}
fn read_chunked(
address: u64,
size: usize,
mut halt: impl FnMut() -> Option<Interruption>,
mut read_chunk: impl FnMut(u64, &mut [u8]) -> Result<usize, DbgEngError>,
) -> Result<MemoryRead, DbgEngError> {
let mut bytes = Vec::new();
let mut chunk = try_buffer(READ_CHUNK_BYTES.min(size), address, size)?;
let short = |done: usize| DbgEngError::ShortRead {
address,
requested: size,
actual: done,
};
while bytes.len() < size {
if let Some(cut_short) = halt() {
return Ok(MemoryRead {
bytes,
cut_short: Some(cut_short),
});
}
let done = bytes.len();
let Some(at) = address.checked_add(done as u64) else {
return Err(short(done));
};
let len = read_chunk_len(address, done, size);
let read = match read_chunk(at, &mut chunk[..len]) {
Ok(read) => read.min(len),
Err(_) if done > 0 => return Err(short(done)),
Err(source) => return Err(source),
};
bytes
.try_reserve(read)
.map_err(|_| DbgEngError::OutOfMemory {
address,
requested: size,
read: done,
})?;
bytes.extend_from_slice(&chunk[..read]);
if read < len {
return Err(short(bytes.len()));
}
}
Ok(MemoryRead {
bytes,
cut_short: None,
})
}
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 ExceptionRecord {
pub code: u32,
pub flags: u32,
pub address: u64,
pub parameters: Vec<u64>,
pub nested: Option<u64>,
}
impl ExceptionRecord {
pub fn noncontinuable(&self) -> bool {
self.flags & EXCEPTION_NONCONTINUABLE != 0
}
fn from_raw(raw: &EXCEPTION_RECORD64) -> Self {
let filled = (raw.NumberParameters as usize).min(raw.ExceptionInformation.len());
Self {
code: raw.ExceptionCode.0 as u32,
flags: raw.ExceptionFlags,
address: raw.ExceptionAddress,
parameters: raw.ExceptionInformation[..filled].to_vec(),
nested: (raw.ExceptionRecord != 0).then_some(raw.ExceptionRecord),
}
}
}
fn no_stored_event(code: HRESULT) -> bool {
code == E_UNEXPECTED
}
#[repr(C, align(8))]
struct ExtraEventInfo([u8; size_of::<DEBUG_LAST_EVENT_INFO_EXCEPTION>()]);
impl Default for ExtraEventInfo {
fn default() -> Self {
Self([0; size_of::<DEBUG_LAST_EVENT_INFO_EXCEPTION>()])
}
}
impl ExtraEventInfo {
const SIZE: u32 = size_of::<DEBUG_LAST_EVENT_INFO_EXCEPTION>() as u32;
fn as_mut_ptr(&mut self) -> *mut std::ffi::c_void {
self.0.as_mut_ptr().cast()
}
fn exception(&self, kind: u32, used: u32) -> Option<(ExceptionRecord, bool)> {
if kind != DEBUG_EVENT_EXCEPTION || used < Self::SIZE {
return None;
}
let info: DEBUG_LAST_EVENT_INFO_EXCEPTION =
unsafe { std::ptr::read_unaligned(self.0.as_ptr().cast()) };
Some((
ExceptionRecord::from_raw(&info.ExceptionRecord),
info.FirstChance != 0,
))
}
}
const EXCEPTION_NONCONTINUABLE: u32 = 0x1;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ThreadContext {
bytes: Vec<u8>,
target: u64,
}
impl ThreadContext {
pub fn as_bytes(&self) -> &[u8] {
&self.bytes
}
pub fn target(&self) -> u64 {
self.target
}
pub fn len(&self) -> usize {
self.bytes.len()
}
pub fn is_empty(&self) -> bool {
self.bytes.is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DebugEvent {
pub kind: u32,
pub process: u32,
pub thread: u32,
pub exception: Option<ExceptionRecord>,
pub first_chance: bool,
pub context: Option<ThreadContext>,
}
#[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, Copy, PartialEq, Eq)]
pub enum InstructionSet {
X86,
Amd64,
Arm64,
Other(u32),
}
impl InstructionSet {
pub fn from_processor_type(machine: u32) -> Self {
match machine {
0x014c => Self::X86,
0x8664 => Self::Amd64,
0xaa64 => Self::Arm64,
other => Self::Other(other),
}
}
pub fn operands_are_read(self) -> bool {
matches!(self, Self::X86 | Self::Amd64 | Self::Arm64)
}
pub fn flow_is_read(self) -> bool {
matches!(self, Self::X86 | Self::Amd64 | Self::Arm64)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Flow {
Fallthrough,
Call(Option<u64>),
Jmp(Option<u64>),
Branch(Option<u64>),
Return,
Trap,
Unknown,
Unreadable,
}
impl Flow {
pub fn target(self) -> Option<u64> {
match self {
Self::Call(target) | Self::Jmp(target) | Self::Branch(target) => target,
_ => None,
}
}
pub fn falls_through(self) -> bool {
matches!(
self,
Self::Fallthrough | Self::Call(_) | Self::Branch(_) | Self::Unknown
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FunctionExtent {
Region { begin: u64, end: u64 },
NoEntry,
Unsupported(InstructionSet),
}
impl FunctionExtent {
pub fn region(self) -> Option<(u64, u64)> {
match self {
Self::Region { begin, end } => Some((begin, end)),
_ => None,
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct MemoryOperand {
pub size: Option<u32>,
pub segment: Option<String>,
pub base: Option<RegisterOperand>,
pub index: Option<RegisterOperand>,
pub scale: u8,
pub displacement: i64,
pub address: Option<u64>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct RegisterOperand {
pub name: String,
pub full: String,
pub width: u32,
}
impl std::fmt::Display for RegisterOperand {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.name)
}
}
impl std::ops::Deref for RegisterOperand {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.name
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Condition {
Equal,
NotEqual,
UnsignedAbove,
UnsignedAboveOrEqual,
UnsignedBelow,
UnsignedBelowOrEqual,
SignedGreater,
SignedGreaterOrEqual,
SignedLess,
SignedLessOrEqual,
Negative,
NotNegative,
Overflow,
NotOverflow,
Parity,
NotParity,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Effect {
Move,
MoveSigned,
LoadAddress,
Compare,
Test,
Add,
Subtract,
ShiftLeft,
ShiftRight,
BitAnd,
BitOr,
BitXor,
Push,
Pop,
Other,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Privilege {
PortIo,
ModelSpecificRegister,
ControlRegister,
DescriptorTable,
InterruptFlag,
CacheOrTlb,
Virtualization,
Other,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Operand {
Register(RegisterOperand),
Immediate(u64),
Memory(MemoryOperand),
Target(u64),
Other(String),
Undecoded(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Instruction {
pub address: u64,
pub bytes: String,
pub text: String,
pub mnemonic: String,
pub operands: Vec<Operand>,
pub flow: Flow,
pub privileged: bool,
pub privilege: Option<Privilege>,
pub effect: Effect,
pub condition: Option<Condition>,
pub writes_flags: bool,
pub writes: Vec<RegisterOperand>,
pub reads: Vec<RegisterOperand>,
}
#[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 enum BreakpointAt {
Address(u64),
Expression(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DataAccess {
Read,
Write,
ReadWrite,
Execute,
Io,
Other(u32),
}
impl DataAccess {
fn to_engine(self) -> u32 {
match self {
Self::Read => DEBUG_BREAK_READ,
Self::Write => DEBUG_BREAK_WRITE,
Self::ReadWrite => DEBUG_BREAK_READ | DEBUG_BREAK_WRITE,
Self::Execute => DEBUG_BREAK_EXECUTE,
Self::Io => DEBUG_BREAK_IO,
Self::Other(bits) => bits,
}
}
fn from_engine(bits: u32) -> Self {
const READ: u32 = DEBUG_BREAK_READ;
const WRITE: u32 = DEBUG_BREAK_WRITE;
const READ_WRITE: u32 = DEBUG_BREAK_READ | DEBUG_BREAK_WRITE;
match bits {
READ => Self::Read,
WRITE => Self::Write,
READ_WRITE => Self::ReadWrite,
DEBUG_BREAK_EXECUTE => Self::Execute,
DEBUG_BREAK_IO => Self::Io,
other => Self::Other(other),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DataWatch {
pub access: DataAccess,
pub size: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum OnExisting {
#[default]
Add,
Replace,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BreakpointSpec {
pub at: BreakpointAt,
pub data: Option<DataWatch>,
pub command: Option<String>,
pub thread: Option<u32>,
pub pass_count: Option<u32>,
pub one_shot: bool,
pub enabled: bool,
pub on_existing: OnExisting,
}
impl BreakpointSpec {
pub fn code(at: BreakpointAt) -> Self {
Self {
at,
data: None,
command: None,
thread: None,
pass_count: None,
one_shot: false,
enabled: true,
on_existing: OnExisting::Add,
}
}
pub fn data(at: BreakpointAt, watch: DataWatch) -> Self {
Self {
data: Some(watch),
..Self::code(at)
}
}
pub fn with_command(mut self, command: impl Into<String>) -> Self {
self.command = Some(command.into());
self
}
pub fn on_thread(mut self, thread: u32) -> Self {
self.thread = Some(thread);
self
}
pub fn with_pass_count(mut self, passes: u32) -> Self {
self.pass_count = Some(passes);
self
}
pub fn one_shot(mut self) -> Self {
self.one_shot = true;
self
}
pub fn disabled(mut self) -> Self {
self.enabled = false;
self
}
pub fn replacing_existing(mut self) -> Self {
self.on_existing = OnExisting::Replace;
self
}
fn validated(&self) -> Result<(), DbgEngError> {
let Some(watch) = self.data else {
return Ok(());
};
if !matches!(watch.size, 1 | 2 | 4 | 8) {
return Err(DbgEngError::InvalidBreakpoint(format!(
"a data breakpoint's size must be 1, 2, 4 or 8 bytes, not {}",
watch.size
)));
}
if let BreakpointAt::Address(address) = self.at
&& !address.is_multiple_of(u64::from(watch.size))
{
return Err(DbgEngError::InvalidBreakpoint(format!(
"a {}-byte data breakpoint must be {}-byte aligned, and {address:#x} is not",
watch.size, watch.size
)));
}
Ok(())
}
fn flags(&self) -> u32 {
let mut flags = 0;
if self.enabled {
flags |= DEBUG_BREAKPOINT_ENABLED;
}
if self.one_shot {
flags |= DEBUG_BREAKPOINT_ONE_SHOT;
}
flags
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BreakpointSet {
pub breakpoint: BreakpointInfo,
pub replaced: Vec<u32>,
pub cut_short: Option<Interruption>,
}
#[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 data: Option<DataWatch>,
pub thread: Option<u32>,
pub enabled: bool,
pub deferred: bool,
pub one_shot: bool,
pub pass_count: u32,
pub passes_remaining: u32,
}
pub fn decode_instruction(bytes: &[u8], address: u64, set: InstructionSet) -> Instruction {
let decoded = decode_operation(bytes, address, set);
let occupies = match decoded.length {
Some(length) => &bytes[..length.min(bytes.len())],
None => bytes,
};
Instruction {
address,
bytes: hex::encode(occupies),
text: String::new(),
mnemonic: decoded.mnemonic,
operands: decoded.operands,
flow: decoded.flow,
privileged: decoded.privileged,
privilege: decoded.privilege,
effect: decoded.effect,
condition: decoded.condition,
writes_flags: decoded.writes_flags,
writes: decoded.writes,
reads: decoded.reads,
}
}
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, set: InstructionSet) -> Instruction {
let mut columns = line.trim().splitn(3, char::is_whitespace);
let (_address, bytes, rest) = (columns.next(), columns.next(), columns.next());
let (bytes, text) = match (bytes, rest) {
(Some(bytes), Some(rest)) => (bytes.to_string(), collapse_spaces(rest)),
(Some(only), None) => (String::new(), collapse_spaces(only)),
_ => (String::new(), collapse_spaces(line)),
};
let raw = hex::decode(&bytes).unwrap_or_default();
let decoded = decode_operation(&raw, address, set);
let mut mnemonic = decoded.mnemonic;
if mnemonic.is_empty() {
mnemonic = text
.split_whitespace()
.next()
.unwrap_or_default()
.to_string();
}
Instruction {
address,
bytes,
text,
mnemonic,
operands: decoded.operands,
flow: decoded.flow,
privileged: decoded.privileged,
privilege: decoded.privilege,
effect: decoded.effect,
condition: decoded.condition,
writes_flags: decoded.writes_flags,
writes: decoded.writes,
reads: decoded.reads,
}
}
#[derive(Debug)]
pub(crate) struct Decoded {
pub(crate) mnemonic: String,
pub(crate) operands: Vec<Operand>,
pub(crate) flow: Flow,
pub(crate) privileged: bool,
pub(crate) privilege: Option<Privilege>,
pub(crate) effect: Effect,
pub(crate) condition: Option<Condition>,
pub(crate) writes_flags: bool,
pub(crate) writes: Vec<RegisterOperand>,
pub(crate) reads: Vec<RegisterOperand>,
pub(crate) length: Option<usize>,
}
impl Decoded {
fn nothing(flow: Flow) -> Self {
Self {
mnemonic: String::new(),
operands: Vec::new(),
flow,
privileged: false,
privilege: None,
effect: Effect::Other,
condition: None,
writes_flags: false,
writes: Vec::new(),
reads: Vec::new(),
length: None,
}
}
}
fn decode_operation(bytes: &[u8], address: u64, set: InstructionSet) -> Decoded {
if bytes.is_empty() {
return Decoded::nothing(Flow::Unreadable);
}
let bitness = match set {
InstructionSet::X86 => 32,
InstructionSet::Amd64 => 64,
InstructionSet::Arm64 => {
let Ok(word) = <[u8; crate::arm64::INSTRUCTION_BYTES]>::try_from(
&bytes[..bytes.len().min(crate::arm64::INSTRUCTION_BYTES)],
) else {
return Decoded::nothing(Flow::Unknown);
};
return crate::arm64::decode(u32::from_be_bytes(word), address);
}
InstructionSet::Other(_) => return Decoded::nothing(Flow::Unknown),
};
let mut decoder =
iced_x86::Decoder::with_ip(bitness, bytes, address, iced_x86::DecoderOptions::NONE);
let decoded = decoder.decode();
if decoded.is_invalid() {
return Decoded::nothing(Flow::Unknown);
}
let mnemonic = format!("{:?}", decoded.mnemonic()).to_lowercase();
let operands = (0..decoded.op_count())
.map(|index| read_decoded_operand(&decoded, index, bitness))
.collect();
let (reads, writes) = touched_registers(&decoded, bitness);
let privilege = decoded_privilege(&decoded);
Decoded {
mnemonic,
operands,
flow: decoded_flow(&decoded),
privileged: privilege.is_some(),
privilege,
effect: decoded_effect(&decoded),
condition: decoded_condition(&decoded),
writes_flags: decoded.rflags_written() != 0,
writes,
reads,
length: Some(decoded.len()),
}
}
fn touched_registers(
decoded: &iced_x86::Instruction,
bitness: u32,
) -> (Vec<RegisterOperand>, Vec<RegisterOperand>) {
with_used_registers(decoded, |used| {
(read_from(used, bitness), written_from(used, bitness))
})
}
fn with_used_registers<R>(
decoded: &iced_x86::Instruction,
read: impl FnOnce(&[iced_x86::UsedRegister]) -> R,
) -> R {
use iced_x86::InstructionInfoOptions;
thread_local! {
static FACTORY: std::cell::RefCell<iced_x86::InstructionInfoFactory> =
std::cell::RefCell::new(iced_x86::InstructionInfoFactory::new());
}
FACTORY.with(|factory| {
let mut factory = factory.borrow_mut();
read(
factory
.info_options(decoded, InstructionInfoOptions::NO_MEMORY_USAGE)
.used_registers(),
)
})
}
fn written_from(used: &[iced_x86::UsedRegister], bitness: u32) -> Vec<RegisterOperand> {
use iced_x86::OpAccess;
registers_from(used, bitness, |access| match access {
OpAccess::Write | OpAccess::CondWrite | OpAccess::ReadWrite | OpAccess::ReadCondWrite => {
true
}
OpAccess::None | OpAccess::Read | OpAccess::CondRead | OpAccess::NoMemAccess => false,
})
}
fn read_from(used: &[iced_x86::UsedRegister], bitness: u32) -> Vec<RegisterOperand> {
use iced_x86::OpAccess;
registers_from(used, bitness, |access| match access {
OpAccess::Read | OpAccess::CondRead | OpAccess::ReadWrite | OpAccess::ReadCondWrite => true,
OpAccess::None | OpAccess::Write | OpAccess::CondWrite | OpAccess::NoMemAccess => false,
})
}
fn registers_from(
used: &[iced_x86::UsedRegister],
bitness: u32,
wanted: impl Fn(iced_x86::OpAccess) -> bool,
) -> Vec<RegisterOperand> {
let mut picked: Vec<RegisterOperand> = Vec::new();
for used in used {
if !wanted(used.access()) || used.register() == iced_x86::Register::None {
continue;
}
let operand = register_operand(used.register(), bitness);
if !picked.iter().any(|seen| seen.name == operand.name) {
picked.push(operand);
}
}
picked
}
fn decoded_effect(decoded: &iced_x86::Instruction) -> Effect {
use iced_x86::Mnemonic;
match decoded.mnemonic() {
Mnemonic::Mov | Mnemonic::Movzx => Effect::Move,
Mnemonic::Movsx | Mnemonic::Movsxd => Effect::MoveSigned,
Mnemonic::Lea => Effect::LoadAddress,
Mnemonic::Cmp => Effect::Compare,
Mnemonic::Test => Effect::Test,
Mnemonic::Add | Mnemonic::Adc | Mnemonic::Inc => Effect::Add,
Mnemonic::Sub | Mnemonic::Sbb | Mnemonic::Dec => Effect::Subtract,
Mnemonic::Shl | Mnemonic::Sal => Effect::ShiftLeft,
Mnemonic::Shr | Mnemonic::Sar => Effect::ShiftRight,
Mnemonic::And => Effect::BitAnd,
Mnemonic::Or => Effect::BitOr,
Mnemonic::Xor => Effect::BitXor,
Mnemonic::Push => Effect::Push,
Mnemonic::Pop => Effect::Pop,
_ => Effect::Other,
}
}
fn decoded_privilege(decoded: &iced_x86::Instruction) -> Option<Privilege> {
use iced_x86::{CpuidFeature, Mnemonic, RflagsBits};
if !decoded.is_privileged() {
return None;
}
let feature = |wanted: &[CpuidFeature]| {
decoded
.cpuid_features()
.iter()
.any(|feature| wanted.contains(feature))
};
let family = if feature(&[
CpuidFeature::VMX,
CpuidFeature::SVM,
CpuidFeature::SKINIT_or_SVM,
CpuidFeature::SEV_SNP,
CpuidFeature::RMPQUERY,
CpuidFeature::TDX,
]) {
Privilege::Virtualization
} else if feature(&[
CpuidFeature::MSR,
CpuidFeature::MSRLIST,
CpuidFeature::WRMSRNS,
]) {
Privilege::ModelSpecificRegister
} else if decoded.mnemonic() == Mnemonic::Xsetbv
|| with_used_registers(decoded, |used| {
used.iter()
.any(|used| used.register().is_cr() || used.register().is_dr())
})
{
Privilege::ControlRegister
} else if (decoded.rflags_set() | decoded.rflags_cleared()) & RflagsBits::IF != 0 {
Privilege::InterruptFlag
} else {
match decoded.mnemonic() {
Mnemonic::In
| Mnemonic::Out
| Mnemonic::Insb
| Mnemonic::Insw
| Mnemonic::Insd
| Mnemonic::Outsb
| Mnemonic::Outsw
| Mnemonic::Outsd => Privilege::PortIo,
Mnemonic::Lgdt | Mnemonic::Lidt | Mnemonic::Lldt | Mnemonic::Ltr => {
Privilege::DescriptorTable
}
Mnemonic::Invd
| Mnemonic::Wbinvd
| Mnemonic::Wbnoinvd
| Mnemonic::Invlpg
| Mnemonic::Invpcid
| Mnemonic::Invlpgb
| Mnemonic::Tlbsync => Privilege::CacheOrTlb,
_ => Privilege::Other,
}
};
Some(family)
}
fn decoded_condition(decoded: &iced_x86::Instruction) -> Option<Condition> {
use iced_x86::ConditionCode;
Some(match decoded.condition_code() {
ConditionCode::None => return None,
ConditionCode::e => Condition::Equal,
ConditionCode::ne => Condition::NotEqual,
ConditionCode::a => Condition::UnsignedAbove,
ConditionCode::ae => Condition::UnsignedAboveOrEqual,
ConditionCode::b => Condition::UnsignedBelow,
ConditionCode::be => Condition::UnsignedBelowOrEqual,
ConditionCode::g => Condition::SignedGreater,
ConditionCode::ge => Condition::SignedGreaterOrEqual,
ConditionCode::l => Condition::SignedLess,
ConditionCode::le => Condition::SignedLessOrEqual,
ConditionCode::s => Condition::Negative,
ConditionCode::ns => Condition::NotNegative,
ConditionCode::o => Condition::Overflow,
ConditionCode::no => Condition::NotOverflow,
ConditionCode::p => Condition::Parity,
ConditionCode::np => Condition::NotParity,
})
}
fn read_decoded_operand(decoded: &iced_x86::Instruction, index: u32, bitness: u32) -> Operand {
use iced_x86::OpKind;
match decoded.op_kind(index) {
OpKind::Register => {
Operand::Register(register_operand(decoded.op_register(index), bitness))
}
OpKind::NearBranch16 | OpKind::NearBranch32 | OpKind::NearBranch64 => {
Operand::Target(canonical_target(decoded))
}
OpKind::Immediate8
| OpKind::Immediate8_2nd
| OpKind::Immediate16
| OpKind::Immediate32
| OpKind::Immediate64
| OpKind::Immediate8to16
| OpKind::Immediate8to32
| OpKind::Immediate8to64
| OpKind::Immediate32to64 => Operand::Immediate(decoded.immediate(index)),
OpKind::Memory => Operand::Memory(read_decoded_memory(decoded, bitness)),
other => Operand::Other(format!("{other:?}").to_lowercase()),
}
}
fn read_decoded_memory(decoded: &iced_x86::Instruction, bitness: u32) -> MemoryOperand {
let size = decoded.memory_size().size();
let base = decoded.memory_base();
let index = decoded.memory_index();
let rip_relative = base == iced_x86::Register::RIP || base == iced_x86::Register::EIP;
let overridden = decoded.segment_prefix() != iced_x86::Register::None;
let address = if overridden {
None
} else if rip_relative {
Some(canonical_address(decoded, decoded.ip_rel_memory_address()))
} else if base == iced_x86::Register::None && index == iced_x86::Register::None {
Some(canonical_address(decoded, decoded.memory_displacement64()))
} else {
None
};
let address_bits = if base != iced_x86::Register::None {
base.size() * 8
} else if index != iced_x86::Register::None && index.size() <= 8 {
index.size() * 8
} else {
decoded.memory_displ_size() as usize * 8
};
let displacement = if rip_relative {
let delta = decoded
.ip_rel_memory_address()
.wrapping_sub(decoded.next_ip());
if base == iced_x86::Register::EIP {
sign_extend(delta & 0xffff_ffff, 32)
} else {
delta as i64
}
} else if base == iced_x86::Register::None && index == iced_x86::Register::None {
decoded.memory_displacement64() as i64
} else {
sign_extend(decoded.memory_displacement64(), address_bits)
};
MemoryOperand {
size: (size != 0).then_some(size as u32),
segment: overridden.then(|| register_name(decoded.segment_prefix())),
base: (base != iced_x86::Register::None).then(|| register_operand(base, bitness)),
index: (index != iced_x86::Register::None).then(|| register_operand(index, bitness)),
scale: decoded.memory_index_scale() as u8,
displacement,
address,
}
}
fn sign_extend(value: u64, bits: usize) -> i64 {
if bits == 0 || bits >= 64 {
return value as i64;
}
let shift = 64 - bits;
((value << shift) as i64) >> shift
}
fn register_name(register: iced_x86::Register) -> String {
format!("{register:?}").to_lowercase()
}
fn register_operand(register: iced_x86::Register, bitness: u32) -> RegisterOperand {
let full = match bitness {
64 => register.full_register(),
_ => register.full_register32(),
};
RegisterOperand {
name: register_name(register),
full: register_name(full),
width: register.size() as u32,
}
}
fn decoded_flow(decoded: &iced_x86::Instruction) -> Flow {
use iced_x86::{Code, FlowControl};
match decoded.flow_control() {
FlowControl::Next => Flow::Fallthrough,
FlowControl::UnconditionalBranch => Flow::Jmp(near_target(decoded)),
FlowControl::IndirectBranch => Flow::Jmp(None),
FlowControl::ConditionalBranch => Flow::Branch(near_target(decoded)),
FlowControl::Return => Flow::Return,
FlowControl::Call => Flow::Call(near_target(decoded)),
FlowControl::IndirectCall => Flow::Call(None),
FlowControl::Exception => Flow::Trap,
FlowControl::Interrupt => match decoded.code() {
Code::Int3 => Flow::Trap,
Code::Int_imm8 if matches!(decoded.immediate8(), 0x29 | 0x03) => Flow::Trap,
_ => Flow::Fallthrough,
},
FlowControl::XbeginXabortXend => match decoded.code() {
Code::Xbegin_rel16 | Code::Xbegin_rel32 => Flow::Branch(near_target(decoded)),
_ => Flow::Fallthrough,
},
}
}
fn near_target(decoded: &iced_x86::Instruction) -> Option<u64> {
use iced_x86::OpKind;
(0..decoded.op_count())
.any(|index| {
matches!(
decoded.op_kind(index),
OpKind::NearBranch16 | OpKind::NearBranch32 | OpKind::NearBranch64
)
})
.then(|| canonical_target(decoded))
}
fn canonical_target(decoded: &iced_x86::Instruction) -> u64 {
canonical_address(decoded, decoded.near_branch_target())
}
fn canonical_address(decoded: &iced_x86::Instruction, value: u64) -> u64 {
if decoded.code_size() == iced_x86::CodeSize::Code64 {
return value;
}
(decoded.ip() & 0xffff_ffff_0000_0000) | (value & 0xffff_ffff)
}
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: RefCell<Vec<TargetInput>>,
state: Arc<ClientState>,
}
impl Default for DebugEngine {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DebuggeeType {
pub class: u32,
pub qualifier: u32,
}
impl DebuggeeType {
pub fn is_kernel(self) -> bool {
self.class == DEBUG_CLASS_KERNEL
}
pub fn is_live_kernel(self) -> bool {
self.is_kernel() && self.qualifier < DEBUG_KERNEL_SMALL_DUMP
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TargetLeft {
KernelRunning,
KernelHalted,
Unspoken,
}
fn require_detached(has_target: bool) -> Result<(), DbgEngError> {
if has_target {
Err(DbgEngError::IncompleteKernelDetach)
} else {
Ok(())
}
}
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");
let state = state_for(&client);
Self {
client,
control,
dataspaces,
symbols,
owns_session: false,
deferred_inputs: RefCell::new(Vec::new()),
state,
}
}
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,
})?;
let state = state_for(&client);
Ok(Self {
client,
control,
dataspaces,
symbols,
owns_session: false,
deferred_inputs: RefCell::new(Vec::new()),
state,
})
}
pub fn interrupt_handle(&self) -> InterruptHandle {
InterruptHandle {
control: self.control.clone(),
state: Arc::clone(&self.state),
}
}
fn begin_operation(&self) -> Operation<'_> {
let id = self
.state
.breaks
.lock()
.unwrap_or_else(|e| e.into_inner())
.begin();
Operation { engine: self, id }
}
pub fn target_identity(&self) -> u64 {
identity_of(&self.client)
}
fn read_virtual_into(&self, address: u64, into: &mut [u8]) -> Result<usize, DbgEngError> {
let size = into.len();
let size_u32 = u32::try_from(size).map_err(|_| DbgEngError::BufferTooLarge(size))?;
let mut read = 0u32;
unsafe {
self.dataspaces.ReadVirtual(
address,
into.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,
})?;
Ok(read as usize)
}
pub fn read_memory(&self, address: u64, size: usize) -> Result<Vec<u8>, DbgEngError> {
u32::try_from(size).map_err(|_| DbgEngError::BufferTooLarge(size))?;
let mut buffer = try_buffer(size, address, size)?;
let read = self.read_virtual_into(address, &mut buffer)?;
if read != size {
return Err(DbgEngError::ShortRead {
address,
requested: size,
actual: read,
});
}
Ok(buffer)
}
pub fn read_memory_bounded(
&self,
address: u64,
size: usize,
budget_ms: u32,
) -> Result<MemoryRead, DbgEngError> {
u32::try_from(size).map_err(|_| DbgEngError::BufferTooLarge(size))?;
let operation = self.begin_operation();
let deadline = (budget_ms > 0)
.then(|| Instant::now().checked_add(Duration::from_millis(u64::from(budget_ms))))
.flatten();
let read = read_chunked(
address,
size,
|| {
if deadline.is_some_and(|deadline| Instant::now() >= deadline) {
return Some(Interruption::Deadline {
after_ms: budget_ms,
});
}
operation
.took_break_request()
.then_some(Interruption::OnRequest)
},
|at, into| self.read_virtual_into(at, into),
);
if matches!(&read, Ok(read) if read.cut_short == Some(Interruption::OnRequest)) {
let _ = self.interrupted();
}
read
}
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_thread_teb(&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.GetCurrentThreadTeb() }.map_err(|source| DbgEngError::Context {
operation: "reading the current thread TEB".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 current_thread_system_id(&self) -> Result<u32, DbgEngError> {
let objects = self.system_objects()?;
unsafe { objects.GetCurrentThreadSystemId() }.map_err(|source| DbgEngError::Context {
operation: "reading the current thread id".into(),
source,
})
}
pub fn current_thread_data_offset(&self) -> Result<u64, DbgEngError> {
let objects = self.system_objects()?;
unsafe { objects.GetCurrentThreadDataOffset() }.map_err(|source| DbgEngError::Context {
operation: "reading the current thread's data offset".into(),
source,
})
}
pub fn current_processor(&self) -> Result<Option<u32>, DbgEngError> {
if !self.is_kernel_target()? {
return Ok(None);
}
let objects = self.system_objects()?;
let current =
unsafe { objects.GetCurrentThreadId() }.map_err(|source| DbgEngError::Context {
operation: "reading the current thread index".into(),
source,
})?;
let processors = unsafe { self.control.GetNumberProcessors() }.map_err(|source| {
DbgEngError::Context {
operation: "counting the target's processors".into(),
source,
}
})?;
let order = std::iter::once(current)
.filter(|&candidate| candidate < processors)
.chain((0..processors).filter(move |&candidate| candidate != current));
let mut unreadable = None;
for processor in order {
match unsafe { objects.GetThreadIdByProcessor(processor) } {
Ok(thread) if thread == current => return Ok(Some(processor)),
Ok(_) => {}
Err(why) => {
unreadable.get_or_insert(why);
}
}
}
match unreadable {
None => Ok(None),
Some(source) => Err(DbgEngError::Context {
operation: "reading which processor the debugger is on".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 virtual_region(&self, address: u64) -> Result<VirtualRegion, DbgEngError> {
let mut info = AlignedMemoryInfo::default();
unsafe { self.dataspaces.QueryVirtual(address, &mut info.0) }.map_err(|source| {
DbgEngError::Context {
operation: format!("querying the memory manager about {address:#x}"),
source,
}
})?;
Ok(VirtualRegion {
base: info.0.BaseAddress,
size: info.0.RegionSize,
state: VirtualState::from_mem_state(info.0.State.0),
protect: info.0.Protect.0,
})
}
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 effective_processor_type(&self) -> Result<u32, DbgEngError> {
unsafe { self.control.GetEffectiveProcessorType() }.map_err(|source| DbgEngError::Context {
operation: "querying effective processor type".into(),
source,
})
}
pub fn debuggee_type(&self) -> Result<DebuggeeType, 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(DebuggeeType { class, qualifier })
}
pub fn kernel_connection_options(&self) -> Result<String, DbgEngError> {
self.refuse_without_a_debuggee()?;
read_engine_string(|buffer, size| unsafe {
self.client.GetKernelConnectionOptions(buffer, size)
})
.map_err(|source| DbgEngError::Context {
operation: "reading the kernel connection options".into(),
source,
})
}
pub fn is_kernel_target(&self) -> Result<bool, DbgEngError> {
Ok(self.debuggee_type()?.is_kernel())
}
pub fn dump_files(&self) -> Result<Vec<String>, DbgEngError> {
self.refuse_without_a_debuggee()?;
let named = |operation: &str, source: windows::core::Error| DbgEngError::Context {
operation: operation.into(),
source,
};
let count = unsafe { self.client.GetNumberDumpFiles() }
.map_err(|source| named("counting the session's dump files", source))?;
let mut files = Vec::with_capacity(count as usize);
for index in 0..count {
let mut kind = 0u32;
let mut needed = 0u32;
unsafe {
self.client
.GetDumpFileWide(index, None, Some(&mut needed), None, &mut kind)
}
.map_err(|source| named("sizing a dump file's name", source))?;
if needed <= 1 {
files.push(String::new());
continue;
}
let mut buffer = vec![0u16; needed as usize];
unsafe {
self.client
.GetDumpFileWide(index, Some(&mut buffer), None, None, &mut kind)
}
.map_err(|source| named("reading a dump file's name", source))?;
files.push(wide_to_string(&buffer));
}
Ok(files)
}
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 {
self.debuggee_type().is_ok_and(DebuggeeType::is_live_kernel)
}
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_the_previous_session();
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_the_previous_session();
Ok(PendingTarget::new(self, WaitKind::KernelBreakIn))
}
fn wait_for_kernel_break_in(&self) -> Result<(), DbgEngError> {
let operation = self.begin_operation();
let pumped = self.pump(Bound::Watchdog(KERNEL_ATTACH_WAIT_MS), &operation);
self.clear_initial_break();
let outcome = pumped?;
let status =
unsafe { self.control.GetExecutionStatus() }.map_err(DbgEngError::CommandFailed)?;
if outcome == WaitOutcome::Deadline || status == DEBUG_STATUS_NO_DEBUGGEE {
return Err(DbgEngError::KernelBreakTimeout);
}
self.absorb_initial_break_artifact();
Ok(())
}
fn wait_for_live_target(&self, registered: &Registered<'_>) -> Result<(), DbgEngError> {
let operation = self.begin_operation();
let deadline = Instant::now() + Duration::from_millis(u64::from(LIVE_WAIT_MS));
let mut waited = false;
let mut asked_to_stop = false;
loop {
let presence = self.presence_of(registered);
match presence {
Presence::Arrived => return Ok(()),
Presence::Unknown if waited => return Ok(()),
Presence::Listed | Presence::Absent | Presence::Unknown => {}
}
let left = u32::try_from(
deadline
.saturating_duration_since(Instant::now())
.as_millis(),
)
.unwrap_or(LIVE_WAIT_MS);
if left == 0 || asked_to_stop {
return match presence {
Presence::Absent if asked_to_stop => Err(DbgEngError::LiveTargetInterrupted),
Presence::Absent => {
if waited {
self.retire_deferred_attachment(registered);
self.discard_registration(registered);
}
Err(DbgEngError::LiveTargetTimeout)
}
_ => Ok(()),
};
}
asked_to_stop = match self.pump(Bound::Finite(left), &operation) {
Ok(outcome) => outcome.broke_in(),
Err(err) => {
if self.holds_nothing_at_all() {
self.retire_deferred_attachment(registered);
}
return Err(err);
}
};
waited = true;
}
}
fn presence_of(&self, registered: &Registered<'_>) -> Presence {
match self.has_target() {
Ok(true) => {}
Ok(false) => return Presence::Absent,
Err(_) => return Presence::Unknown,
}
let Ok(held) = self.session_processes() else {
return Presence::Unknown;
};
let attached = self.attached_pids();
self.state
.arrivals
.lock()
.unwrap_or_else(|e| e.into_inner())
.presence(registered.id, &held, &attached)
}
fn holds_nothing_at_all(&self) -> bool {
matches!(self.has_target(), Ok(false))
}
fn retire_deferred_attachment(&self, registered: &Registered<'_>) {
let pid = self
.state
.arrivals
.lock()
.unwrap_or_else(|e| e.into_inner())
.attached_pid(registered.id);
let Some(pid) = pid else {
return;
};
let mut attached = self
.state
.attached_processes
.lock()
.unwrap_or_else(|e| e.into_inner());
if attached.get(&pid) == Some(&Attachment::Deferred) {
attached.remove(&pid);
}
}
fn discard_registration(&self, registered: &Registered<'_>) {
self.state
.arrivals
.lock()
.unwrap_or_else(|e| e.into_inner())
.discard(registered.id);
}
fn attached_pids(&self) -> HashSet<u32> {
self.state
.attached_processes
.lock()
.unwrap_or_else(|e| e.into_inner())
.keys()
.copied()
.collect()
}
fn last_event_process(&self) -> Result<u32, DbgEngError> {
let mut kind = 0u32;
let mut process = 0u32;
let mut thread = 0u32;
unsafe {
self.control.GetLastEventInformation(
&mut kind,
&mut process,
&mut thread,
None,
0,
None,
None,
None,
)
}
.map_err(|source| DbgEngError::Context {
operation: "reading which target the last event belongs to".into(),
source,
})?;
Ok(process)
}
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(|source| printed(DbgEngError::CommandFailed(source), &output_buffer))?;
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);
let operation = self.begin_operation();
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.break_in_only();
})
});
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 cut_short = operation.cut_short_by(by_watchdog, timeout_ms);
let interrupted = cut_short.is_some();
if interrupted {
let _ = self.interrupted();
}
if !interrupted {
result.map_err(|source| printed(DbgEngError::CommandFailed(source), &output_buffer))?;
}
Ok(CommandRun {
output: String::from_utf8_lossy(&output_buffer).to_string(),
cut_short,
target_gone: self.lost_its_target(),
})
}
pub fn wait_for_event(&self, timeout_ms: u32) -> Result<WaitOutcome, DbgEngError> {
let operation = self.begin_operation();
self.pump(Bound::Finite(timeout_ms), &operation)
}
fn pump(&self, bound: Bound, operation: &Operation<'_>) -> Result<WaitOutcome, DbgEngError> {
let (result, arrived, by_watchdog) = match bound {
Bound::Finite(timeout_ms) => {
let hr = unsafe {
(Interface::vtable(&self.control).WaitForEvent)(
Interface::as_raw(&self.control),
0,
timeout_ms,
)
};
let result = if hr.is_err() {
Err(windows::core::Error::from_hresult(HRESULT(hr.0)))
} else {
Ok(())
};
(result, hr == S_OK, false)
}
Bound::Watchdog(timeout_ms) | Bound::WatchdogExit(timeout_ms) => {
let handle = self.interrupt_handle();
let watchdog = Watchdog::arm(
Duration::from_millis(u64::from(timeout_ms)),
move || {
if matches!(bound, Bound::WatchdogExit(_)) {
let _ = unsafe {
handle.control.SetInterrupt(
windows::Win32::System::Diagnostics::Debug::Extensions::DEBUG_INTERRUPT_EXIT,
)
};
} else {
let _ = handle.break_in_only();
}
},
);
let result = unsafe { self.control.WaitForEvent(0, WAIT_INFINITE) };
let by_watchdog = watchdog.disarm();
let arrived = result.is_ok();
(result, arrived, by_watchdog)
}
};
let asked = operation.took_break_request();
if by_watchdog {
return Ok(WaitOutcome::Deadline);
}
if asked {
return Ok(WaitOutcome::OnRequest);
}
result.map_err(DbgEngError::CommandFailed)?;
if !arrived {
return Ok(WaitOutcome::Expired);
}
Ok(WaitOutcome::Stopped {
process: self.record_where_it_stopped(),
})
}
fn record_where_it_stopped(&self) -> Option<(u32, u32)> {
let (Ok(id), Ok(held)) = (self.last_event_process(), self.session_processes()) else {
return None;
};
self.note_attachments_present(&held);
let entry = held.iter().copied().find(|(held, _)| *held == id)?;
let attached = self.attached_pids();
self.state
.arrivals
.lock()
.unwrap_or_else(|e| e.into_inner())
.deliver(entry, &attached);
Some(entry)
}
pub fn execute_and_wait(
&self,
command: &str,
timeout_ms: u32,
) -> Result<CommandRun, DbgEngError> {
let operation = self.begin_operation();
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 pumped = exec
.is_ok()
.then(|| self.pump(Bound::Watchdog(timeout_ms), &operation));
unsafe {
let _ = self.client.SetOutputCallbacks(None);
}
let (waited, cut_short) = match pumped {
Some(Ok(outcome)) => (Ok(()), outcome.cut_short(timeout_ms)),
Some(Err(err)) => (Err(err), None),
None => (Ok(()), operation.cut_short_by(false, timeout_ms)),
};
let interrupted = cut_short.is_some();
let target_gone = self.lost_its_target();
if interrupted {
let _ = self.interrupted();
} else if !target_gone {
exec.map_err(|source| printed(DbgEngError::CommandFailed(source), &output_buffer))?;
waited.map_err(|error| printed(error, &output_buffer))?;
}
Ok(CommandRun {
output: String::from_utf8_lossy(&output_buffer).to_string(),
cut_short,
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);
}
let operation = self.begin_operation();
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 pumped = self.pump(Bound::Watchdog(timeout_ms), &operation);
unsafe {
let _ = self.client.SetOutputCallbacks(None);
}
let (waited, cut_short) = match pumped {
Ok(outcome) => (Ok(()), outcome.cut_short(timeout_ms)),
Err(err) => (Err(err), None),
};
let interrupted = cut_short.is_some();
let target_gone = self.lost_its_target();
if interrupted {
let _ = self.interrupted();
} else if !target_gone {
waited.map_err(|error| printed(error, &output_buffer))?;
}
Ok(Some(CommandRun {
output: String::from_utf8_lossy(&output_buffer).to_string(),
cut_short,
target_gone,
}))
}
pub fn run_to_address(
&self,
address: u64,
timeout_ms: u32,
) -> Result<RunToResult, DbgEngError> {
let operation = self.begin_operation();
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 pumped = exec
.is_ok()
.then(|| self.pump(Bound::Watchdog(timeout_ms), &operation));
let (waited, cut_short) = match pumped {
Some(Ok(outcome)) => (Ok(()), outcome.cut_short(timeout_ms)),
Some(Err(err)) => (Err(err), None),
None => (Ok(()), operation.cut_short_by(false, timeout_ms)),
};
let expired = matches!(cut_short, Some(Interruption::Deadline { .. }));
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(|source| printed(DbgEngError::CommandFailed(source), output.as_bytes()))?;
waited.map_err(|error| printed(error, output.as_bytes()))?;
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 set_breakpoint_callback(&self, callback: BreakpointCallback) -> Result<(), DbgEngError> {
let callbacks: IDebugEventContextCallbacks = DebugEventContextCallbacks {
callback,
client: self.client.as_raw(),
}
.into();
unsafe { self.client.SetEventContextCallbacks(Some(&callbacks)) }.map_err(|source| {
DbgEngError::Context {
operation: "registering event context callbacks".into(),
source,
}
})
}
pub fn clear_breakpoint_callback(&self) -> Result<(), DbgEngError> {
unsafe { self.client.SetEventContextCallbacks(None) }.map_err(|source| {
DbgEngError::Context {
operation: "unregistering event context callbacks".into(),
source,
}
})
}
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");
}
fn drain_an_unowned_break(&self, ours: Option<OperationId>) {
let scope = self.state.breaks.lock().unwrap_or_else(|e| e.into_inner());
if scope.running.iter().any(|running| Some(*running) != ours) {
return;
}
let _ = self.interrupted();
}
pub fn reload_symbols(&self, args: &str) -> Result<(), DbgEngError> {
let args = CString::new(args).map_err(|_| DbgEngError::InvalidCommand)?;
self.drain_an_unowned_break(None);
unsafe {
self.symbols
.Reload(PCSTR::from_raw(args.as_ptr() as *const u8))
.map_err(DbgEngError::OperationFailed)
}
}
pub fn reload_symbols_bounded(
&self,
args: &str,
timeout_ms: u32,
) -> Result<SymbolReload, DbgEngError> {
let args = CString::new(args).map_err(|_| DbgEngError::InvalidCommand)?;
let operation = self.begin_operation();
self.drain_an_unowned_break(Some(operation.id));
let watchdog = (timeout_ms > 0).then(|| {
let handle = self.interrupt_handle();
Watchdog::arm(Duration::from_millis(u64::from(timeout_ms)), move || {
let _ = handle.break_in_only();
})
});
let result = unsafe {
self.symbols
.Reload(PCSTR::from_raw(args.as_ptr() as *const u8))
};
let by_watchdog = watchdog.is_some_and(Watchdog::disarm);
let cut_short = operation.cut_short_by(by_watchdog, timeout_ms);
if cut_short.is_some() {
let _ = self.interrupted();
} else {
result.map_err(DbgEngError::OperationFailed)?;
}
Ok(SymbolReload { cut_short })
}
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 integer_register(&self, name: &str) -> Result<u64, DbgEngError> {
let registers: IDebugRegisters =
self.client.cast().map_err(|source| DbgEngError::Context {
operation: "obtaining the register interface".into(),
source,
})?;
let name_c = CString::new(name).map_err(|_| DbgEngError::Register {
name: name.to_string(),
reason: "the name contains a NUL",
})?;
let index = unsafe { registers.GetIndexByName(PCSTR::from_raw(name_c.as_ptr().cast())) }
.map_err(|source| DbgEngError::Context {
operation: format!("looking up register `{name}`"),
source,
})?;
let mut value = DEBUG_VALUE::default();
unsafe { registers.GetValue(index, &mut value) }.map_err(|source| {
DbgEngError::Context {
operation: format!("reading register `{name}`"),
source,
}
})?;
match RegisterValue::decode(&value) {
RegisterValue::Int(value) => Ok(value),
RegisterValue::Unavailable => Err(DbgEngError::Register {
name: name.to_string(),
reason: "the engine holds no value for it",
}),
RegisterValue::Float(_) | RegisterValue::Bytes(_) => Err(DbgEngError::Register {
name: name.to_string(),
reason: "it holds no integer",
}),
}
}
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 last_event(&self) -> Result<Option<DebugEvent>, DbgEngError> {
let mut kind = 0u32;
let mut process = 0u32;
let mut thread = 0u32;
let mut extra = ExtraEventInfo::default();
let mut used = 0u32;
unsafe {
self.control.GetLastEventInformation(
&mut kind,
&mut process,
&mut thread,
Some(extra.as_mut_ptr()),
ExtraEventInfo::SIZE,
Some(&mut used),
None,
None,
)
}
.map_err(|source| DbgEngError::Context {
operation: "reading the engine's last event".into(),
source,
})?;
if kind == NO_EVENT_KIND {
return Ok(None);
}
let exception = extra.exception(kind, used);
Ok(Some(DebugEvent {
kind,
process,
thread,
first_chance: exception.as_ref().is_some_and(|(_, first)| *first),
exception: exception.map(|(record, _)| record),
context: None,
}))
}
pub fn stored_event(&self) -> Result<Option<DebugEvent>, DbgEngError> {
let mut kind = 0u32;
let mut process = 0u32;
let mut thread = 0u32;
let mut refusal = None;
for (rung, &size) in STORED_CONTEXT_SIZES.iter().enumerate() {
let mut context = vec![0u8; size as usize];
let mut context_used = 0u32;
let mut extra = ExtraEventInfo::default();
let mut extra_used = 0u32;
match unsafe {
self.control.GetStoredEventInformation(
&mut kind,
&mut process,
&mut thread,
Some(context.as_mut_ptr().cast()),
size,
Some(&mut context_used),
Some(extra.as_mut_ptr()),
ExtraEventInfo::SIZE,
Some(&mut extra_used),
)
} {
Ok(())
if context_used as usize >= context.len()
&& rung + 1 < STORED_CONTEXT_SIZES.len() =>
{
continue;
}
Ok(()) => {
context.truncate((context_used as usize).min(context.len()));
let exception = extra.exception(kind, extra_used);
return Ok(Some(DebugEvent {
kind,
process,
thread,
first_chance: exception.as_ref().is_some_and(|(_, first)| *first),
exception: exception.map(|(record, _)| record),
context: (!context.is_empty()).then(|| ThreadContext {
bytes: context,
target: self.target_identity(),
}),
}));
}
Err(why) if no_stored_event(why.code()) => return Ok(None),
Err(why) if why.code() == E_INVALIDARG => refusal = Some(why),
Err(why) => {
refusal = Some(why);
break;
}
}
}
Err(DbgEngError::Context {
operation: "reading the event this target was stored on".into(),
source: refusal.unwrap_or_else(|| windows::core::Error::from(E_FAIL)),
})
}
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(self.resolve_frames(&raw))
}
pub fn stack_frames_from(
&self,
context: &ThreadContext,
max_frames: usize,
) -> Result<Vec<StackFrame>, DbgEngError> {
if max_frames == 0 {
return Ok(Vec::new());
}
if context.target != self.target_identity() {
return Err(DbgEngError::ContextFromAnotherTarget);
}
if context.is_empty() {
return Err(DbgEngError::Context {
operation: "walking the stack of a recorded context that has no registers".into(),
source: windows::core::Error::from(E_INVALIDARG),
});
}
let mut raw = vec![DEBUG_STACK_FRAME::default(); max_frames];
let mut filled = 0u32;
unsafe {
self.control.GetContextStackTrace(
Some(context.as_bytes().as_ptr().cast()),
context.len() as u32,
Some(&mut raw),
None,
0,
0,
Some(&mut filled),
)
}
.map_err(|source| DbgEngError::Context {
operation: "walking the stack of a recorded register context".into(),
source,
})?;
raw.truncate((filled as usize).min(max_frames));
Ok(self.resolve_frames(&raw))
}
fn resolve_frames(&self, raw: &[DEBUG_STACK_FRAME]) -> Vec<StackFrame> {
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 set = self.instruction_set();
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, set));
if next <= at {
break;
}
at = next;
}
Ok(out)
}
pub fn decode_range(
&self,
address: u64,
length: usize,
) -> Result<Vec<Instruction>, DbgEngError> {
let set = InstructionSet::from_processor_type(self.effective_processor_type()?);
let bitness = match set {
InstructionSet::X86 => 32,
InstructionSet::Amd64 => 64,
InstructionSet::Arm64 => return self.decode_range_arm64(address, length),
InstructionSet::Other(machine) => {
return Err(DbgEngError::UndecodedInstructionSet { machine });
}
};
let bytes = self.read_memory(address, length)?;
let mut decoder =
iced_x86::Decoder::with_ip(bitness, &bytes, address, iced_x86::DecoderOptions::NONE);
let mut out = Vec::new();
while decoder.can_decode() {
let at = decoder.ip();
let decoded = decoder.decode();
let offset = (at - address) as usize;
let encoding = bytes
.get(offset..offset + decoded.len())
.unwrap_or_default();
let (reads, writes) = match decoded.is_invalid() {
true => (Vec::new(), Vec::new()),
false => touched_registers(&decoded, bitness),
};
let privilege = match decoded.is_invalid() {
true => None,
false => decoded_privilege(&decoded),
};
out.push(Instruction {
address: at,
bytes: hex::encode(encoding),
text: String::new(),
mnemonic: if decoded.is_invalid() {
String::new()
} else {
format!("{:?}", decoded.mnemonic()).to_lowercase()
},
operands: if decoded.is_invalid() {
Vec::new()
} else {
(0..decoded.op_count())
.map(|index| read_decoded_operand(&decoded, index, bitness))
.collect()
},
flow: if decoded.is_invalid() {
Flow::Unknown
} else {
decoded_flow(&decoded)
},
privileged: privilege.is_some(),
privilege,
effect: if decoded.is_invalid() {
Effect::Other
} else {
decoded_effect(&decoded)
},
condition: (!decoded.is_invalid())
.then(|| decoded_condition(&decoded))
.flatten(),
writes_flags: !decoded.is_invalid() && decoded.rflags_written() != 0,
writes,
reads,
});
}
Ok(out)
}
fn decode_range_arm64(
&self,
address: u64,
length: usize,
) -> Result<Vec<Instruction>, DbgEngError> {
let bytes = self.read_memory(address, length)?;
Ok(bytes
.chunks_exact(crate::arm64::INSTRUCTION_BYTES)
.enumerate()
.map(|(index, chunk)| {
let at = address + (index * crate::arm64::INSTRUCTION_BYTES) as u64;
let word = u32::from_le_bytes(chunk.try_into().expect("chunks_exact gives four"));
let decoded = crate::arm64::decode(word, at);
Instruction {
address: at,
bytes: hex::encode(word.to_be_bytes()),
text: String::new(),
mnemonic: decoded.mnemonic,
operands: decoded.operands,
flow: decoded.flow,
privileged: decoded.privileged,
privilege: decoded.privilege,
effect: decoded.effect,
condition: decoded.condition,
writes_flags: decoded.writes_flags,
writes: decoded.writes,
reads: decoded.reads,
}
})
.collect())
}
pub fn instruction_set(&self) -> InstructionSet {
match self.effective_processor_type() {
Ok(machine) => InstructionSet::from_processor_type(machine),
Err(_) => InstructionSet::Other(0),
}
}
pub fn function_extent(&self, address: u64) -> Result<FunctionExtent, DbgEngError> {
let set = InstructionSet::from_processor_type(self.effective_processor_type()?);
let expected = match set {
InstructionSet::Amd64 => 3 * std::mem::size_of::<u32>(),
InstructionSet::Arm64 => 2 * std::mem::size_of::<u32>(),
InstructionSet::X86 | InstructionSet::Other(_) => {
return Ok(FunctionExtent::Unsupported(set));
}
};
let mut entry = [0u32; 3];
let mut needed = 0u32;
if let Err(source) = unsafe {
self.symbols.GetFunctionEntryByOffset(
address,
0,
Some(entry.as_mut_ptr().cast()),
std::mem::size_of_val(&entry) as u32,
Some(&mut needed),
)
} {
if source.code() == E_NOINTERFACE {
return Ok(FunctionExtent::NoEntry);
}
return Err(DbgEngError::Context {
operation: format!("reading the function entry for {address:#x}"),
source,
});
}
if needed as usize != expected {
return Ok(FunctionExtent::Unsupported(set));
}
let begin = entry[0] as u64;
if begin == 0 {
return Ok(FunctionExtent::NoEntry);
}
let Some(module) = self.module_at(address)? else {
return Ok(FunctionExtent::NoEntry);
};
let Some(length) = (match set {
InstructionSet::Amd64 => (entry[1] as u64).checked_sub(begin),
InstructionSet::Arm64 => self.arm64_function_length(module.base, entry[1])?,
InstructionSet::X86 | InstructionSet::Other(_) => None,
}) else {
return Ok(FunctionExtent::NoEntry);
};
let end = begin + length;
if end <= begin {
return Ok(FunctionExtent::NoEntry);
}
let (begin, end) = (module.base + begin, module.base + end);
if !(begin..end).contains(&address) {
return Ok(FunctionExtent::NoEntry);
}
Ok(FunctionExtent::Region { begin, end })
}
fn arm64_function_length(
&self,
module_base: u64,
second_word: u32,
) -> Result<Option<u64>, DbgEngError> {
let rva = match crate::arm64::unwind_record(second_word) {
crate::arm64::UnwindRecord::PackedLength(length) => return Ok(Some(length)),
crate::arm64::UnwindRecord::Reserved => return Ok(None),
crate::arm64::UnwindRecord::XdataAt(rva) => rva,
};
let at = module_base + rva as u64;
let header = self.read_memory(at, std::mem::size_of::<u32>())?;
let header = <[u8; 4]>::try_from(header.as_slice()).map_err(|_| DbgEngError::Context {
operation: format!("reading the unwind header at {at:#x}"),
source: S_FALSE.into(),
})?;
Ok(crate::arm64::xdata_length(u32::from_le_bytes(header)))
}
pub fn symbol_for(&self, address: u64) -> Option<(String, u64)> {
let (name, displacement) = self.symbol_at(address);
name.map(|name| (name, displacement))
}
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.GetBreakpointByIndex2(index) }.map_err(|source| {
DbgEngError::Context {
operation: format!("reading breakpoint at index {index}"),
source,
}
})?;
let breakpoint = std::mem::ManuallyDrop::new(breakpoint);
out.push(breakpoint_info(&breakpoint, &format!("at index {index}"))?);
}
Ok(out)
}
pub fn set_breakpoint(&self, spec: &BreakpointSpec) -> Result<BreakpointSet, DbgEngError> {
self.set_breakpoint_bounded(spec, 0)
}
pub fn set_breakpoint_bounded(
&self,
spec: &BreakpointSpec,
timeout_ms: u32,
) -> Result<BreakpointSet, DbgEngError> {
spec.validated()?;
let command = spec
.command
.as_deref()
.map(|text| CString::new(text).map_err(|_| DbgEngError::InvalidCommand))
.transpose()?;
let (breakpoint, cut_short) = self.new_breakpoint(spec, timeout_ms)?;
let resolved = match unsafe { breakpoint.breakpoint.GetOffset() } {
Ok(address) if address != DEBUG_INVALID_OFFSET => Some(address),
_ => None,
};
if let (Some(watch), Some(address)) = (spec.data, resolved)
&& !address.is_multiple_of(u64::from(watch.size))
{
return Err(DbgEngError::InvalidBreakpoint(format!(
"a {}-byte data breakpoint must be {}-byte aligned, and this location resolved to \
{address:#x}, which is not",
watch.size, watch.size
)));
}
unsafe {
if let Some(command) = &command {
breakpoint
.breakpoint
.SetCommand(PCSTR::from_raw(command.as_ptr().cast()))
.map_err(DbgEngError::BreakpointFailed)?;
}
if let Some(passes) = spec.pass_count {
breakpoint
.breakpoint
.SetPassCount(passes)
.map_err(DbgEngError::BreakpointFailed)?;
}
if let Some(thread) = spec.thread {
breakpoint
.breakpoint
.SetMatchThreadId(thread)
.map_err(DbgEngError::BreakpointFailed)?;
}
if let Some(watch) = spec.data {
breakpoint
.breakpoint
.SetDataParameters(watch.size, watch.access.to_engine())
.map_err(DbgEngError::BreakpointFailed)?;
}
}
if spec.flags() != 0 {
unsafe { breakpoint.breakpoint.AddFlags(spec.flags()) }
.map_err(DbgEngError::BreakpointFailed)?;
}
let info = breakpoint_info(&breakpoint.breakpoint, "just set")?;
breakpoint.keep();
let replaced = match (spec.on_existing, resolved) {
(OnExisting::Replace, Some(address)) => self.remove_breakpoints_at(address, info.id),
_ => Vec::new(),
};
Ok(BreakpointSet {
breakpoint: info,
replaced,
cut_short,
})
}
fn new_breakpoint(
&self,
spec: &BreakpointSpec,
timeout_ms: u32,
) -> Result<(ScopedBreakpoint<'_>, Option<Interruption>), DbgEngError> {
let kind = match spec.data {
Some(_) => DEBUG_BREAKPOINT_DATA,
None => DEBUG_BREAKPOINT_CODE,
};
let breakpoint = ScopedBreakpoint::new(self, kind)?;
let cut_short = match &spec.at {
BreakpointAt::Address(address) => {
unsafe { breakpoint.breakpoint.SetOffset(*address) }
.map_err(DbgEngError::BreakpointFailed)?;
None
}
BreakpointAt::Expression(expression) => {
let text =
CString::new(expression.as_str()).map_err(|_| DbgEngError::InvalidCommand)?;
let operation = self.begin_operation();
let watchdog = (timeout_ms > 0).then(|| {
let handle = self.interrupt_handle();
Watchdog::arm(Duration::from_millis(u64::from(timeout_ms)), move || {
let _ = handle.break_in_only();
})
});
let result = unsafe {
breakpoint
.breakpoint
.SetOffsetExpression(PCSTR::from_raw(text.as_ptr().cast()))
};
let by_watchdog = watchdog.is_some_and(Watchdog::disarm);
let cut_short = operation.cut_short_by(by_watchdog, timeout_ms);
let interrupted = cut_short.is_some();
if interrupted {
let _ = self.interrupted();
}
if !interrupted {
result.map_err(DbgEngError::BreakpointFailed)?;
}
cut_short
}
};
Ok((breakpoint, cut_short))
}
fn remove_breakpoints_at(&self, address: u64, keep: u32) -> Vec<u32> {
let Ok(held) = self.breakpoints() else {
return Vec::new();
};
held.into_iter()
.filter(|held| held.id != keep && held.address == Some(address))
.map(|held| held.id)
.filter(|id| self.remove_breakpoint(*id).is_ok())
.collect()
}
pub fn remove_breakpoint(&self, id: u32) -> Result<(), DbgEngError> {
let breakpoint = self.breakpoint_by_id(id)?;
unsafe { self.control.RemoveBreakpoint2(&*breakpoint) }.map_err(|source| {
DbgEngError::Context {
operation: format!("removing breakpoint {id}"),
source,
}
})
}
pub fn enable_breakpoint(&self, id: u32, enabled: bool) -> Result<(), DbgEngError> {
let breakpoint = self.breakpoint_by_id(id)?;
let result = unsafe {
match enabled {
true => breakpoint.AddFlags(DEBUG_BREAKPOINT_ENABLED),
false => breakpoint.RemoveFlags(DEBUG_BREAKPOINT_ENABLED),
}
};
result.map_err(|source| DbgEngError::Context {
operation: format!(
"{} breakpoint {id}",
if enabled { "enabling" } else { "disabling" }
),
source,
})
}
fn breakpoint_by_id(
&self,
id: u32,
) -> Result<std::mem::ManuallyDrop<IDebugBreakpoint2>, DbgEngError> {
let breakpoint = unsafe { self.control.GetBreakpointById2(id) }.map_err(|source| {
DbgEngError::Context {
operation: format!("looking up breakpoint {id}"),
source,
}
})?;
Ok(std::mem::ManuallyDrop::new(breakpoint))
}
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_processes_that_left();
self.enable_initial_break()?;
let before = self.session_processes().ok();
let mut wide = to_wide(command_line);
unsafe {
self.client.CreateProcessWide(
0,
PWSTR::from_raw(wide.as_mut_ptr()),
DEBUG_ONLY_THIS_PROCESS | CREATE_NO_WINDOW,
)
}
.map_err(DbgEngError::OperationFailed)?;
self.retain_deferred_input(TargetInput::Wide(wide));
Ok(PendingTarget::live(self, Arrival::Launched(before)))
}
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_processes_that_left();
self.claim_attached(pid);
Ok(PendingTarget::live(self, Arrival::Attached(pid)))
}
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_the_previous_session();
Ok(())
}
pub fn open_trace(&self, path: &str) -> Result<(), DbgEngError> {
self.open_dump(path)
}
fn retain_deferred_input(&self, input: TargetInput) {
self.deferred_inputs.borrow_mut().push(input);
}
fn release_deferred_inputs(&self) {
self.deferred_inputs.borrow_mut().clear();
}
fn claim_attached(&self, pid: u32) {
self.state
.attached_processes
.lock()
.unwrap_or_else(|e| e.into_inner())
.insert(pid, Attachment::Deferred);
}
fn prune_processes_that_left(&self) {
let Ok(held) = self.session_processes() else {
return;
};
self.note_attachments_present(&held);
self.state
.arrivals
.lock()
.unwrap_or_else(|e| e.into_inner())
.forget_departed(&held);
self.state
.attached_processes
.lock()
.unwrap_or_else(|e| e.into_inner())
.retain(|pid, state| {
*state == Attachment::Deferred || held.iter().any(|(_, held)| held == pid)
});
}
fn note_attachments_present(&self, held: &[(u32, u32)]) {
for (pid, state) in self
.state
.attached_processes
.lock()
.unwrap_or_else(|e| e.into_inner())
.iter_mut()
{
if held.iter().any(|(_, held)| held == pid) {
*state = Attachment::Joined;
}
}
}
fn forget_pending_opens(&self) {
self.state
.arrivals
.lock()
.unwrap_or_else(|e| e.into_inner())
.forget_all();
}
fn forget_the_previous_session(&self) {
reissue_identity(&self.client);
self.state
.attached_processes
.lock()
.unwrap_or_else(|e| e.into_inner())
.clear();
self.state
.arrivals
.lock()
.unwrap_or_else(|e| e.into_inner())
.forget_all();
}
pub fn attached_to_a_live_process(&self) -> bool {
let attached = self
.state
.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_key(pid)))
}
pub fn end_session(&self) -> Result<TargetLeft, DbgEngError> {
reissue_identity(&self.client);
let (left, session_ended, ended) = if self.is_live_kernel() {
let (resumed, detached) = self.resume_and_detach_live_kernel();
let left = match resumed {
Ok(()) => TargetLeft::KernelRunning,
Err(_) => TargetLeft::KernelHalted,
};
(left, detached.is_ok(), detached)
} else {
let detached = self.detach_attached_processes();
let ended = unsafe { self.client.EndSession(DEBUG_END_PASSIVE) }
.map_err(DbgEngError::OperationFailed);
(TargetLeft::Unspoken, ended.is_ok(), ended.and(detached))
};
if session_ended {
self.release_deferred_inputs();
self.forget_pending_opens();
}
ended.map(|()| left)
}
fn resume_and_detach_live_kernel(&self) -> (Result<(), DbgEngError>, Result<(), DbgEngError>) {
let resumed = self.quit_and_detach_target();
let detached = unsafe { self.client.EndSession(DEBUG_END_PASSIVE) }
.map_err(DbgEngError::OperationFailed);
(resumed, detached)
}
fn quit_and_detach_target(&self) -> Result<(), DbgEngError> {
self.refuse_without_a_debuggee()?;
self.clear_all_breakpoints()?;
self.spend_pending_break_ins();
self.execute_command("qd")?;
require_detached(self.has_target()?)
}
const DRAIN_RESUME_MS: u32 = 500;
const DRAIN_ATTEMPTS: usize = 5;
const DRAIN_FREE_RUNS: usize = 2;
const DRAIN_BUDGET: Duration = Duration::from_secs(4);
fn spend_pending_break_ins(&self) {
let mut drain = match unsafe { self.control.GetNumberProcessors() } {
Ok(processors) => Drain::for_processors(processors),
Err(_) => Drain::default(),
};
let started = Instant::now();
while drain.resume_again() && started.elapsed() < Self::DRAIN_BUDGET {
match self.execute_and_wait("g", Self::DRAIN_RESUME_MS) {
Ok(run) => drain.saw(Resumed::of(&run)),
Err(_) => break,
}
}
}
fn clear_all_breakpoints(&self) -> Result<(), DbgEngError> {
let count = unsafe { self.control.GetNumberBreakpoints() }
.map_err(DbgEngError::BreakpointFailed)?;
for _ in 0..count {
let breakpoint = std::mem::ManuallyDrop::new(
unsafe { self.control.GetBreakpointByIndex2(0) }
.map_err(DbgEngError::BreakpointFailed)?,
);
unsafe { self.control.RemoveBreakpoint2(&*breakpoint) }
.map_err(DbgEngError::BreakpointFailed)?;
}
Ok(())
}
fn detach_attached_processes(&self) -> Result<(), DbgEngError> {
let attached = std::mem::take(
&mut *self
.state
.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_key(&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)
}
pub 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(self.deferred_inputs.get_mut()));
return;
}
if self.is_live_kernel() {
let (_, detached) = self.resume_and_detach_live_kernel();
if detached.is_ok() {
self.forget_pending_opens();
}
return;
}
let _ = self.detach_attached_processes();
let ended = unsafe { self.client.EndSession(DEBUG_END_PASSIVE) };
if ended.is_ok() {
self.forget_pending_opens();
}
}
}
enum WaitKind<'a> {
Live(Registered<'a>),
KernelBreakIn,
KernelAnnouncement(kernel_attach::AnnouncementGuard<'a>),
}
struct Registered<'a> {
engine: &'a DebugEngine,
id: ArrivalId,
}
impl Drop for Registered<'_> {
fn drop(&mut self) {
self.engine
.state
.arrivals
.lock()
.unwrap_or_else(|e| e.into_inner())
.forget(self.id);
}
}
#[derive(Debug)]
enum Arrival {
Launched(Option<Vec<(u32, u32)>>),
Attached(u32),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Presence {
Arrived,
Listed,
Absent,
Unknown,
}
#[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<'a>,
}
impl<'a> PendingTarget<'a> {
fn new(engine: &'a DebugEngine, kind: WaitKind<'a>) -> Self {
Self { engine, kind }
}
fn live(engine: &'a DebugEngine, arrival: Arrival) -> Self {
let id = engine
.state
.arrivals
.lock()
.unwrap_or_else(|e| e.into_inner())
.register(arrival);
Self::new(engine, WaitKind::Live(Registered { engine, id }))
}
pub fn wait(self) -> Result<(), DbgEngError> {
match &self.kind {
WaitKind::Live(registered) => self.engine.wait_for_live_target(registered),
WaitKind::KernelBreakIn => self.engine.wait_for_kernel_break_in(),
WaitKind::KernelAnnouncement(observer) => observer.wait(),
}
}
}
#[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(())
}
}
fn breakpoint_info(
breakpoint: &IDebugBreakpoint2,
location: &str,
) -> Result<BreakpointInfo, DbgEngError> {
let id = unsafe { breakpoint.GetId() }.map_err(|source| DbgEngError::Context {
operation: format!("reading the id of breakpoint {location}"),
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);
let data = (kind == BreakpointKind::Data)
.then(|| {
let mut size = 0u32;
let mut access = 0u32;
unsafe { breakpoint.GetDataParameters(&mut size, &mut access) }
.ok()
.map(|()| DataWatch {
access: DataAccess::from_engine(access),
size,
})
})
.flatten();
Ok(BreakpointInfo {
id,
kind,
address,
expression,
command,
thread,
data,
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),
})
}
struct ScopedBreakpoint<'a> {
control: &'a IDebugControl4,
breakpoint: std::mem::ManuallyDrop<IDebugBreakpoint2>,
keep: bool,
}
impl<'a> ScopedBreakpoint<'a> {
fn new(engine: &'a DebugEngine, kind: u32) -> Result<Self, DbgEngError> {
let breakpoint = unsafe { engine.control.AddBreakpoint2(kind, DEBUG_ANY_ID) }
.map_err(DbgEngError::BreakpointFailed)?;
Ok(Self {
control: &engine.control,
breakpoint: std::mem::ManuallyDrop::new(breakpoint),
keep: false,
})
}
fn at(engine: &'a DebugEngine, address: u64) -> Result<Self, DbgEngError> {
let scoped = Self::new(engine, DEBUG_BREAKPOINT_CODE)?;
unsafe {
scoped
.breakpoint
.SetOffset(address)
.map_err(DbgEngError::BreakpointFailed)?;
scoped
.breakpoint
.AddFlags(DEBUG_BREAKPOINT_ENABLED)
.map_err(DbgEngError::BreakpointFailed)?;
}
Ok(scoped)
}
fn keep(mut self) {
self.keep = true;
}
}
impl Drop for ScopedBreakpoint<'_> {
fn drop(&mut self) {
if !self.keep {
unsafe {
let _ = self.control.RemoveBreakpoint2(&*self.breakpoint);
}
}
}
}
#[cfg(test)]
mod tests {
use windows::Win32::System::Diagnostics::Debug::Extensions::{
DEBUG_CLASS_UNINITIALIZED, DEBUG_CLASS_USER_WINDOWS, DEBUG_KERNEL_CONNECTION,
DEBUG_KERNEL_DUMP, DEBUG_KERNEL_FULL_DUMP, DEBUG_KERNEL_LOCAL, 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_USER_WINDOWS_PROCESS, DEBUG_USER_WINDOWS_SMALL_DUMP, DEBUG_VALUE_0,
DEBUG_VALUE_INVALID, DEBUG_VALUE_TYPES,
};
use super::*;
#[test]
fn test_a_kernel_dump_is_a_kernel_target_and_not_a_live_one() {
let kind = |class, qualifier| DebuggeeType { class, qualifier };
let live = kind(DEBUG_CLASS_KERNEL, DEBUG_KERNEL_CONNECTION);
assert!(live.is_kernel() && live.is_live_kernel());
let local = kind(DEBUG_CLASS_KERNEL, DEBUG_KERNEL_LOCAL);
assert!(local.is_kernel() && local.is_live_kernel());
for qualifier in [
DEBUG_KERNEL_SMALL_DUMP,
DEBUG_KERNEL_DUMP,
DEBUG_KERNEL_FULL_DUMP,
] {
let dump = kind(DEBUG_CLASS_KERNEL, qualifier);
assert!(dump.is_kernel(), "{qualifier} is a kernel target");
assert!(!dump.is_live_kernel(), "{qualifier} is a dump, not a link");
}
for qualifier in [DEBUG_USER_WINDOWS_PROCESS, DEBUG_USER_WINDOWS_SMALL_DUMP] {
let user = kind(DEBUG_CLASS_USER_WINDOWS, qualifier);
assert!(!user.is_kernel() && !user.is_live_kernel());
}
let nothing = kind(DEBUG_CLASS_UNINITIALIZED, 0);
assert!(!nothing.is_kernel() && !nothing.is_live_kernel());
assert_ne!(live, kind(DEBUG_CLASS_KERNEL, DEBUG_KERNEL_FULL_DUMP));
assert_ne!(
kind(DEBUG_CLASS_USER_WINDOWS, DEBUG_USER_WINDOWS_PROCESS),
kind(DEBUG_CLASS_USER_WINDOWS, DEBUG_USER_WINDOWS_SMALL_DUMP)
);
}
#[test]
fn test_quit_must_release_the_target_before_reporting_resume() {
assert!(require_detached(false).is_ok());
assert!(matches!(
require_detached(true),
Err(DbgEngError::IncompleteKernelDetach)
));
}
fn ran(cut_short: Option<Interruption>, target_gone: bool) -> CommandRun {
CommandRun {
output: String::new(),
cut_short,
target_gone,
}
}
fn drain_over(resumes: &[Resumed]) -> Drain {
let mut drain = Drain::default();
let mut next = resumes.iter();
while drain.resume_again() {
match next.next() {
Some(resumed) => drain.saw(*resumed),
None => panic!(
"the drain asked for resume {} and the script had none",
drain.spent + 1
),
}
}
drain
}
#[test]
fn test_one_free_resume_does_not_end_the_break_in_drain() {
let mut drain = Drain::default();
drain.saw(Resumed::OnABreak);
drain.saw(Resumed::Freely);
assert!(
drain.resume_again(),
"stopping on a single free resume is the version that froze the guest"
);
drain.saw(Resumed::Freely);
assert!(!drain.resume_again(), "two in a row is what ends it");
assert_eq!(drain.delivered, 1, "one break-in was spent, not three");
}
#[test]
fn test_a_break_in_between_two_free_resumes_restarts_the_count() {
let drain = drain_over(&[
Resumed::Freely,
Resumed::OnABreak,
Resumed::Freely,
Resumed::Freely,
]);
assert_eq!(drain.spent, 4);
assert_eq!(drain.delivered, 1);
}
#[test]
fn test_a_drain_outlasts_one_stop_per_processor() {
let feed = |processors: u32, resumes: &[Resumed]| {
let mut drain = Drain::for_processors(processors);
for resumed in resumes {
assert!(
drain.resume_again(),
"{processors} processors: the drain gave up after {} of {} resumes with the \
script still owing",
drain.spent,
drain.attempts
);
drain.saw(*resumed);
}
drain
};
for processors in [1u32, 2, 4, 5, 8, 64] {
let owed = usize::try_from(processors).unwrap() - 1;
let mut script = vec![Resumed::OnABreak; owed];
script.push(Resumed::Freely);
let part_way = feed(processors, &script);
assert!(
part_way.resume_again(),
"{processors} processors owe {owed} stops, and one free run after them is not \
evidence the last was delivered"
);
script.push(Resumed::Freely);
let drained = feed(processors, &script);
assert_eq!(drained.delivered, owed, "every owed stop is spent");
assert!(
!drained.resume_again(),
"two consecutive free runs end it, whatever the processor count"
);
}
}
#[test]
fn test_a_small_target_still_gets_the_measured_floor() {
for processors in [0u32, 1, 2, 3] {
assert_eq!(
Drain::for_processors(processors).attempts,
DebugEngine::DRAIN_ATTEMPTS,
"{processors} processors must not shrink the drain below its floor"
);
}
assert_eq!(
Drain::for_processors(4).attempts,
DebugEngine::DRAIN_ATTEMPTS + 1,
"and above the floor it is one per processor plus the free runs"
);
}
#[test]
fn test_a_target_that_breaks_every_time_still_ends_the_drain() {
let drain = drain_over(&[Resumed::OnABreak; DebugEngine::DRAIN_ATTEMPTS]);
assert_eq!(drain.spent, DebugEngine::DRAIN_ATTEMPTS);
assert_eq!(drain.delivered, DebugEngine::DRAIN_ATTEMPTS);
}
#[test]
fn test_a_host_interrupt_is_not_counted_as_a_free_resume() {
let mut drain = Drain::default();
drain.saw(Resumed::Freely);
drain.saw(Resumed::OnRequest);
assert!(
drain.resume_again(),
"an interrupt and a deadline are not two free runs"
);
assert_eq!(drain.free_runs, 1, "and it does not reset the count either");
assert_eq!(drain.delivered, 0, "nor is it a break-in delivered");
}
#[test]
fn test_a_target_that_goes_ends_the_break_in_drain() {
let drain = drain_over(&[Resumed::Gone]);
assert_eq!(drain.spent, 1);
assert_eq!(drain.delivered, 0);
}
#[test]
fn test_a_resume_that_lost_its_target_is_read_as_gone_however_it_ended() {
assert_eq!(Resumed::of(&ran(None, true)), Resumed::Gone);
assert_eq!(
Resumed::of(&ran(Some(Interruption::Deadline { after_ms: 500 }), true)),
Resumed::Gone,
"a deadline on a run whose target went is not evidence the target ran freely"
);
assert_eq!(
Resumed::of(&ran(Some(Interruption::Deadline { after_ms: 500 }), false)),
Resumed::Freely
);
assert_eq!(
Resumed::of(&ran(Some(Interruption::OnRequest), false)),
Resumed::OnRequest
);
assert_eq!(Resumed::of(&ran(None, false)), Resumed::OnABreak);
}
#[test]
#[cfg(not(miri))]
fn test_exit_watchdog_reports_a_deadline_not_an_observed_stop() {
let _debuggee = one_debuggee();
let e = DebugEngine::new();
e.launch_process("ping.exe -n 30 127.0.0.1")
.expect("launch failed");
unsafe { e.control.SetExecutionStatus(DEBUG_STATUS_GO) }.unwrap();
{
let operation = e.begin_operation();
assert_eq!(
e.pump(Bound::WatchdogExit(100), &operation).unwrap(),
WaitOutcome::Deadline
);
}
e.end_session().expect("test process cleanup failed");
}
#[test]
#[cfg(not(miri))]
fn test_quit_detach_refuses_an_engine_without_a_target() {
let _debuggee = one_debuggee();
let engine = DebugEngine::new();
assert!(matches!(
engine.quit_and_detach_target(),
Err(DbgEngError::NoDebuggee)
));
}
#[test]
#[cfg(not(miri))]
fn test_quit_detach_removes_breakpoints_and_leaves_an_attached_process_alive() {
let _debuggee = one_debuggee();
let mut target = a_process_to_attach_to();
let pid = target.id();
let result = std::panic::catch_unwind(|| {
let engine = DebugEngine::new();
engine.attach_process(pid).expect("attach failed");
let pc = engine
.instruction_pointer()
.expect("no instruction pointer");
for _ in 0..2 {
engine
.set_breakpoint(&BreakpointSpec::code(BreakpointAt::Address(pc)))
.expect("set breakpoint failed");
}
assert_eq!(engine.breakpoints().expect("list failed").len(), 2);
engine.clear_all_breakpoints().expect("clear failed");
assert!(engine.breakpoints().expect("list failed").is_empty());
engine
.set_breakpoint(&BreakpointSpec::code(BreakpointAt::Address(pc)))
.expect("set breakpoint failed");
engine.quit_and_detach_target().expect("quit failed");
assert!(!engine.has_target().expect("status failed"));
assert_eq!(exit_code_of(pid), Some(STILL_RUNNING));
engine.end_session().expect("cleanup failed");
assert_eq!(exit_code_of(pid), Some(STILL_RUNNING));
});
let _ = target.kill();
let _ = target.wait();
if let Err(panic) = result {
std::panic::resume_unwind(panic);
}
}
#[test]
#[cfg_attr(
miri,
ignore = "decodes through iced-x86; see MIRI AND THE DECODER above"
)]
fn test_a_standalone_decode_reports_only_the_bytes_it_consumed() {
let nop = decode_instruction(&[0x90, 0xcc], 0x1000, InstructionSet::Amd64);
assert_eq!(nop.mnemonic, "nop");
assert_eq!(nop.bytes, "90");
let int3 = decode_instruction(&[0xcc], 0x1001, InstructionSet::Amd64);
assert_eq!(int3.mnemonic, "int3");
let load = decode_instruction(
&[0x48, 0x8b, 0x05, 0x00, 0x00, 0x00, 0x00, 0x90],
0x1000,
InstructionSet::Amd64,
);
assert_eq!(load.mnemonic, "mov");
assert_eq!(load.bytes, "488b0500000000");
}
#[test]
fn test_a_standalone_a64_decode_reports_one_word() {
let stp = decode_instruction(
&[0xa9, 0xbf, 0x7b, 0xfd, 0xd5, 0x03, 0x20, 0x1f],
0x1000,
InstructionSet::Arm64,
);
assert_eq!(stp.mnemonic, "stp");
assert_eq!(stp.bytes, "a9bf7bfd");
let unknown = decode_instruction(&[0x0f, 0x0b], 0x1000, InstructionSet::Other(0x1234));
assert_eq!(unknown.bytes, "0f0b");
assert_eq!(unknown.flow, Flow::Unknown);
}
#[test]
fn test_an_arrival_is_delivered_to_one_open_and_claimed() {
let none = HashSet::new();
let mut arrivals = Arrivals::default();
let first = arrivals.register(Arrival::Launched(Some(Vec::new())));
let second = arrivals.register(Arrival::Launched(Some(Vec::new())));
arrivals.deliver((0, 100), &none);
assert_eq!(
arrivals.presence(first, &[(0, 100)], &none),
Presence::Arrived,
"the first-registered open did not get the first arrival"
);
assert_ne!(
arrivals.presence(second, &[(0, 100)], &none),
Presence::Arrived,
"both launches were given one arrival, which is the ambiguity this removes"
);
arrivals.deliver((0, 100), &none);
assert_ne!(
arrivals.presence(second, &[(0, 100)], &none),
Presence::Arrived,
"a repeat stop on an already-delivered process was given to the next open in line"
);
arrivals.deliver((1, 200), &none);
assert_eq!(
arrivals.presence(second, &[(0, 100), (1, 200)], &none),
Presence::Arrived,
"the second launch was not given the arrival nobody had claimed"
);
arrivals.forget(first);
assert_eq!(
arrivals.presence(first, &[(0, 100)], &none),
Presence::Absent,
"a forgotten open still answered about a process"
);
}
#[test]
fn test_a_claim_stops_excluding_once_its_process_leaves() {
let none = HashSet::new();
let mut arrivals = Arrivals::default();
let held_open = arrivals.register(Arrival::Launched(Some(Vec::new())));
arrivals.deliver((0, 100), &none);
assert_eq!(
arrivals.presence(held_open, &[(0, 100)], &none),
Presence::Arrived,
"the launch was not given its process, so nothing here is under test"
);
arrivals.forget_departed(&[]);
let reattach = arrivals.register(Arrival::Attached(100));
arrivals.deliver((0, 100), &none);
assert_eq!(
arrivals.presence(reattach, &[(0, 100)], &none),
Presence::Arrived,
"the reattach was refused its own stop as another open's claim, so its wait() times \
out on a target sitting in front of it"
);
assert_ne!(
arrivals.presence(held_open, &[(0, 100)], &none),
Presence::Arrived,
"an open whose target left the session still reports it as arrived"
);
}
#[test]
fn test_a_departed_claim_does_not_reopen_the_open_that_held_it() {
let none = HashSet::new();
let mut arrivals = Arrivals::default();
let alone = arrivals.register(Arrival::Attached(100));
arrivals.deliver((0, 100), &none);
arrivals.forget_departed(&[]);
assert_eq!(
arrivals.presence(alone, &[(0, 100)], &none),
Presence::Absent,
"an open that finished reported a reattached process as its own still to come, so its \
wait() pumps for an initial break that has already happened"
);
let mut arrivals = Arrivals::default();
let finished = arrivals.register(Arrival::Attached(100));
arrivals.deliver((0, 100), &none);
assert_eq!(
arrivals.presence(finished, &[(0, 100)], &none),
Presence::Arrived,
"the attach was not given its process, so nothing here is under test"
);
arrivals.forget_departed(&[]);
let reattach = arrivals.register(Arrival::Attached(100));
arrivals.deliver((0, 100), &none);
assert_eq!(
arrivals.presence(reattach, &[(0, 100)], &none),
Presence::Arrived,
"the reattach's own stop went to the open whose process had left, so its wait() times \
out on a target sitting in front of it"
);
assert_eq!(
arrivals.presence(finished, &[(0, 100)], &none),
Presence::Absent,
"an open that finished and whose target then left took somebody else's process rather \
than staying finished"
);
}
#[test]
fn test_a_finished_attach_does_not_keep_a_launch_from_a_reused_pid() {
let none = HashSet::new();
let mut arrivals = Arrivals::default();
let attach = arrivals.register(Arrival::Attached(100));
arrivals.deliver((0, 100), &none);
arrivals.forget_departed(&[]);
let launch = arrivals.register(Arrival::Launched(Some(Vec::new())));
arrivals.deliver((1, 100), &none);
assert_eq!(
arrivals.presence(launch, &[(1, 100)], &none),
Presence::Arrived,
"the launch was refused its own stop by an attach that is finished with the pid, so \
nobody claimed it and the guard times out on a target in front of it"
);
assert_eq!(
arrivals.presence(attach, &[(1, 100)], &none),
Presence::Absent,
"the finished attach took a process it did not attach to"
);
}
#[test]
fn test_an_arrived_claim_is_read_against_what_the_session_holds() {
let none = HashSet::new();
let mut arrivals = Arrivals::default();
let open = arrivals.register(Arrival::Attached(100));
arrivals.deliver((0, 100), &none);
assert_eq!(
arrivals.presence(open, &[(0, 100), (1, 200)], &none),
Presence::Arrived,
"the attach was not given its process, so nothing here is under test"
);
assert_eq!(
arrivals.presence(open, &[(1, 200)], &none),
Presence::Absent,
"a claim on a process that has left the session still reported it as arrived, so the \
guard's wait() returns Ok for a target the caller cannot read"
);
}
#[test]
fn test_an_abandoned_launch_keeps_its_place_and_an_attach_does_not() {
let none = HashSet::new();
let mut arrivals = Arrivals::default();
let abandoned = arrivals.register(Arrival::Launched(Some(Vec::new())));
arrivals.forget(abandoned);
let mine = arrivals.register(Arrival::Launched(Some(Vec::new())));
arrivals.deliver((0, 100), &none);
assert_ne!(
arrivals.presence(mine, &[(0, 100)], &none),
Presence::Arrived,
"the next launch was given the abandoned launch's process, so its wait() returns for a \
target it never asked for"
);
arrivals.deliver((1, 200), &none);
assert_eq!(
arrivals.presence(mine, &[(0, 100), (1, 200)], &none),
Presence::Arrived,
"the next launch was refused a process nobody else was entitled to"
);
let attach = arrivals.register(Arrival::Attached(300));
arrivals.forget(attach);
assert_eq!(
arrivals.presence(attach, &[(2, 300)], &none),
Presence::Absent,
"an abandoned attach was kept in the register, where the engine's own attachment record \
is what covers it"
);
let timed_out = arrivals.register(Arrival::Launched(Some(Vec::new())));
arrivals.discard(timed_out);
let after = arrivals.register(Arrival::Launched(Some(Vec::new())));
arrivals.deliver((3, 400), &none);
assert_eq!(
arrivals.presence(after, &[(3, 400)], &none),
Presence::Arrived,
"a launch whose wait timed out kept its place in the queue and took the next launch's \
process"
);
}
#[test]
fn test_a_claim_is_inherited_by_every_open_still_waiting() {
let none = HashSet::new();
let mut arrivals = Arrivals::default();
let first = arrivals.register(Arrival::Launched(Some(Vec::new())));
let second = arrivals.register(Arrival::Launched(Some(Vec::new())));
let third = arrivals.register(Arrival::Launched(Some(Vec::new())));
arrivals.deliver((0, 100), &none);
arrivals.forget(first);
arrivals.deliver((1, 200), &none);
arrivals.forget(second);
let held = [(0, 100), (1, 200)];
arrivals.deliver((0, 100), &none);
arrivals.deliver((1, 200), &none);
assert_ne!(
arrivals.presence(third, &held, &none),
Presence::Arrived,
"the third launch was given a process one of its predecessors had already been given, \
so its wait() returns Ok for a target that never stopped"
);
let held = [(0, 100), (1, 200), (2, 300)];
arrivals.deliver((2, 300), &none);
assert_eq!(
arrivals.presence(third, &held, &none),
Presence::Arrived,
"the third launch was refused a process nobody else had been given, so an inheritance \
is refusing arrivals wholesale rather than the ones it names"
);
}
#[test]
fn test_a_claim_outlives_the_open_that_made_it() {
let none = HashSet::new();
let mut arrivals = Arrivals::default();
let first = arrivals.register(Arrival::Launched(Some(Vec::new())));
let second = arrivals.register(Arrival::Launched(Some(Vec::new())));
arrivals.deliver((0, 100), &none);
arrivals.forget(first);
arrivals.deliver((0, 100), &none);
assert_ne!(
arrivals.presence(second, &[(0, 100)], &none),
Presence::Arrived,
"a process the first launch had been given was handed to the second once the first \
guard was gone, so its wait() returns Ok for a target that never stopped"
);
arrivals.forget(second);
let third = arrivals.register(Arrival::Launched(Some(Vec::new())));
arrivals.deliver((0, 100), &none);
assert_eq!(
arrivals.presence(third, &[(0, 100)], &none),
Presence::Arrived,
"an open registered after everything else had finished inherited a claim that should \
have gone with them, which is the lifecycle this shape exists not to have"
);
}
#[test]
fn test_a_pending_attach_keeps_its_process_from_a_pending_launch() {
let none = HashSet::new();
let mut arrivals = Arrivals::default();
let launch = arrivals.register(Arrival::Launched(Some(Vec::new())));
let attach = arrivals.register(Arrival::Attached(200));
arrivals.deliver((0, 200), &none);
assert_ne!(
arrivals.presence(launch, &[(0, 200)], &none),
Presence::Arrived,
"the launch claimed the process a pending attach had named"
);
assert_eq!(
arrivals.presence(attach, &[(0, 200)], &none),
Presence::Arrived,
"the attach did not get its own process"
);
}
#[test]
fn test_the_engine_does_not_cross_threads_and_the_handle_does() {
struct Ask<T>(std::marker::PhantomData<T>);
trait Cannot {
const CAN: bool = false;
}
impl<T> Cannot for Ask<T> {}
impl<T: Send> Ask<T> {
const CAN: bool = true;
}
struct AskShared<T>(std::marker::PhantomData<T>);
trait CannotShare {
const CAN: bool = false;
}
impl<T> CannotShare for AskShared<T> {}
impl<T: Sync> AskShared<T> {
const CAN: bool = true;
}
assert!(
!<Ask<DebugEngine>>::CAN,
"DebugEngine is Send again, so an engine can be moved to a thread that is not the one \
it is affine to"
);
assert!(
!<AskShared<DebugEngine>>::CAN,
"DebugEngine is Sync again, so two threads can make concurrent &self calls into an \
engine that cannot take them"
);
assert!(
<Ask<InterruptHandle>>::CAN,
"InterruptHandle is no longer Send"
);
assert!(
<AskShared<InterruptHandle>>::CAN,
"InterruptHandle is no longer Sync"
);
}
#[test]
fn test_a_break_request_names_the_operation_it_is_for() {
let mut scope = BreakScope::default();
assert_eq!(scope.innermost(), None, "an idle scope named an operation");
let first = scope.begin();
assert_eq!(scope.innermost(), Some(first));
scope.record(first);
assert!(scope.take(first), "the operation asked for did not see it");
assert!(
!scope.take(first),
"a request was taken twice, so it could be charged to two operations"
);
scope.record(first);
scope.end(first);
let second = scope.begin();
assert!(
!scope.take(second),
"a request filed against {first:?} was answered for {second:?}"
);
scope.end(second);
assert!(
scope.running.is_empty() && scope.asked.is_empty(),
"closing every operation left {scope:?} behind"
);
}
#[test]
fn test_a_nested_operation_does_not_take_its_parents_request() {
let mut scope = BreakScope::default();
let outer = scope.begin();
scope.record(outer);
let inner = scope.begin();
assert_eq!(
scope.innermost(),
Some(inner),
"a break raised now would stop the inner operation, so it must be filed against it"
);
assert!(
!scope.take(inner),
"the inner operation was handed the outer one's request"
);
scope.end(inner);
assert_eq!(
scope.innermost(),
Some(outer),
"the outer operation did not resume when the inner one ended"
);
assert!(
scope.take(outer),
"the outer operation's request did not survive an inner operation"
);
scope.end(outer);
}
#[test]
#[cfg(not(miri))]
fn test_an_operations_request_dies_with_it() {
let _debuggee = one_debuggee();
let e = DebugEngine::new();
{
let operation = e.begin_operation();
e.state
.breaks
.lock()
.unwrap_or_else(|err| err.into_inner())
.record(operation.id);
}
let scope = e.state.breaks.lock().unwrap_or_else(|err| err.into_inner());
assert!(
scope.running.is_empty() && scope.asked.is_empty(),
"an operation that never read its request left {scope:?} for the next one"
);
}
#[test]
#[cfg(not(miri))]
#[ignore = "needs a live debuggee; run manually with --ignored"]
fn test_a_discarded_request_drains_the_engines_pending_break() {
let e = DebugEngine::new();
e.launch_process("cmd.exe /c exit").expect("launch failed");
let filed = e.interrupt_handle().interrupt().expect("interrupt failed");
assert_eq!(
filed,
BreakRequest::NothingRunning,
"an engine between operations filed the request as {filed:?}"
);
assert!(
e.interrupted().expect("GetInterrupt failed"),
"the engine holds no pending request, so this test cannot tell a drain from a no-op"
);
{
let operation = e.begin_operation();
let filed = e.interrupt_handle().interrupt().expect("interrupt failed");
assert_eq!(
filed,
BreakRequest::Raised {
operation: operation.id
},
"the request was not filed against the operation that was running"
);
}
assert!(
!e.interrupted().expect("GetInterrupt failed"),
"an operation closed on a request nobody read and left the engine's own break pending, \
so the next operation is stopped by it with nothing to say why"
);
let _ = e.end_session();
}
#[cfg(not(miri))]
#[test]
#[ignore = "needs a live debuggee; run manually with --ignored"]
fn test_a_bounded_read_costs_little_against_an_unbounded_one() {
let e = DebugEngine::new();
e.launch_process("cmd.exe /c ping -n 30 127.0.0.1")
.expect("launch failed");
let ntdll = e
.modules()
.expect("modules failed")
.into_iter()
.find(|module| module.name.to_ascii_lowercase().contains("ntdll"))
.expect("the target has no ntdll to read");
let size = (ntdll.size as usize).min(1024 * 1024);
assert!(
size > 16 * READ_CHUNK_BYTES,
"ntdll is {size} bytes here, which is too few chunks to measure"
);
let _ = e
.read_memory(ntdll.base, size)
.expect("ntdll's image did not read whole");
let at = Instant::now();
let whole = e
.read_memory(ntdll.base, size)
.expect("unbounded read failed");
let unbounded = at.elapsed();
let at = Instant::now();
let pieces = e
.read_memory_bounded(ntdll.base, size, 60_000)
.expect("bounded read failed");
let bounded = at.elapsed();
assert_eq!(
pieces.cut_short, None,
"a read that finished well inside a 60s budget reported itself cut short"
);
assert_eq!(
pieces.bytes, whole,
"reading in pieces did not produce the bytes one call does"
);
println!(
"{size} bytes in {} chunks: one call {unbounded:?}, chunked {bounded:?}",
size.div_ceil(READ_CHUNK_BYTES)
);
let _ = e.end_session();
}
fn recording_reader(
calls: &RefCell<Vec<(u64, usize)>>,
) -> impl FnMut(u64, &mut [u8]) -> Result<usize, DbgEngError> + '_ {
move |at, into| {
calls.borrow_mut().push((at, into.len()));
for (offset, byte) in into.iter_mut().enumerate() {
*byte = at.wrapping_add(offset as u64) as u8;
}
Ok(into.len())
}
}
fn expected_bytes(address: u64, size: usize) -> Vec<u8> {
(0..size)
.map(|offset| address.wrapping_add(offset as u64) as u8)
.collect()
}
#[test]
fn test_a_bounded_read_stops_between_chunks_and_keeps_what_it_had() {
const BASE: u64 = 0x1_0000;
let calls = RefCell::new(Vec::new());
let polls = RefCell::new(0usize);
let read = read_chunked(
BASE,
4 * READ_CHUNK_BYTES,
|| {
let mut polls = polls.borrow_mut();
*polls += 1;
(*polls == 3).then_some(Interruption::Deadline { after_ms: 250 })
},
recording_reader(&calls),
)
.expect("a read cut short reports its stop rather than failing");
assert_eq!(
read.cut_short,
Some(Interruption::Deadline { after_ms: 250 }),
"a read that stopped on its deadline did not say so, so its caller reads a short \
buffer as the range it asked for"
);
assert_eq!(
read.bytes,
expected_bytes(BASE, 2 * READ_CHUNK_BYTES),
"the bytes read before the stop are not the prefix of the range asked for"
);
assert_eq!(
*calls.borrow(),
vec![
(BASE, READ_CHUNK_BYTES),
(BASE + READ_CHUNK_BYTES as u64, READ_CHUNK_BYTES)
],
"the stop did not land between two chunks"
);
}
#[test]
fn test_a_read_cut_short_holds_what_it_read_not_what_it_asked_for() {
const HUGE: usize = 64 * 1024 * 1024;
let calls = RefCell::new(Vec::new());
let polls = RefCell::new(0usize);
let read = read_chunked(
0x6_0000,
HUGE,
|| {
let mut polls = polls.borrow_mut();
*polls += 1;
(*polls == 3).then_some(Interruption::Deadline { after_ms: 1 })
},
recording_reader(&calls),
)
.expect("read failed");
assert_eq!(read.bytes.len(), 2 * READ_CHUNK_BYTES);
assert!(
read.bytes.capacity() < HUGE / 8,
"a read cut short after {} bytes is holding a buffer of {} — sized from the request, \
which is also an allocation made before the first deadline check",
read.bytes.len(),
read.bytes.capacity()
);
}
#[test]
fn test_a_buffer_the_host_cannot_give_is_an_error_not_an_abort() {
let error = try_buffer(usize::MAX, 0x7_0000, 1024)
.expect_err("a buffer larger than the address space was handed back as a buffer");
match error {
DbgEngError::OutOfMemory {
address,
requested,
read,
} => {
assert_eq!(
address, 0x7_0000,
"the error did not name the read it was for"
);
assert_eq!(requested, 1024);
assert_eq!(
read, 0,
"nothing had been read when the buffer was asked for"
);
}
other => panic!("a buffer that could not be reserved was reported as {other:?}"),
}
}
#[test]
fn test_a_read_that_finished_is_never_reported_cut_short() {
const BASE: u64 = 0x2_0000;
const SIZE: usize = 3 * READ_CHUNK_BYTES;
let calls = RefCell::new(Vec::new());
let read = read_chunked(
BASE,
SIZE,
{
let polls = RefCell::new(0usize);
move || {
let mut polls = polls.borrow_mut();
*polls += 1;
(*polls > 3).then_some(Interruption::OnRequest)
}
},
recording_reader(&calls),
)
.expect("read failed");
assert_eq!(
read.cut_short, None,
"a complete read reported itself cut short"
);
assert_eq!(read.bytes, expected_bytes(BASE, SIZE));
}
#[test]
fn test_a_read_inside_one_page_is_one_transfer_the_bound_cannot_reach() {
let calls = RefCell::new(Vec::new());
let read =
read_chunked(0x3_0010, 16, || None, recording_reader(&calls)).expect("read failed");
assert_eq!(read.bytes.len(), 16);
assert_eq!(
*calls.borrow(),
vec![(0x3_0010, 16)],
"a sub-page read was split, which costs a call and bounds nothing"
);
}
#[test]
fn test_every_chunk_lies_within_one_page() {
let page = READ_CHUNK_BYTES as u64;
for base in [0u64, 1, 8, page - 1, page, page + 1, 3 * page - 8] {
for size in [1usize, 16, READ_CHUNK_BYTES, 5 * READ_CHUNK_BYTES + 3] {
let address = 0x10_0000 + base;
let mut done = 0usize;
let mut chunks = Vec::new();
while done < size {
let len = read_chunk_len(address, done, size);
assert!(
len > 0,
"a zero-length chunk would never finish {address:#x}+{size}"
);
chunks.push((address + done as u64, len));
done += len;
}
assert_eq!(
done, size,
"the chunks of {address:#x}+{size} do not cover it"
);
for (index, (at, len)) in chunks.iter().enumerate() {
assert!(
at / page == (at + *len as u64 - 1) / page,
"chunk {index} of {address:#x}+{size} straddles a page: {at:#x}+{len}"
);
assert!(*len <= READ_CHUNK_BYTES);
}
let (_, first) = chunks[0];
assert_eq!(
first,
size.min((page - address % page) as usize),
"the first chunk of {address:#x}+{size} was not trimmed to the page boundary"
);
}
}
}
#[test]
fn test_a_short_chunk_is_the_short_read_a_single_call_would_have_reported() {
const BASE: u64 = 0x4_0000;
const SIZE: usize = 4 * READ_CHUNK_BYTES;
const GOT: usize = READ_CHUNK_BYTES / 3;
let reads = RefCell::new(0usize);
let error = read_chunked(
BASE,
SIZE,
|| None,
|_, into| {
let mut reads = reads.borrow_mut();
*reads += 1;
Ok(if *reads == 3 { GOT } else { into.len() })
},
)
.expect_err("a range the target cannot answer for is an error, not a cut-short read");
match error {
DbgEngError::ShortRead {
address,
requested,
actual,
} => {
assert_eq!(address, BASE, "the short read named a chunk, not the range");
assert_eq!(
requested, SIZE,
"the short read named a chunk's size, not the range's"
);
assert_eq!(
actual,
2 * READ_CHUNK_BYTES + GOT,
"the bytes already read were not counted"
);
}
other => panic!("a short chunk was reported as {other:?}"),
}
}
#[test]
fn test_a_first_chunk_that_fails_is_its_own_error_and_a_later_one_is_a_short_read() {
const BASE: u64 = 0x5_0000;
const SIZE: usize = 3 * READ_CHUNK_BYTES;
let fail_at = |nth: usize| {
let reads = RefCell::new(0usize);
read_chunked(
BASE,
SIZE,
|| None,
move |_, into| {
let mut reads = reads.borrow_mut();
*reads += 1;
if *reads == nth {
Err(DbgEngError::NoDebuggee)
} else {
Ok(into.len())
}
},
)
};
assert!(
matches!(fail_at(1), Err(DbgEngError::NoDebuggee)),
"a read that never got a byte swallowed the engine's reason for refusing it"
);
assert!(
matches!(
fail_at(2),
Err(DbgEngError::ShortRead {
requested: SIZE,
actual: READ_CHUNK_BYTES,
..
})
),
"a chunk that failed with bytes already read was not reported as the short read it is"
);
}
#[test]
fn test_a_range_whose_end_wraps_stops_at_the_wrap() {
let address = u64::MAX - READ_CHUNK_BYTES as u64 + 1;
let calls = RefCell::new(Vec::new());
let error = read_chunked(
address,
2 * READ_CHUNK_BYTES,
|| None,
recording_reader(&calls),
)
.expect_err("a range that wraps was read as though it did not");
assert!(
matches!(
error,
DbgEngError::ShortRead {
actual: READ_CHUNK_BYTES,
..
}
),
"a wrapping range reported {error:?} rather than the bytes it did read"
);
assert_eq!(
calls.borrow().len(),
1,
"the read carried on past the top of the address space"
);
}
#[test]
fn test_a_code_breakpoint_spec_is_armed_and_adds_rather_than_replaces() {
let spec = BreakpointSpec::code(BreakpointAt::Address(0x1000));
assert!(spec.enabled);
assert!(!spec.one_shot);
assert_eq!(spec.data, None);
assert_eq!(spec.on_existing, OnExisting::Add);
assert_eq!(spec.flags(), DEBUG_BREAKPOINT_ENABLED);
let disabled = BreakpointSpec::code(BreakpointAt::Address(0x1000)).disabled();
assert_eq!(disabled.flags(), 0);
}
#[test]
fn test_a_spec_sends_only_the_two_flags_a_client_owns() {
let spec = BreakpointSpec::code(BreakpointAt::Expression("nt!Foo".into())).one_shot();
assert_eq!(
spec.flags(),
DEBUG_BREAKPOINT_ENABLED | DEBUG_BREAKPOINT_ONE_SHOT
);
assert_eq!(spec.flags() & DEBUG_BREAKPOINT_DEFERRED, 0);
}
#[test]
fn test_a_data_spec_keeps_the_code_defaults_and_carries_its_watch() {
let watch = DataWatch {
access: DataAccess::Write,
size: 8,
};
let spec = BreakpointSpec::data(BreakpointAt::Address(0x2000), watch)
.with_command(".echo hit; gc")
.on_thread(7)
.with_pass_count(3)
.replacing_existing();
assert_eq!(spec.data, Some(watch));
assert!(spec.enabled);
assert_eq!(spec.command.as_deref(), Some(".echo hit; gc"));
assert_eq!(spec.thread, Some(7));
assert_eq!(spec.pass_count, Some(3));
assert_eq!(spec.on_existing, OnExisting::Replace);
}
#[test]
fn test_each_data_access_maps_to_its_own_engine_flags() {
assert_eq!(DataAccess::Read.to_engine(), DEBUG_BREAK_READ);
assert_eq!(DataAccess::Write.to_engine(), DEBUG_BREAK_WRITE);
assert_eq!(
DataAccess::ReadWrite.to_engine(),
DEBUG_BREAK_READ | DEBUG_BREAK_WRITE
);
assert_eq!(DataAccess::Execute.to_engine(), DEBUG_BREAK_EXECUTE);
assert_eq!(DataAccess::Io.to_engine(), DEBUG_BREAK_IO);
assert_ne!(
DataAccess::Read.to_engine(),
DataAccess::ReadWrite.to_engine()
);
}
#[test]
fn test_a_data_access_survives_the_round_trip_to_the_engine() {
for access in [
DataAccess::Read,
DataAccess::Write,
DataAccess::ReadWrite,
DataAccess::Execute,
DataAccess::Io,
] {
assert_eq!(
DataAccess::from_engine(access.to_engine()),
access,
"{access:?} did not survive"
);
}
let odd = DEBUG_BREAK_EXECUTE | DEBUG_BREAK_WRITE;
assert_eq!(DataAccess::from_engine(odd), DataAccess::Other(odd));
assert_eq!(DataAccess::Other(odd).to_engine(), odd);
}
#[test]
fn test_a_data_breakpoint_is_refused_for_a_bad_size_or_alignment() {
let watch = |size| DataWatch {
access: DataAccess::ReadWrite,
size,
};
for size in [0, 3, 5, 16] {
assert!(
BreakpointSpec::data(BreakpointAt::Address(0x1000), watch(size))
.validated()
.is_err(),
"a {size}-byte data breakpoint should be refused"
);
}
for size in [1, 2, 4, 8] {
assert!(
BreakpointSpec::data(BreakpointAt::Address(0x1000), watch(size))
.validated()
.is_ok(),
"a {size}-byte data breakpoint at an aligned address should be accepted"
);
}
assert!(
BreakpointSpec::data(BreakpointAt::Address(0x1004), watch(8))
.validated()
.is_err()
);
assert!(
BreakpointSpec::data(BreakpointAt::Expression("nt!Foo+1".into()), watch(8))
.validated()
.is_ok()
);
assert!(
BreakpointSpec::code(BreakpointAt::Address(0x1001))
.validated()
.is_ok()
);
}
#[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]
#[cfg_attr(
miri,
ignore = "decodes through iced-x86; see MIRI AND THE DECODER above"
)]
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",
InstructionSet::Amd64,
);
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",
InstructionSet::Arm64,
);
assert_eq!(arm64.bytes, "a9bf7bfd");
assert_eq!(arm64.text, "stp fp,lr,[sp,#-0x10]!");
}
#[test]
#[cfg_attr(
miri,
ignore = "decodes through iced-x86; see MIRI AND THE DECODER above"
)]
fn test_an_a64_byte_column_is_the_word_and_not_the_memory() {
let call = split_instruction(
0xfffff802_e9e5df70,
"fffff802`e9e5df70 97fffcc4 bl nt!KeBugCheck2 (fffff802`e9e5d280)",
InstructionSet::Arm64,
);
assert_eq!(
call.flow,
Flow::Call(Some(0xfffff802_e9e5d280)),
"the destination the engine itself printed: {call:?}"
);
assert_eq!(call.mnemonic, "bl");
assert_eq!(call.operands, [Operand::Target(0xfffff802_e9e5d280)]);
assert!(InstructionSet::Arm64.operands_are_read());
assert!(InstructionSet::Arm64.flow_is_read());
}
#[test]
#[cfg_attr(
miri,
ignore = "decodes through iced-x86; see MIRI AND THE DECODER above"
)]
fn test_an_a64_rendering_carries_decoded_operands_beside_it() {
let load = split_instruction(
0xfffff802_e9e5d5b8,
"fffff802`e9e5d5b8 f9426909 ldr x9,[x8,#0x4D0]",
InstructionSet::Arm64,
);
assert_eq!(
load.text, "ldr x9,[x8,#0x4D0]",
"the engine's own, verbatim"
);
assert_eq!(load.mnemonic, "ldr");
assert_eq!(load.effect, Effect::Move);
assert_eq!(load.flow, Flow::Fallthrough);
let Some(Operand::Memory(memory)) = load.operands.get(1) else {
panic!("the memory operand was not read: {load:?}");
};
assert_eq!(memory.displacement, 0x4d0);
assert_eq!(memory.base.as_deref(), Some("x8"));
assert_eq!(load.writes.len(), 1, "{load:?}");
assert_eq!(load.writes[0].full, "x9");
assert_eq!(load.reads.len(), 1, "{load:?}");
assert_eq!(load.reads[0].full, "x8");
}
fn register(name: &str, full: &str, width: u32) -> RegisterOperand {
RegisterOperand {
name: name.to_string(),
full: full.to_string(),
width,
}
}
#[test]
#[cfg_attr(
miri,
ignore = "decodes through iced-x86; see MIRI AND THE DECODER above"
)]
fn test_a_register_operand_carries_the_full_width_register_the_decoder_names() {
let decode = |bytes: &str, text: &str, set| {
split_instruction(0x1000, &format!("00001000`00000000 {bytes} {text}"), set)
};
let register = |one: &Instruction, index: usize| match &one.operands[index] {
Operand::Register(register) => {
(register.name.clone(), register.full.clone(), register.width)
}
other => panic!("expected a register operand: {other:?}"),
};
let wide = decode("4189c5", "mov r13d,eax", InstructionSet::Amd64);
assert_eq!(
register(&wide, 0),
("r13d".to_string(), "r13".to_string(), 4),
"{wide:?}"
);
assert_eq!(
register(&wide, 1),
("eax".to_string(), "rax".to_string(), 4)
);
let bytes = decode("88c4", "mov ah,al", InstructionSet::Amd64);
assert_eq!(
register(&bytes, 0),
("ah".to_string(), "rax".to_string(), 1),
"and the width is what says a copy of it is not the whole value"
);
let scalar = decode("f30f10c1", "movss xmm0,xmm1", InstructionSet::Amd64);
assert_eq!(
register(&scalar, 1),
("xmm1".to_string(), "zmm1".to_string(), 16),
"the register is sixteen bytes however many the copy moves, and it is part of a `zmm` for the same reason `eax` is part of `rax`: {scalar:?}"
);
let narrow = decode("8bc1", "mov eax,ecx", InstructionSet::X86);
assert_eq!(
register(&narrow, 0),
("eax".to_string(), "eax".to_string(), 4)
);
assert_eq!(
register(&narrow, 1),
("ecx".to_string(), "ecx".to_string(), 4)
);
}
#[test]
#[cfg_attr(
miri,
ignore = "decodes through iced-x86; see MIRI AND THE DECODER above"
)]
fn test_a_conditional_branch_carries_its_condition_and_its_signedness() {
let decode = |bytes: &str, text: &str| {
split_instruction(
0x1000,
&format!("00001000`00000000 {bytes} {text}"),
InstructionSet::Amd64,
)
};
for (bytes, text, expected) in [
("7402", "je 1004", Condition::Equal),
("7502", "jne 1004", Condition::NotEqual),
("7702", "ja 1004", Condition::UnsignedAbove),
("7302", "jae 1004", Condition::UnsignedAboveOrEqual),
("7202", "jb 1004", Condition::UnsignedBelow),
("7602", "jbe 1004", Condition::UnsignedBelowOrEqual),
("7f02", "jg 1004", Condition::SignedGreater),
("7d02", "jge 1004", Condition::SignedGreaterOrEqual),
("7c02", "jl 1004", Condition::SignedLess),
("7e02", "jle 1004", Condition::SignedLessOrEqual),
] {
let one = decode(bytes, text);
assert_ne!(
one.flow,
Flow::Unknown,
"`{text}` must decode, or the condition is about nothing: {one:?}"
);
assert_eq!(one.condition, Some(expected), "`{text}`: {one:?}");
}
let unconditional = decode("eb02", "jmp 1004");
assert_eq!(unconditional.condition, None, "{unconditional:?}");
let arithmetic = decode("83e804", "sub eax,4");
assert_eq!(arithmetic.condition, None, "{arithmetic:?}");
}
#[test]
#[cfg_attr(
miri,
ignore = "decodes through iced-x86; see MIRI AND THE DECODER above"
)]
fn test_an_instruction_carries_what_it_does_and_whether_it_set_the_flags() {
let decode = |bytes: &str, text: &str| {
split_instruction(
0x1000,
&format!("00001000`00000000 {bytes} {text}"),
InstructionSet::Amd64,
)
};
for (bytes, text, effect, flags) in [
("8bc1", "mov eax,ecx", Effect::Move, false),
("0fb6c1", "movzx eax,cl", Effect::Move, false),
("4863c1", "movsxd rax,ecx", Effect::MoveSigned, false),
("488d0c10", "lea rcx,[rax+rdx]", Effect::LoadAddress, false),
("83f804", "cmp eax,4", Effect::Compare, true),
("85c0", "test eax,eax", Effect::Test, true),
("83c004", "add eax,4", Effect::Add, true),
("83e804", "sub eax,4", Effect::Subtract, true),
("c1e802", "shr eax,2", Effect::ShiftRight, true),
("c1e002", "shl eax,2", Effect::ShiftLeft, true),
("83e00f", "and eax,0Fh", Effect::BitAnd, true),
("50", "push rax", Effect::Push, false),
("58", "pop rax", Effect::Pop, false),
("c3", "ret", Effect::Other, false),
("0fa3c1", "bt ecx,eax", Effect::Other, true),
("f7d8", "neg eax", Effect::Other, true),
] {
let one = decode(bytes, text);
assert_ne!(one.flow, Flow::Unknown, "`{text}` must decode: {one:?}");
assert_eq!(one.effect, effect, "`{text}`: {one:?}");
assert_eq!(one.writes_flags, flags, "`{text}` flags: {one:?}");
}
}
#[test]
#[cfg_attr(
miri,
ignore = "decodes through iced-x86; see MIRI AND THE DECODER above"
)]
fn test_an_instruction_says_which_registers_it_writes() {
let writes = |bytes: &str| {
let raw = hex::decode(bytes).expect("the fixture is hex");
let decoded = decode_operation(&raw, 0x1000, InstructionSet::Amd64);
assert_ne!(
decoded.flow,
Flow::Unknown,
"`{bytes}` must decode: {decoded:?}"
);
let mut full: Vec<String> = decoded.writes.iter().map(|w| w.full.clone()).collect();
full.sort();
(decoded.mnemonic.clone(), full)
};
assert_eq!(
writes("b801000000"),
("mov".to_string(), vec!["rax".to_string()])
);
assert_eq!(
writes("91"),
(
"xchg".to_string(),
vec!["rax".to_string(), "rcx".to_string()]
)
);
assert_eq!(
writes("4195"),
(
"xchg".to_string(),
vec!["r13".to_string(), "rax".to_string()]
)
);
assert_eq!(
writes("f7e1"),
(
"mul".to_string(),
vec!["rax".to_string(), "rdx".to_string()]
),
"`mul ecx` names `ecx` and writes neither of these"
);
assert_eq!(
writes("0fa2"),
(
"cpuid".to_string(),
vec![
"rax".to_string(),
"rbx".to_string(),
"rcx".to_string(),
"rdx".to_string()
]
)
);
assert_eq!(
writes("f3a5"),
(
"movsd".to_string(),
vec!["rcx".to_string(), "rdi".to_string(), "rsi".to_string()]
),
"a repeated string move walks both pointers and the count"
);
assert_eq!(
writes("0f45c1"),
("cmovne".to_string(), vec!["rax".to_string()])
);
assert_eq!(writes("3bc1"), ("cmp".to_string(), Vec::new()));
let undecoded = decode_operation(
&[0x00, 0x01, 0x02, 0x03],
0x1000,
InstructionSet::Other(0xaa64),
);
assert_eq!(undecoded.flow, Flow::Unknown);
assert!(undecoded.writes.is_empty(), "{undecoded:?}");
}
#[test]
#[cfg_attr(
miri,
ignore = "decodes through iced-x86; see MIRI AND THE DECODER above"
)]
fn test_an_instruction_says_which_registers_it_reads() {
let reads = |bytes: &str| {
let raw = hex::decode(bytes).expect("the fixture is hex");
let decoded = decode_operation(&raw, 0x1000, InstructionSet::Amd64);
assert_ne!(
decoded.flow,
Flow::Unknown,
"`{bytes}` must decode: {decoded:?}"
);
let mut full: Vec<String> = decoded.reads.iter().map(|r| r.full.clone()).collect();
full.sort();
(decoded.mnemonic.clone(), full)
};
let named = |bytes: &str| {
let raw = hex::decode(bytes).expect("the fixture is hex");
let decoded = decode_operation(&raw, 0x1000, InstructionSet::Amd64);
let mut full: Vec<String> = decoded
.operands
.iter()
.filter_map(|operand| match operand {
Operand::Register(register) => Some(register.full.clone()),
_ => None,
})
.collect();
full.sort();
full
};
assert_eq!(
reads("f7e1"),
(
"mul".to_string(),
vec!["rax".to_string(), "rcx".to_string()]
)
);
assert_eq!(named("f7e1"), vec!["rcx".to_string()]);
assert_eq!(
reads("0fb1ca"),
(
"cmpxchg".to_string(),
vec!["rax".to_string(), "rcx".to_string(), "rdx".to_string()]
)
);
assert_eq!(named("0fb1ca"), vec!["rcx".to_string(), "rdx".to_string()]);
let scasd = decode_operation(
&hex::decode("af").expect("hex"),
0x1000,
InstructionSet::Amd64,
);
assert_eq!(
scasd
.reads
.iter()
.map(|r| r.full.as_str())
.collect::<std::collections::BTreeSet<_>>(),
["rax", "rdi"].into_iter().collect()
);
assert!(
!scasd.writes.iter().any(|w| w.full == "rax"),
"scasd reads eax and writes no part of rax: {scasd:?}"
);
assert!(scasd.writes_flags, "{scasd:?}");
assert_eq!(
reads("f2ae"),
(
"scasb".to_string(),
vec!["rax".to_string(), "rcx".to_string(), "rdi".to_string()]
)
);
assert_eq!(named("f2ae"), vec!["rax".to_string()]);
assert_eq!(
reads("83790805"),
("cmp".to_string(), vec!["rcx".to_string()])
);
assert!(named("83790805").is_empty());
assert_eq!(
reads("23c1"),
(
"and".to_string(),
vec!["rax".to_string(), "rcx".to_string()]
)
);
assert_eq!(
reads("0f45c1"),
(
"cmovne".to_string(),
vec!["rax".to_string(), "rcx".to_string()]
)
);
assert_eq!(reads("b801000000"), ("mov".to_string(), Vec::new()));
let copy = decode_operation(
&hex::decode("8bc1").expect("hex"),
0x1000,
InstructionSet::Amd64,
);
assert_eq!(copy.reads.len(), 1, "{copy:?}");
assert_eq!(copy.reads[0].name, "ecx");
assert_eq!(copy.reads[0].full, "rcx");
assert_eq!(copy.reads[0].width, 4);
let undecoded = decode_operation(
&[0x00, 0x01, 0x02, 0x03],
0x1000,
InstructionSet::Other(0xaa64),
);
assert_eq!(undecoded.flow, Flow::Unknown);
assert!(undecoded.reads.is_empty(), "{undecoded:?}");
}
#[test]
#[cfg_attr(
miri,
ignore = "decodes through iced-x86; see MIRI AND THE DECODER above"
)]
fn test_privilege_is_the_decoders_answer_rather_than_a_list_of_mnemonics() {
let decode = |bytes: &str, text: &str| {
split_instruction(
0x1000,
&format!("00001000`00000000 {bytes} {text}"),
InstructionSet::Amd64,
)
};
for (bytes, text) in [
("fa", "cli"),
("fb", "sti"),
("0f06", "clts"),
("0f01f0", "lmsw ax"),
("0f01c2", "vmlaunch"),
("0f32", "rdmsr"),
("0f20d8", "mov rax,cr3"),
] {
let one = decode(bytes, text);
assert_ne!(
one.flow,
Flow::Unknown,
"`{text}` must actually decode, or `privileged` is answering about nothing: {one:?}"
);
assert!(
one.privileged,
"`{text}` requires privilege and the decoder knows it: {one:?}"
);
}
for (bytes, text) in [
("90", "nop"),
("4889d8", "mov rax,rbx"),
("c3", "ret"),
("0f31", "rdtsc"),
] {
let one = decode(bytes, text);
assert_ne!(one.flow, Flow::Unknown, "`{text}` must decode: {one:?}");
assert!(!one.privileged, "`{text}` needs no privilege: {one:?}");
}
let arm64 = split_instruction(
0x1000,
"00001000`00000000 d4000002 hvc #0",
InstructionSet::Arm64,
);
assert!(arm64.privileged, "{arm64:?}");
assert_eq!(arm64.flow, Flow::Fallthrough);
let nop = split_instruction(
0x1000,
"00001000`00000000 d503201f nop",
InstructionSet::Arm64,
);
assert!(!nop.privileged, "{nop:?}");
let arm32 = split_instruction(
0x1000,
"00001000`00000000 ef000000 svc #0",
InstructionSet::Other(0x01c4),
);
assert!(!arm32.privileged);
assert_eq!(arm32.flow, Flow::Unknown);
}
#[test]
#[cfg_attr(
miri,
ignore = "decodes through iced-x86; see MIRI AND THE DECODER above"
)]
fn test_a_privileged_instruction_says_which_family_it_reaches() {
use Privilege::*;
let (x64, x86) = (InstructionSet::Amd64, InstructionSet::X86);
for (set, bytes, text, family) in [
(x64, "ec", "in al,dx", Some(PortIo)),
(x64, "e680", "out 80h,al", Some(PortIo)),
(x64, "6c", "insb", Some(PortIo)),
(x64, "0f32", "rdmsr", Some(ModelSpecificRegister)),
(x64, "0f30", "wrmsr", Some(ModelSpecificRegister)),
(x64, "0f22d8", "mov cr3,rax", Some(ControlRegister)),
(x64, "0f20d8", "mov rax,cr3", Some(ControlRegister)),
(x64, "440f22c0", "mov cr8,rax", Some(ControlRegister)),
(x64, "0f23f8", "mov dr7,rax", Some(ControlRegister)),
(x64, "0f06", "clts", Some(ControlRegister)),
(x64, "0f01f0", "lmsw ax", Some(ControlRegister)),
(x64, "0f01d1", "xsetbv", Some(ControlRegister)),
(x64, "0f0110", "lgdt [rax]", Some(DescriptorTable)),
(x64, "0f0118", "lidt [rax]", Some(DescriptorTable)),
(x64, "0f00d0", "lldt ax", Some(DescriptorTable)),
(x64, "0f00d8", "ltr ax", Some(DescriptorTable)),
(x64, "fa", "cli", Some(InterruptFlag)),
(x64, "fb", "sti", Some(InterruptFlag)),
(x64, "0f08", "invd", Some(CacheOrTlb)),
(x64, "0f09", "wbinvd", Some(CacheOrTlb)),
(x64, "0f0138", "invlpg [rax]", Some(CacheOrTlb)),
(x64, "660f388200", "invpcid rax,[rax]", Some(CacheOrTlb)),
(x64, "0f01c2", "vmlaunch", Some(Virtualization)),
(x64, "0f01d8", "vmrun", Some(Virtualization)),
(x64, "660f388000", "invept rax,[rax]", Some(Virtualization)),
(x64, "0f01de", "skinit", Some(Virtualization)),
(x64, "f4", "hlt", Some(Other)),
(x64, "0f01f8", "swapgs", Some(Other)),
(x64, "480f07", "sysretq", Some(Other)),
(x64, "0f01cb", "stac", Some(Other)),
(x64, "0f0100", "sgdt [rax]", None),
(x64, "0f01c1", "vmcall", None),
(x64, "0f31", "rdtsc", None),
(x64, "4889d8", "mov rax,rbx", None),
(x86, "0f22d8", "mov cr3,eax", Some(ControlRegister)),
(x86, "fa", "cli", Some(InterruptFlag)),
] {
let one = decode_instruction(&hex::decode(bytes).unwrap(), 0x1000, set);
assert_ne!(one.flow, Flow::Unknown, "`{text}` must decode: {one:?}");
assert_eq!(one.bytes, bytes, "`{text}` must decode whole: {one:?}");
assert_eq!(one.privilege, family, "`{text}`: {one:?}");
assert_eq!(one.privileged, family.is_some(), "`{text}`: {one:?}");
}
}
#[test]
#[cfg_attr(
miri,
ignore = "decodes through iced-x86; see MIRI AND THE DECODER above"
)]
fn test_an_instructions_address_comes_from_the_walk_not_the_rendering() {
let one = split_instruction(0x1000, "deadbeef`deadbeef 90 nop", InstructionSet::Amd64);
assert_eq!(one.address, 0x1000);
assert_eq!(one.text, "nop");
assert_eq!(one.mnemonic, "nop");
}
#[test]
#[cfg_attr(
miri,
ignore = "decodes through iced-x86; see MIRI AND THE DECODER above"
)]
fn test_an_unrecognised_line_keeps_its_text_rather_than_inventing_an_encoding() {
let two_columns =
split_instruction(0x1000, "fffff803`89201234 ????", InstructionSet::Amd64);
assert!(two_columns.bytes.is_empty(), "{two_columns:?}");
assert_eq!(two_columns.text, "????");
let one_column = split_instruction(0x1000, "???", InstructionSet::Amd64);
assert!(one_column.bytes.is_empty(), "{one_column:?}");
assert_eq!(one_column.text, "???");
}
#[test]
#[cfg_attr(
miri,
ignore = "decodes through iced-x86; see MIRI AND THE DECODER above"
)]
fn test_an_unreadable_rendering_is_not_walked_through() {
for set in [
InstructionSet::Amd64,
InstructionSet::X86,
InstructionSet::Other(0xaa64),
] {
let nothing = split_instruction(0x1000, "fffff803`89201234 ????", set);
assert_eq!(nothing.flow, Flow::Unreadable, "{set:?}: {nothing:?}");
assert!(
!nothing.flow.falls_through(),
"{set:?}: a walk would step through unreadable bytes"
);
assert_eq!(nothing.mnemonic, "????");
}
}
#[test]
#[cfg_attr(
miri,
ignore = "decodes through iced-x86; see MIRI AND THE DECODER above"
)]
fn test_an_unread_instruction_set_reports_no_operands_and_no_flow() {
let arm32 = split_instruction(
0xfffff803_89201234,
"fffff803`89201234 0a000001 beq nt!KiFoo+0x1c (fffff803`89201240)",
InstructionSet::Other(0x01c4),
);
assert_eq!(arm32.mnemonic, "beq");
assert!(arm32.operands.is_empty(), "{arm32:?}");
assert_eq!(arm32.flow, Flow::Unknown);
assert!(arm32.flow.falls_through());
assert!(
arm32.text.contains("nt!KiFoo"),
"the rendering is still the rendering: {arm32:?}"
);
}
#[test]
#[cfg_attr(
miri,
ignore = "decodes through iced-x86; see MIRI AND THE DECODER above"
)]
fn test_an_a64_conditional_branch_keeps_both_edges() {
let arm64 = split_instruction(
0xfffff803_89201234,
"fffff803`89201234 54000060 b.eq nt!KiFoo+0x1c (fffff803`89201240)",
InstructionSet::Arm64,
);
assert_eq!(arm64.mnemonic, "b.eq");
assert_eq!(arm64.operands, [Operand::Target(0xfffff803_89201240)]);
assert_eq!(arm64.condition, Some(Condition::Equal));
assert_eq!(arm64.flow, Flow::Branch(Some(0xfffff803_89201240)));
assert!(arm64.flow.falls_through());
assert!(
arm64.text.contains("nt!KiFoo"),
"the rendering is still the rendering: {arm64:?}"
);
let ret = split_instruction(
0x1000,
"00001000`00000000 d65f03c0 ret",
InstructionSet::Arm64,
);
assert_eq!(ret.flow, Flow::Return);
let unreadable = split_instruction(
0x1000,
"00001000`00000000 ?? ???",
InstructionSet::Arm64,
);
assert_eq!(unreadable.flow, Flow::Unreadable);
assert!(!unreadable.flow.falls_through());
}
#[test]
#[cfg_attr(
miri,
ignore = "decodes through iced-x86; see MIRI AND THE DECODER above"
)]
fn test_the_operands_come_from_the_encoding_and_not_from_the_rendering() {
let lied_to = split_instruction(
0x1000,
"00001000 e80b000000 call module!std::map<int,int>::operator[]() (deadbeef`deadbeef)",
InstructionSet::Amd64,
);
assert_eq!(lied_to.flow, Flow::Call(Some(0x1010)), "{lied_to:?}");
assert_eq!(lied_to.operands, vec![Operand::Target(0x1010)]);
assert_eq!(lied_to.mnemonic, "call");
}
#[test]
#[cfg_attr(
miri,
ignore = "decodes through iced-x86; see MIRI AND THE DECODER above"
)]
fn test_x64_operands_are_read_as_values() {
let one = |encoding: &str, at: u64| {
split_instruction(at, &format!("00001000 {encoding} x"), InstructionSet::Amd64)
};
let cmp = one("4181fd30006d00", 0x1000);
assert_eq!(cmp.mnemonic, "cmp");
assert_eq!(
cmp.operands,
vec![
Operand::Register(register("r13d", "r13", 4)),
Operand::Immediate(0x6d_0030)
]
);
assert_eq!(cmp.flow, Flow::Fallthrough);
let load = one("488b82b8000000", 0x1000);
let Some(Operand::Memory(memory)) = load.operands.get(1) else {
panic!("the memory operand was not read: {load:?}");
};
assert_eq!(memory.size, Some(8));
assert_eq!(memory.base.as_deref(), Some("rdx"));
assert_eq!(memory.displacement, 0xb8);
assert_eq!(memory.index, None);
assert_eq!(
memory.address, None,
"a based reference has no static address"
);
let call = one("e8fb0f0000", 0x1000);
assert_eq!(call.flow, Flow::Call(Some(0x2000)));
assert_eq!(call.operands, vec![Operand::Target(0x2000)]);
let thunk = one("ff15fa0f0000", 0x1000);
assert_eq!(thunk.flow, Flow::Call(None));
let Some(Operand::Memory(slot)) = thunk.operands.first() else {
panic!("the thunk's operand was not read as memory: {thunk:?}");
};
assert_eq!(slot.base.as_deref(), Some("rip"));
assert_eq!(slot.address, Some(0x2000), "{slot:?}");
let table = one("ff24c500200000", 0x1000);
assert_eq!(table.flow, Flow::Jmp(None));
let Some(Operand::Memory(entry)) = table.operands.first() else {
panic!("the jump table's operand was not read as memory: {table:?}");
};
assert_eq!(entry.index.as_deref(), Some("rax"));
assert_eq!(entry.scale, 8);
assert_eq!(entry.displacement, 0x2000);
let kpcr = one("65488b042588010000", 0x1000);
let Some(Operand::Memory(pcr)) = kpcr.operands.get(1) else {
panic!("the segment-overridden operand was not read: {kpcr:?}");
};
assert_eq!(pcr.segment.as_deref(), Some("gs"));
assert_eq!(pcr.displacement, 0x188);
assert_eq!(pcr.base, None);
let byte_register = one("b405", 0x1000);
assert_eq!(
byte_register.operands,
vec![
Operand::Register(register("ah", "rax", 1)),
Operand::Immediate(5)
]
);
let wide = one("48b80000000000000080", 0x1000);
assert_eq!(
wide.operands.get(1),
Some(&Operand::Immediate(0x8000_0000_0000_0000))
);
}
#[test]
#[cfg_attr(
miri,
ignore = "decodes through iced-x86; see MIRI AND THE DECODER above"
)]
fn test_flow_separates_the_edges_a_walk_may_take() {
let one = |encoding: &str| {
split_instruction(
0x1000,
&format!("00001000 {encoding} x"),
InstructionSet::Amd64,
)
.flow
};
assert_eq!(one("c3"), Flow::Return, "ret");
assert_eq!(one("0f0b"), Flow::Trap, "ud2");
assert_eq!(one("ffd0"), Flow::Call(None), "call rax");
assert_eq!(one("ffe0"), Flow::Jmp(None), "jmp rax");
assert_eq!(one("31db"), Flow::Fallthrough, "xor ebx,ebx");
assert_eq!(one("740e"), Flow::Branch(Some(0x1010)), "je");
assert_eq!(one("eb0e"), Flow::Jmp(Some(0x1010)), "jmp short");
let locked = split_instruction(0x1000, "00001000 f0ff00 x", InstructionSet::Amd64);
assert_eq!(locked.mnemonic, "inc", "lock inc dword ptr [rax]");
assert_eq!(locked.flow, Flow::Fallthrough);
}
#[test]
#[cfg_attr(
miri,
ignore = "decodes through iced-x86; see MIRI AND THE DECODER above"
)]
fn test_bytes_that_do_not_decode_are_unknown_rather_than_unreadable() {
let in_64 = split_instruction(0x1000, "00001000 06 x", InstructionSet::Amd64);
assert_eq!(in_64.flow, Flow::Unknown, "{in_64:?}");
assert!(
in_64.flow.falls_through(),
"a walk must not stop at an instruction this decoder does not know"
);
let in_32 = split_instruction(0x1000, "00001000 06 x", InstructionSet::X86);
assert_eq!(in_32.mnemonic, "push");
assert_eq!(in_32.flow, Flow::Fallthrough);
}
#[test]
#[cfg_attr(
miri,
ignore = "decodes through iced-x86; see MIRI AND THE DECODER above"
)]
fn test_a_static_address_is_only_claimed_where_one_exists() {
let one = |encoding: &str| {
let decoded = split_instruction(
0x1000,
&format!("00001000 {encoding} x"),
InstructionSet::Amd64,
);
decoded
.operands
.iter()
.find_map(|operand| match operand {
Operand::Memory(memory) => Some(memory.clone()),
_ => None,
})
.unwrap_or_else(|| panic!("no memory operand in {decoded:?}"))
};
let kpcr = one("65488b042588010000");
assert_eq!(kpcr.segment.as_deref(), Some("gs"));
assert_eq!(kpcr.displacement, 0x188);
assert_eq!(
kpcr.address, None,
"a segment override has no address the instruction alone knows: {kpcr:?}"
);
let thunk = one("ff15fa0f0000");
assert_eq!(thunk.base.as_deref(), Some("rip"));
assert_eq!(thunk.address, Some(0x2000), "{thunk:?}");
assert_eq!(
thunk.displacement, 0xffa,
"the encoded displacement, not a second copy of the target: {thunk:?}"
);
let absolute = one("488b042500200000");
assert_eq!(absolute.address, Some(0x2000));
assert_eq!(absolute.displacement, 0x2000);
assert_eq!(absolute.segment, None);
let high = split_instruction(
0xffffffff_80001000,
"ffffffff`80001000 a100200080 x",
InstructionSet::X86,
);
let Some(Operand::Memory(global)) = high.operands.get(1) else {
panic!("no absolute operand in {high:?}");
};
assert_eq!(
global.address,
Some(0xffffffff_80002000),
"the high half was dropped: {global:?}"
);
}
#[test]
#[cfg_attr(
miri,
ignore = "decodes through iced-x86; see MIRI AND THE DECODER above"
)]
fn test_a_negative_displacement_survives_a_narrow_address_width() {
let memory = |encoding: &str, set: InstructionSet| {
let decoded = split_instruction(0x1000, &format!("00001000 {encoding} x"), set);
decoded
.operands
.iter()
.find_map(|operand| match operand {
Operand::Memory(memory) => Some(memory.clone()),
_ => None,
})
.unwrap_or_else(|| panic!("no memory operand in {decoded:?}"))
};
let local32 = memory("8b45f8", InstructionSet::X86);
assert_eq!(local32.base.as_deref(), Some("ebp"));
assert_eq!(local32.displacement, -8, "{local32:?}");
let local64 = memory("488b45f8", InstructionSet::Amd64);
assert_eq!(local64.base.as_deref(), Some("rbp"));
assert_eq!(local64.displacement, -8, "{local64:?}");
assert_eq!(memory("8b4508", InstructionSet::X86).displacement, 8);
assert_eq!(memory("488b4508", InstructionSet::Amd64).displacement, 8);
let scaled = memory("8b4488f8", InstructionSet::X86);
assert_eq!(scaled.index.as_deref(), Some("ecx"));
assert_eq!(scaled.scale, 4);
assert_eq!(scaled.displacement, -8, "{scaled:?}");
let gather = memory("c4e279920c95f8ffffff", InstructionSet::X86);
assert!(
gather
.index
.as_deref()
.is_some_and(|r| r.starts_with("xmm")),
"not the VSIB form this is about: {gather:?}"
);
assert_eq!(gather.base, None);
assert_eq!(gather.displacement, -8, "{gather:?}");
let evex = memory("62f17c48284102", InstructionSet::Amd64);
assert_eq!(evex.base.as_deref(), Some("rcx"), "{evex:?}");
assert_eq!(evex.displacement, 128, "{evex:?}");
}
#[test]
#[cfg_attr(
miri,
ignore = "decodes through iced-x86; see MIRI AND THE DECODER above"
)]
fn test_an_eip_relative_displacement_is_computed_at_its_own_width() {
let operand = |at: u64, encoding: &str| {
let decoded =
split_instruction(at, &format!("00001000 {encoding} x"), InstructionSet::Amd64);
decoded
.operands
.iter()
.find_map(|operand| match operand {
Operand::Memory(memory) => Some(memory.clone()),
_ => None,
})
.unwrap_or_else(|| panic!("no memory operand in {decoded:?}"))
};
let overridden = operand(0xffffffff_80001000, "67488b05fa0f0000");
assert_eq!(overridden.base.as_deref(), Some("eip"), "{overridden:?}");
assert_eq!(overridden.displacement, 0xffa, "{overridden:?}");
let plain = operand(0xfffff805_5ec04750, "488b05fa0f0000");
assert_eq!(plain.base.as_deref(), Some("rip"));
assert_eq!(plain.displacement, 0xffa, "{plain:?}");
}
#[test]
#[cfg_attr(
miri,
ignore = "decodes through iced-x86; see MIRI AND THE DECODER above"
)]
fn test_a_32_bit_target_stays_in_its_instructions_address_space() {
let high = split_instruction(
0xffffffff_80001000,
"ffffffff`80001000 e8fb0f0000 x",
InstructionSet::X86,
);
assert_eq!(
high.flow,
Flow::Call(Some(0xffffffff_80002000)),
"the high half of the address was dropped: {high:?}"
);
assert_eq!(high.operands, vec![Operand::Target(0xffffffff_80002000)]);
let low = split_instruction(0x00401000, "00401000 e8fb0f0000 x", InstructionSet::X86);
assert_eq!(low.flow, Flow::Call(Some(0x00402000)), "{low:?}");
let wide = split_instruction(
0xfffff805_5ec04750,
"fffff805`5ec04750 e8fb0f0000 x",
InstructionSet::Amd64,
);
assert_eq!(wide.flow, Flow::Call(Some(0xfffff805_5ec05750)), "{wide:?}");
let crossing = split_instruction(
0xfffff805_fffffff0,
"fffff805`fffffff0 e8fb0f0000 x",
InstructionSet::Amd64,
);
assert_eq!(
crossing.flow,
Flow::Call(Some(0xfffff806_00000ff0)),
"a 64-bit branch across a 4 GB boundary was dragged back: {crossing:?}"
);
}
#[test]
#[cfg_attr(
miri,
ignore = "decodes through iced-x86; see MIRI AND THE DECODER above"
)]
fn test_a_32_bit_address_is_not_sign_extended() {
let straddling =
split_instruction(0x00401000, "00401000 a100200080 x", InstructionSet::X86);
let Some(Operand::Memory(global)) = straddling.operands.get(1) else {
panic!("no absolute operand in {straddling:?}");
};
assert_eq!(
global.address,
Some(0x0000_0000_8000_2000),
"a 32-bit address was sign-extended into a space the engine does not use: {global:?}"
);
let ordinary = split_instruction(0x00401000, "00401000 e8fb0f0000 x", InstructionSet::X86);
assert_eq!(ordinary.flow, Flow::Call(Some(0x00402000)));
}
#[test]
#[cfg_attr(
miri,
ignore = "decodes through iced-x86; see MIRI AND THE DECODER above"
)]
fn test_hlt_keeps_its_wake_up_edge() {
let halt = split_instruction(0x1000, "00001000 f4 hlt", InstructionSet::Amd64);
assert_eq!(halt.mnemonic, "hlt");
assert_eq!(halt.flow, Flow::Fallthrough);
assert!(
halt.flow.falls_through(),
"an idle loop continues past its halt"
);
}
#[test]
#[cfg_attr(
miri,
ignore = "decodes through iced-x86; see MIRI AND THE DECODER above"
)]
fn test_a_software_interrupt_is_classified_by_its_vector() {
let one = |encoding: &str| {
split_instruction(
0x1000,
&format!("00001000 {encoding} x"),
InstructionSet::Amd64,
)
.flow
};
assert_eq!(one("cd29"), Flow::Trap, "int 29h — __fastfail");
assert_eq!(one("cc"), Flow::Trap, "int 3 — the one-byte breakpoint");
assert_eq!(one("cd03"), Flow::Trap, "int 3 — the two-byte form");
assert_eq!(
one("cd2e"),
Flow::Fallthrough,
"the 32-bit system call returns, and the walk must go on past it"
);
assert_eq!(one("cd2d"), Flow::Fallthrough);
assert!(one("cd2e").falls_through());
assert!(!one("cd29").falls_through());
}
#[test]
#[cfg_attr(
miri,
ignore = "decodes through iced-x86; see MIRI AND THE DECODER above"
)]
fn test_the_transactional_instructions_keep_the_edges_they_have() {
let begin = split_instruction(0x1000, "00001000 c7f80a000000 x", InstructionSet::Amd64);
assert_eq!(begin.mnemonic, "xbegin");
assert_eq!(begin.flow, Flow::Branch(Some(0x1010)));
assert!(
begin.flow.falls_through(),
"a transaction that starts goes on"
);
let abort = split_instruction(0x1000, "00001000 c6f800 x", InstructionSet::Amd64);
assert_eq!(abort.mnemonic, "xabort");
assert_eq!(abort.flow, Flow::Fallthrough);
}
#[test]
#[cfg_attr(
miri,
ignore = "decodes through iced-x86; see MIRI AND THE DECODER above"
)]
fn test_an_operand_width_comes_from_the_encoding() {
let width = |encoding: &str, index: u32| {
let one = split_instruction(
0x1000,
&format!("00001000 {encoding} x"),
InstructionSet::Amd64,
);
match one.operands.get(index as usize) {
Some(Operand::Memory(memory)) => memory.size,
other => panic!("{encoding} operand {index} was not memory: {other:?} in {one:?}"),
}
};
assert_eq!(width("8818", 0), Some(1));
assert_eq!(width("668918", 0), Some(2));
assert_eq!(width("8918", 0), Some(4));
assert_eq!(width("488918", 0), Some(8));
assert_eq!(width("0f6f00", 1), Some(8), "an MMX operand is 64 bits");
assert_eq!(width("660f6f00", 1), Some(16));
assert_eq!(width("c5fd6f00", 1), Some(32));
}
#[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))]
#[test]
fn test_a_pump_through_one_wrapper_completes_an_open_held_by_another() {
let _debuggee = one_debuggee();
let owner = DebugEngine::new();
let pending = owner
.launch_process_begin("cmd.exe /c ping -n 30 127.0.0.1")
.expect("launch failed");
let borrowed = DebugEngine::from_client_interface(owner.client.clone());
let outcome = borrowed
.wait_for_event(LIVE_WAIT_MS)
.expect("the outside pump failed");
assert!(
matches!(outcome, WaitOutcome::Stopped { .. }),
"the pump that realises the launch answered {outcome:?}, so nothing was delivered"
);
let WaitKind::Live(registered) = &pending.kind else {
panic!("a launch guard is not a live open");
};
assert!(
matches!(owner.presence_of(registered), Presence::Arrived),
"a stop pumped through the second wrapper did not reach the open the first is holding, \
so its wait() will spend the whole bound on an event that has happened"
);
drop(pending);
owner.end_session().expect("end_session failed");
}
#[cfg(not(miri))]
#[test]
fn test_a_drop_lets_go_of_the_sessions_pending_opens() {
let _debuggee = one_debuggee();
let borrowed = {
let owner = DebugEngine::new();
let borrowed = DebugEngine::from_client_interface(owner.client.clone());
let pending = owner
.launch_process_begin("cmd.exe /c ping -n 30 127.0.0.1")
.expect("launch failed");
std::mem::forget(pending);
assert_eq!(
borrowed
.state
.arrivals
.lock()
.unwrap_or_else(|err| err.into_inner())
.pending
.len(),
1,
"the open was not registered, so nothing here is under test"
);
borrowed
};
assert!(
borrowed
.state
.arrivals
.lock()
.unwrap_or_else(|err| err.into_inner())
.pending
.is_empty(),
"an open registered against a session the owner's drop ended is still first in line, \
so the next launch through this wrapper loses its stop to it"
);
}
#[cfg(not(miri))]
#[test]
fn test_a_sessions_attachments_are_visible_through_every_wrapper() {
let _debuggee = one_debuggee();
let mut theirs = a_process_to_attach_to();
{
let owner = DebugEngine::new();
owner.attach_process(theirs.id()).expect("attach failed");
let borrowed = DebugEngine::from_client_interface(owner.client.clone());
assert!(
borrowed.attached_to_a_live_process(),
"a wrapper that did not perform the attach cannot see it, so its teardown will \
take a process it was only looking at"
);
let _ = borrowed.end_session();
}
assert_eq!(
exit_code_of(theirs.id()),
Some(STILL_RUNNING),
"the attached process was taken by a teardown through the wrapper that did not attach \
to it"
);
let _ = theirs.kill();
let _ = theirs.wait();
}
#[cfg(not(miri))]
#[test]
fn test_two_launches_pending_at_once_are_told_apart() {
let _debuggee = one_debuggee();
let e = DebugEngine::new();
let first = e
.launch_process_begin("cmd.exe /c ping -n 30 127.0.0.1")
.expect("first launch failed");
let second = e
.launch_process_begin("cmd.exe /c ping -n 30 127.0.0.2")
.expect("second launch failed");
for _ in 0..2 {
e.wait_for_event(LIVE_WAIT_MS).expect("a pump failed");
}
let (WaitKind::Live(one), WaitKind::Live(two)) = (&first.kind, &second.kind) else {
panic!("a launch guard is not a live open");
};
let arrivals = e
.state
.arrivals
.lock()
.unwrap_or_else(|err| err.into_inner());
let claimed = |id: ArrivalId| {
arrivals
.pending
.iter()
.find(|pending| pending.id == id)
.and_then(|pending| pending.claim.held())
};
let (a, b) = (claimed(one.id), claimed(two.id));
assert!(
a.is_some() && b.is_some(),
"two waits realised {a:?} and {b:?}, so one launch was never delivered anything and \
this says nothing about telling them apart"
);
assert_ne!(
a, b,
"both launches were given the same arrival, which is the ambiguity a register of \
pending opens exists to remove"
);
drop(arrivals);
drop(first);
drop(second);
e.end_session().expect("end_session failed");
}
#[test]
#[cfg(not(miri))]
fn test_an_abandoned_launch_does_not_hand_its_process_to_the_next_one() {
let _debuggee = one_debuggee();
for round in 1..=5 {
an_abandoned_launch_round(round);
}
}
#[cfg(not(miri))]
fn an_abandoned_launch_round(round: usize) {
let e = DebugEngine::new();
let abandoned = e
.launch_process_begin("cmd.exe /c ping -n 30 127.0.0.1")
.expect("the first launch would not begin");
drop(abandoned);
let mine = e
.launch_process_begin("ping.exe -n 30 127.0.0.2")
.expect("the second launch would not begin");
let WaitKind::Live(registered) = &mine.kind else {
panic!("a launch guard is not a live open");
};
let id = registered.id;
let claim_of = |id: ArrivalId| {
e.state
.arrivals
.lock()
.unwrap_or_else(|err| err.into_inner())
.pending
.iter()
.find(|pending| pending.id == id)
.and_then(|pending| pending.claim.held())
};
let mut claimed = None;
for _ in 0..5 {
claimed = claim_of(id);
if claimed.is_some() {
break;
}
e.wait_for_event(LIVE_WAIT_MS).expect("a pump failed");
}
let mine = claimed.expect("nothing was delivered to the second launch in five pumps");
let absorbed = e
.state
.arrivals
.lock()
.unwrap_or_else(|err| err.into_inner())
.pending
.iter()
.find(|pending| pending.abandoned)
.and_then(|pending| pending.claim.held());
let absorbed = absorbed.unwrap_or_else(|| {
panic!(
"round {round}: the abandoned launch's entry was given nothing, so its process was \
left for whoever asked next"
)
});
assert_ne!(
absorbed, mine,
"round {round}: the abandoned launch and the one after it were given the same process"
);
let _ = e.end_session();
}
#[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() {
const SETTLE: Duration = WATCHDOG_REPEAT.checked_div(4).expect("a nonzero divisor");
const ROUNDS: usize = 5;
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);
});
assert!(
!watchdog.disarm(),
"a watchdog 30s from its deadline reported firing"
);
assert_eq!(fires.load(Ordering::SeqCst), 0);
let mut took = Vec::with_capacity(ROUNDS);
for _ in 0..ROUNDS {
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);
});
let armed = Instant::now();
while fires.load(Ordering::SeqCst) == 0 {
assert!(
armed.elapsed() < Duration::from_secs(10),
"a watchdog armed with no time at all never fired"
);
thread::yield_now();
}
thread::sleep(SETTLE);
let started = Instant::now();
let fired = watchdog.disarm();
took.push(started.elapsed());
assert!(fired, "a watchdog past its deadline reported not firing");
}
took.sort_unstable();
let median = took[ROUNDS / 2];
assert!(
median < WATCHDOG_REPEAT / 2,
"the median of {ROUNDS} disarms was {median:?} ({took:?}), against a \
{WATCHDOG_REPEAT:?} poll interval; it waits for a wake-up, not for a poll"
);
}
#[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))]
fn console_process_list() -> Vec<u32> {
use windows::Win32::System::Console::GetConsoleProcessList;
let mut buf = vec![0u32; 64];
loop {
let n = unsafe { GetConsoleProcessList(&mut buf) } as usize;
if n == 0 {
return Vec::new(); }
if n <= buf.len() {
buf.truncate(n);
return buf;
}
buf = vec![0u32; n];
}
}
#[cfg(not(miri))]
fn has_a_visible_window(pid: u32) -> bool {
use windows::Win32::Foundation::{HWND, LPARAM};
use windows::Win32::UI::WindowsAndMessaging::{
EnumWindows, GetWindowThreadProcessId, IsWindowVisible,
};
use windows::core::BOOL;
unsafe extern "system" fn visit(window: HWND, state: LPARAM) -> BOOL {
let search = unsafe { &mut *(state.0 as *mut Search) };
let mut owner = 0u32;
unsafe { GetWindowThreadProcessId(window, Some(&mut owner)) };
if owner == search.pid && unsafe { IsWindowVisible(window) }.as_bool() {
search.found = true;
return BOOL(0); }
BOOL(1)
}
struct Search {
pid: u32,
found: bool,
}
let mut search = Search { pid, found: false };
let _ = unsafe { EnumWindows(Some(visit), LPARAM(&raw mut search as isize)) };
search.found
}
#[cfg(not(miri))]
#[test]
#[ignore = "needs a live debuggee; run manually with --ignored"]
fn test_a_launched_target_has_a_console_of_its_own_and_no_window() {
use std::os::windows::process::CommandExt;
let probe =
std::env::temp_dir().join(format!("dbgscope-console-probe-{}.txt", std::process::id()));
let _ = std::fs::remove_file(&probe);
let e = DebugEngine::new();
e.launch_process(&format!("cmd.exe /c mode con > \"{}\"", probe.display()))
.expect("launch failed");
let target = e
.current_process_system_id()
.expect("the debuggee's system id");
let ours = console_process_list();
assert!(
!ours.contains(&target),
"the debuggee ({target}) joined this process's console ({ours:?}), so its stdout is \
this process's stdout — which for an MCP host is the JSON-RPC channel"
);
const CREATE_NEW_CONSOLE: u32 = 0x0000_0010;
let mut control = std::process::Command::new("ping.exe")
.args(["-n", "30", "127.0.0.1"])
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.creation_flags(CREATE_NEW_CONSOLE)
.spawn()
.expect("spawn the control");
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(15);
let mut calibrated = false;
while std::time::Instant::now() < deadline {
if has_a_visible_window(control.id()) {
calibrated = true;
break;
}
std::thread::sleep(std::time::Duration::from_millis(50));
}
let debuggee_has_one = has_a_visible_window(target);
let _ = control.kill();
let _ = control.wait();
let _ = e.execute_and_wait("g", 30_000);
let reported = std::fs::read_to_string(&probe).unwrap_or_default();
let _ = std::fs::remove_file(&probe);
assert!(
reported.contains("Status for device CON"),
"`mode con` in the debuggee reported no console (`{}`), so the target was launched \
with none at all rather than with one of its own — which is `DETACHED_PROCESS`, not \
this flag, and would take its console APIs with it",
reported.trim()
);
let _ = e.end_session();
assert!(
calibrated,
"a control spawned with CREATE_NEW_CONSOLE showed no window of its own either, so \
this host cannot tell the two flags apart and the window half of this test did not \
run. Not a regression in `launch_process`: run it on a desktop whose default \
terminal is the console host."
);
assert!(
!debuggee_has_one,
"the debuggee ({target}) has a visible window, so it was given a console on the \
desktop — every launch then steals the foreground (#129)"
);
}
#[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();
}
#[test]
#[cfg(not(miri))]
fn test_a_pending_break_does_not_empty_the_next_reload() {
let _debuggee = one_debuggee();
let e = DebugEngine::new();
e.launch_process("ping.exe -n 30 127.0.0.1")
.expect("launch failed");
e.reload_symbols("").expect("unbounded reload failed");
let held = e.modules().expect("modules failed").len();
assert!(held > 0, "the launched target reports no modules to reload");
for (what, bounded) in [("bounded", true), ("unbounded", false)] {
let raised = e
.interrupt_handle()
.interrupt()
.expect("SetInterrupt failed");
assert_eq!(
raised,
BreakRequest::NothingRunning,
"the break was meant to land between operations"
);
if bounded {
let reload = e
.reload_symbols_bounded("", 60_000)
.expect("bounded reload failed");
assert_eq!(
reload.cut_short, None,
"a break raised before the reload began was charged to it"
);
} else {
e.reload_symbols("").expect("unbounded reload failed");
}
assert_eq!(
e.modules().expect("modules failed").len(),
held,
"the {what} reload begun under a pending break did not rebuild the inventory"
);
assert!(
!e.interrupted().expect("GetInterrupt failed"),
"the {what} reload left the break pending for whatever runs next"
);
}
let _ = e.end_session();
}
#[test]
#[cfg(not(miri))]
fn test_a_reload_inside_an_operation_leaves_its_break_pending() {
let _debuggee = one_debuggee();
let e = DebugEngine::new();
e.launch_process("ping.exe -n 30 127.0.0.1")
.expect("launch failed");
e.reload_symbols("").expect("unbounded reload failed");
for (what, bounded) in [("bounded", true), ("unbounded", false)] {
let outer = e.begin_operation();
let raised = e
.interrupt_handle()
.interrupt()
.expect("SetInterrupt failed");
assert_eq!(
raised,
BreakRequest::Raised {
operation: outer.id
},
"the request was meant for the enclosing operation"
);
let reload = if bounded {
e.reload_symbols_bounded("", 60_000).map(|_| ())
} else {
e.reload_symbols("")
};
println!("{what} reload under the enclosing operation's break: {reload:?}");
let pending = e.interrupted().expect("GetInterrupt failed");
assert!(
reload.is_err() || pending,
"the {what} reload nested in an operation threw away the break a host had filed \
against that operation: the reload finished and nothing was left pending, so \
the enclosing wait would never have broken"
);
assert!(
outer.took_break_request(),
"and the request is still the enclosing operation's to take"
);
}
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();
}
#[test]
#[cfg(not(miri))]
fn test_a_refused_command_keeps_what_the_engine_printed() {
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 command = ".echo before the failure; ? nowhere!Nope";
let paths = [
("execute_command", e.execute_command(command).map(|_| ())),
(
"execute_command_bounded",
e.execute_command_bounded(command, 10_000).map(|_| ()),
),
(
"execute_and_wait",
e.execute_and_wait(command, 10_000).map(|_| ()),
),
];
for (path, outcome) in paths {
let error = match outcome {
Ok(()) => panic!("{path}: `{command}` succeeded, so nothing here is under test"),
Err(error) => error,
};
let printed = error
.output()
.unwrap_or_else(|| panic!("{path}: the failure carried no capture: {error:?}"));
assert!(
printed.contains("before the failure"),
"{path}: what the first segment printed was lost: {printed:?}"
);
assert!(
printed.contains("Couldn't resolve error at 'nowhere!Nope'"),
"{path}: the engine's reason was not in the capture: {printed:?}"
);
}
e.end_session().expect("end_session failed");
}
#[cfg(not(miri))]
static ONE_DEBUGGEE: Mutex<()> = Mutex::new(());
#[cfg(not(miri))]
pub(super) 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))]
#[cfg(not(miri))]
fn watching_the_only_process(e: &DebugEngine) -> Registered<'_> {
let held = e.session_processes().expect("could not list the session");
let [(_, pid)] = held[..] else {
panic!("this session holds {held:?}, and the helper wants exactly one process");
};
registered(e, Arrival::Attached(pid))
}
#[cfg(not(miri))]
fn registered(e: &DebugEngine, what: Arrival) -> Registered<'_> {
let id = e
.state
.arrivals
.lock()
.unwrap_or_else(|err| err.into_inner())
.register(what);
Registered { engine: e, id }
}
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_only_a_deferred_attachment_is_retired() {
let _debuggee = one_debuggee();
let mut theirs = a_process_to_attach_to();
{
let e = DebugEngine::new();
let attaching = e.attach_process_begin(theirs.id()).expect("attach failed");
let WaitKind::Live(registered) = &attaching.kind else {
panic!("an attach guard is not a live open");
};
e.state
.attached_processes
.lock()
.unwrap_or_else(|err| err.into_inner())
.insert(theirs.id(), Attachment::Joined);
e.retire_deferred_attachment(registered);
assert!(
e.state
.attached_processes
.lock()
.unwrap_or_else(|err| err.into_inner())
.contains_key(&theirs.id()),
"a joined attachment was retired, so its teardown will take somebody else's process"
);
e.state
.attached_processes
.lock()
.unwrap_or_else(|err| err.into_inner())
.insert(theirs.id(), Attachment::Deferred);
e.retire_deferred_attachment(registered);
assert!(
!e.state
.attached_processes
.lock()
.unwrap_or_else(|err| err.into_inner())
.contains_key(&theirs.id()),
"an attachment that can never join was kept, so a later process inheriting its pid \
is treated as one this engine only attached to"
);
drop(attaching);
let _ = e.end_session();
}
let _ = theirs.kill();
let _ = theirs.wait();
}
#[test]
#[cfg(not(miri))]
fn test_only_an_empty_session_proves_an_attach_produced_nothing() {
let _debuggee = one_debuggee();
let e = DebugEngine::new();
assert!(
e.holds_nothing_at_all(),
"a fresh engine holds a target, so nothing here means what it says"
);
e.launch_process("ping.exe -n 30 127.0.0.1")
.expect("launch failed");
assert!(
!e.holds_nothing_at_all(),
"a session with a live target read as holding nothing, so a failed pump would retire \
an attach the engine has not yet had a chance to process"
);
let _ = e.end_session();
}
#[test]
#[cfg(not(miri))]
fn test_a_failed_pump_retires_a_deferred_attachment() {
let _debuggee = one_debuggee();
let e = DebugEngine::new();
assert!(
!e.has_target().expect("could not read the execution status"),
"this engine holds a target, so the pump will not fail and nothing is under test"
);
let absent = 0x7FFF_FFF0;
e.claim_attached(absent);
let waiting = registered(&e, Arrival::Attached(absent));
let outcome = e.wait_for_live_target(&waiting);
assert!(
matches!(outcome, Err(DbgEngError::CommandFailed(_))),
"the open answered {outcome:?} rather than failing, so it did not take the ending this \
test is about"
);
assert!(
!e.state
.attached_processes
.lock()
.unwrap_or_else(|err| err.into_inner())
.contains_key(&absent),
"a pump that failed with the session holding nothing left the record behind, so a \
later process inheriting that pid is treated as one this engine only attached to"
);
}
#[test]
#[cfg(not(miri))]
#[ignore = "waits out LIVE_WAIT_MS; run manually with --ignored"]
fn test_an_open_that_waits_out_its_bound_retires_a_deferred_attachment() {
let _debuggee = one_debuggee();
let e = DebugEngine::new();
e.launch_process("ping.exe -n 60 127.0.0.1")
.expect("launch failed");
let absent = e
.session_processes()
.expect("could not list the session")
.iter()
.map(|(_, pid)| *pid)
.max()
.unwrap_or(4)
+ 1000;
e.claim_attached(absent);
let waiting = registered(&e, Arrival::Attached(absent));
let outcome = e.wait_for_live_target(&waiting);
assert!(
matches!(outcome, Err(DbgEngError::LiveTargetTimeout)),
"the open answered {outcome:?} rather than timing out, so it did not reach the branch \
that retires the record"
);
assert!(
!e.state
.attached_processes
.lock()
.unwrap_or_else(|err| err.into_inner())
.contains_key(&absent),
"the open waited its whole bound and left the record behind"
);
let _ = e.end_session();
}
#[test]
#[cfg(not(miri))]
fn test_a_prune_does_not_take_an_attachment_that_has_not_joined() {
let _debuggee = one_debuggee();
let mut theirs = a_process_to_attach_to();
{
let e = DebugEngine::new();
let attaching = e.attach_process_begin(theirs.id()).expect("attach failed");
assert!(
!e.session_processes()
.expect("could not list the session")
.iter()
.any(|(_, pid)| *pid == theirs.id()),
"the attached process is already listed, so this window does not exist here and \
the test is asserting nothing"
);
let launching = e
.launch_process_begin("cmd.exe /c ping -n 30 127.0.0.1")
.expect("launch failed");
assert_eq!(
e.state
.attached_processes
.lock()
.unwrap_or_else(|err| err.into_inner())
.get(&theirs.id()),
Some(&Attachment::Deferred),
"the prune took a record of an attachment that had not joined yet, so the teardown \
will treat somebody else's process as one this engine launched"
);
attaching.wait().expect("the attach should complete");
launching.wait().expect("the launch should complete");
let _ = e.end_session();
}
assert_eq!(
exit_code_of(theirs.id()),
Some(STILL_RUNNING),
"the attached process was taken by the session it was only being looked at from"
);
let _ = theirs.kill();
let _ = theirs.wait();
}
#[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.state
.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_a_guard_whose_target_already_arrived_does_not_wait_for_another_event() {
let _debuggee = one_debuggee();
let e = DebugEngine::new();
let pending = e
.launch_process_begin("cmd.exe /c ping -n 30 127.0.0.1")
.expect("launch failed");
e.wait_for_event(LIVE_WAIT_MS)
.expect("the outside pump failed");
let started = Instant::now();
pending.wait().expect("wait on an arrived target failed");
let took = started.elapsed();
assert!(
took < Duration::from_millis(u64::from(LIVE_WAIT_MS / 10)),
"wait() took {took:?} on a target already in the session; it waited for another event \
rather than asking whether its own had arrived"
);
assert_eq!(
e.session_processes()
.expect("could not list the session's processes")
.len(),
1,
"the launched process is not in the session the wait said it had joined"
);
e.end_session().expect("end_session failed");
let mut theirs = a_process_to_attach_to();
let e = DebugEngine::new();
let pending = e
.launch_process_begin("cmd.exe /c ping -n 30 127.0.0.1")
.expect("launch failed");
e.wait_for_event(LIVE_WAIT_MS)
.expect("the outside pump failed");
e.attach_process(theirs.id()).expect("attach failed");
let started = Instant::now();
pending
.wait()
.expect("wait on an arrived target whose stop was overwritten failed");
let took = started.elapsed();
assert!(
took < Duration::from_millis(u64::from(LIVE_WAIT_MS / 10)),
"wait() took {took:?} on a target already in the session; a later target's event is \
not evidence that this one never arrived"
);
e.end_session().expect("end_session failed");
let _ = theirs.kill();
let _ = theirs.wait();
}
#[test]
#[cfg(not(miri))]
fn test_a_stop_says_which_process_it_stopped_on() {
let _debuggee = one_debuggee();
let e = DebugEngine::new();
let pending = e
.launch_process_begin("cmd.exe /c ping -n 30 127.0.0.1")
.expect("launch failed");
let outcome = e
.wait_for_event(LIVE_WAIT_MS)
.expect("the outside pump failed");
let WaitOutcome::Stopped { process } = outcome else {
panic!("the pump that realised the launch answered {outcome:?} rather than a stop");
};
let held = e.session_processes().expect("could not list the session");
assert_eq!(
process.map(|pair| vec![pair]).unwrap_or_default(),
held,
"the stop named {process:?}, which is not the one process this session holds"
);
let WaitKind::Live(registered) = &pending.kind else {
panic!("a launch guard is not a live open");
};
assert!(
matches!(e.presence_of(registered), Presence::Arrived),
"the stop was not delivered to the open that was waiting for it, so its `wait()` would \
pump again for an event that has already happened"
);
drop(pending);
e.end_session().expect("end_session failed");
}
#[test]
#[cfg(not(miri))]
fn test_a_wait_with_no_debuggee_fails_rather_than_expiring() {
let _debuggee = one_debuggee();
let e = DebugEngine::new();
assert!(
!e.has_target().expect("could not read the execution status"),
"a fresh engine reported a debuggee"
);
let waited = e.wait_for_event(300);
assert!(
waited.is_err(),
"a wait on an engine with no debuggee returned {waited:?} instead of failing — if it \
now expires, `presence_of`'s empty-session arm is reachable and wants a bound test"
);
}
#[test]
#[cfg(not(miri))]
fn test_an_expired_wait_records_no_stop() {
let _debuggee = one_debuggee();
let e = DebugEngine::new();
e.launch_process("ping.exe -n 30 127.0.0.1")
.expect("launch failed");
let watcher = watching_the_only_process(&e);
let outcome = e
.wait_for_event(300)
.expect("the wait should expire, not fail");
assert_eq!(
outcome,
WaitOutcome::Expired,
"a wait with nothing to report answered {outcome:?}, so the bound did not expire and \
nothing here is under test"
);
assert!(
!matches!(e.presence_of(&watcher), Presence::Arrived),
"a wait that stopped on nothing delivered a stop to an open waiting for one"
);
}
#[test]
#[cfg(not(miri))]
fn test_a_host_requested_break_records_no_stop() {
let _debuggee = one_debuggee();
let e = DebugEngine::new();
e.launch_process("ping.exe -n 30 127.0.0.1")
.expect("launch failed");
let watcher = watching_the_only_process(&e);
let handle = e.interrupt_handle();
let asked = std::thread::spawn(move || {
std::thread::sleep(Duration::from_millis(200));
handle.interrupt()
});
let run = e
.execute_and_wait("g", 30_000)
.expect("the go should return");
asked.join().expect("interrupting thread panicked").ok();
assert!(
matches!(run.cut_short, Some(Interruption::OnRequest)),
"the run ended as {:?}, so no host-requested break was under test",
run.cut_short
);
assert!(
!matches!(e.presence_of(&watcher), Presence::Arrived),
"a break a host asked for was delivered as the target arriving"
);
}
#[test]
#[cfg(not(miri))]
fn test_a_timed_out_run_leaves_no_interrupt_standing() {
let _debuggee = one_debuggee();
let e = DebugEngine::new();
e.launch_process("ping.exe -n 30 127.0.0.1")
.expect("launch failed");
let here = e
.instruction_pointer()
.expect("could not read the instruction pointer");
let run = e.run_to_address(here, 0).expect("run_to_address failed");
assert!(
matches!(run.outcome, RunToOutcome::Timeout),
"the run ended as {:?}, so its watchdog never raised the flag under test",
run.outcome
);
let scope = e.state.breaks.lock().unwrap_or_else(|err| err.into_inner());
assert!(
scope.running.is_empty() && scope.asked.is_empty(),
"a timed-out run left {scope:?} behind, so the next operation inherits it"
);
}
#[test]
#[cfg(not(miri))]
fn test_a_host_interrupt_ends_a_live_open_rather_than_pumping_through_it() {
let _debuggee = one_debuggee();
let e = DebugEngine::new();
e.launch_process("ping.exe -n 30 127.0.0.1")
.expect("launch failed");
unsafe { e.control.SetExecutionStatus(DEBUG_STATUS_GO) }.expect("could not set it running");
let handle = e.interrupt_handle();
let asked = std::thread::spawn(move || {
std::thread::sleep(Duration::from_millis(200));
handle.interrupt()
});
let absent = e
.session_processes()
.expect("could not list the session")
.iter()
.map(|(_, pid)| *pid)
.max()
.unwrap_or(4)
+ 1000;
let waiting = registered(&e, Arrival::Attached(absent));
let outcome = e.wait_for_live_target(&waiting);
asked.join().expect("interrupting thread panicked").ok();
assert!(
matches!(outcome, Err(DbgEngError::LiveTargetInterrupted)),
"a live open a host interrupted answered {outcome:?}, so it pumped through the break"
);
}
#[test]
#[cfg(not(miri))]
fn test_a_host_requested_break_records_no_stop_on_the_finite_wait() {
let _debuggee = one_debuggee();
let e = DebugEngine::new();
e.launch_process("ping.exe -n 30 127.0.0.1")
.expect("launch failed");
let watcher = watching_the_only_process(&e);
unsafe { e.control.SetExecutionStatus(DEBUG_STATUS_GO) }.expect("could not set it running");
let handle = e.interrupt_handle();
let asked = std::thread::spawn(move || {
std::thread::sleep(Duration::from_millis(200));
handle.interrupt()
});
let outcome = e
.wait_for_event(10_000)
.expect("the wait should return on the break");
asked.join().expect("interrupting thread panicked").ok();
assert_eq!(
outcome,
WaitOutcome::OnRequest,
"the finite wait answered {outcome:?} for a break a host asked for"
);
assert_eq!(
e.execution_status().ok(),
Some(DEBUG_STATUS_BREAK),
"the target is not stopped, so no break landed and nothing here is under test"
);
assert!(
!matches!(e.presence_of(&watcher), Presence::Arrived),
"a break a host asked for was delivered as an arrival by the finite wait"
);
}
#[test]
#[cfg(not(miri))]
fn test_the_openers_prune_clears_a_claim_on_a_departed_process() {
let _debuggee = one_debuggee();
let mut stays = a_process_to_attach_to();
{
let e = DebugEngine::new();
e.attach_process(stays.id()).expect("attach failed");
let launched = e
.launch_process_begin("cmd.exe /c ping -n 30 127.0.0.1")
.expect("launch failed");
let WaitKind::Live(registered) = &launched.kind else {
panic!("a launch guard is not a live open");
};
for _ in 0..5 {
if matches!(e.presence_of(registered), Presence::Arrived) {
break;
}
e.wait_for_event(LIVE_WAIT_MS)
.expect("the outside pump failed");
}
assert!(
matches!(e.presence_of(registered), Presence::Arrived),
"the launch was not delivered its process, so nothing here is under test"
);
let held = e.session_processes().expect("could not list the session");
let (id, pid) = *held
.iter()
.find(|(_, pid)| *pid != stays.id())
.expect("the launched process is not in the session");
e.execute_command(&format!("|{id}s"))
.expect("could not select the process to detach");
e.execute_command(".detach").expect("could not detach it");
let reattached = e.attach_process_begin(pid).expect("reattach failed");
reattached.wait().expect("the reattach should complete");
let back = e
.session_processes()
.expect("could not list the session")
.iter()
.any(|entry| *entry == (id, pid));
assert!(
back,
"the reattach did not get the pair ({id}, {pid}) back, so this run never met the \
collision and asserts nothing"
);
assert!(
!matches!(e.presence_of(registered), Presence::Arrived),
"the departed process's claim survived the opener's prune, so the pair it holds \
refuses the next open its own stop"
);
drop(launched);
let _ = e.end_session();
}
let _ = stays.kill();
let _ = stays.wait();
}
#[test]
#[cfg(not(miri))]
fn test_reclaiming_an_engine_id_does_not_reclaim_its_arrival() {
let _debuggee = one_debuggee();
let mut leaves = a_process_to_attach_to();
let mut stays = a_process_to_attach_to();
{
let e = DebugEngine::new();
e.attach_process(leaves.id()).expect("first attach failed");
e.attach_process(stays.id()).expect("second attach failed");
let held = e.session_processes().expect("could not list the session");
let (id, _) = *held
.iter()
.find(|(_, pid)| *pid == leaves.id())
.expect("the first process is not in the session");
e.execute_command(&format!("|{id}s"))
.expect("could not select the process to detach");
e.execute_command(".detach").expect("could not detach it");
let guard = e
.attach_process_begin(leaves.id())
.expect("reattach failed");
let WaitKind::Live(registered) = &guard.kind else {
panic!("an attach guard is not a live open");
};
assert!(
!matches!(e.presence_of(registered), Presence::Arrived),
"the reattach's open was arrived before its wait, so it inherited an answer about \
the process that left"
);
guard.wait().expect("the reattach should complete");
}
for p in [&mut leaves, &mut stays] {
let _ = p.kill();
let _ = p.wait();
}
}
#[test]
#[cfg(not(miri))]
fn test_a_watchdog_forced_break_records_no_stop() {
let _debuggee = one_debuggee();
let e = DebugEngine::new();
e.launch_process("ping.exe -n 30 127.0.0.1")
.expect("launch failed");
let watcher = watching_the_only_process(&e);
let run = e.execute_and_wait("g", 300).expect("the go should return");
assert!(
matches!(run.cut_short, Some(Interruption::Deadline { .. })),
"the target stopped on its own ({:?}), so no forced break was under test",
run.cut_short
);
assert!(
!matches!(e.presence_of(&watcher), Presence::Arrived),
"the watchdog's own Ctrl+Break was delivered as the target arriving"
);
}
#[test]
#[cfg(not(miri))]
fn test_a_session_holding_nothing_is_absence_rather_than_a_question() {
let _debuggee = one_debuggee();
let e = DebugEngine::new();
assert!(
matches!(
e.presence_of(®istered(&e, Arrival::Attached(4))),
Presence::Absent
),
"an engine with no debuggee could not say an attached pid was missing"
);
assert!(
matches!(
e.presence_of(®istered(&e, Arrival::Launched(None))),
Presence::Absent
),
"an engine with no debuggee deferred to the launch snapshot it does not have"
);
}
#[test]
#[cfg(not(miri))]
fn test_the_last_event_names_its_process_by_engine_id() {
let _debuggee = one_debuggee();
let mut theirs = a_process_to_attach_to();
let e = DebugEngine::new();
e.attach_process(theirs.id()).expect("attach failed");
e.launch_process("cmd.exe /c ping -n 30 127.0.0.1")
.expect("launch failed");
let held = e
.session_processes()
.expect("could not list the session's processes");
let stopped_on = e
.last_event_process()
.expect("the engine names no last event after a launch");
let (_, pid) = held
.iter()
.find(|(id, _)| *id == stopped_on)
.unwrap_or_else(|| {
panic!("the last event's process {stopped_on} is no engine id in {held:?}")
});
assert_ne!(
*pid,
theirs.id(),
"the launch's wait ended on the attached process's event rather than its own: {held:?}"
);
e.end_session().expect("end_session failed");
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"
);
}
#[test]
fn test_a_record_carries_only_the_parameters_it_filled() {
let mut raw = EXCEPTION_RECORD64 {
ExceptionCode: windows::Win32::Foundation::NTSTATUS(0xc000_0409_u32 as i32),
ExceptionFlags: EXCEPTION_NONCONTINUABLE,
ExceptionRecord: 0,
ExceptionAddress: 0x7ff6_3074_28f9,
NumberParameters: 3,
__unusedAlignment: 0,
ExceptionInformation: [0; 15],
};
raw.ExceptionInformation[0] = 0x7;
raw.ExceptionInformation[1] = 0xffff_ffff_8000_ffff;
raw.ExceptionInformation[2] = 0x28f;
raw.ExceptionInformation[3] = 0xdead_beef;
let record = ExceptionRecord::from_raw(&raw);
assert_eq!(record.code, 0xc000_0409, "the code was not read as a u32");
assert_eq!(
record.parameters,
vec![0x7, 0xffff_ffff_8000_ffff, 0x28f],
"the leftover in slot 3 was read as a parameter"
);
assert!(record.noncontinuable(), "the flag was not decoded");
assert_eq!(
record.nested, None,
"a zero nested pointer is no nested record, not a record at address zero"
);
raw.NumberParameters = 99;
assert_eq!(
ExceptionRecord::from_raw(&raw).parameters.len(),
15,
"an over-large NumberParameters was not clamped to the array"
);
raw.NumberParameters = 0;
raw.ExceptionRecord = 0x1234;
let nested = ExceptionRecord::from_raw(&raw);
assert_eq!(nested.nested, Some(0x1234));
assert!(
nested.parameters.is_empty(),
"a zero-parameter record carried parameters"
);
}
#[test]
fn test_extra_event_info_is_read_as_an_exception_only_when_it_holds_one() {
let mut extra = ExtraEventInfo::default();
let info = DEBUG_LAST_EVENT_INFO_EXCEPTION {
ExceptionRecord: EXCEPTION_RECORD64 {
ExceptionCode: windows::Win32::Foundation::NTSTATUS(0x8000_0003_u32 as i32),
ExceptionFlags: 0,
ExceptionRecord: 0,
ExceptionAddress: 0x7ffc_9b83_d78d,
NumberParameters: 1,
__unusedAlignment: 0,
ExceptionInformation: [0; 15],
},
FirstChance: 1,
};
unsafe { std::ptr::write(extra.as_mut_ptr().cast(), info) };
let (record, first_chance) = extra
.exception(DEBUG_EVENT_EXCEPTION, ExtraEventInfo::SIZE)
.expect("an exception event with a full write reported no exception");
assert_eq!(record.code, 0x8000_0003);
assert_eq!(record.address, 0x7ffc_9b83_d78d);
assert_eq!(record.parameters, vec![0]);
assert!(first_chance, "the first-chance flag was dropped");
assert!(
extra
.exception(DEBUG_EVENT_BREAKPOINT, ExtraEventInfo::SIZE)
.is_none(),
"a breakpoint event's extra information was read as an exception record"
);
assert!(
extra
.exception(DEBUG_EVENT_EXCEPTION, ExtraEventInfo::SIZE - 1)
.is_none(),
"a write shorter than the struct was read as the whole struct"
);
}
#[test]
#[cfg(not(miri))]
fn test_an_engine_with_no_event_reports_none_rather_than_a_kind_of_zero() {
let _debuggee = one_debuggee();
let e = DebugEngine::new();
assert!(
matches!(e.last_event(), Ok(None)),
"an engine that has seen no event did not report none — which means either it failed \
the call, or kind 0 was dressed up as an event"
);
}
#[test]
#[cfg(not(miri))]
fn test_an_opener_that_replaces_the_session_invalidates_the_old_contexts() {
let _guard = one_debuggee();
let dump = std::env::temp_dir().join(format!(
"dbgscope-replace-{}-{}.dmp",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_nanos())
));
let _ = std::fs::remove_file(&dump);
let e = DebugEngine::new();
if e.launch_process("cmd.exe /c ping -n 30 127.0.0.1").is_err() {
println!("SKIPPED: could not launch a target to dump");
return;
}
let written = e.execute_command(&format!(".dump /m \"{}\"", dump.display()));
let _ = e.end_session();
drop(e);
if written.is_err() || !dump.exists() {
println!("SKIPPED: this engine would not write a dump ({written:?})");
return;
}
let e = DebugEngine::new();
let path = dump.display().to_string();
assert!(
e.open_dump(&path).is_ok(),
"could not open the dump written"
);
let _ = e.wait_for_event(60_000);
let first = e.target_identity();
let stale = ThreadContext {
bytes: vec![0u8; 1232],
target: first,
};
assert!(
e.open_dump(&path).is_ok(),
"could not reopen the dump through the same engine"
);
let _ = e.wait_for_event(60_000);
assert_ne!(
e.target_identity(),
first,
"an opener replaced the session without reissuing the identity, so every context and \
scope saved from the previous target is still accepted against this one"
);
assert!(
matches!(
e.stack_frames_from(&stale, 8),
Err(DbgEngError::ContextFromAnotherTarget)
),
"a context from the replaced target was walked against the new one"
);
let _ = e.end_session();
drop(e);
let _ = std::fs::remove_file(&dump);
}
#[test]
#[cfg(not(miri))]
fn test_a_context_from_another_target_is_refused() {
let _debuggee = one_debuggee();
let e = DebugEngine::new();
let stale = ThreadContext {
bytes: vec![0u8; 1232],
target: e.target_identity().wrapping_add(1),
};
assert!(
matches!(
e.stack_frames_from(&stale, 8),
Err(DbgEngError::ContextFromAnotherTarget)
),
"a context from another target was walked rather than refused"
);
let current = ThreadContext {
bytes: vec![0u8; 1232],
target: e.target_identity(),
};
assert!(
!matches!(
e.stack_frames_from(¤t, 8),
Err(DbgEngError::ContextFromAnotherTarget)
),
"a context from this very engine was refused as another target's"
);
}
#[test]
fn test_only_e_unexpected_means_there_is_no_stored_event() {
assert!(no_stored_event(E_UNEXPECTED));
for other in [E_INVALIDARG, E_NOINTERFACE, S_OK, S_FALSE] {
assert!(
!no_stored_event(other),
"{other:?} was taken for the absence of a stored event"
);
}
}
#[test]
fn test_a_breakpoint_action_reaches_the_engine_as_its_status() {
for (action, status) in [
(BreakpointAction::Default, DEBUG_STATUS_NO_CHANGE),
(BreakpointAction::Go, DEBUG_STATUS_GO),
(BreakpointAction::Break, DEBUG_STATUS_BREAK),
] {
assert_eq!(action.status(), status, "{action:?}");
assert_eq!(
HRESULT::from(event_status(action.status())),
HRESULT(status as i32),
"{action:?} did not reach the engine as its own status"
);
}
}
#[test]
#[cfg(not(miri))]
fn test_a_breakpoint_callback_reads_its_hit_and_lets_it_go_past() {
let _debuggee = one_debuggee();
let engine = DebugEngine::new();
engine
.launch_process("ping.exe -n 30 127.0.0.1")
.expect("launch failed");
let set = engine
.set_breakpoint(&BreakpointSpec::code(BreakpointAt::Expression(
"ntdll!RtlAllocateHeap".into(),
)))
.expect("set breakpoint failed");
let address = set
.breakpoint
.address
.expect("an exported function resolves");
let pc = match engine.processor_type() {
Ok(0xaa64) => "pc",
_ => "rip",
};
type Seen = Result<(u32, u64, u64, u64), String>;
let hits: std::rc::Rc<RefCell<Vec<Seen>>> = std::rc::Rc::default();
let recorded = std::rc::Rc::clone(&hits);
let callback: BreakpointCallback = Box::new(move |hit: &BreakpointHit<'_>| {
let read = (|| {
Ok::<_, DbgEngError>((
hit.id()?,
hit.address()?,
hit.engine().integer_register(pc)?,
hit.engine().current_thread_data_offset()?,
))
})();
let mut recorded = recorded.borrow_mut();
recorded.push(read.map_err(|e| e.to_string()));
if recorded.len() < 2 {
BreakpointAction::Go
} else {
BreakpointAction::Break
}
});
engine
.set_breakpoint_callback(callback)
.expect("registering the callbacks failed");
let run = engine.execute_and_wait("g", 20_000);
engine
.clear_breakpoint_callback()
.expect("unregistering the callbacks failed");
let run = run.expect("go failed");
assert!(
run.cut_short.is_none() && !run.target_gone,
"the run must end on the callback's Break: {run:?}"
);
let hits = hits.borrow();
assert_eq!(
hits.len(),
2,
"Go on the first hit and Break on the second: {hits:?}"
);
for hit in hits.iter() {
let (id, at, pc_value, thread) = hit.clone().expect("every read through the view");
assert_eq!(id, set.breakpoint.id);
assert_eq!(at, address);
assert_eq!(pc_value, address, "`{pc}` at the hit is the breakpoint");
assert_ne!(thread, 0, "a thread's data offset");
}
assert_eq!(engine.instruction_pointer().ok(), Some(address));
let not_integer = engine
.register_values()
.expect("registers")
.into_iter()
.find(|register| {
matches!(
register.value,
RegisterValue::Float(_) | RegisterValue::Bytes(_)
)
})
.map(|register| register.name);
if let Some(name) = not_integer {
assert!(
matches!(
engine.integer_register(&name),
Err(DbgEngError::Register { .. })
),
"`{name}` read as an integer"
);
}
engine
.remove_breakpoint(set.breakpoint.id)
.expect("removing the breakpoint failed");
engine.end_session().expect("ending the session failed");
}
#[test]
fn test_a_panicking_breakpoint_callback_leaves_the_engine_its_default() {
assert_eq!(
unwound_to_default(|| panic!("a callback that panicked")),
BreakpointAction::Default
);
assert_eq!(
unwound_to_default(|| BreakpointAction::Go),
BreakpointAction::Go
);
}
}
fn event_status(status: u32) -> windows::core::Result<()> {
if status == DEBUG_STATUS_NO_CHANGE {
Ok(())
} else {
Err(windows::core::Error::from_hresult(HRESULT(status as i32)))
}
}
fn unwound_to_default(callback: impl FnOnce() -> BreakpointAction) -> BreakpointAction {
std::panic::catch_unwind(std::panic::AssertUnwindSafe(callback))
.unwrap_or(BreakpointAction::Default)
}
#[windows::core::implement(
windows::Win32::System::Diagnostics::Debug::Extensions::IDebugEventContextCallbacks
)]
struct DebugEventContextCallbacks {
callback: BreakpointCallback,
client: *mut std::ffi::c_void,
}
#[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,
_context_size: u32,
) -> windows::core::Result<()> {
let Some(bp) = bp.as_ref() else {
return Ok(());
};
let Some(client) = (unsafe { IDebugClient6::from_raw_borrowed(&self.client) }) else {
return Ok(());
};
let Ok(hit) = BreakpointHit::new(bp, client.clone()) else {
return Ok(());
};
event_status(unwound_to_default(|| (self.callback)(&hit)).status())
}
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(())
}
}