use std::io::Write;
use std::time::{SystemTime, UNIX_EPOCH};
use dbgscope::dbgeng::{DbgEngError, DebugEngine};
const WAIT_MS: u32 = 2_000;
const TARGETS: &[&str] = &["cmd.exe /c exit", "cmd.exe /c ping -n 30 127.0.0.1"];
const NO_DEBUGGEE: u32 = 7;
#[derive(Clone, Copy, Debug)]
enum Step {
Resume(&'static str),
Raw(&'static str),
RunTo(&'static str),
Settle,
InterruptThenResume,
}
const CORPUS: &[Step] = &[
Step::Resume("g"),
Step::Resume("p"),
Step::Resume("t"),
Step::Resume("gu"),
Step::Raw("g"),
Step::Raw("p"),
Step::Raw("t"),
Step::Raw("g; g"),
Step::Raw(".if (1) { g }"),
Step::Raw("bp ntdll!NtCreateFile \".echo FUZZ-HIT; g\""),
Step::Raw("bp ntdll!NtCreateFile"),
Step::Raw("bc *"),
Step::Raw("k 3"),
Step::Raw("r"),
Step::Raw("lm"),
Step::Raw(".echo alive"),
Step::Raw(".lastevent"),
Step::Raw("sxe ld:ntdll.dll"),
Step::Raw(".detach"),
Step::Raw(".kill"),
Step::Raw("q"),
Step::Raw("qd"),
Step::RunTo("ntdll!NtCreateFile"),
Step::RunTo("ntdll!NtTerminateProcess"),
Step::Settle,
Step::InterruptThenResume,
];
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Health {
Holding,
Gone,
}
fn main() {
let args: Vec<String> = std::env::args().collect();
let seed = numeric(&args, "--seed").unwrap_or_else(|| {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|since| since.as_nanos() as u64)
.unwrap_or(0x243F_6A88_85A3_08D3)
| 1
});
let rounds = numeric(&args, "--rounds").unwrap_or(20);
let steps = numeric(&args, "--steps").unwrap_or(8);
println!("session_fuzz: seed {seed}, {rounds} rounds of up to {steps} steps");
println!("replay this run with: cargo run --example session_fuzz -- --seed {seed}");
println!();
let mut rng = Rng::new(seed);
let mut violations = 0u64;
for round in 1..=rounds {
let target = TARGETS[rng.below(TARGETS.len() as u64) as usize];
println!("---- round {round}/{rounds}: {target}");
flush();
let engine = DebugEngine::new();
if let Err(why) = engine.launch_process(target) {
println!(" launch failed, skipping the round: {why}");
continue;
}
let mut sequence = Vec::new();
for _ in 0..steps {
let step = CORPUS[rng.below(CORPUS.len() as u64) as usize];
sequence.push(step);
println!(" {}", describe(step));
flush();
run(&engine, step);
match check(&engine) {
Ok(Health::Holding) => {}
Ok(Health::Gone) => {
println!(" (the target is gone; the round ends here)");
break;
}
Err(violation) => {
violations += 1;
println!();
println!("!! VIOLATION on round {round}: {violation}");
println!("!! target: {target}");
println!("!! sequence: {sequence:?}");
println!("!! replay: --seed {seed} --rounds {rounds} --steps {steps}");
println!();
break;
}
}
}
let _ = engine.end_session();
}
flush();
match violations {
0 => println!("\nno violations in {rounds} rounds (seed {seed})"),
n => {
println!("\n{n} violation(s) — see above (seed {seed})");
std::process::exit(1);
}
}
}
fn run(e: &DebugEngine, step: Step) {
match step {
Step::Resume(command) => {
let _ = e.execute_and_wait(command, WAIT_MS);
}
Step::Raw(command) => {
let _ = e.execute_command_bounded(command, WAIT_MS);
let _ = e.settle(WAIT_MS);
}
Step::RunTo(symbol) => {
if let Some(address) = evaluate(e, symbol) {
let _ = e.run_to_address(address, WAIT_MS);
}
}
Step::Settle => {
let _ = e.settle(WAIT_MS);
}
Step::InterruptThenResume => {
let _ = e.interrupt_handle().interrupt();
let _ = e.execute_and_wait("g", WAIT_MS);
}
}
}
fn check(e: &DebugEngine) -> Result<Health, String> {
let status = e
.execution_status()
.map_err(|why| format!("the engine could not say what state it is in: {why}"))?;
if status == NO_DEBUGGEE {
for command in ["k 3", "r", ".echo alive"] {
match e.execute_command_bounded(command, WAIT_MS) {
Err(DbgEngError::NoDebuggee) => {}
Err(other) => {
return Err(format!(
"the target is gone and `{command}` answered `{other}` instead of saying so"
));
}
Ok(run) => {
return Err(format!(
"the target is gone and `{command}` answered anyway: {:?}",
run.output
));
}
}
}
match e.execute_and_wait("g", WAIT_MS) {
Err(DbgEngError::NoDebuggee) => {}
other => {
return Err(format!(
"the target is gone and a resume was not refused: {other:?}"
));
}
}
match e.settle(WAIT_MS) {
Ok(None) => {}
other => {
return Err(format!(
"the target is gone and settle found work: {other:?}"
));
}
}
return Ok(Health::Gone);
}
match e.is_running() {
Ok(false) => {}
Ok(true) => {
return Err(
"the engine holds a target and reads as running — a resume was left unpumped"
.to_string(),
);
}
Err(why) => return Err(format!("the run state could not be read: {why}")),
}
e.execute_command_bounded(".echo alive", WAIT_MS)
.map_err(|why| format!("the engine holds a target and refused `.echo`: {why}"))?;
Ok(Health::Holding)
}
fn evaluate(e: &DebugEngine, expr: &str) -> Option<u64> {
let out = e.execute_command(&format!("? {expr}")).ok()?;
let tail = out.split("Evaluate expression: ").nth(1)?;
let hex = tail.split(" = ").nth(1)?.trim().replace('`', "");
u64::from_str_radix(hex.split_whitespace().next()?, 16).ok()
}
fn describe(step: Step) -> String {
match step {
Step::Resume(command) => format!("resume `{command}`"),
Step::Raw(command) => format!("raw + settle `{command}`"),
Step::RunTo(symbol) => format!("run_to {symbol}"),
Step::Settle => "settle".to_string(),
Step::InterruptThenResume => "interrupt, then resume".to_string(),
}
}
fn flush() {
let _ = std::io::stdout().flush();
}
fn numeric(args: &[String], flag: &str) -> Option<u64> {
let at = args.iter().position(|arg| arg == flag)?;
args.get(at + 1)?.parse().ok()
}
struct Rng(u64);
impl Rng {
fn new(seed: u64) -> Self {
Self(seed | 1)
}
fn next(&mut self) -> u64 {
self.0 ^= self.0 >> 12;
self.0 ^= self.0 << 25;
self.0 ^= self.0 >> 27;
self.0.wrapping_mul(0x2545_F491_4F6C_DD1D)
}
fn below(&mut self, bound: u64) -> u64 {
self.next() % bound.max(1)
}
}