use crate::args::OutputFormat;
use crate::call_tree::{
AnalysisSummary, CallTreeNode, CrateCodePoint, build_call_tree_parallel_filtered,
collect_crate_code_points,
};
use crate::cargo::find_project_root;
use crate::config::Config;
use crate::heuristics::{
ABORT_SYMBOL_PATTERNS, LIBRARY_PANIC_PATTERNS, PANIC_SYMBOL_PATTERNS,
is_library_dependency_path, is_stdlib_function,
};
use crate::sym::{
CallGraph, DebugInfo, LibraryCallGraph, SymbolIndex, ValidSourceFiles,
find_all_symbols_matching, find_symbol_address, find_symbol_containing, load_debug_info,
matches_crate_pattern_validated,
};
use dashmap::DashSet;
use goblin::mach::Mach::Binary;
use goblin::mach::MachO;
use indicatif::{ProgressBar, ProgressStyle};
use std::collections::HashSet;
use std::io::{self, IsTerminal};
use std::path::Path;
use std::sync::Arc;
use std::time::Instant;
pub fn create_spinner(show_progress: bool, message: &str) -> Option<ProgressBar> {
if !show_progress || !io::stderr().is_terminal() {
return None;
}
let spinner = ProgressBar::new_spinner();
spinner.set_style(
ProgressStyle::default_spinner()
.template(" {spinner:.cyan} {msg} [{elapsed}]")
.expect("valid template"),
);
spinner.set_message(message.to_string());
spinner.enable_steady_tick(std::time::Duration::from_millis(100));
Some(spinner)
}
pub fn finish_spinner(spinner: Option<ProgressBar>, message: &str) {
if let Some(s) = spinner {
s.finish_with_message(message.to_string());
}
}
pub struct BinaryAnalysisResult {
pub summary: AnalysisSummary,
pub code_points: Vec<CrateCodePoint>,
}
impl BinaryAnalysisResult {
pub fn empty() -> Self {
Self {
summary: AnalysisSummary::default(),
code_points: Vec::new(),
}
}
pub fn merge(&mut self, other: BinaryAnalysisResult) {
use std::collections::HashMap;
let mut points_map: HashMap<(String, u32), CrateCodePoint> = self
.code_points
.drain(..)
.map(|cp| ((cp.file.clone(), cp.line), cp))
.collect();
for cp in other.code_points {
let key = (cp.file.clone(), cp.line);
if let Some(existing) = points_map.get_mut(&key) {
existing.causes.extend(cp.causes);
} else {
points_map.insert(key, cp);
}
}
self.code_points = points_map.into_values().collect();
self.code_points
.sort_by(|a, b| a.file.cmp(&b.file).then_with(|| a.line.cmp(&b.line)));
let points: HashSet<_> = self
.code_points
.iter()
.map(|cp| (cp.file.clone(), cp.line))
.collect();
let files: HashSet<_> = self.code_points.iter().map(|cp| cp.file.clone()).collect();
self.summary = AnalysisSummary::from_points(points, files);
}
}
#[derive(Hash, Eq, PartialEq)]
struct PanicCaller {
file: String,
name: String,
line: u32,
column: Option<u32>,
target: String,
}
#[allow(clippy::too_many_arguments)]
pub fn analyze_macho(
macho: &MachO,
buffer: &[u8],
binary_path: &Path,
crate_src_path: Option<&str>,
show_timings: bool,
config: &Config,
output: &OutputFormat,
) -> BinaryAnalysisResult {
let show_progress = output.show_progress();
let total_start = Instant::now();
let project_root = find_project_root(binary_path);
let valid_files = project_root
.as_ref()
.map(|root| ValidSourceFiles::from_project_root(root));
if show_progress {
eprintln!(" Finding entry points...");
}
let step_start = Instant::now();
let mut entry_points: Vec<(String, String, u64)> = Vec::new();
for pattern in PANIC_SYMBOL_PATTERNS {
if let Ok(Some((sym, dem))) = find_symbol_containing(macho, pattern)
&& let Some(addr) = find_symbol_address(macho, &sym)
{
entry_points.push((sym, dem, addr));
break; }
}
if let Ok(abort_symbols) = find_all_symbols_matching(macho, ABORT_SYMBOL_PATTERNS) {
for (sym, dem) in abort_symbols {
if let Some(addr) = find_symbol_address(macho, &sym) {
if !entry_points.iter().any(|(_, _, a)| *a == addr) {
entry_points.push((sym, dem, addr));
}
}
}
}
if show_timings {
eprintln!(" [timing] Find entry points: {:?}", step_start.elapsed());
}
if entry_points.is_empty() {
return BinaryAnalysisResult::empty();
}
if show_progress && entry_points.len() > 1 {
eprintln!(
" Found {} entry points (panic + abort)",
entry_points.len()
);
}
if show_progress {
eprintln!(" Loading debug information...");
}
let step_start = Instant::now();
let debug_info = load_debug_info(macho, binary_path, !show_progress);
if show_timings {
eprintln!(" [timing] Load debug info: {:?}", step_start.elapsed());
}
let spinner = create_spinner(show_progress, "Scanning for function calls...");
let step_start = Instant::now();
let symbol_index = SymbolIndex::new(macho);
let call_graph = match &debug_info {
DebugInfo::Embedded => {
CallGraph::build_with_debug_info(
macho,
buffer,
macho,
buffer,
crate_src_path,
show_timings,
symbol_index.as_ref(),
)
.or_else(|e| {
eprintln!("Warning: debug-enriched call graph failed: {e}. Falling back to symbol-only graph.");
CallGraph::build(macho, buffer, symbol_index.as_ref())
})
.unwrap_or_else(|e| {
eprintln!("Error: call graph build failed: {e}");
CallGraph::empty()
})
}
DebugInfo::DSym(dsym_info) => dsym_info.with_debug_macho(|debug_macho| {
if let Binary(debug_mach) = debug_macho {
CallGraph::build_with_debug_info(
macho,
buffer,
debug_mach,
dsym_info.borrow_debug_buffer(),
crate_src_path,
show_timings,
symbol_index.as_ref(),
)
.or_else(|e| {
eprintln!("Warning: debug-enriched call graph failed: {e}. Falling back to symbol-only graph.");
CallGraph::build(macho, buffer, symbol_index.as_ref())
})
.unwrap_or_else(|e| {
eprintln!("Error: call graph build failed: {e}");
CallGraph::empty()
})
} else {
CallGraph::build(macho, buffer, symbol_index.as_ref()).unwrap_or_else(|e| {
eprintln!("Error: call graph build failed: {e}");
CallGraph::empty()
})
}
}),
DebugInfo::DebugMap(_) | DebugInfo::None => {
CallGraph::build(macho, buffer, symbol_index.as_ref()).unwrap_or_else(|e| {
eprintln!("Error: call graph build failed: {e}");
CallGraph::empty()
})
}
};
finish_spinner(spinner, "Scanning complete");
if show_timings {
eprintln!(" [timing] Build call graph: {:?}", step_start.elapsed());
}
let mut final_result = BinaryAnalysisResult::empty();
let visited = Arc::new(DashSet::new());
let spinner = create_spinner(show_progress, "Building call trees...");
let step_start = Instant::now();
for (_mangled, demangled, target_addr) in &entry_points {
if !visited.insert(*target_addr) {
continue;
}
let mut root = CallTreeNode::new_root(demangled.clone());
root.callers = build_call_tree_parallel_filtered(
&call_graph,
*target_addr,
&visited,
crate_src_path,
valid_files.as_ref(),
);
if let Some(crate_path) = crate_src_path {
let (code_points, _summary) = collect_crate_code_points(
&root,
crate_path,
config,
valid_files.as_ref(),
project_root.as_deref(),
);
let entry_result = BinaryAnalysisResult {
summary: AnalysisSummary::default(),
code_points,
};
final_result.merge(entry_result);
}
}
let total_nodes = visited.len();
finish_spinner(
spinner,
&format!(
"Built call trees ({} nodes from {} entry points)",
total_nodes,
entry_points.len()
),
);
if show_timings {
eprintln!(
" [timing] Build call trees (with pruning): {:?}",
step_start.elapsed()
);
eprintln!(" [timing] TOTAL: {:?}", total_start.elapsed());
}
final_result
}
pub fn analyze_archive(
archive: &goblin::archive::Archive,
buffer: &[u8],
binary_path: &Path,
crate_src_path: Option<&str>,
show_timings: bool,
config: &Config,
output: &OutputFormat,
) -> BinaryAnalysisResult {
let project_root = find_project_root(binary_path);
let valid_files = project_root
.as_ref()
.map(|root| ValidSourceFiles::from_project_root(root));
let file_in_scope = |file: &str| {
crate_src_path.is_none_or(|paths| {
let file = file.replace('\\', "/");
matches_crate_pattern_validated(&file, paths, valid_files.as_ref())
})
};
let show_progress = output.show_progress();
let total_start = Instant::now();
if show_progress {
eprintln!(" Building library call graph from relocations...");
}
let step_start = Instant::now();
let mut merged_graph = LibraryCallGraph::empty();
for member_name in archive.members() {
if !member_name.ends_with(".o") {
continue;
}
let member_data = match archive.extract(member_name, buffer) {
Ok(data) => data,
Err(e) => {
if show_progress {
eprintln!(" Warning: Failed to extract {}: {}", member_name, e);
}
continue;
}
};
match MachO::parse(member_data, 0) {
Ok(obj_macho) => {
match LibraryCallGraph::build_from_object(&obj_macho, member_data, crate_src_path) {
Ok(obj_graph) => merged_graph.merge(obj_graph),
Err(e) => {
if show_progress {
eprintln!(
" Warning: Failed to build call graph for {}: {}",
member_name, e
);
}
}
}
}
Err(e) => {
if show_progress {
eprintln!(
" Warning: Failed to parse {} as Mach-O: {}",
member_name, e
);
}
}
}
}
if show_timings {
eprintln!(
" [timing] Build library call graph: {:?}",
step_start.elapsed()
);
}
if merged_graph.is_empty() {
if show_progress {
println!("\nNo call graph data found in archive");
}
return BinaryAnalysisResult::empty();
}
if show_progress {
eprintln!(" Finding panic callers...");
}
let step_start = Instant::now();
let mut panic_callers: HashSet<PanicCaller> = HashSet::new();
for target_sym in merged_graph.target_symbols() {
let is_panic_symbol = LIBRARY_PANIC_PATTERNS
.iter()
.any(|p| target_sym.contains(p))
|| target_sym.contains("core::panicking::")
|| (target_sym.contains("std::panicking::")
&& !target_sym.contains("set_hook")
&& !target_sym.contains("take_hook"));
if !is_panic_symbol {
continue;
}
for caller_info in merged_graph.get_callers(target_sym) {
if is_stdlib_function(&caller_info.caller_name) {
continue;
}
let dwarf_file = caller_info
.caller_file
.as_ref()
.filter(|f| !is_library_dependency_path(f));
if let Some(file) = dwarf_file
&& file_in_scope(file)
&& let Some(line) = caller_info.line
{
panic_callers.insert(PanicCaller {
file: file.clone(),
name: caller_info.caller_name.to_string(),
line,
column: caller_info.column,
target: target_sym.to_string(),
});
}
}
}
if show_timings {
eprintln!(" [timing] Find panic callers: {:?}", step_start.elapsed());
}
if panic_callers.is_empty() {
if show_progress {
println!("\nNo panics in crate");
}
return BinaryAnalysisResult::empty();
}
let mut sorted_callers: Vec<_> = panic_callers.into_iter().collect();
sorted_callers.sort_by(|a, b| (&a.file, a.line, &a.name).cmp(&(&b.file, b.line, &b.name)));
let mut code_points: Vec<CrateCodePoint> = sorted_callers
.into_iter()
.map(|caller| {
let mut causes = std::collections::HashSet::new();
if let Some(cause) =
crate::heuristics::detect_panic_cause(&caller.target, Some(&caller.file))
{
causes.insert(cause);
}
CrateCodePoint {
name: caller.name,
file: caller.file,
line: caller.line,
column: caller.column,
causes,
children: Vec::new(), is_direct_panic: true, called_function: None, }
})
.collect();
for point in &mut code_points {
if point.causes.is_empty() {
point.causes.insert(crate::panic_cause::PanicCause::Unknown);
}
}
code_points.retain(|point| {
point.causes.iter().any(|c| {
let denied_in_func = config.is_denied_at(c, Some(&point.file), Some(&point.name));
let denied_in_called = point
.called_function
.as_ref()
.map(|cf| config.is_denied_at(c, Some(&point.file), Some(cf)))
.unwrap_or(true);
denied_in_func && denied_in_called
})
});
for point in &mut code_points {
let file = point.file.clone();
let name = point.name.clone();
let called = point.called_function.clone();
point.causes.retain(|c| {
let denied_in_func = config.is_denied_at(c, Some(&file), Some(&name));
let denied_in_called = called
.as_ref()
.map(|cf| config.is_denied_at(c, Some(&file), Some(cf)))
.unwrap_or(true);
denied_in_func && denied_in_called
});
}
let mut seen: std::collections::HashMap<(String, u32), usize> =
std::collections::HashMap::new();
let mut deduped: Vec<CrateCodePoint> = Vec::new();
for point in code_points {
let key = (point.file.clone(), point.line);
if let Some(&idx) = seen.get(&key) {
deduped[idx].causes.extend(point.causes);
} else {
seen.insert(key, deduped.len());
deduped.push(point);
}
}
if show_timings {
eprintln!(" [timing] TOTAL: {:?}", total_start.elapsed());
}
let mut points: HashSet<(String, u32)> = HashSet::new();
let mut files_affected: HashSet<String> = HashSet::new();
for point in &deduped {
points.insert((point.file.clone(), point.line));
files_affected.insert(point.file.clone());
}
BinaryAnalysisResult {
summary: AnalysisSummary::from_points(points, files_affected),
code_points: deduped,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::panic_cause::PanicCause;
#[test]
fn test_binary_analysis_result_empty() {
let result = BinaryAnalysisResult::empty();
assert!(result.code_points.is_empty());
assert_eq!(result.summary.panic_points(), 0);
assert_eq!(result.summary.files_affected(), 0);
}
fn make_code_point(file: &str, line: u32, cause: PanicCause) -> CrateCodePoint {
let mut causes = HashSet::new();
causes.insert(cause);
CrateCodePoint {
name: "test_func".to_string(),
file: file.to_string(),
line,
column: None,
causes,
children: Vec::new(),
is_direct_panic: true,
called_function: None,
}
}
#[test]
fn test_binary_analysis_result_merge_disjoint() {
let mut result1 = BinaryAnalysisResult {
summary: AnalysisSummary::default(),
code_points: vec![make_code_point("src/a.rs", 10, PanicCause::UnwrapNone)],
};
let result2 = BinaryAnalysisResult {
summary: AnalysisSummary::default(),
code_points: vec![make_code_point("src/b.rs", 20, PanicCause::UnwrapErr)],
};
result1.merge(result2);
assert_eq!(result1.code_points.len(), 2);
assert_eq!(result1.summary.panic_points(), 2);
assert_eq!(result1.summary.files_affected(), 2);
}
#[test]
fn test_binary_analysis_result_merge_same_location() {
let mut result1 = BinaryAnalysisResult {
summary: AnalysisSummary::default(),
code_points: vec![make_code_point("src/main.rs", 10, PanicCause::UnwrapNone)],
};
let result2 = BinaryAnalysisResult {
summary: AnalysisSummary::default(),
code_points: vec![make_code_point("src/main.rs", 10, PanicCause::ExpectNone)],
};
result1.merge(result2);
assert_eq!(result1.code_points.len(), 1);
assert_eq!(result1.code_points[0].causes.len(), 2);
assert!(
result1.code_points[0]
.causes
.contains(&PanicCause::UnwrapNone)
);
assert!(
result1.code_points[0]
.causes
.contains(&PanicCause::ExpectNone)
);
}
#[test]
fn test_binary_analysis_result_merge_sorted() {
let mut result1 = BinaryAnalysisResult {
summary: AnalysisSummary::default(),
code_points: vec![make_code_point("src/z.rs", 100, PanicCause::UnwrapNone)],
};
let result2 = BinaryAnalysisResult {
summary: AnalysisSummary::default(),
code_points: vec![
make_code_point("src/a.rs", 10, PanicCause::UnwrapErr),
make_code_point("src/a.rs", 5, PanicCause::ExplicitPanic),
],
};
result1.merge(result2);
assert_eq!(result1.code_points.len(), 3);
assert_eq!(result1.code_points[0].file, "src/a.rs");
assert_eq!(result1.code_points[0].line, 5);
assert_eq!(result1.code_points[1].file, "src/a.rs");
assert_eq!(result1.code_points[1].line, 10);
assert_eq!(result1.code_points[2].file, "src/z.rs");
assert_eq!(result1.code_points[2].line, 100);
}
#[test]
fn test_binary_analysis_result_merge_empty() {
let mut result1 = BinaryAnalysisResult {
summary: AnalysisSummary::default(),
code_points: vec![make_code_point("src/main.rs", 10, PanicCause::UnwrapNone)],
};
let result2 = BinaryAnalysisResult::empty();
result1.merge(result2);
assert_eq!(result1.code_points.len(), 1);
}
}