mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-06 19:35:13 +02:00
258 lines
11 KiB
Rust
258 lines
11 KiB
Rust
//! Context-sensitive inline analysis, cache, body, and attribution types.
|
|
//!
|
|
//! The cache ([`InlineCache`]) is keyed by `(FuncKey, ArgTaintSig)`,
|
|
//! where [`ArgTaintSig`] is per-arg cap bits only (not origin identity).
|
|
//! Stored values ([`CachedInlineShape`]) capture the structural shape of
|
|
//! the callee's return taint; caller-specific origins are re-attributed
|
|
//! at apply time.
|
|
|
|
use crate::labels::Cap;
|
|
use crate::ssa::ir::{SsaBody, Terminator};
|
|
use crate::summary::ssa_summary::PathFactReturnEntry;
|
|
use crate::symbol::FuncKey;
|
|
use crate::taint::domain::{TaintOrigin, VarTaint};
|
|
use petgraph::graph::NodeIndex;
|
|
use smallvec::SmallVec;
|
|
use std::collections::HashMap;
|
|
|
|
/// Maximum SSA blocks in a callee body before skipping inline analysis.
|
|
pub(super) const MAX_INLINE_BLOCKS: usize = 500;
|
|
|
|
/// Compact cache key: per-arg-position cap bits (sorted, non-empty
|
|
/// only). Origin identity is not part of the key.
|
|
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
|
pub(crate) struct ArgTaintSig(pub(super) SmallVec<[(usize, u32); 4]>);
|
|
|
|
/// Call-site-adapted result of inline-analyzing a callee. Built fresh
|
|
/// per call site so origins point to the current caller's chain.
|
|
#[derive(Clone, Debug)]
|
|
pub(crate) struct InlineResult {
|
|
pub(super) return_taint: Option<VarTaint>,
|
|
/// PathFact on the return value. Non-top when the callee body
|
|
/// provably narrows it (e.g. a `sanitize_path` early-returning on
|
|
/// `s.contains("..")`).
|
|
pub(super) return_path_fact: crate::abstract_interp::PathFact,
|
|
/// Per-return-path decomposition of `return_path_fact`. Non-empty
|
|
/// when the callee has ≥2 return blocks with different predicate
|
|
/// gates.
|
|
#[allow(dead_code)]
|
|
pub(super) return_path_facts: SmallVec<[PathFactReturnEntry; 2]>,
|
|
}
|
|
|
|
/// Structural (callsite-agnostic) summary of an inline-analyzed
|
|
/// callee. `None` means "no return taint for this arg shape", still
|
|
/// meaningful, short-circuits subsequent calls with matching caps.
|
|
#[derive(Clone, Debug)]
|
|
pub(crate) struct CachedInlineShape(pub(super) Option<ReturnShape>);
|
|
|
|
/// Structural parts of a non-trivial inline-analysis result.
|
|
///
|
|
/// Split from the full [`VarTaint`] so that cached entries can be re-used
|
|
/// across call sites with matching arg-cap signatures but differing source
|
|
/// origins. See the module-level note above on origin attribution.
|
|
#[derive(Clone, Debug)]
|
|
pub(crate) struct ReturnShape {
|
|
/// Return value caps (cap bits only, structural).
|
|
pub(super) caps: Cap,
|
|
/// Origins produced **inside the callee body** (e.g. `Source` op fired
|
|
/// in the callee). `node` is set to a placeholder; at apply time the
|
|
/// caller remaps it to its own call-site NodeIndex. `source_span` is
|
|
/// stable (from the callee CFG) and preserved as-is.
|
|
pub(super) internal_origins: SmallVec<[TaintOrigin; 2]>,
|
|
/// Bit i set = callee's `Param(i)` seed taint reached the return value.
|
|
/// At apply time, caller arg origins at matching positions are
|
|
/// unioned into the applied `VarTaint`. Params beyond 63 are
|
|
/// dropped (matches `SmallBitSet`); rare and still cap-correct.
|
|
pub(super) param_provenance: u64,
|
|
/// Whether the receiver (`SelfParam`) seed taint flowed to return.
|
|
pub(super) receiver_provenance: bool,
|
|
pub(super) uses_summary: bool,
|
|
/// PathFact of the return value, observed from the callee exit
|
|
/// state under Top-seeded Params. Describes the callee's intrinsic
|
|
/// narrowing.
|
|
pub(super) return_path_fact: crate::abstract_interp::PathFact,
|
|
/// Per-return-path decomposition of the return value. Populated
|
|
/// when the callee has ≥2 return blocks with different predicates.
|
|
pub(super) return_path_facts: SmallVec<[PathFactReturnEntry; 2]>,
|
|
}
|
|
|
|
impl CachedInlineShape {
|
|
/// Cap bits of the return value, or zero if this shape records "no
|
|
/// return taint". Used by [`inline_cache_fingerprint`].
|
|
fn return_caps_bits(&self) -> u32 {
|
|
self.0.as_ref().map(|s| s.caps.bits()).unwrap_or(0)
|
|
}
|
|
}
|
|
|
|
/// Cache for context-sensitive inline analysis results, keyed by
|
|
/// canonical [`FuncKey`] so same-name definitions in different scopes
|
|
/// never collide.
|
|
pub(crate) type InlineCache = HashMap<(FuncKey, ArgTaintSig), CachedInlineShape>;
|
|
|
|
/// Drop every entry from the inline cache between SCC fixpoint
|
|
/// iterations so stale results don't leak forward.
|
|
#[allow(dead_code)]
|
|
pub(crate) fn inline_cache_clear_epoch(cache: &mut InlineCache) {
|
|
cache.clear();
|
|
}
|
|
|
|
/// Set-equal fingerprint of the inline cache, used by the SCC
|
|
/// orchestrator to detect convergence.
|
|
#[allow(dead_code)]
|
|
pub(crate) fn inline_cache_fingerprint(
|
|
cache: &InlineCache,
|
|
) -> HashMap<(FuncKey, ArgTaintSig), u32> {
|
|
cache
|
|
.iter()
|
|
.map(|(k, v)| (k.clone(), v.return_caps_bits()))
|
|
.collect()
|
|
}
|
|
|
|
/// CFG node metadata embedded in cross-file callee bodies.
|
|
///
|
|
/// Stores a full serde-able [`crate::cfg::NodeInfo`] snapshot rather
|
|
/// than projecting individual fields, so the indexed-scan path can
|
|
/// rehydrate an equivalent `Cfg` (see [`rebuild_body_graph`]) and feed
|
|
/// the same `&Cfg` into the taint engine regardless of whether the
|
|
/// body came from pass 1 or SQLite.
|
|
#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
|
pub struct CrossFileNodeMeta {
|
|
/// Full `NodeInfo` snapshot for this body-local NodeIndex.
|
|
pub info: crate::cfg::NodeInfo,
|
|
}
|
|
|
|
/// Pre-lowered and optimized SSA body for a function,
|
|
/// ready for context-sensitive re-analysis with different argument taint.
|
|
///
|
|
/// For intra-file use, `node_meta` is empty and the original CFG is used.
|
|
/// For cross-file persistence, `node_meta` carries the minimal CFG
|
|
/// metadata needed by the symex executor.
|
|
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
|
pub struct CalleeSsaBody {
|
|
pub ssa: SsaBody,
|
|
pub opt: crate::ssa::OptimizeResult,
|
|
pub param_count: usize,
|
|
/// Per-NodeIndex CFG metadata for cross-file bodies.
|
|
/// Empty for intra-file bodies (the original CFG is used instead).
|
|
#[serde(default, skip_serializing_if = "std::collections::HashMap::is_empty")]
|
|
pub node_meta: std::collections::HashMap<u32, CrossFileNodeMeta>,
|
|
/// The body's own CFG graph. Populated for intra-file bodies so that
|
|
/// inline analysis can reference the correct graph (per-body CFGs have
|
|
/// body-local NodeIndex spaces). `None` for cross-file deserialized
|
|
/// bodies.
|
|
#[serde(skip)]
|
|
pub body_graph: Option<crate::cfg::Cfg>,
|
|
}
|
|
|
|
/// Populate `node_meta` from the original CFG for cross-file persistence.
|
|
///
|
|
/// Returns `true` if all referenced NodeIndex values were resolved
|
|
/// successfully. Returns `false` if any node was out of bounds (body is
|
|
/// ineligible for cross-file use).
|
|
pub fn populate_node_meta(body: &mut CalleeSsaBody, cfg: &crate::cfg::Cfg) -> bool {
|
|
// Collect every NodeIndex this body references, then snapshot each one's
|
|
// NodeInfo into `node_meta`. Done in two passes so the inner loop can
|
|
// mutate `body.node_meta` without borrow-checker conflicts on
|
|
// `body.ssa.blocks`.
|
|
//
|
|
// `Terminator::Branch.cond` must be captured as well: it is consumed by
|
|
// `compute_succ_states` via `cfg[*cond]`, so without it the synthesized
|
|
// cross-file proxy CFG (`rebuild_body_graph`) ends up too small whenever
|
|
// the callee body has any conditional branch whose `cond` index sits
|
|
// past the maximum `inst.cfg_node` index, inline analysis then panics
|
|
// with an out-of-bounds index.
|
|
let mut referenced: Vec<NodeIndex> = Vec::new();
|
|
for block in &body.ssa.blocks {
|
|
for inst in block.phis.iter().chain(block.body.iter()) {
|
|
referenced.push(inst.cfg_node);
|
|
}
|
|
if let Terminator::Branch { cond, .. } = &block.terminator {
|
|
referenced.push(*cond);
|
|
}
|
|
}
|
|
for node in referenced {
|
|
let idx = node.index() as u32;
|
|
if body.node_meta.contains_key(&idx) {
|
|
continue;
|
|
}
|
|
if node.index() >= cfg.node_count() {
|
|
return false;
|
|
}
|
|
let info = cfg[node].clone();
|
|
body.node_meta.insert(idx, CrossFileNodeMeta { info });
|
|
}
|
|
true
|
|
}
|
|
|
|
/// Synthesize a proxy [`crate::cfg::Cfg`] from `node_meta` so the taint
|
|
/// engine can index `cfg[inst.cfg_node]` uniformly on the indexed-scan
|
|
/// path.
|
|
///
|
|
/// When the callee body was loaded from SQLite, `body_graph` is `None`
|
|
/// (it is `#[serde(skip)]`), but `node_meta` carries a full
|
|
/// [`crate::cfg::NodeInfo`] for every referenced NodeIndex (see
|
|
/// [`populate_node_meta`]). This helper rebuilds a petgraph `Cfg` with
|
|
/// nodes at exactly the right NodeIndex positions so the taint engine's
|
|
/// existing indexing works without change.
|
|
///
|
|
/// Returns `true` if a proxy graph was freshly installed. Idempotent:
|
|
/// subsequent calls are cheap no-ops once `body_graph` is `Some`. No-op
|
|
/// for intra-file bodies (which arrive with `body_graph` already set and
|
|
/// `node_meta` empty).
|
|
pub fn rebuild_body_graph(body: &mut CalleeSsaBody) -> bool {
|
|
if body.body_graph.is_some() {
|
|
return false;
|
|
}
|
|
if body.node_meta.is_empty() {
|
|
return false;
|
|
}
|
|
// Determine the maximum NodeIndex referenced by the SSA so the
|
|
// synthesized graph has an entry at every position the engine may
|
|
// index. We fill any unreferenced intermediate indices with
|
|
// `NodeInfo::default()`.
|
|
//
|
|
// Walks both instruction `cfg_node`s and `Terminator::Branch.cond` ,
|
|
// the latter is read by `compute_succ_states` via `cfg[*cond]`, so
|
|
// missing it produces an OOB panic when a conditional branch's cond
|
|
// node has a higher index than any `inst.cfg_node` in the body.
|
|
let mut max_idx: u32 = 0;
|
|
for block in &body.ssa.blocks {
|
|
for inst in block.phis.iter().chain(block.body.iter()) {
|
|
let idx = inst.cfg_node.index() as u32;
|
|
if idx > max_idx {
|
|
max_idx = idx;
|
|
}
|
|
}
|
|
if let Terminator::Branch { cond, .. } = &block.terminator {
|
|
let idx = cond.index() as u32;
|
|
if idx > max_idx {
|
|
max_idx = idx;
|
|
}
|
|
}
|
|
}
|
|
// Also consider node_meta keys, they should be a subset of the
|
|
// SSA-referenced indices, but be defensive.
|
|
for &k in body.node_meta.keys() {
|
|
if k > max_idx {
|
|
max_idx = k;
|
|
}
|
|
}
|
|
|
|
use petgraph::graph::Graph;
|
|
let mut graph: crate::cfg::Cfg = Graph::new();
|
|
// petgraph allocates sequential NodeIndex values. Insert placeholders
|
|
// up to and including max_idx.
|
|
for i in 0..=max_idx {
|
|
let info = body
|
|
.node_meta
|
|
.get(&i)
|
|
.map(|m| m.info.clone())
|
|
.unwrap_or_default();
|
|
graph.add_node(info);
|
|
}
|
|
// Edges are not consulted by the taint engine during inline analysis
|
|
// (control flow comes from `SsaBlock::preds`/`succs` and
|
|
// `SsaBlock::terminator`), so we leave the graph edge-free.
|
|
body.body_graph = Some(graph);
|
|
true
|
|
}
|