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/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)
}