Skip to content
This repository was archived by the owner on Nov 1, 2023. It is now read-only.

Commit 2ff9c47

Browse files
authored
Explicitly track debuggee thread state to detect thread exit/debug event races (#1000)
- Define an enum to track the debugger's current understanding of debuggee thread state - Update our private suspend/resume methods to update and return the current state - Detect thread exit/debug event races in suspend/resume calls
1 parent a58df95 commit 2ff9c47

File tree

1 file changed

+91
-25
lines changed

1 file changed

+91
-25
lines changed

src/agent/debugger/src/target.rs

+91-25
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,17 @@
33

44
#![allow(clippy::single_match)]
55

6-
use std::{num::NonZeroU64, path::Path};
6+
use std::{io, num::NonZeroU64, path::Path};
77

8-
use anyhow::Result;
8+
use anyhow::{format_err, Result};
99
use log::{debug, error, trace};
1010
use rand::{thread_rng, Rng};
11-
use win_util::{last_os_error, process};
11+
use win_util::process;
1212
use winapi::{
13-
shared::minwindef::{DWORD, LPCVOID},
13+
shared::{
14+
minwindef::{DWORD, LPCVOID},
15+
winerror::ERROR_ACCESS_DENIED,
16+
},
1417
um::{
1518
processthreadsapi::{ResumeThread, SuspendThread},
1619
winbase::Wow64SuspendThread,
@@ -31,55 +34,117 @@ pub(crate) enum StepState {
3134
SingleStep,
3235
}
3336

37+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
38+
enum ThreadState {
39+
Runnable,
40+
Suspended,
41+
Exited,
42+
}
43+
3444
struct ThreadInfo {
3545
#[allow(unused)]
3646
id: u32,
3747
handle: HANDLE,
38-
suspended: bool,
48+
state: ThreadState,
3949
wow64: bool,
4050
}
4151

52+
const SUSPEND_RESUME_ERROR_CODE: DWORD = -1i32 as DWORD;
53+
4254
impl ThreadInfo {
4355
fn new(id: u32, handle: HANDLE, wow64: bool) -> Self {
4456
ThreadInfo {
4557
id,
4658
handle,
4759
wow64,
48-
suspended: false,
60+
state: ThreadState::Runnable,
4961
}
5062
}
5163

52-
fn resume_thread(&mut self) -> Result<()> {
53-
if !self.suspended {
54-
return Ok(());
64+
fn resume_thread(&mut self) -> Result<ThreadState> {
65+
if self.state == ThreadState::Runnable {
66+
return Ok(self.state);
5567
}
5668

57-
let suspend_count = unsafe { ResumeThread(self.handle) };
58-
if suspend_count == (-1i32 as DWORD) {
59-
Err(last_os_error())
60-
} else {
61-
self.suspended = false;
62-
Ok(())
69+
let prev_suspend_count = unsafe { ResumeThread(self.handle) };
70+
71+
match prev_suspend_count {
72+
SUSPEND_RESUME_ERROR_CODE => {
73+
let os_error = io::Error::last_os_error();
74+
75+
if Self::is_os_error_from_exited_thread(&os_error)? {
76+
self.state = ThreadState::Exited;
77+
} else {
78+
return Err(os_error.into());
79+
}
80+
}
81+
0 => {
82+
// No-op: thread was runnable, and is still runnable.
83+
self.state = ThreadState::Runnable;
84+
}
85+
1 => {
86+
// Was suspended, now runnable.
87+
self.state = ThreadState::Runnable;
88+
}
89+
_ => {
90+
// Previous suspend count > 1. Was suspended, still is.
91+
self.state = ThreadState::Suspended;
92+
}
6393
}
94+
95+
Ok(self.state)
6496
}
6597

66-
fn suspend_thread(&mut self) -> Result<()> {
67-
if self.suspended {
68-
return Ok(());
98+
fn suspend_thread(&mut self) -> Result<ThreadState> {
99+
if self.state == ThreadState::Suspended {
100+
return Ok(self.state);
69101
}
70102

71-
let suspend_count = if self.wow64 {
103+
let prev_suspend_count = if self.wow64 {
72104
unsafe { Wow64SuspendThread(self.handle) }
73105
} else {
74106
unsafe { SuspendThread(self.handle) }
75107
};
76108

77-
if suspend_count == (-1i32 as DWORD) {
78-
Err(last_os_error())
79-
} else {
80-
self.suspended = true;
81-
Ok(())
109+
match prev_suspend_count {
110+
SUSPEND_RESUME_ERROR_CODE => {
111+
let os_error = io::Error::last_os_error();
112+
113+
if Self::is_os_error_from_exited_thread(&os_error)? {
114+
self.state = ThreadState::Exited;
115+
} else {
116+
return Err(os_error.into());
117+
}
118+
}
119+
_ => {
120+
// Suspend count was incremented. Even if the matched value is 0, it means
121+
// the current suspend count is 1, and the thread is suspended.
122+
self.state = ThreadState::Suspended;
123+
}
82124
}
125+
126+
Ok(self.state)
127+
}
128+
129+
fn is_os_error_from_exited_thread(os_error: &io::Error) -> Result<bool> {
130+
let raw_os_error = os_error
131+
.raw_os_error()
132+
.ok_or_else(|| format_err!("OS error missing raw error"))?;
133+
134+
let exited = match raw_os_error as DWORD {
135+
ERROR_ACCESS_DENIED => {
136+
// Assume, as a debugger, we always have the `THREAD_SUSPEND_RESUME`
137+
// access right, and thus we should interpret this error to mean that
138+
// the thread has exited.
139+
//
140+
// This means we are observing a race between OS-level thread exit and
141+
// the (pending) debug event.
142+
true
143+
}
144+
_ => false,
145+
};
146+
147+
Ok(exited)
83148
}
84149
}
85150

@@ -104,6 +169,7 @@ pub struct Target {
104169
sym_initialize_state: SymInitalizeState,
105170
exited: bool,
106171

172+
// Map of thread ID to thread info.
107173
thread_info: fnv::FnvHashMap<DWORD, ThreadInfo>,
108174

109175
// We cache the current thread context for possible repeated queries and modifications.
@@ -118,7 +184,7 @@ pub struct Target {
118184
// Breakpoints that are not yet resolved to a virtual address, so either an RVA or symbol.
119185
unresolved_breakpoints: Vec<UnresolvedBreakpoint>,
120186

121-
// Map of thread to stepping state (e.g. breakpoint address to restore breakpoints)
187+
// Map of thread ID to stepping state (e.g. breakpoint address to restore breakpoints)
122188
single_step: fnv::FnvHashMap<DWORD, StepState>,
123189

124190
// When stepping after hitting a breakpoint, we need to restore the breakpoint.

0 commit comments

Comments
 (0)