crates/dwarves-tune-graph/src/lib.rs
//! # dwarves-tune-graph: the graph cross-check of syscall tuning
//!
//! A Heimdall probe records the syscalls a program MADE while its tests (or a
//! workload driver) ran: a SUBSET of what the program can do. This crate
//! computes the POSSIBLE syscalls from the program's entry points (a
//! SUPERSET) and compares the two:
//!
//! | finding | meaning | what to do |
//! |---|---|---|
//! | possible, unseen, **direct** path | code that can make the call was never driven | add a test or a workload step (a better near-miss report) |
//! | possible, unseen, only through **indirect** calls | reachable only if an address-taken function is called through a pointer | review; often dead in practice |
//! | **seen, not in the graph** | the graph missed it (dynamic dispatch it could not follow, a syscall number it could not resolve, `dlopen`, JIT) | review the gap; the probe is the truth |
//!
//! **It guides review and tests only. It never widens a filter**: a generated
//! profile holds what the probe saw, nothing the graph merely thinks possible.
//!
//! ## The graph
//!
//! The graph is a BINARY call graph (the method of *sysfilter*, DeMarinis et al.,
//! RAID 2020, and *Confine*, Ghavamnia et al., RAID 2020): the ELF is decoded
//! (x86-64, iced-x86) from its entry points (the ELF entry, `main`, the
//! init arrays, and any `--entry` symbol), following direct calls and jumps,
//! PLT and GOT calls into the shared libraries it needs (each library is
//! decoded the same way), and every `syscall` instruction whose number is a
//! constant in `eax` (or in the number argument of a dispatcher such as
//! libc's `syscall(2)`). An indirect call it cannot follow makes every
//! ADDRESS-TAKEN function reachable (functions whose address appears in a
//! relocation or a `lea` of reached code): that is the sound-ish superset.
//! What the walk reached through direct edges alone is reported apart.
//!
//! A binary graph sees what the compiler emitted, C and Rust alike, the
//! libraries included; a source graph would stop at the first dependency.
//!
//! ## Licence boundary
//!
//! MIT OR Apache-2.0. It reads Heimdall's JSON files (`<arch>-<libc>.probe.json`
//! and the OCI `.oci.json` profile) and never links Heimdall's AGPL crates.
use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
mod elf;
mod profile;
pub mod srcgraph;
mod walk;
mod xcheck;
pub use profile::read_seen;
pub use xcheck::{cross_check, render_text};
/// The report format id.
pub const REPORT_FORMAT: &str = "dwarves.tune.xcheck/1";
/// Why an analysis failed.
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("{path}: {err}")]
Io { path: PathBuf, err: std::io::Error },
#[error("{path}: {msg}")]
Elf { path: PathBuf, msg: String },
#[error("{path}: {msg}")]
Profile { path: PathBuf, msg: String },
#[error("{path}: only x86-64 ELF is supported so far (found {found})")]
Arch { path: PathBuf, found: String },
#[error("entry point {0:?}: no such function symbol in the binary")]
NoEntry(String),
#[error("{path}: not a Graph JSON file: {msg}")]
Graph { path: PathBuf, msg: String },
}
/// How to build the graph.
#[derive(Clone, Debug, Default)]
pub struct Options {
/// Extra entry points (function symbols of the binary, mangled or not).
pub entries: Vec<String>,
/// Only these entry points (no ELF entry, `main` or init arrays).
pub only_entries: bool,
/// Directories searched for the binary's shared libraries, before the
/// default ones (`DT_RUNPATH`/`DT_RPATH`, then the system dirs).
pub lib_dirs: Vec<PathBuf>,
/// A root the default library dirs are taken under (an unpacked OCI image).
pub root: Option<PathBuf>,
/// Do not follow shared libraries.
pub no_libs: bool,
}
/// The graph's types are the shared evidence crate's (kernel tooling fix 20):
/// one definition of `graph.json` for this reader, nornir's `mimir-room
/// possible` writer and Build Thing's possible set. A source graph's extra
/// fields (`format`, `uncertain`, `callee`, `reason`, `targets`,
/// `reached_functions`) are optional there and kept when read.
pub use nordisk_kernel_evidence::graph::{Graph, ModuleInfo, Reach, Site};
/// What a probe saw.
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Seen {
/// The file it was read from.
pub source: String,
/// `probe` (a probe.json, calls with runs) or `oci` (a profile: names only).
pub kind: String,
pub workload: String,
/// Syscall name -> the runs that made it (empty for an OCI profile).
pub calls: BTreeMap<String, BTreeSet<String>>,
/// The runs that drive an error path.
pub error_runs: BTreeSet<String>,
/// Calls the probe could not name.
pub unknown: Vec<String>,
}
/// One possible-but-unseen call.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Untested {
pub syscall: String,
pub direct: bool,
pub path: Vec<String>,
pub site: String,
/// A short note (`vdso`, ...), empty when none.
pub note: String,
}
/// One seen-but-not-in-graph call.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Gap {
pub syscall: String,
pub runs: Vec<String>,
/// The graph's likely blind spot.
pub hint: String,
}
/// A call seen only on error-path runs (a probe near-miss), with the graph's view.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct NearMiss {
pub syscall: String,
pub runs: Vec<String>,
/// `direct`, `indirect` or `not in graph`.
pub graph: String,
pub path: Vec<String>,
}
/// The cross-check.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Report {
pub format: String,
pub binary: String,
pub profile: String,
pub workload: String,
pub entries: Vec<String>,
pub modules: Vec<ModuleInfo>,
pub possible: usize,
pub possible_direct: usize,
pub seen: usize,
/// Seen and in the graph.
pub confirmed: Vec<String>,
/// Possible but unseen: direct paths first.
pub untested: Vec<Untested>,
/// Seen but not in the graph.
pub gaps: Vec<Gap>,
pub near_miss: Vec<NearMiss>,
pub unresolved_sites: usize,
pub missing_libs: Vec<String>,
pub dlopen: bool,
/// Always true: the report never widens a filter.
pub guides_only: bool,
pub notes: Vec<String>,
}
/// Read a [`Graph`] JSON file (the owner's possible-syscalls engine writes it).
pub fn read_graph(path: &Path) -> Result<Graph, Error> {
let text = std::fs::read_to_string(path).map_err(|err| Error::Io {
path: path.to_path_buf(),
err,
})?;
serde_json::from_str(&text).map_err(|e| Error::Graph {
path: path.to_path_buf(),
msg: e.to_string(),
})
}
/// Build the possible-syscall graph of `binary`.
pub fn analyze(binary: &Path, opts: &Options) -> Result<Graph, Error> {
walk::analyze(binary, opts)
}