use std::{
fs,
path::{Path, PathBuf},
process::Command,
};
use anyhow::{Context, Result, bail};
use crate::{
Category, CategorySet, FuncKey,
util::{Map, Set},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Verdict {
Confirmed,
Absent,
Unverified,
}
impl Verdict {
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::Confirmed => "confirmed",
Self::Absent => "absent",
Self::Unverified => "unverified",
}
}
}
#[derive(Debug, Clone, Copy, Default)]
struct Facts {
reaches: CategorySet,
open: bool,
}
#[derive(Debug, Default)]
pub struct Verdicts {
facts: Map<String, Facts>,
}
const SYMBOLLESS: &[Category] = &[
Category::RefCountOverflow,
Category::Unknown,
Category::Foreign,
Category::DynCall,
Category::FnPointer,
Category::GenericBound,
];
impl Verdicts {
#[must_use]
pub fn of(&self, key: &FuncKey, category: Category) -> Verdict {
if SYMBOLLESS.contains(&category) {
return Verdict::Unverified;
}
let Some(facts) = self.facts.get(&key.0) else {
return Verdict::Unverified;
};
if facts.reaches.contains(category) {
return Verdict::Confirmed;
}
if facts.open {
return Verdict::Unverified;
}
Verdict::Absent
}
}
fn entry_categories(demangled: &str) -> Option<CategorySet> {
use Category::{
AllocFailure, Borrow, CapacityOverflow, DivideByZero, Explicit, Fmt,
Index, MisalignedRef, NullDeref, Overflow, Poison, RemainderByZero,
StrBoundary, UbCheck, Unwrap,
};
let unwraps = [Unwrap, Poison, Fmt].into_iter().collect();
let table: &[(&str, CategorySet)] = &[
(
"core::panicking::panic_bounds_check",
CategorySet::single(Index),
),
("core::slice::index::slice_", CategorySet::single(Index)),
(
"core::str::slice_error_fail",
CategorySet::single(StrBoundary),
),
("core::option::unwrap_failed", unwraps),
("core::option::expect_failed", unwraps),
("core::result::unwrap_failed", unwraps),
("core::cell::panic_already", CategorySet::single(Borrow)),
(
"alloc::raw_vec::capacity_overflow",
CategorySet::single(CapacityOverflow),
),
(
"alloc::raw_vec::handle_error",
[CapacityOverflow, AllocFailure].into_iter().collect(),
),
("handle_alloc_error", CategorySet::single(AllocFailure)),
("panic_const_div_by_zero", CategorySet::single(DivideByZero)),
(
"panic_const_rem_by_zero",
CategorySet::single(RemainderByZero),
),
("panic_const_coroutine", CategorySet::single(Explicit)),
("panic_const_async", CategorySet::single(Explicit)),
("panic_const_gen_fn", CategorySet::single(Explicit)),
("panic_const_", CategorySet::single(Overflow)),
(
"panic_misaligned_pointer",
CategorySet::single(MisalignedRef),
),
("panic_null_pointer", CategorySet::single(NullDeref)),
(
"core::panicking::panic_nounwind",
[Explicit, UbCheck].into_iter().collect(),
),
("resume_unwind", CategorySet::single(Explicit)),
("len_mismatch_fail", CategorySet::single(Explicit)),
("core::panicking::", CategorySet::single(Explicit)),
];
table
.iter()
.find(|(name, _)| demangled.contains(name))
.map(|(_, set)| *set)
}
pub fn sweep(tree: &Path) -> Result<Verdicts> {
let objects = libraries_in(tree);
if objects.is_empty() {
bail!(
"no compiled library found under {}; run the analysis first",
tree.display()
);
}
let objdump = llvm_tool("llvm-objdump")?;
let mut graph = Graphed::default();
for object in &objects {
let listing = Command::new(&objdump)
.arg("-d")
.arg("-r")
.arg("--no-show-raw-insn")
.arg(object)
.output()
.with_context(|| {
format!("could not disassemble {}", object.display())
})?;
graph.read(&String::from_utf8_lossy(&listing.stdout));
}
Ok(graph.resolve())
}
#[derive(Debug, Default)]
struct Graphed {
calls: Map<String, Set<String>>,
open: Set<String>,
}
impl Graphed {
fn read(&mut self, listing: &str) {
let mut current: Option<String> = None;
for line in listing.lines() {
if let Some(label) = function_label(line) {
self.calls.entry(label.clone()).or_default();
current = Some(label);
continue;
}
let Some(function) = ¤t else { continue };
if let Some(target) = relocation_target(line) {
self.calls
.entry(function.clone())
.or_default()
.insert(target);
continue;
}
if line.contains("call") && line.contains("*%") {
self.open.insert(function.clone());
}
}
}
fn resolve(self) -> Verdicts {
let mut out = Verdicts::default();
for name in self.calls.keys() {
let mut facts = Facts::default();
let mut seen: Set<&str> = Set::default();
let mut stack: Vec<&str> = vec![name];
while let Some(at) = stack.pop() {
if !seen.insert(at) {
continue;
}
if self.open.contains(at) {
facts.open = true;
}
let Some(callees) = self.calls.get(at) else {
let readable =
format!("{:#}", rustc_demangle::demangle(at));
match entry_categories(&readable) {
Some(set) => {
facts.reaches = facts.reaches.union(set);
}
None => facts.open = true,
}
continue;
};
for callee in callees {
stack.push(callee);
}
}
out.facts.insert(name.clone(), facts);
}
out
}
}
fn function_label(line: &str) -> Option<String> {
let rest = line.strip_suffix(">:")?;
let (address, name) = rest.split_once(" <")?;
if address.is_empty() || !address.bytes().all(|b| b.is_ascii_hexdigit()) {
return None;
}
Some(name.to_owned())
}
fn relocation_target(line: &str) -> Option<String> {
if !line.contains(": R_") {
return None;
}
let raw = line.split_whitespace().last()?;
let raw = raw
.rfind(['+', '-'])
.filter(|at| raw[at + 1..].starts_with("0x"))
.map_or(raw, |at| &raw[..at]);
if let Some(inner) = raw.split_once("._R").map(|(_, sym)| sym) {
return raw.starts_with(".text").then(|| format!("_R{inner}"));
}
if let Some(inner) = raw.split_once("._ZN").map(|(_, sym)| sym) {
return raw.starts_with(".text").then(|| format!("_ZN{inner}"));
}
if raw.starts_with('.') {
return None;
}
Some(raw.to_owned())
}
fn libraries_in(tree: &Path) -> Vec<PathBuf> {
let mut out = Vec::new();
let mut stack = vec![tree.to_path_buf()];
while let Some(dir) = stack.pop() {
let Ok(entries) = fs::read_dir(&dir) else {
continue;
};
for entry in entries.filter_map(Result::ok) {
let path = entry.path();
if path.is_dir() {
if path.file_name().is_some_and(|name| name == "deps") {
continue;
}
stack.push(path);
continue;
}
let named_lib = path
.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| {
name.starts_with("lib")
&& Path::new(name)
.extension()
.is_some_and(|ext| ext == "rlib")
});
let under_release = path
.parent()
.and_then(Path::file_name)
.is_some_and(|name| name == "release" || name == "debug");
if named_lib && under_release {
out.push(path);
}
}
}
out
}
fn llvm_tool(name: &str) -> Result<PathBuf> {
let sysroot = Command::new("rustc")
.arg("--print")
.arg("sysroot")
.output()
.context("could not run rustc to find the sysroot")?;
let sysroot = String::from_utf8_lossy(&sysroot.stdout);
let host = Command::new("rustc")
.arg("-vV")
.output()
.context("could not run rustc to find the host")?;
let host = String::from_utf8_lossy(&host.stdout)
.lines()
.find_map(|line| line.strip_prefix("host: ").map(str::to_owned))
.context("rustc did not report a host triple")?;
let tool = PathBuf::from(sysroot.trim())
.join("lib")
.join("rustlib")
.join(host.trim())
.join("bin")
.join(name);
if !tool.exists() {
bail!(
"{name} is missing from the toolchain; install the llvm-tools \
component"
);
}
Ok(tool)
}