use std::thread::sleep;
use std::time::{Duration, Instant};
use dbgscope::dbgeng::{DbgEngError, DebugEngine, PendingTarget, RunToOutcome};
const DEAD_CONN: &str = "net:port=50999,key=1.2.3.4";
enum Target {
Local,
Connection(String),
}
impl Target {
fn begin<'a>(&self, e: &'a DebugEngine) -> Result<PendingTarget<'a>, DbgEngError> {
match self {
Target::Local => e.attach_local_kernel_begin(),
Target::Connection(conn) => e.attach_kernel_begin(conn),
}
}
fn fused(&self, e: &DebugEngine) -> Result<(), DbgEngError> {
match self {
Target::Local => e.attach_local_kernel(),
Target::Connection(conn) => e.attach_kernel(conn),
}
}
fn opener(&self) -> &'static str {
match self {
Target::Local => "attach_local_kernel",
Target::Connection(_) => "attach_kernel",
}
}
}
fn run(e: &DebugEngine, cmd: &str) {
println!("--- {cmd} ---");
match e.execute_command(cmd) {
Ok(out) => print!("{out}"),
Err(err) => println!("ERR: {err}"),
}
println!();
}
fn check_break_in_bookkeeping(e: &DebugEngine) {
let address = match e.symbol_offset("nt!NtCreateFile") {
Ok(address) => {
println!("nt!NtCreateFile resolved to {address:#x}");
address
}
Err(err) => {
println!("[??] cannot resolve nt!NtCreateFile: {err} — symbols unavailable, skipping");
return;
}
};
println!("=== run to nt!NtCreateFile (expect Hit, not the initial-break artifact) ===");
let result = match e.run_to_address(address, 60_000) {
Ok(result) => result,
Err(err) => {
println!("ERR: {err}");
return;
}
};
print!("{}", result.output);
println!();
match result.outcome {
RunToOutcome::Hit => {
println!("[ok] reached the real breakpoint — INITIAL_BREAK cleared, artifact absorbed")
}
RunToOutcome::StoppedElsewhere { stopped_at } => {
let site = e
.execute_command(&format!("ln {stopped_at:#x}"))
.unwrap_or_default();
print!("{site}");
if site.contains("DbgBreakPointWithStatus") {
println!(
"[FAIL] stopped at the initial-break artifact — wait()'s bookkeeping did not run"
);
} else {
println!(
"[??] stopped at {stopped_at:#x}, not the breakpoint — read the output above"
);
}
}
RunToOutcome::Timeout => println!(
"[??] inconclusive — the 60s bound expired and the watchdog forced the stop, which \
is indistinguishable from the artifact. Re-run against a target that reaches \
nt!NtCreateFile (any file I/O on the guest will do)."
),
RunToOutcome::TargetGone => println!(
"[??] inconclusive — the engine lost the target during the run; the session is over."
),
}
}
fn split_attach(e: &DebugEngine, target: &Target) {
println!(
"=== 1. {}_begin / wait (the split path) ===",
target.opener()
);
let began = Instant::now();
match target.begin(e) {
Ok(pending) => {
println!(
"[commit] AttachKernel returned OK in {:?} — the target is ours from here, \
even though the link is not up yet",
began.elapsed()
);
let waited = Instant::now();
match pending.wait() {
Ok(()) => println!("wait OK in {:?} — target broken in", waited.elapsed()),
Err(err) => {
println!("wait ERR: {err} (the attach still happened — do not retry it)");
return;
}
}
}
Err(err) => {
println!("begin ERR: {err} (nothing was claimed; retry is clean)");
return;
}
}
run(e, "vertarget");
check_break_in_bookkeeping(e);
}
fn end_session_leaves_target_running(e: &DebugEngine, target: &Target) -> bool {
println!(
"\n=== 2. {} (fused) -> end_session -> re-attach ===",
target.opener()
);
match target.fused(e) {
Ok(()) => println!("attach #1 OK"),
Err(err) => {
println!("attach #1 ERR: {err}");
return false;
}
}
run(e, "vertarget");
run(e, "bp nt!NtCreateFile");
println!("=== go (to nt!NtCreateFile) ===");
match e.execute_and_wait("g", 60_000) {
Ok(run) => print!("{}", run.output),
Err(err) => println!("ERR: {err}"),
}
println!();
println!("=== end_session (should resume + detach, leaving target RUNNING) ===");
match e.end_session() {
Ok(()) => println!("end_session ok"),
Err(err) => println!("end_session ERR: {err}"),
}
println!("--- sleeping 8s; if the fix works the guest is RUNNING during this ---");
sleep(Duration::from_secs(8));
println!("=== re-attach to read uptime again ===");
match target.fused(e) {
Ok(()) => println!("attach #2 OK"),
Err(err) => {
println!("attach #2 ERR: {err} (=> target was frozen/wedged, fix FAILED)");
return false;
}
}
run(e, "vertarget"); true
}
fn timeout_probe() {
let e = DebugEngine::new();
println!("=== deliberate timeout: dial {DEAD_CONN}; nothing will ever answer ===");
let began = Instant::now();
match e.attach_kernel_begin(DEAD_CONN) {
Ok(pending) => {
println!(
"[commit] AttachKernel OK in {:?} — the transport is claimed, the link is not up",
began.elapsed()
);
println!(
"waiting; expect this to block indefinitely (measured: past 300s, bound is 60s) \
because SetInterrupt cannot cancel a dial that never connected. Ctrl+C to stop."
);
let waited = Instant::now();
match pending.wait() {
Err(DbgEngError::KernelBreakTimeout) => println!(
"[!] wait() -> KernelBreakTimeout after {:?} — the bound fired, which it did \
not when this was measured; wait_for_event_bounded's doc comment is stale",
waited.elapsed()
),
Ok(()) => println!(
"[FAIL] wait() reported success after {:?} with no target on the wire",
waited.elapsed()
),
Err(err) => println!("[??] wait() -> {err} after {:?}", waited.elapsed()),
}
}
Err(err) => println!("begin ERR: {err}"),
}
let _ = e.end_session();
}
fn main() {
let Some(arg) = std::env::args().nth(1) else {
eprintln!(
"usage: kdtest <connection-string> | local | --timeout-probe\n \
e.g. kdtest \"net:port=50000,key=w.x.y.z\""
);
return;
};
if arg == "--timeout-probe" {
timeout_probe();
return;
}
let target = if arg == "local" {
Target::Local
} else {
Target::Connection(arg)
};
let e = DebugEngine::new();
split_attach(&e, &target);
let _ = e.end_session();
let compared = end_session_leaves_target_running(&e, &target);
let _ = e.end_session();
if compared {
println!("\ndone (compare the two 'System Uptime' lines from section 2)");
} else {
println!("\ndone (section 2 did not complete — no uptimes to compare)");
}
}