use idakit::prelude::*;
use idakit_runner_macros::kernel_test;
#[kernel_test(read_only)]
fn cfg() {
crate::common::with_canonical_db(run);
}
fn run(idb: &mut Database) {
let cfg = first_multiblock_cfg(idb).expect("a function with at least two basic blocks");
structure_is_sound(&cfg);
ranges_are_disjoint(&cfg);
block_kind_matches_its_edges(&cfg);
entry_and_lookup(&cfg);
instructions_walk_the_entry_block(idb, &cfg);
exits_leave_the_function(idb);
knobs_behave(idb, cfg.function());
build_is_deterministic(idb, cfg.function());
non_function_is_rejected(idb);
raw_predecessors_never_reach_nproper(cfg.function());
println!(
"cfg OK: {} blocks, edges sound and symmetric, ranges disjoint, kinds match their \
edges, exits + knobs + determinism + NoFunction verified",
cfg.len()
);
}
fn first_multiblock_cfg(idb: &Database) -> Option<FlowChart> {
idb.functions().take(4000).find_map(|f| {
let cfg = f.flowchart().ok()?;
(cfg.len() >= 2).then_some(cfg)
})
}
fn structure_is_sound(cfg: &FlowChart) {
for (id, b) in cfg.blocks() {
assert!(b.end() > b.start(), "every block spans a non-empty range");
for &s in b.successors() {
assert!(s.index() < cfg.len(), "successor handle in range");
assert!(
cfg.block(s).predecessors().contains(&id),
"edge {:?} -> {:?} is not mirrored in predecessors",
id.index(),
s.index()
);
}
for &p in b.predecessors() {
assert!(p.index() < cfg.len(), "predecessor handle in range");
}
for e in b.exits() {
assert!(
cfg.block_at(e.target).is_none(),
"exit target {:#x} lies in no block of the graph",
e.target
);
}
}
}
fn ranges_are_disjoint(cfg: &FlowChart) {
let mut ranges: Vec<_> = cfg.blocks().map(|(_, b)| b.range()).collect();
ranges.sort_by_key(|r| r.start);
for w in ranges.windows(2) {
assert!(
w[0].end <= w[1].start,
"block ranges overlap: {:#x}..{:#x} and {:#x}..{:#x}",
w[0].start,
w[0].end,
w[1].start,
w[1].end
);
}
}
fn block_kind_matches_its_edges(cfg: &FlowChart) {
for (_, b) in cfg.blocks() {
let has_continuation = !b.successors().is_empty() || !b.exits().is_empty();
match b.kind() {
BasicBlockKind::Normal => assert!(
has_continuation,
"a Normal block at {:#x} has no successor or exit",
b.start()
),
BasicBlockKind::Return => assert!(
b.successors().is_empty(),
"a Return block at {:#x} has an in-function successor",
b.start()
),
BasicBlockKind::CondReturn => assert!(
has_continuation,
"a CondReturn block at {:#x} has no non-returning continuation",
b.start()
),
BasicBlockKind::IndirectJump | BasicBlockKind::NoReturn | BasicBlockKind::Error => {}
}
}
}
fn entry_and_lookup(cfg: &FlowChart) {
let entry = cfg.entry();
assert!(entry.index() == 0, "the entry is block 0");
let start = cfg.block(entry).start();
assert!(
cfg.block_at(start) == Some(entry),
"block_at should map the entry's start to the entry block"
);
if start > Address::new_const(0) {
assert!(
cfg.block_at(Address::new_const(0)).is_none(),
"block_at should miss an address outside every block"
);
}
}
fn instructions_walk_the_entry_block(idb: &Database, cfg: &FlowChart) {
let entry = cfg.block(cfg.entry());
let insns: Vec<_> = idb.instructions_in(entry.range()).collect();
assert!(!insns.is_empty(), "the entry block decodes an instruction");
assert!(
insns[0].address == entry.start(),
"the first instruction sits at the block start"
);
for instruction in &insns {
assert!(
instruction.address >= entry.start() && instruction.address < entry.end(),
"instruction {:#x} escapes the block range",
instruction.address
);
}
}
fn exits_leave_the_function(idb: &Database) {
let cfg = idb
.functions()
.find_map(|f| {
let cfg = f.flowchart().ok()?;
let has_exit = cfg.blocks().any(|(_, b)| !b.exits().is_empty());
has_exit.then_some(cfg)
})
.expect("a function with an external exit");
structure_is_sound(&cfg);
let exits: usize = cfg.blocks().map(|(_, b)| b.exits().len()).sum();
let mut noreturn_cross_checked = false;
for (_, b) in cfg.blocks() {
for e in b.exits() {
assert!(
cfg.block_at(e.target).is_none(),
"exit target {:#x} should lie outside every block",
e.target
);
if let Some(callee) = idb.functions().find(|f| f.address() == e.target) {
assert!(
e.noreturn == callee.is_noreturn(),
"exit {:#x} noreturn={} disagrees with the callee's own is_noreturn()",
e.target,
e.noreturn
);
noreturn_cross_checked = true;
}
}
}
if !noreturn_cross_checked {
println!(
"cfg: verified {exits} external exit(s) leave the function; skipping noreturn \
cross-check, no exit target resolved to a mapped function"
);
return;
}
println!("cfg: verified {exits} external exit(s) leave the function, noreturn cross-checked");
}
fn knobs_behave(idb: &Database, function: Address) {
let base = idb.flowchart(function).expect("base cfg");
let split = idb
.function(function)
.flowchart_with()
.call_ends(true)
.call()
.expect("call-ends cfg");
assert!(
split.len() >= base.len(),
"call_ends splits more (or equal) blocks: {} < {}",
split.len(),
base.len()
);
let no_ext = idb
.function(function)
.flowchart_with()
.externals(false)
.call()
.expect("no-externals cfg");
assert!(
no_ext.blocks().all(|(_, b)| b.exits().is_empty()),
"externals(false) records no exits"
);
let no_preds = idb
.function(function)
.flowchart_with()
.predecessors(false)
.call()
.expect("no-preds cfg");
assert!(
no_preds.blocks().all(|(_, b)| b.predecessors().is_empty()),
"predecessors(false) leaves every predecessor list empty"
);
}
fn build_is_deterministic(idb: &Database, function: Address) {
let first = idb.flowchart(function).expect("first cfg build");
let second = idb.flowchart(function).expect("second cfg build");
assert!(
first == second,
"rebuilding the same function's CFG should be deterministic"
);
}
fn non_function_is_rejected(idb: &Database) {
let Some(start) = idb
.segments()
.find(|s| !s.is_executable())
.and_then(|s| s.start())
else {
println!("cfg: no non-executable segment; skipping the NoFunction check");
return;
};
let r = idb.flowchart(start);
assert!(
matches!(r, Err(Error::NoFunction { .. })),
"a non-function address should be NoFunction, got {r:?}"
);
}
fn raw_predecessors_never_reach_nproper(function: Address) {
use idakit_sys as sys;
let fc = sys::cfg_build(function.get(), 0).expect("cxx cfg_build for the raw pred check");
let nproper = sys::cfg_nproper(&fc);
for n in 0..nproper {
let preds = sys::cfg_preds(&fc, n).expect("cfg_preds within nproper");
for p in preds {
assert!(
(p as usize) < nproper,
"cfg_preds({n}) returned {p}, at or past nproper ({nproper})"
);
}
}
}