Skip to content

Commit 21374b3

Browse files
authored
Defer setting coverage breakpoints (microsoft#2832)
1 parent 0c58a59 commit 21374b3

File tree

2 files changed

+164
-41
lines changed

2 files changed

+164
-41
lines changed

src/agent/coverage/src/record/windows.rs

Lines changed: 163 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -4,36 +4,49 @@
44
use std::collections::BTreeMap;
55
use std::path::Path;
66

7-
use anyhow::{anyhow, Error, Result};
7+
use anyhow::{anyhow, bail, Error, Result};
8+
use debuggable_module::debuginfo::{DebugInfo, Function};
89
use debuggable_module::load_module::LoadModule;
910
use debuggable_module::loader::Loader;
1011
use debuggable_module::path::FilePath;
1112
use debuggable_module::windows::WindowsModule;
12-
use debuggable_module::Offset;
13+
use debuggable_module::{Module, Offset};
1314
use debugger::{BreakpointId, BreakpointType, DebugEventHandler, Debugger, ModuleLoadInfo};
1415

1516
use crate::allowlist::TargetAllowList;
1617
use crate::binary::{self, BinaryCoverage};
1718

19+
// For a new module image, we defer setting coverage breakpoints until exit from one of these
20+
// functions (when present). This avoids breaking hotpatching routines in the ASan interceptor
21+
// initializers.
22+
//
23+
// We will use these constants with a prefix comparison. Only check up to the first open paren to
24+
// avoid false negatives if the demangled representation of parameters ever varies.
25+
const PROCESS_IMAGE_DEFERRAL_TRIGGER: &str = "__asan::AsanInitInternal(";
26+
const LIBRARY_IMAGE_DEFERRAL_TRIGGER: &str = "DllMain(";
27+
1828
pub struct WindowsRecorder<'data> {
1929
allowlist: TargetAllowList,
2030
breakpoints: Breakpoints,
31+
deferred_breakpoints: BTreeMap<BreakpointId, (Breakpoint, DeferralState)>,
2132
pub coverage: BinaryCoverage,
2233
loader: &'data Loader,
23-
modules: BTreeMap<FilePath, WindowsModule<'data>>,
34+
modules: BTreeMap<FilePath, (WindowsModule<'data>, DebugInfo)>,
2435
pub stop_error: Option<Error>,
2536
}
2637

2738
impl<'data> WindowsRecorder<'data> {
2839
pub fn new(loader: &'data Loader, allowlist: TargetAllowList) -> Self {
2940
let breakpoints = Breakpoints::default();
41+
let deferred_breakpoints = BTreeMap::new();
3042
let coverage = BinaryCoverage::default();
3143
let modules = BTreeMap::new();
3244
let stop_error = None;
3345

3446
Self {
3547
allowlist,
3648
breakpoints,
49+
deferred_breakpoints,
3750
coverage,
3851
loader,
3952
modules,
@@ -66,11 +79,59 @@ impl<'data> WindowsRecorder<'data> {
6679
self.insert_module(dbg, module)
6780
}
6881

69-
fn try_on_breakpoint(&mut self, _dbg: &mut Debugger, id: BreakpointId) -> Result<()> {
70-
let breakpoint = self
71-
.breakpoints
72-
.remove(id)
73-
.ok_or_else(|| anyhow!("stopped on dangling breakpoint"))?;
82+
fn try_on_breakpoint(&mut self, dbg: &mut Debugger, id: BreakpointId) -> Result<()> {
83+
if let Some((trigger, state)) = self.deferred_breakpoints.remove(&id) {
84+
match state {
85+
DeferralState::NotEntered => {
86+
debug!(
87+
"hit trigger breakpoint (not-entered) for deferred coverage breakpoints"
88+
);
89+
90+
// Find the return address.
91+
let frame = dbg.get_current_frame()?;
92+
let ret = frame.return_address();
93+
let id = dbg.new_address_breakpoint(ret, BreakpointType::OneTime)?;
94+
95+
// Update the state for this deferral to set module coverage breakpoints on ret.
96+
let thread_id = dbg.get_current_thread_id();
97+
let state = DeferralState::PendingReturn { thread_id };
98+
self.deferred_breakpoints.insert(id, (trigger, state));
99+
}
100+
DeferralState::PendingReturn { thread_id } => {
101+
debug!(
102+
"hit trigger breakpoint (pending-return) for deferred coverage breakpoints"
103+
);
104+
105+
if dbg.get_current_thread_id() == thread_id {
106+
debug!("correct thread hit pending-return trigger breakpoint");
107+
108+
// We've returned from the trigger function, and on the same thread.
109+
//
110+
// It's safe to set coverage breakpoints.
111+
self.set_module_breakpoints(dbg, trigger.module)?;
112+
} else {
113+
warn!("wrong thread saw pending-return trigger breakpoint, resetting");
114+
115+
// Hit a ret breakpoint, but on the wrong thread. Reset it so the correct
116+
// thread has a chance to see it.
117+
//
118+
// We only defer breakpoints in image initialization code, so we don't
119+
// expect to reach this code in practice.
120+
let id = trigger.set(dbg)?;
121+
self.deferred_breakpoints.insert(id, (trigger, state));
122+
}
123+
}
124+
}
125+
126+
return Ok(());
127+
}
128+
129+
let breakpoint = self.breakpoints.remove(id);
130+
131+
let Some(breakpoint) = breakpoint else {
132+
let stack = dbg.get_current_stack()?;
133+
bail!("stopped on dangling breakpoint, debuggee stack:\n{}", stack);
134+
};
74135

75136
let coverage = self
76137
.coverage
@@ -91,6 +152,11 @@ impl<'data> WindowsRecorder<'data> {
91152
fn insert_module(&mut self, dbg: &mut Debugger, module: &ModuleLoadInfo) -> Result<()> {
92153
let path = FilePath::new(module.path().to_string_lossy())?;
93154

155+
if self.modules.contains_key(&path) {
156+
warn!("module coverage already initialized, skipping");
157+
return Ok(());
158+
}
159+
94160
if !self.allowlist.modules.is_allowed(&path) {
95161
debug!("not inserting denylisted module: {path}");
96162
return Ok(());
@@ -103,72 +169,123 @@ impl<'data> WindowsRecorder<'data> {
103169
return Ok(());
104170
};
105171

106-
let coverage = binary::find_coverage_sites(&module, &self.allowlist)?;
172+
let debuginfo = module.debuginfo()?;
173+
self.modules.insert(path.clone(), (module, debuginfo));
174+
175+
self.set_or_defer_module_breakpoints(dbg, path)?;
176+
177+
Ok(())
178+
}
179+
180+
fn set_or_defer_module_breakpoints(
181+
&mut self,
182+
dbg: &mut Debugger,
183+
path: FilePath,
184+
) -> Result<()> {
185+
let (_module, debuginfo) = &self.modules[&path];
186+
187+
// For borrowck.
188+
let mut trigger = None;
189+
190+
for function in debuginfo.functions() {
191+
// Called on process startup.
192+
if function.name.starts_with(PROCESS_IMAGE_DEFERRAL_TRIGGER) {
193+
trigger = Some(function.clone());
194+
break;
195+
}
196+
197+
// Called on shared library load.
198+
if function.name.starts_with(LIBRARY_IMAGE_DEFERRAL_TRIGGER) {
199+
trigger = Some(function.clone());
200+
break;
201+
}
202+
}
203+
204+
if let Some(trigger) = trigger {
205+
debug!("deferring coverage breakpoints for module {}", path);
206+
self.defer_module_breakpoints(dbg, path, trigger)
207+
} else {
208+
debug!(
209+
"immediately setting coverage breakpoints for module {}",
210+
path
211+
);
212+
self.set_module_breakpoints(dbg, path)
213+
}
214+
}
215+
216+
fn defer_module_breakpoints(
217+
&mut self,
218+
dbg: &mut Debugger,
219+
path: FilePath,
220+
trigger: Function,
221+
) -> Result<()> {
222+
let state = DeferralState::NotEntered;
223+
let entry_breakpoint = Breakpoint::new(path, trigger.offset);
224+
let id = entry_breakpoint.set(dbg)?;
225+
226+
self.deferred_breakpoints
227+
.insert(id, (entry_breakpoint, state));
228+
229+
Ok(())
230+
}
231+
232+
fn set_module_breakpoints(&mut self, dbg: &mut Debugger, path: FilePath) -> Result<()> {
233+
let (module, _) = &self.modules[&path];
234+
let coverage = binary::find_coverage_sites(module, &self.allowlist)?;
107235

108236
for offset in coverage.as_ref().keys().copied() {
109237
let breakpoint = Breakpoint::new(path.clone(), offset);
110238
self.breakpoints.set(dbg, breakpoint)?;
111239
}
112240

113-
self.coverage.modules.insert(path.clone(), coverage);
241+
let count = coverage.offsets.len();
242+
debug!("set {} breakpoints for module {}", count, path);
114243

115-
self.modules.insert(path, module);
244+
self.coverage.modules.insert(path, coverage);
116245

117246
Ok(())
118247
}
119248
}
120249

121250
#[derive(Debug, Default)]
122251
struct Breakpoints {
123-
id_to_offset: BTreeMap<BreakpointId, Offset>,
124-
offset_to_breakpoint: BTreeMap<Offset, Breakpoint>,
252+
id_to_breakpoint: BTreeMap<BreakpointId, Breakpoint>,
125253
}
126254

127255
impl Breakpoints {
128256
pub fn set(&mut self, dbg: &mut Debugger, breakpoint: Breakpoint) -> Result<()> {
129-
if self.is_set(&breakpoint) {
130-
return Ok(());
131-
}
132-
133-
self.write(dbg, breakpoint)
134-
}
135-
136-
// Unguarded action that ovewrites both the target process address space and our index
137-
// of known breakpoints. Callers must use `set()`, which avoids redundant breakpoint
138-
// setting.
139-
fn write(&mut self, dbg: &mut Debugger, breakpoint: Breakpoint) -> Result<()> {
140-
// The `debugger` crates tracks loaded modules by base name. If a path or file
141-
// name is used, software breakpoints will not be written.
142-
let name = Path::new(breakpoint.module.base_name());
143-
let id = dbg.new_rva_breakpoint(name, breakpoint.offset.0, BreakpointType::OneTime)?;
144-
145-
self.id_to_offset.insert(id, breakpoint.offset);
146-
self.offset_to_breakpoint
147-
.insert(breakpoint.offset, breakpoint);
148-
257+
let id = breakpoint.set(dbg)?;
258+
self.id_to_breakpoint.insert(id, breakpoint);
149259
Ok(())
150260
}
151261

152-
pub fn is_set(&self, breakpoint: &Breakpoint) -> bool {
153-
self.offset_to_breakpoint.contains_key(&breakpoint.offset)
154-
}
155-
262+
/// Remove a registered breakpoint from the index.
263+
///
264+
/// This method should be called when a breakpoint is hit to retrieve its associated data.
265+
/// It does NOT clear the actual breakpoint in the debugger engine.
156266
pub fn remove(&mut self, id: BreakpointId) -> Option<Breakpoint> {
157-
let offset = self.id_to_offset.remove(&id)?;
158-
self.offset_to_breakpoint.remove(&offset)
267+
self.id_to_breakpoint.remove(&id)
159268
}
160269
}
161270

162271
#[derive(Clone, Debug)]
163272
struct Breakpoint {
164-
module: FilePath,
165-
offset: Offset,
273+
pub module: FilePath,
274+
pub offset: Offset,
166275
}
167276

168277
impl Breakpoint {
169278
pub fn new(module: FilePath, offset: Offset) -> Self {
170279
Self { module, offset }
171280
}
281+
282+
pub fn set(&self, dbg: &mut Debugger) -> Result<BreakpointId> {
283+
// The `debugger` crates tracks loaded modules by base name. If a path or file
284+
// name is used, software breakpoints will not be written.
285+
let name = Path::new(self.module.base_name());
286+
let id = dbg.new_rva_breakpoint(name, self.offset.0, BreakpointType::OneTime)?;
287+
Ok(id)
288+
}
172289
}
173290

174291
impl<'data> DebugEventHandler for WindowsRecorder<'data> {
@@ -193,3 +310,8 @@ impl<'data> DebugEventHandler for WindowsRecorder<'data> {
193310
}
194311
}
195312
}
313+
314+
enum DeferralState {
315+
NotEntered,
316+
PendingReturn { thread_id: u64 },
317+
}

src/agent/debuggable-module/src/debuginfo.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ impl DebugInfo {
4040
}
4141
}
4242

43+
#[derive(Clone, Debug)]
4344
pub struct Function {
4445
pub name: String,
4546
pub offset: Offset,

0 commit comments

Comments
 (0)