diff --git a/src/agent/coverage/examples/block_coverage.rs b/src/agent/coverage/examples/block_coverage.rs index cdf90a0fb..b08358f06 100644 --- a/src/agent/coverage/examples/block_coverage.rs +++ b/src/agent/coverage/examples/block_coverage.rs @@ -1,46 +1,45 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use std::path::PathBuf; +use std::process::Command; use anyhow::Result; use structopt::StructOpt; +#[derive(Debug, PartialEq, StructOpt)] +struct Opt { + #[cfg(target_os = "linux")] + #[structopt(short, long)] + filter: Option, + + #[structopt(min_values = 1)] + cmd: Vec, +} + #[cfg(target_os = "windows")] fn main() -> Result<()> { - use std::process::Command; - env_logger::init(); - let mut args = std::env::args().skip(1); - let exe = args.next().unwrap(); - let args: Vec<_> = args.collect(); + let opt = Opt::from_args(); + log::info!("recording coverage for: {:?}", opt.cmd); - let mut cmd = Command::new(exe); - cmd.args(&args); + let mut cmd = Command::new(&opt.cmd[0]); + cmd.args(&opt.cmd[1..]); let coverage = coverage::block::windows::record(cmd)?; - let hit = coverage.count_blocks_hit(); - let found = coverage.count_blocks(); - let percent = 100.0 * (hit as f64) / (found as f64); - log::info!("block coverage = {}/{} ({:.2}%)", hit, found, percent); + for (module, cov) in coverage.iter() { + let total = cov.blocks.len(); + let hit: u32 = cov.blocks.values().map(|b| b.count).sum(); + let percent = 100.0 * (hit as f64) / (total as f64); + log::info!("module = {}, {} / {} ({:.2}%)", module, hit, total, percent); + } Ok(()) } -#[derive(Debug, PartialEq, StructOpt)] -struct Opt { - #[structopt(short, long)] - filter: Option, - - cmd: Vec, -} - #[cfg(target_os = "linux")] fn main() -> Result<()> { - use std::process::Command; - use coverage::block::linux::Recorder; use coverage::code::{CmdFilter, CmdFilterDef}; diff --git a/src/agent/coverage/src/block/windows.rs b/src/agent/coverage/src/block/windows.rs index 768f32acd..b68e48cec 100644 --- a/src/agent/coverage/src/block/windows.rs +++ b/src/agent/coverage/src/block/windows.rs @@ -7,162 +7,261 @@ use std::time::{Duration, Instant}; use anyhow::Result; use debugger::{ - dbghelp::SymInfo, debugger::{BreakpointId, BreakpointType, DebugEventHandler, Debugger}, target::Module, }; -use crate::{AppCoverageBlocks, ModuleCoverageBlocks}; +use crate::block::CommandBlockCov; +use crate::cache::ModuleCache; +use crate::code::ModulePath; -pub fn record(cmd: Command) -> Result { - let mut handler = BlockCoverageHandler::new(); - - let (mut dbg, _child) = Debugger::init(cmd, &mut handler)?; - dbg.run(&mut handler)?; - - Ok(handler.coverage) +pub fn record(cmd: Command) -> Result { + let mut cache = ModuleCache::default(); + let recorder = Recorder::new(&mut cache); + let timeout = Duration::from_secs(5); + let mut handler = RecorderEventHandler::new(recorder, timeout); + handler.run(cmd)?; + Ok(handler.recorder.into_coverage()) } -pub struct BlockCoverageHandler { - bp_to_block: BTreeMap, - coverage: AppCoverageBlocks, +#[derive(Debug)] +pub struct RecorderEventHandler<'a> { + recorder: Recorder<'a>, started: Instant, - max_duration: Duration, timed_out: bool, + timeout: Duration, } -impl BlockCoverageHandler { - pub fn new() -> Self { - let coverage = AppCoverageBlocks::new(); - let bp_to_block = BTreeMap::default(); +impl<'a> RecorderEventHandler<'a> { + pub fn new(recorder: Recorder<'a>, timeout: Duration) -> Self { let started = Instant::now(); - let max_duration = Duration::from_secs(5); let timed_out = false; Self { - bp_to_block, - coverage, - max_duration, + recorder, started, timed_out, + timeout, } } - pub fn pc(&self, dbg: &mut Debugger) -> Result<(u64, Option)> { - use iced_x86::Register::RIP; - - let pc = dbg.read_register_u64(RIP)?; - let sym = dbg.get_symbol(pc).ok(); - - Ok((pc, sym)) + pub fn time_out(&self) -> bool { + self.timed_out } - fn add_module(&mut self, dbg: &mut Debugger, module: &Module) { - let bitset = crate::pe::process_image(module.path(), false); - let bitset = match bitset { - Ok(bitset) => bitset, - Err(err) => { - // If we can't add the module, continue debugging. - // We don't expect to have symbols for every module. - log::warn!( - "cannot record coverage for module = {}, err = {}", - module.path().display(), - err, - ); - return; - } - }; + pub fn timeout(&self) -> Duration { + self.timeout + } - let module_coverage = ModuleCoverageBlocks::new(module.path(), module.name(), bitset); + pub fn run(&mut self, cmd: Command) -> Result<()> { + let (mut dbg, _child) = Debugger::init(cmd, self)?; + dbg.run(self)?; + Ok(()) + } - let m = self.coverage.add_module(module_coverage); - let module_coverage = &self.coverage.modules()[m]; - for (b, block) in module_coverage.blocks().iter().enumerate() { - let bp = - dbg.register_breakpoint(module.name(), block.rva() as u64, BreakpointType::OneTime); - self.bp_to_block.insert(bp, (m, b)); + fn on_poll(&mut self, dbg: &mut Debugger) { + if !self.timed_out && self.started.elapsed() > self.timeout { + self.timed_out = true; + dbg.quit_debugging(); } - - log::debug!( - "inserted {} breakpoints for module {}", - module_coverage.blocks().len(), - module.path().display(), - ); } fn stop(&self, dbg: &mut Debugger) { dbg.quit_debugging(); } +} - fn handle_on_create_process(&mut self, dbg: &mut Debugger, module: &Module) -> Result<()> { - dbg.target().sym_initialize()?; +#[derive(Debug)] +pub struct Recorder<'a> { + breakpoints: Breakpoints, - log::info!( - "exe loaded: {}, {} bytes", - module.path().display(), - module.image_size(), - ); + // Reference to allow in-memory reuse across runs. + cache: &'a mut ModuleCache, - self.add_module(dbg, module); + // Note: this could also be a reference to enable reuse across runs, to + // support implicit calculation of total coverage for a corpus. For now, + // assume callers will merge this into a separate struct when needed. + coverage: CommandBlockCov, +} - Ok(()) +impl<'a> Recorder<'a> { + pub fn new(cache: &'a mut ModuleCache) -> Self { + let breakpoints = Breakpoints::default(); + let coverage = CommandBlockCov::default(); + + Self { + breakpoints, + cache, + coverage, + } } - fn handle_on_load_dll(&mut self, dbg: &mut Debugger, module: &Module) { - log::info!( - "dll loaded: {}, {} bytes", - module.path().display(), - module.image_size(), - ); - - self.add_module(dbg, module); + pub fn coverage(&self) -> &CommandBlockCov { + &self.coverage } - fn handle_on_breakpoint(&mut self, dbg: &mut Debugger, bp: BreakpointId) -> Result<()> { - let (pc, _sym) = self.pc(dbg)?; + pub fn into_coverage(self) -> CommandBlockCov { + self.coverage + } - if let Some(&(m, b)) = self.bp_to_block.get(&bp) { + pub fn on_create_process(&mut self, dbg: &mut Debugger, module: &Module) -> Result<()> { + log::debug!("process created: {}", module.path().display()); + + if let Err(err) = dbg.target().sym_initialize() { + log::error!( + "unable to initialize symbol handler for new process {}: {:?}", + module.path().display(), + err, + ); + } + + self.insert_module(dbg, module) + } + + pub fn on_load_dll(&mut self, dbg: &mut Debugger, module: &Module) -> Result<()> { + log::debug!("DLL loaded: {}", module.path().display()); + + self.insert_module(dbg, module) + } + + pub fn on_breakpoint(&mut self, dbg: &mut Debugger, id: BreakpointId) -> Result<()> { + if let Some(breakpoint) = self.breakpoints.get(id) { if log::max_level() == log::Level::Trace { - let module = &self.coverage.modules()[m]; - let block = &module.blocks()[b]; - let name = module.name().display(); - log::trace!("{:>16x}: {}+{:x}", pc, name, block.rva()); + let name = breakpoint.module.name().to_string_lossy(); + let offset = breakpoint.offset; + let pc = dbg.read_program_counter()?; + + if let Ok(sym) = dbg.get_symbol(pc) { + log::trace!( + "{:>16x}: {}+{:x} ({}+{:x})", + pc, + name, + offset, + sym.symbol(), + sym.displacement(), + ); + } else { + log::trace!("{:>16x}: {}+{:x}", pc, name, offset); + } } - self.coverage.report_block_hit(m, b); + self.coverage + .increment(breakpoint.module, breakpoint.offset)?; } else { - log::error!("no block for breakpoint"); + let pc = if let Ok(pc) = dbg.read_program_counter() { + format!("{:x}", pc) + } else { + "???".into() + }; + + log::error!("hit breakpoint without data, id = {}, pc = {}", id.0, pc); } Ok(()) } - fn handle_on_poll(&mut self, dbg: &mut Debugger) { - if !self.timed_out && self.started.elapsed() > self.max_duration { - self.timed_out = true; - dbg.quit_debugging(); + fn insert_module(&mut self, dbg: &mut Debugger, module: &Module) -> Result<()> { + let path = ModulePath::new(module.path().to_owned())?; + + match self.cache.fetch(&path) { + Ok(Some(info)) => { + let new = self.coverage.insert(&path, info.blocks.iter().copied()); + + if !new { + return Ok(()); + } + + self.breakpoints + .set(dbg, module, info.blocks.iter().copied())?; + + log::debug!("set {} breakpoints for module {}", info.blocks.len(), path); + } + Ok(None) => { + log::warn!("could not find module: {}", path); + } + Err(err) => { + log::warn!("could not disassemble module {}: {:?}", path, err); + } } + + Ok(()) } } -impl DebugEventHandler for BlockCoverageHandler { +impl<'a> DebugEventHandler for RecorderEventHandler<'a> { fn on_create_process(&mut self, dbg: &mut Debugger, module: &Module) { - if self.handle_on_create_process(dbg, module).is_err() { + if self.recorder.on_create_process(dbg, module).is_err() { self.stop(dbg); } } fn on_load_dll(&mut self, dbg: &mut Debugger, module: &Module) { - self.handle_on_load_dll(dbg, module); + if self.recorder.on_load_dll(dbg, module).is_err() { + self.stop(dbg); + } } fn on_breakpoint(&mut self, dbg: &mut Debugger, bp: BreakpointId) { - if self.handle_on_breakpoint(dbg, bp).is_err() { + if self.recorder.on_breakpoint(dbg, bp).is_err() { self.stop(dbg); } } fn on_poll(&mut self, dbg: &mut Debugger) { - self.handle_on_poll(dbg); + self.on_poll(dbg); } } + +/// Relates opaque, runtime-generated breakpoint IDs to their corresponding +/// location, via module and offset. +#[derive(Clone, Debug, Default)] +struct Breakpoints { + // Breakpoint-associated module paths, referenced by index to save space and + // avoid copying. + modules: Vec, + + // Map of breakpoint IDs to data which pick out an code location. For a + // value `(module, offset)`, `module` is an index into `self.modules`, and + // `offset` is a VA offset relative to the module base. + registered: BTreeMap, +} + +impl Breakpoints { + pub fn get(&self, id: BreakpointId) -> Option> { + let (module_index, offset) = self.registered.get(&id).copied()?; + let module = self.modules.get(module_index)?; + Some(BreakpointData { module, offset }) + } + + pub fn set( + &mut self, + dbg: &mut Debugger, + module: &Module, + offsets: impl Iterator, + ) -> Result<()> { + // From the `target::Module`, create and save a `ModulePath`. + let module_path = ModulePath::new(module.path().to_owned())?; + let module_index = self.modules.len(); + self.modules.push(module_path); + + for offset in offsets { + // Register the breakpoint in the running target address space. + let id = dbg.register_breakpoint(module.name(), offset, BreakpointType::OneTime); + + // Associate the opaque `BreakpointId` with the module and offset. + self.registered.insert(id, (module_index, offset)); + } + + log::debug!("{} total registered modules", self.modules.len()); + log::debug!("{} total registered breakpoints", self.registered.len()); + + Ok(()) + } +} + +/// Code location data associated with an opaque breakpoint ID. +#[derive(Clone, Copy, Debug)] +pub struct BreakpointData<'a> { + pub module: &'a ModulePath, + pub offset: u64, +} diff --git a/src/agent/coverage/src/cache.rs b/src/agent/coverage/src/cache.rs index ee6f5ac28..08e66e64d 100644 --- a/src/agent/coverage/src/cache.rs +++ b/src/agent/coverage/src/cache.rs @@ -6,7 +6,6 @@ use serde::{Deserialize, Serialize}; use std::collections::{BTreeSet, HashMap}; use crate::code::{ModuleIndex, ModulePath}; -use crate::disasm::ModuleDisassembler; #[derive(Clone, Debug, Default, Deserialize, Serialize)] pub struct ModuleCache { @@ -28,11 +27,19 @@ impl ModuleCache { Ok(self.cached.get(path)) } + #[cfg(target_os = "linux")] pub fn insert(&mut self, path: &ModulePath) -> Result<()> { let entry = ModuleInfo::new_elf(path)?; self.cached.insert(path.clone(), entry); Ok(()) } + + #[cfg(target_os = "windows")] + pub fn insert(&mut self, path: &ModulePath) -> Result<()> { + let entry = ModuleInfo::new_pe(path)?; + self.cached.insert(path.clone(), entry); + Ok(()) + } } #[derive(Clone, Debug, Deserialize, Serialize)] @@ -48,10 +55,24 @@ impl ModuleInfo { #[cfg(target_os = "linux")] pub fn new_elf(path: &ModulePath) -> Result { let data = std::fs::read(path)?; - let module = ModuleIndex::parse_elf(path.clone(), &data)?; - let disasm = ModuleDisassembler::new(&module, &data)?; + let elf = goblin::elf::Elf::parse(&data)?; + let module = ModuleIndex::index_elf(path.clone(), &elf)?; + let disasm = crate::disasm::ModuleDisassembler::new(&module, &data)?; let blocks = disasm.find_blocks(); Ok(Self { module, blocks }) } + + #[cfg(target_os = "windows")] + pub fn new_pe(path: &ModulePath) -> Result { + let file = std::fs::File::open(path)?; + let data = unsafe { memmap2::Mmap::map(&file)? }; + + let pe = goblin::pe::PE::parse(&data)?; + let module = ModuleIndex::index_pe(path.clone(), &pe); + let offsets = crate::pe::process_module(path, &data, &pe, false)?; + let blocks = offsets.ones().map(|off| off as u64).collect(); + + Ok(Self { module, blocks }) + } } diff --git a/src/agent/coverage/src/code.rs b/src/agent/coverage/src/code.rs index f4a238394..d629a84cc 100644 --- a/src/agent/coverage/src/code.rs +++ b/src/agent/coverage/src/code.rs @@ -88,25 +88,30 @@ impl From for PathBuf { } } +/// Index over an executable module and its symbols. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct ModuleIndex { + /// Absolute path to the module's backing file. pub path: ModulePath, + + /// Preferred virtual address of the module's base image. pub base_va: u64, + + /// Index over the module's symbols. pub symbols: SymbolIndex, } impl ModuleIndex { + /// Build a new index over a parsed ELF module. #[cfg(target_os = "linux")] - pub fn parse_elf(path: ModulePath, data: &[u8]) -> Result { + pub fn index_elf(path: ModulePath, elf: &goblin::elf::Elf) -> Result { use anyhow::format_err; - use goblin::elf::{self, program_header::PT_LOAD}; - - let object = elf::Elf::parse(data)?; + use goblin::elf::program_header::PT_LOAD; // Calculate the module base address as the lowest preferred VA of any loadable segment. // // https://refspecs.linuxbase.org/elf/gabi4+/ch5.pheader.html#base_address - let base_va = object + let base_va = elf .program_headers .iter() .filter(|h| h.p_type == PT_LOAD) @@ -116,14 +121,14 @@ impl ModuleIndex { let mut symbols = SymbolIndex::default(); - for sym in object.syms.iter() { + for sym in elf.syms.iter() { if sym.st_size == 0 { log::debug!("skipping size 0 symbol: {:x?}", sym); continue; } if sym.is_function() { - let name = match object.strtab.get(sym.st_name) { + let name = match elf.strtab.get(sym.st_name) { None => { log::error!("symbol not found in symbol string table: {:?}", sym); continue; @@ -153,7 +158,7 @@ impl ModuleIndex { // A symbol is defined relative to some section, identified by `st_shndx`, an index // into the section header table. We'll use the section header to compute the file // offset of the symbol. - let section = object + let section = elf .section_headers .get(sym.st_shndx) .cloned() @@ -195,6 +200,21 @@ impl ModuleIndex { symbols, }) } + + /// Build a new index over a parsed PE module. + #[cfg(target_os = "windows")] + pub fn index_pe(path: ModulePath, pe: &goblin::pe::PE) -> Self { + let base_va = pe.image_base as u64; + + // Not yet implemented. + let symbols = SymbolIndex::default(); + + Self { + path, + base_va, + symbols, + } + } } #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] diff --git a/src/agent/coverage/src/lib.rs b/src/agent/coverage/src/lib.rs index 641aec884..aa337c1c0 100644 --- a/src/agent/coverage/src/lib.rs +++ b/src/agent/coverage/src/lib.rs @@ -10,10 +10,7 @@ mod intel; pub mod pe; pub mod block; - -#[cfg(target_os = "linux")] pub mod cache; - pub mod code; pub mod demangle; @@ -22,157 +19,3 @@ pub mod disasm; pub mod filter; mod region; - -use std::{ - ffi::OsString, - fs::File, - path::{Path, PathBuf}, -}; - -use anyhow::Result; -use fixedbitset::FixedBitSet; -use serde::{Deserialize, Serialize}; - -pub const COVERAGE_MAP: &str = "coverage-map"; - -#[derive(Deserialize, Serialize, Debug, Clone)] -pub struct Block { - rva: u32, - hit: bool, -} - -impl Block { - pub fn new(rva: u32, hit: bool) -> Self { - Block { rva, hit } - } - - pub fn rva(&self) -> u32 { - self.rva - } - - pub fn hit(&self) -> bool { - self.hit - } - - pub fn set_hit(&mut self) { - self.hit = true; - } -} - -#[derive(Debug, Deserialize, Serialize, Clone)] -pub struct ModuleCoverageBlocks { - module: OsString, - path: PathBuf, - blocks: Vec, -} - -impl ModuleCoverageBlocks { - pub fn new( - path: impl Into, - module: impl Into, - rvas_bitset: FixedBitSet, - ) -> Self { - let blocks: Vec<_> = rvas_bitset - .ones() - .map(|rva| Block::new(rva as u32, false)) - .collect(); - - ModuleCoverageBlocks { - path: path.into(), - module: module.into(), - blocks, - } - } - - pub fn path(&self) -> &Path { - &self.path - } - - pub fn name(&self) -> &Path { - Path::new(&self.module) - } - - pub fn blocks(&self) -> &[Block] { - &self.blocks - } - - pub fn set_block_hit(&mut self, block_index: usize) { - self.blocks[block_index].set_hit(); - } - - pub fn count_blocks_hit(&self) -> usize { - self.blocks.iter().filter(|b| b.hit).count() - } -} - -#[derive(Debug, Deserialize, Serialize, Clone)] -pub struct AppCoverageBlocks { - modules: Vec, -} - -impl AppCoverageBlocks { - pub fn new() -> Self { - let modules = vec![]; - Self { modules } - } - - pub fn modules(&self) -> &[ModuleCoverageBlocks] { - &self.modules - } - - pub fn add_module(&mut self, module: ModuleCoverageBlocks) -> usize { - let idx = self.modules.len(); - self.modules.push(module); - idx - } - - pub fn report_block_hit(&mut self, module_index: usize, block_index: usize) { - self.modules[module_index].set_block_hit(block_index); - } - - pub fn save(&self, path: impl AsRef) -> Result<()> { - let cov_file = File::create(path)?; - bincode::serialize_into(&cov_file, self)?; - Ok(()) - } - - pub fn count_blocks(&self) -> usize { - self.modules().iter().map(|m| m.blocks().len()).sum() - } - - pub fn count_blocks_hit(&self) -> usize { - self.modules().iter().map(|m| m.count_blocks_hit()).sum() - } -} - -/// Statically analyze the specified images to discover the basic block -/// entry points and write out the results in a file in `output_dir`. -#[cfg(target_os = "windows")] -pub fn run_init(output_dir: PathBuf, modules: Vec, function: bool) -> Result<()> { - let mut result = AppCoverageBlocks::new(); - for module in modules { - if module.is_file() { - let rvas_bitset = pe::process_image(&module, function)?; - - let module_name = module.file_stem().unwrap(); // Unwrap guaranteed by `is_file` test above. - let module_rvas = ModuleCoverageBlocks::new(module.clone(), &module_name, rvas_bitset); - result.add_module(module_rvas); - } else { - anyhow::bail!("Cannot find file `{}`", module.as_path().display()); - } - } - - let output_file = output_dir.join(COVERAGE_MAP); - result.save(&output_file)?; - - Ok(()) -} - -/// Load a coverage map created by `run_init`. -pub fn load_coverage_map(output_dir: &Path) -> Result> { - if let Ok(cov_file) = File::open(output_dir.join(COVERAGE_MAP)) { - Ok(Some(bincode::deserialize_from(cov_file)?)) - } else { - Ok(None) - } -} diff --git a/src/agent/coverage/src/pe.rs b/src/agent/coverage/src/pe.rs index 8c74a77d0..0985f6f33 100644 --- a/src/agent/coverage/src/pe.rs +++ b/src/agent/coverage/src/pe.rs @@ -152,7 +152,7 @@ fn find_blocks( blocks: &mut FixedBitSet, address_map: &AddressMap, pe: &PE, - mmap: &Mmap, + data: &[u8], functions_only: bool, ) -> Result<()> { let file_alignment = pe @@ -217,7 +217,7 @@ fn find_blocks( intel::find_blocks( bitness, - &mmap[file_offset..file_offset + (code_len as usize)], + &data[file_offset..file_offset + (code_len as usize)], rva.0, blocks, ); @@ -228,12 +228,12 @@ fn find_blocks( Ok(()) } -pub fn process_image>(image_path: P, functions_only: bool) -> Result { - let file = File::open(image_path.as_ref())?; - let mmap = unsafe { Mmap::map(&file)? }; - - let pe = PE::parse(mmap.as_ref())?; - +pub fn process_module( + pe_path: impl AsRef, + data: &[u8], + pe: &PE, + functions_only: bool, +) -> Result { if let Some(DebugData { image_debug_directory: _, codeview_pdb70_debug_info: Some(cv), @@ -248,13 +248,13 @@ pub fn process_image>(image_path: P, functions_only: bool) -> Res anyhow::bail!( "pdb `{}` doesn't match image `{}`", pdb_path, - image_path.as_ref().display() + pe_path.as_ref().display() ); } let address_map = pdb.address_map()?; - let mut blocks = FixedBitSet::with_capacity(mmap.len()); + let mut blocks = FixedBitSet::with_capacity(data.len()); let proc_sym_info = collect_proc_symbols(&mut pdb.global_symbols()?.iter())?; find_blocks( @@ -262,7 +262,7 @@ pub fn process_image>(image_path: P, functions_only: bool) -> Res &mut blocks, &address_map, &pe, - &mmap, + data, functions_only, )?; @@ -277,7 +277,7 @@ pub fn process_image>(image_path: P, functions_only: bool) -> Res &mut blocks, &address_map, &pe, - &mmap, + data, functions_only, )?; } @@ -288,3 +288,11 @@ pub fn process_image>(image_path: P, functions_only: bool) -> Res anyhow::bail!("PE missing codeview pdb debug info") } + +pub fn process_image(path: impl AsRef, functions_only: bool) -> Result { + let file = File::open(path.as_ref())?; + let mmap = unsafe { Mmap::map(&file)? }; + let pe = PE::parse(&mmap)?; + + process_module(path, &mmap, &pe, functions_only) +} diff --git a/src/agent/debugger/src/debugger.rs b/src/agent/debugger/src/debugger.rs index cafbd6f1d..d0c026714 100644 --- a/src/agent/debugger/src/debugger.rs +++ b/src/agent/debugger/src/debugger.rs @@ -678,6 +678,10 @@ impl Debugger { self.target.read_register_u64(reg) } + pub fn read_program_counter(&mut self) -> Result { + self.target.read_program_counter() + } + pub fn read_flags_register(&mut self) -> Result { self.target.read_flags_register() } diff --git a/src/agent/debugger/src/target.rs b/src/agent/debugger/src/target.rs index 828f78675..38cc2084f 100644 --- a/src/agent/debugger/src/target.rs +++ b/src/agent/debugger/src/target.rs @@ -595,6 +595,10 @@ impl Target { Ok(current_context.get_register_u64(reg)) } + pub fn read_program_counter(&mut self) -> Result { + self.read_register_u64(iced_x86::Register::RIP) + } + pub fn read_flags_register(&mut self) -> Result { let current_context = self.get_current_context()?; Ok(current_context.get_flags()) diff --git a/src/agent/input-tester/src/crash_detector.rs b/src/agent/input-tester/src/crash_detector.rs index 977a77160..49bd06e74 100644 --- a/src/agent/input-tester/src/crash_detector.rs +++ b/src/agent/input-tester/src/crash_detector.rs @@ -14,12 +14,11 @@ use std::{ }; use anyhow::Result; -use coverage::AppCoverageBlocks; +use coverage::{block::windows::Recorder as BlockCoverageRecorder, cache::ModuleCache}; use debugger::{ - debugger::{BreakpointId, BreakpointType, DebugEventHandler, Debugger}, + debugger::{BreakpointId, DebugEventHandler, Debugger}, target::Module, }; -use fnv::FnvHashMap; use log::{debug, error, trace}; use win_util::{ pipe_handle::{pipe, PipeReaderNonBlocking}, @@ -135,18 +134,17 @@ struct CrashDetectorEventHandler<'a> { stderr_buffer: Vec, debugger_output: String, exceptions: Vec, - id_to_block: Option>, - coverage_map: Option<&'a mut AppCoverageBlocks>, + coverage: Option>, } impl<'a> CrashDetectorEventHandler<'a> { pub fn new( - coverage_map: Option<&'a mut AppCoverageBlocks>, stdout: PipeReaderNonBlocking, stderr: PipeReaderNonBlocking, ignore_first_chance_exceptions: bool, start_time: Instant, max_duration: Duration, + coverage: Option>, ) -> Self { Self { start_time, @@ -160,48 +158,11 @@ impl<'a> CrashDetectorEventHandler<'a> { stderr_buffer: vec![], debugger_output: String::new(), exceptions: vec![], - id_to_block: None, - coverage_map, + coverage, } } } -/// Register the coverage breakpoints and build a mapping from `BreakpointId` to the pair -/// (module index, block index). -/// -/// The debugger passes a `BreakpointId` to the `on_breakpoint` callback - the same id returned -/// when the breakpoint is registered. -/// -/// This mapping is convenient so that we do not need to track module load addresses and later -/// map between breakpoint address and module/rva pairs. -fn prepare_coverage_breakpoints( - debugger: &mut Debugger, - coverage_map: &Option<&mut AppCoverageBlocks>, -) -> Option> { - if let Some(coverage_map) = coverage_map { - let mut id_to_block = fnv::FnvHashMap::default(); - for (m, module) in coverage_map.modules().iter().enumerate() { - let name = module.name(); - for (i, block) in module.blocks().iter().enumerate() { - // For better performance, we can skip registering breakpoints that have - // been hit as we only care about new coverage. - if !block.hit() { - let id = debugger.register_breakpoint( - name, - block.rva() as u64, - BreakpointType::OneTime, - ); - - id_to_block.insert(id, (m, i)); - } - } - } - Some(id_to_block) - } else { - None - } -} - fn is_vcpp_notification(exception: &EXCEPTION_DEBUG_INFO, target_process_handle: HANDLE) -> bool { if exception.ExceptionRecord.ExceptionCode == vcpp_debugger::EXCEPTION_VISUALCPP_DEBUGGER { match VcppDebuggerExceptionInfo::from_exception_record( @@ -281,10 +242,6 @@ impl<'a> DebugEventHandler for CrashDetectorEventHandler<'a> { DBG_EXCEPTION_NOT_HANDLED } - fn on_create_process(&mut self, debugger: &mut Debugger, _module: &Module) { - prepare_coverage_breakpoints(debugger, &self.coverage_map); - } - fn on_output_debug_string(&mut self, _debugger: &mut Debugger, message: String) { self.debugger_output.push_str(&message); } @@ -313,12 +270,29 @@ impl<'a> DebugEventHandler for CrashDetectorEventHandler<'a> { } } - fn on_breakpoint(&mut self, _debugger: &mut Debugger, id: BreakpointId) { - if let Some(id_to_block) = &self.id_to_block { - if let Some(&(mod_idx, block_idx)) = id_to_block.get(&id) { - if let Some(coverage_map) = &mut self.coverage_map { - coverage_map.report_block_hit(mod_idx, block_idx); - } + fn on_create_process(&mut self, dbg: &mut Debugger, module: &Module) { + if let Some(coverage) = &mut self.coverage { + if let Err(err) = coverage.on_create_process(dbg, module) { + error!("error recording coverage on create process: {:?}", err); + dbg.quit_debugging(); + } + } + } + + fn on_load_dll(&mut self, dbg: &mut Debugger, module: &Module) { + if let Some(coverage) = &mut self.coverage { + if let Err(err) = coverage.on_load_dll(dbg, module) { + error!("error recording coverage on load DLL: {:?}", err); + dbg.quit_debugging(); + } + } + } + + fn on_breakpoint(&mut self, dbg: &mut Debugger, bp: BreakpointId) { + if let Some(coverage) = &mut self.coverage { + if let Err(err) = coverage.on_breakpoint(dbg, bp) { + error!("error recording coverage on breakpoint: {:?}", err); + dbg.quit_debugging(); } } } @@ -326,13 +300,13 @@ impl<'a> DebugEventHandler for CrashDetectorEventHandler<'a> { /// This function runs the application under a debugger to detect any crashes in /// the process or any children processes. -pub fn test_process( +pub fn test_process<'a>( app_path: impl AsRef, args: &[impl AsRef], env: &HashMap, max_duration: Duration, ignore_first_chance_exceptions: bool, - coverage_map: Option<&mut AppCoverageBlocks>, + cache: Option<&'a mut ModuleCache>, ) -> Result { debug!("Running: {}", logging::command_invocation(&app_path, args)); @@ -350,14 +324,15 @@ pub fn test_process( command.env(k, v); } + let recorder = cache.map(BlockCoverageRecorder::new); let start_time = Instant::now(); let mut event_handler = CrashDetectorEventHandler::new( - coverage_map, stdout_reader, stderr_reader, ignore_first_chance_exceptions, start_time, max_duration, + recorder, ); let (mut debugger, mut child) = Debugger::init(command, &mut event_handler)?; debugger.run(&mut event_handler)?; diff --git a/src/agent/input-tester/src/tester.rs b/src/agent/input-tester/src/tester.rs index 82bc6d190..571927952 100644 --- a/src/agent/input-tester/src/tester.rs +++ b/src/agent/input-tester/src/tester.rs @@ -20,7 +20,6 @@ use std::{ }; use anyhow::{Context, Result}; -use coverage::AppCoverageBlocks; use log::{error, info, trace, warn}; use num_cpus; use rayon::{prelude::*, ThreadPoolBuilder}; @@ -53,19 +52,13 @@ const MAX_CRASH_SAMPLES: usize = 10; pub struct InputTestResult { pub debugger_result: DebuggerResult, pub input_path: PathBuf, - pub blocks_covered: Option, } impl InputTestResult { - pub fn new( - debugger_result: DebuggerResult, - input_path: PathBuf, - blocks_covered: Option, - ) -> Self { + pub fn new(debugger_result: DebuggerResult, input_path: PathBuf) -> Self { InputTestResult { debugger_result, input_path, - blocks_covered, } } } @@ -78,7 +71,6 @@ pub struct Tester { ignore_first_chance_exceptions: bool, appverif_controller: Option, bugs_found_dir: PathBuf, - coverage_map: Option, } impl Tester { @@ -116,7 +108,6 @@ impl Tester { None }; - let coverage_map = coverage::load_coverage_map(&output_dir)?; Ok(Arc::new(Tester { appverif_controller, driver, @@ -125,7 +116,6 @@ impl Tester { max_run_s, ignore_first_chance_exceptions, bugs_found_dir, - coverage_map, })) } @@ -133,20 +123,16 @@ impl Tester { pub fn test_application(&self, input_path: impl AsRef) -> Result { let app_args = args_with_input_file_applied(&self.driver_args, &input_path)?; - let mut coverage_map = self.coverage_map.clone(); - crash_detector::test_process( &self.driver, &app_args, &self.driver_env, Duration::from_secs(self.max_run_s), self.ignore_first_chance_exceptions, - coverage_map.as_mut(), + None, ) .and_then(|result| { - let blocks_hit = coverage_map.map_or(None, |map| Some(map.count_blocks_hit())); - let result = - InputTestResult::new(result, PathBuf::from(input_path.as_ref()), blocks_hit); + let result = InputTestResult::new(result, PathBuf::from(input_path.as_ref())); log_input_test_result(&result); Ok(result) })