crates/dwarves-tune-graph/tests/graph.rs
//! The graph cross-check on real binaries: small C programs built here with
//! the system C compiler (glibc, dynamic) and musl-gcc (static).
use std::path::{Path, PathBuf};
use std::process::Command;
use dwarves_tune_graph::{analyze, cross_check, read_seen, render_text, Options};
/// vhangup (153) by inline asm on the normal path; sethostname (libc
/// wrapper) on a rare path; pivot_root (155) through syscall(2) in a
/// function that is only ever called through a pointer.
const PROG: &str = r#"
#include <unistd.h>
#include <sys/syscall.h>
static long raw_vhangup(void) {
long r;
__asm__ volatile("syscall" : "=a"(r) : "a"(153L) : "rcx", "r11", "memory");
return r;
}
__attribute__((noinline)) void rare_path(void) { sethostname(0, 0); }
__attribute__((noinline)) void via_pointer(void) { syscall(SYS_pivot_root, 0, 0); }
void (*volatile fp)(void) = via_pointer;
int main(int argc, char **argv) {
(void)argv;
raw_vhangup();
if (argc > 5) rare_path();
if (argc > 7) fp();
write(1, "x", 1);
return 0;
}
"#;
fn build(dir: &Path, cc: &str, extra: &[&str], name: &str) -> PathBuf {
let src = dir.join("prog.c");
std::fs::write(&src, PROG).unwrap();
let out = dir.join(name);
let st = Command::new(cc)
.args(["-O1", "-g0", "-o"])
.arg(&out)
.arg(&src)
.args(extra)
.status()
.unwrap_or_else(|e| panic!("{cc} is needed for these tests (the toolchain image has it): {e}"));
assert!(st.success(), "{cc} failed");
out
}
fn assert_shape(g: &dwarves_tune_graph::Graph) {
let v = g.syscalls.get("vhangup").expect("vhangup (inline asm in main's callee) is possible");
assert!(v.direct, "vhangup is on a direct path: {v:?}");
assert!(v.path.iter().any(|f| f == "main"), "path goes through main: {:?}", v.path);
let s = g.syscalls.get("sethostname").expect("sethostname (libc wrapper) is possible");
assert!(s.direct, "{s:?}");
assert!(s.path.iter().any(|f| f == "rare_path"), "{:?}", s.path);
let p = g.syscalls.get("pivot_root").expect("pivot_root (syscall(2) with a constant number) is possible");
assert!(!p.direct, "pivot_root is reached only through a function pointer: {p:?}");
assert!(g.syscalls.get("write").map(|r| r.direct).unwrap_or(false), "write is direct");
}
#[test]
#[ignore = "binary walk: owner-generated"]
fn a_dynamic_glibc_program_is_walked_into_libc() {
let d = tempfile::tempdir().unwrap();
let bin = build(d.path(), "cc", &[], "prog-glibc");
let g = analyze(&bin, &Options::default()).unwrap();
assert!(
g.modules.iter().any(|m| m.name.starts_with("libc.so")),
"libc.so was loaded: {:?}",
g.modules
);
assert!(g.missing_libs.is_empty(), "{:?}", g.missing_libs);
assert_shape(&g);
}
#[test]
#[ignore = "binary walk: owner-generated"]
fn a_static_musl_program_resolves_the_syscall_dispatcher() {
let d = tempfile::tempdir().unwrap();
let bin = build(d.path(), "musl-gcc", &["-static"], "prog-musl");
let g = analyze(&bin, &Options::default()).unwrap();
assert_eq!(g.modules.len(), 1, "static: one module");
assert_shape(&g);
}
#[test]
#[ignore = "binary walk: owner-generated"]
fn only_entries_restricts_the_walk() {
let d = tempfile::tempdir().unwrap();
let bin = build(d.path(), "musl-gcc", &["-static"], "prog-musl");
let opts = Options {
entries: vec!["rare_path".into()],
only_entries: true,
..Options::default()
};
let g = analyze(&bin, &opts).unwrap();
assert!(g.syscalls.contains_key("sethostname"));
assert!(!g.syscalls.contains_key("vhangup"), "main is not an entry now");
assert_eq!(g.entries, vec!["rare_path".to_string()]);
let bad = Options {
entries: vec!["no_such_fn".into()],
..Options::default()
};
assert!(analyze(&bin, &bad).is_err(), "an unknown entry is an error");
}
const PROBE: &str = r#"{
"workload": "prog",
"arch": "x86_64",
"libc": "glibc",
"runs": [
{"name": "basic", "error_path": false, "exit": "exit 0"},
{"name": "bad-input", "error_path": true, "exit": "exit 1"}
],
"calls": [
{"syscall": "write", "args": {"0": "0x1"}, "runs": ["basic"]},
{"syscall": "write", "args": {"0": "0x2"}, "runs": ["bad-input"]},
{"syscall": "vhangup", "runs": ["basic"]},
{"syscall": "io_uring_setup", "runs": ["basic"]},
{"syscall": "sethostname", "runs": ["bad-input"]}
]
}
"#;
#[test]
#[ignore = "binary walk: owner-generated"]
fn the_cross_check_names_untested_paths_gaps_and_near_misses() {
let d = tempfile::tempdir().unwrap();
let bin = build(d.path(), "cc", &[], "prog-glibc");
let g = analyze(&bin, &Options::default()).unwrap();
let pj = d.path().join("x86_64-glibc.probe.json");
std::fs::write(&pj, PROBE).unwrap();
let seen = read_seen(&pj).unwrap();
assert_eq!(seen.kind, "probe");
assert_eq!(seen.calls["write"].len(), 2, "args variants fold into one name");
let r = cross_check(&g, &seen);
assert!(r.guides_only);
for c in ["write", "vhangup", "sethostname"] {
assert!(r.confirmed.contains(&c.to_string()), "{c} confirmed: {:?}", r.confirmed);
}
assert_eq!(r.gaps.len(), 1, "{:?}", r.gaps);
assert_eq!(r.gaps[0].syscall, "io_uring_setup");
assert_eq!(r.gaps[0].runs, vec!["basic".to_string()]);
assert!(!r.gaps[0].hint.is_empty());
let pr = r.untested.iter().find(|u| u.syscall == "pivot_root").expect("pivot_root untested");
assert!(!pr.direct);
let first_indirect = r.untested.iter().position(|u| !u.direct).unwrap();
assert!(r.untested[first_indirect..].iter().all(|u| !u.direct), "direct paths first");
assert_eq!(r.near_miss.len(), 1, "{:?}", r.near_miss);
assert_eq!(r.near_miss[0].syscall, "sethostname");
assert_eq!(r.near_miss[0].graph, "direct");
assert_eq!(r.possible, g.syscalls.len());
assert!(r.possible_direct < r.possible);
let text = render_text(&r);
assert!(text.contains("never widens"), "{text}");
assert!(text.contains("io_uring_setup"), "{text}");
// The JSON report round-trips.
let j = serde_json::to_string(&r).unwrap();
let back: dwarves_tune_graph::Report = serde_json::from_str(&j).unwrap();
assert_eq!(back, r);
}
#[test]
fn an_oci_profile_reads_as_names() {
let d = tempfile::tempdir().unwrap();
let p = d.path().join("x86_64-glibc.oci.json");
std::fs::write(
&p,
r#"{"defaultAction":"SCMP_ACT_ERRNO","architectures":["SCMP_ARCH_X86_64"],
"syscalls":[{"names":["write","read"],"action":"SCMP_ACT_ALLOW"},
{"names":["openat"],"action":"SCMP_ACT_ALLOW","args":[{"index":2,"value":0,"op":"SCMP_CMP_EQ"}]},
{"names":["ptrace"],"action":"SCMP_ACT_ERRNO"}]}"#,
)
.unwrap();
let s = read_seen(&p).unwrap();
assert_eq!(s.kind, "oci");
let names: Vec<_> = s.calls.keys().cloned().collect();
assert_eq!(names, vec!["openat", "read", "write"], "only allowed calls");
assert!(read_seen(&d.path().join("missing.json")).is_err());
}