Skip to content
This repository was archived by the owner on Nov 1, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions src/agent/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/agent/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[workspace]
members = [
"atexit",
"coverage",
"coverage-legacy",
"debuggable-module",
"debugger",
Expand Down
26 changes: 26 additions & 0 deletions src/agent/coverage/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[package]
name = "coverage"
version = "0.1.0"
edition = "2021"
license = "MIT"

[dependencies]
anyhow = "1.0"
debuggable-module = { path = "../debuggable-module" }
iced-x86 = "1.17"
log = "0.4.17"
regex = "1.0"
symbolic = { version = "10.1", features = ["debuginfo", "demangle", "symcache"] }
thiserror = "1.0"

[target.'cfg(target_os = "windows")'.dependencies]
debugger = { path = "../debugger" }

[target.'cfg(target_os = "linux")'.dependencies]
pete = "0.9"
# For procfs, opt out of the `chrono` freature; it pulls in an old version
# of `time`. We do not use the methods that the `chrono` feature enables.
procfs = { version = "0.12", default-features = false, features=["flate2"] }

[dev-dependencies]
clap = { version = "4.0", features = ["derive"] }
65 changes: 65 additions & 0 deletions src/agent/coverage/examples/coverage.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
use std::process::Command;
use std::time::Duration;

use anyhow::Result;
use clap::Parser;
use coverage::allowlist::{AllowList, TargetAllowList};
use coverage::binary::BinaryCoverage;

#[derive(Parser, Debug)]
struct Args {
#[arg(long)]
module_allowlist: Option<String>,

#[arg(long)]
source_allowlist: Option<String>,

#[arg(short, long)]
timeout: Option<u64>,

command: Vec<String>,
}

const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5);

fn main() -> Result<()> {
let args = Args::parse();

let timeout = args
.timeout
.map(Duration::from_millis)
.unwrap_or(DEFAULT_TIMEOUT);

let mut cmd = Command::new(&args.command[0]);
if args.command.len() > 1 {
cmd.args(&args.command[1..]);
}

let mut allowlist = TargetAllowList::default();

if let Some(path) = &args.module_allowlist {
allowlist.modules = AllowList::load(path)?;
}

if let Some(path) = &args.source_allowlist {
allowlist.source_files = AllowList::load(path)?;
}

let coverage = coverage::record::record(cmd, timeout, allowlist)?;

dump_modoff(coverage)?;

Ok(())
}

fn dump_modoff(coverage: BinaryCoverage) -> Result<()> {
for (module, coverage) in &coverage.modules {
for (offset, count) in coverage.as_ref() {
if count.reached() {
println!("{}+{offset:x}", module.base_name());
}
}
}

Ok(())
}
156 changes: 156 additions & 0 deletions src/agent/coverage/src/allowlist.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use anyhow::Result;
use regex::{Regex, RegexSet};
use std::path::Path;

#[derive(Clone, Debug, Default)]
pub struct TargetAllowList {
pub functions: AllowList,
pub modules: AllowList,
pub source_files: AllowList,
}

impl TargetAllowList {
pub fn new(modules: AllowList, source_files: AllowList) -> Self {
// Allow all.
let functions = AllowList::default();

Self {
functions,
modules,
source_files,
}
}
}

#[derive(Clone, Debug)]
pub struct AllowList {
allow: RegexSet,
deny: RegexSet,
}

impl AllowList {
pub fn new(allow: RegexSet, deny: RegexSet) -> Self {
Self { allow, deny }
}

pub fn load(path: impl AsRef<Path>) -> Result<Self> {
let path = path.as_ref();
let text = std::fs::read_to_string(path)?;
Self::parse(&text)
}

pub fn parse(text: &str) -> Result<Self> {
use std::io::{BufRead, BufReader};

let reader = BufReader::new(text.as_bytes());

let mut allow = vec![];
let mut deny = vec![];

// We could just collect and pass to the `RegexSet` ctor.
//
// Instead, check each rule individually for diagnostic purposes.
for (index, line) in reader.lines().enumerate() {
let line = line?;

match AllowListLine::parse(&line) {
Ok(valid) => {
use AllowListLine::*;

match valid {
Blank | Comment => {
// Ignore.
}
Allow(re) => {
allow.push(re);
}
Deny(re) => {
deny.push(re);
}
}
}
Err(err) => {
// Ignore invalid lines, but warn.
let line_number = index + 1;
warn!("error at line {}: {}", line_number, err);
}
}
}

let allow = RegexSet::new(allow.iter().map(|re| re.as_str()))?;
let deny = RegexSet::new(deny.iter().map(|re| re.as_str()))?;
let allowlist = AllowList::new(allow, deny);

Ok(allowlist)
}

pub fn is_allowed(&self, path: impl AsRef<str>) -> bool {
let path = path.as_ref();

// Allowed if rule-allowed but not excluded by a negative (deny) rule.
self.allow.is_match(path) && !self.deny.is_match(path)
}
}

impl Default for AllowList {
fn default() -> Self {
// Unwrap-safe due to valid constant expr.
let allow = RegexSet::new([".*"]).unwrap();
let deny = RegexSet::empty();

AllowList::new(allow, deny)
}
}

pub enum AllowListLine {
Blank,
Comment,
Allow(Regex),
Deny(Regex),
}

impl AllowListLine {
pub fn parse(line: &str) -> Result<Self> {
let line = line.trim();

// Allow and ignore blank lines.
if line.is_empty() {
return Ok(Self::Blank);
}

// Support comments of the form `# <comment>`.
if line.starts_with("# ") {
return Ok(Self::Comment);
}

// Deny rules are of the form `! <rule>`.
if let Some(expr) = line.strip_prefix("! ") {
let re = glob_to_regex(expr)?;
return Ok(Self::Deny(re));
}

// Try to interpret as allow rule.
let re = glob_to_regex(line)?;
Ok(Self::Allow(re))
}
}

#[allow(clippy::single_char_pattern)]
fn glob_to_regex(expr: &str) -> Result<Regex> {
// Don't make users escape Windows path separators.
let expr = expr.replace(r"\", r"\\");

// Translate glob wildcards into quantified regexes.
let expr = expr.replace("*", ".*");

// Anchor to line start and end.
let expr = format!("^{expr}$");

Ok(Regex::new(&expr)?)
}

#[cfg(test)]
mod tests;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
a/*
! a/c
# c
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
a/*
! a/c
c
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*
4 changes: 4 additions & 0 deletions src/agent/coverage/src/allowlist/test-data/allow-all.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
a
a/b
b
c
2 changes: 2 additions & 0 deletions src/agent/coverage/src/allowlist/test-data/allow-some.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
a
b
Empty file.
Loading