Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/dwarves

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());
}