From 61269de87fbf2bd51c9edff826b1a0f4bb1b3de2 Mon Sep 17 00:00:00 2001 From: "Celina G. Val" Date: Tue, 24 Sep 2024 06:55:27 -0700 Subject: [PATCH 1/6] Analyze unsafe code reachability Add callgraph analysis to scanner in order to find the distance between functions in a crate and unsafe functions. For that, we build the crate call graph and collect the unsafe functions. After that, do reverse BFS traversal from the unsafe functions and store the distance to other functions. The result is stored in a new csv file. --- .../tool-scanner/scanner-test.expected | 4 +- .../tool-scanner/scanner-test.sh | 2 +- tests/script-based-pre/tool-scanner/test.rs | 13 +++ tools/scanner/src/analysis.rs | 84 ++++++++++++--- tools/scanner/src/call_graph.rs | 101 ++++++++++++++++++ tools/scanner/src/lib.rs | 9 +- 6 files changed, 194 insertions(+), 19 deletions(-) create mode 100644 tools/scanner/src/call_graph.rs diff --git a/tests/script-based-pre/tool-scanner/scanner-test.expected b/tests/script-based-pre/tool-scanner/scanner-test.expected index f55a883434ee..7e5b3875979d 100644 --- a/tests/script-based-pre/tool-scanner/scanner-test.expected +++ b/tests/script-based-pre/tool-scanner/scanner-test.expected @@ -1,6 +1,6 @@ 5 test_scan_fn_loops.csv -19 test_scan_functions.csv +20 test_scan_functions.csv 5 test_scan_input_tys.csv 16 test_scan_overall.csv 3 test_scan_recursion.csv -5 test_scan_unsafe_ops.csv +6 test_scan_unsafe_ops.csv diff --git a/tests/script-based-pre/tool-scanner/scanner-test.sh b/tests/script-based-pre/tool-scanner/scanner-test.sh index 2cd5a33a3f8e..7ac7c5fd74dc 100755 --- a/tests/script-based-pre/tool-scanner/scanner-test.sh +++ b/tests/script-based-pre/tool-scanner/scanner-test.sh @@ -17,4 +17,4 @@ cargo run -p scanner test.rs --crate-type lib wc -l *csv popd -rm -rf ${OUT_DIR} +#rm -rf ${OUT_DIR} diff --git a/tests/script-based-pre/tool-scanner/test.rs b/tests/script-based-pre/tool-scanner/test.rs index f6a141f2a708..0cdebde134f2 100644 --- a/tests/script-based-pre/tool-scanner/test.rs +++ b/tests/script-based-pre/tool-scanner/test.rs @@ -14,6 +14,11 @@ pub fn generic() -> T { T::default() } +pub fn blah() { + ok(); + assert_eq!(u8::default(), 0); +} + pub struct RecursiveType { pub inner: Option<*const RecursiveType>, } @@ -102,3 +107,11 @@ pub fn start_recursion() { pub fn not_recursive() { let _ = ok(); } + +extern "C" { + fn external_function(); +} + +pub fn call_external() { + unsafe { external_function() }; +} diff --git a/tools/scanner/src/analysis.rs b/tools/scanner/src/analysis.rs index 7c8b8d7468da..3ab5b0130253 100644 --- a/tools/scanner/src/analysis.rs +++ b/tools/scanner/src/analysis.rs @@ -11,9 +11,10 @@ use serde::{Serialize, Serializer, ser::SerializeStruct}; use stable_mir::mir::mono::Instance; use stable_mir::mir::visit::{Location, PlaceContext, PlaceRef}; use stable_mir::mir::{ - BasicBlock, Body, MirVisitor, Mutability, ProjectionElem, Safety, Terminator, TerminatorKind, + BasicBlock, Body, CastKind, MirVisitor, Mutability, NonDivergingIntrinsic, ProjectionElem, + Rvalue, Safety, Statement, StatementKind, Terminator, TerminatorKind, }; -use stable_mir::ty::{AdtDef, AdtKind, FnDef, GenericArgs, MirConst, RigidTy, Ty, TyKind}; +use stable_mir::ty::{Abi, AdtDef, AdtKind, FnDef, GenericArgs, MirConst, RigidTy, Ty, TyKind}; use stable_mir::visitor::{Visitable, Visitor}; use stable_mir::{CrateDef, CrateItem}; use std::collections::{HashMap, HashSet}; @@ -23,7 +24,7 @@ use std::path::{Path, PathBuf}; #[derive(Clone, Debug)] pub struct OverallStats { /// The key and value of each counter. - counters: Vec<(&'static str, usize)>, + pub counters: Vec<(&'static str, usize)>, /// TODO: Group stats per function. fn_stats: HashMap, } @@ -35,6 +36,12 @@ struct FnStats { has_unsafe_ops: Option, has_unsupported_input: Option, has_loop_or_iterator: Option, + /// How many degrees of separation to unsafe code if any? + /// - `None` if this function is indeed safe. + /// - 0 if this function contains unsafe code (including invoking unsafe fns). + /// - 1 if this function calls a safe abstraction. + /// - 2+ if this function calls other functions that call safe abstractions. + unsafe_distance: Option, } impl FnStats { @@ -45,6 +52,7 @@ impl FnStats { has_unsafe_ops: None, has_unsupported_input: None, has_loop_or_iterator: None, + unsafe_distance: None, } } } @@ -232,24 +240,24 @@ impl OverallStats { macro_rules! fn_props { ($(#[$attr:meta])* - struct $name:ident { + $vis:vis struct $name:ident { $( $(#[$prop_attr:meta])* $prop:ident, )+ }) => { #[derive(Debug)] - struct $name { + $vis struct $name { fn_name: String, $($(#[$prop_attr])* $prop: usize,)+ } impl $name { - const fn num_props() -> usize { + pub const fn num_props() -> usize { [$(stringify!($prop),)+].len() } - fn new(fn_name: String) -> Self { + pub fn new(fn_name: String) -> Self { Self { fn_name, $($prop: 0,)+} } } @@ -369,7 +377,7 @@ impl Visitor for TypeVisitor<'_> { } } -fn dump_csv(mut out_path: PathBuf, data: &[T]) { +pub(crate) fn dump_csv(mut out_path: PathBuf, data: &[T]) { out_path.set_extension("csv"); info(format!("Write file: {out_path:?}")); let mut writer = WriterBuilder::new().delimiter(b';').from_path(&out_path).unwrap(); @@ -379,17 +387,23 @@ fn dump_csv(mut out_path: PathBuf, data: &[T]) { } fn_props! { - struct FnUnsafeOperations { + pub struct FnUnsafeOperations { inline_assembly, /// Dereference a raw pointer. /// This is also counted when we access a static variable since it gets translated to a raw pointer. unsafe_dereference, - /// Call an unsafe function or method. + /// Call an unsafe function or method including C-FFI. unsafe_call, /// Access or modify a mutable static variable. unsafe_static_access, /// Access fields of unions. unsafe_union_access, + /// Invoke external functions (this is a subset of `unsafe_call`. + extern_call, + /// Transmute operations. + transmute, + /// Cast raw pointer to reference. + unsafe_cast, } } @@ -419,9 +433,21 @@ impl MirVisitor for BodyVisitor<'_> { fn visit_terminator(&mut self, term: &Terminator, location: Location) { match &term.kind { TerminatorKind::Call { func, .. } => { - let fn_sig = func.ty(self.body.locals()).unwrap().kind().fn_sig().unwrap(); - if fn_sig.value.safety == Safety::Unsafe { + let TyKind::RigidTy(RigidTy::FnDef(fn_def, _)) = + func.ty(self.body.locals()).unwrap().kind() + else { + return self.super_terminator(term, location); + }; + let fn_sig = fn_def.fn_sig().skip_binder(); + if fn_sig.safety == Safety::Unsafe { self.props.unsafe_call += 1; + if !matches!( + fn_sig.abi, + Abi::Rust | Abi::RustCold | Abi::RustCall | Abi::RustIntrinsic + ) && !fn_def.has_body() + { + self.props.extern_call += 1; + } } } TerminatorKind::InlineAsm { .. } => self.props.inline_assembly += 1, @@ -430,6 +456,34 @@ impl MirVisitor for BodyVisitor<'_> { self.super_terminator(term, location) } + fn visit_rvalue(&mut self, rvalue: &Rvalue, location: Location) { + if let Rvalue::Cast(cast_kind, operand, ty) = rvalue { + match cast_kind { + CastKind::Transmute => { + self.props.transmute += 1; + } + _ => { + let operand_ty = operand.ty(self.body.locals()).unwrap(); + if ty.kind().is_ref() && operand_ty.kind().is_raw_ptr() { + self.props.unsafe_cast += 1; + } + } + } + }; + self.super_rvalue(rvalue, location); + } + + fn visit_statement(&mut self, stmt: &Statement, location: Location) { + if matches!( + &stmt.kind, + StatementKind::Intrinsic(NonDivergingIntrinsic::CopyNonOverlapping(_)) + ) { + // Treat this as invoking the copy intrinsic. + self.props.unsafe_call += 1; + } + self.super_statement(stmt, location) + } + fn visit_projection_elem( &mut self, place: PlaceRef, @@ -674,9 +728,9 @@ impl Recursion { } } -struct FnCallVisitor<'a> { - body: &'a Body, - fns: Vec, +pub struct FnCallVisitor<'a> { + pub body: &'a Body, + pub fns: Vec, } impl MirVisitor for FnCallVisitor<'_> { diff --git a/tools/scanner/src/call_graph.rs b/tools/scanner/src/call_graph.rs new file mode 100644 index 000000000000..81d81da317cb --- /dev/null +++ b/tools/scanner/src/call_graph.rs @@ -0,0 +1,101 @@ +// Copyright Kani Contributors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +//! Provide different static analysis to be performed in the call graph + +use crate::analysis::{FnCallVisitor, FnUnsafeOperations, OverallStats}; +use stable_mir::mir::{MirVisitor, Safety}; +use stable_mir::ty::{FnDef, RigidTy, Ty, TyKind}; +use stable_mir::{CrateDef, CrateDefType}; +use std::collections::hash_map::Entry; +use std::collections::{HashMap, VecDeque}; +use std::hash::{Hash, Hasher}; +use std::path::PathBuf; + +impl OverallStats { + /// Iterate over all functions defined in this crate and log any unsafe operation. + pub fn unsafe_distance(&mut self, filename: PathBuf) { + let all_items = stable_mir::all_local_items(); + let mut queue = + all_items.into_iter().filter_map(|item| Node::try_new(item.ty())).collect::>(); + // Build call graph + let mut call_graph = CallGraph::default(); + while let Some(node) = queue.pop() { + if let Entry::Vacant(e) = call_graph.nodes.entry(node.def) { + e.insert(node); + let Some(body) = node.def.body() else { + continue; + }; + let mut visitor = FnCallVisitor { body: &body, fns: vec![] }; + visitor.visit_body(&body); + queue.extend(visitor.fns.iter().map(|def| Node::try_new(def.ty()).unwrap())); + for callee in &visitor.fns { + call_graph.rev_edges.entry(*callee).or_default().push(node.def) + } + call_graph.edges.insert(node.def, visitor.fns); + } + } + + // Calculate the distance between unsafe functions and functions with unsafe operation. + let mut queue = call_graph + .nodes + .values() + .filter_map(|node| node.has_unsafe.then_some((node.def, 0))) + .collect::>(); + let mut visited: HashMap = HashMap::from_iter(queue.iter().cloned()); + while let Some(current) = queue.pop_front() { + for caller in call_graph.rev_edges.entry(current.0).or_default() { + if !visited.contains_key(caller) { + let distance = current.1 + 1; + visited.insert(*caller, distance); + queue.push_back((*caller, distance)) + } + } + } + let krate = stable_mir::local_crate(); + let transitive_unsafe = visited + .into_iter() + .filter_map(|(def, distance)| (def.krate() == krate).then_some((def.name(), distance))) + .collect::>(); + self.counters.push(("transitive_unsafe", transitive_unsafe.len())); + crate::analysis::dump_csv(filename, &transitive_unsafe); + } +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +struct Node { + def: FnDef, + is_unsafe: bool, + has_unsafe: bool, +} + +impl Node { + fn try_new(ty: Ty) -> Option { + let kind = ty.kind(); + let TyKind::RigidTy(RigidTy::FnDef(def, _)) = kind else { + return None; + }; + let has_unsafe = if let Some(body) = def.body() { + let unsafe_ops = FnUnsafeOperations::new(def.name()).collect(&body); + unsafe_ops.has_unsafe() + } else { + true + }; + let fn_sig = kind.fn_sig().unwrap(); + let is_unsafe = fn_sig.skip_binder().safety == Safety::Unsafe; + Some(Node { def, is_unsafe, has_unsafe }) + } +} + +impl Hash for Node { + fn hash(&self, state: &mut H) { + self.def.hash(state) + } +} + +#[derive(Default, Debug)] +struct CallGraph { + nodes: HashMap, + edges: HashMap>, + rev_edges: HashMap>, +} diff --git a/tools/scanner/src/lib.rs b/tools/scanner/src/lib.rs index fe2f30acb435..effcceb27dec 100644 --- a/tools/scanner/src/lib.rs +++ b/tools/scanner/src/lib.rs @@ -10,7 +10,8 @@ #![feature(rustc_private)] -mod analysis; +pub mod analysis; +pub mod call_graph; extern crate rustc_driver; extern crate rustc_interface; @@ -65,6 +66,8 @@ pub enum Analysis { FnLoops, /// Collect information about recursion via direct calls. Recursion, + /// Collect information about transitive usage of unsafe. + UnsafeDistance, } fn info(msg: String) { @@ -75,6 +78,9 @@ fn info(msg: String) { /// This function invoke the required analyses in the given order. fn analyze_crate(tcx: TyCtxt, analyses: &[Analysis]) -> ControlFlow<()> { + if stable_mir::local_crate().name == "build_script_build" { + return ControlFlow::Continue(()); + } let object_file = tcx.output_filenames(()).path(OutputType::Object); let base_path = object_file.as_path().to_path_buf(); // Use name for now to make it more friendly. Change to base_path.file_stem() to avoid conflict. @@ -96,6 +102,7 @@ fn analyze_crate(tcx: TyCtxt, analyses: &[Analysis]) -> ControlFlow<()> { Analysis::UnsafeOps => crate_stats.unsafe_operations(out_path), Analysis::FnLoops => crate_stats.loops(out_path), Analysis::Recursion => crate_stats.recursion(out_path), + Analysis::UnsafeDistance => crate_stats.unsafe_distance(out_path), } } crate_stats.store_csv(base_path, &file_stem); From 65778ec9cd0588564488aa8ba39dde805ed6a805 Mon Sep 17 00:00:00 2001 From: Carolyn Zech Date: Mon, 21 Apr 2025 11:59:13 -0400 Subject: [PATCH 2/6] Address PR comments --- tests/script-based-pre/tool-scanner/scanner-test.expected | 3 ++- tests/script-based-pre/tool-scanner/scanner-test.sh | 2 +- tests/script-based-pre/tool-scanner/test.rs | 6 +----- tools/scanner/src/analysis.rs | 6 +++--- tools/scanner/src/call_graph.rs | 6 +++++- 5 files changed, 12 insertions(+), 11 deletions(-) diff --git a/tests/script-based-pre/tool-scanner/scanner-test.expected b/tests/script-based-pre/tool-scanner/scanner-test.expected index 7e5b3875979d..44c9704c3b98 100644 --- a/tests/script-based-pre/tool-scanner/scanner-test.expected +++ b/tests/script-based-pre/tool-scanner/scanner-test.expected @@ -1,6 +1,7 @@ 5 test_scan_fn_loops.csv 20 test_scan_functions.csv 5 test_scan_input_tys.csv -16 test_scan_overall.csv +17 test_scan_overall.csv 3 test_scan_recursion.csv +9 test_scan_unsafe_distance.csv 6 test_scan_unsafe_ops.csv diff --git a/tests/script-based-pre/tool-scanner/scanner-test.sh b/tests/script-based-pre/tool-scanner/scanner-test.sh index 7ac7c5fd74dc..2cd5a33a3f8e 100755 --- a/tests/script-based-pre/tool-scanner/scanner-test.sh +++ b/tests/script-based-pre/tool-scanner/scanner-test.sh @@ -17,4 +17,4 @@ cargo run -p scanner test.rs --crate-type lib wc -l *csv popd -#rm -rf ${OUT_DIR} +rm -rf ${OUT_DIR} diff --git a/tests/script-based-pre/tool-scanner/test.rs b/tests/script-based-pre/tool-scanner/test.rs index 0cdebde134f2..be45a653d55e 100644 --- a/tests/script-based-pre/tool-scanner/test.rs +++ b/tests/script-based-pre/tool-scanner/test.rs @@ -14,11 +14,6 @@ pub fn generic() -> T { T::default() } -pub fn blah() { - ok(); - assert_eq!(u8::default(), 0); -} - pub struct RecursiveType { pub inner: Option<*const RecursiveType>, } @@ -112,6 +107,7 @@ extern "C" { fn external_function(); } +/// Ensure scanner finds unsafe calls to external functions. pub fn call_external() { unsafe { external_function() }; } diff --git a/tools/scanner/src/analysis.rs b/tools/scanner/src/analysis.rs index 3ab5b0130253..acd6a72e6fb8 100644 --- a/tools/scanner/src/analysis.rs +++ b/tools/scanner/src/analysis.rs @@ -1,7 +1,7 @@ // Copyright Kani Contributors // SPDX-License-Identifier: Apache-2.0 OR MIT -//! Provide different static analysis to be performed in the crate under compilation +//! Provide passes that perform intra-function analysis on the crate under compilation use crate::info; use csv::WriterBuilder; @@ -253,11 +253,11 @@ macro_rules! fn_props { } impl $name { - pub const fn num_props() -> usize { + $vis const fn num_props() -> usize { [$(stringify!($prop),)+].len() } - pub fn new(fn_name: String) -> Self { + $vis fn new(fn_name: String) -> Self { Self { fn_name, $($prop: 0,)+} } } diff --git a/tools/scanner/src/call_graph.rs b/tools/scanner/src/call_graph.rs index 81d81da317cb..0e2d1760492a 100644 --- a/tools/scanner/src/call_graph.rs +++ b/tools/scanner/src/call_graph.rs @@ -1,7 +1,11 @@ // Copyright Kani Contributors // SPDX-License-Identifier: Apache-2.0 OR MIT -//! Provide different static analysis to be performed in the call graph +//! Provide passes that perform inter-function analysis on the crate under compilation. +//! +//! This module also includes a `CallGraph` structure to help the analysis. +//! For now, we build the CallGraph as part of the pass, but as we add more analysis, +//! the call-graph could be reused by different analysis. use crate::analysis::{FnCallVisitor, FnUnsafeOperations, OverallStats}; use stable_mir::mir::{MirVisitor, Safety}; From 749fa1e65799e05de0196bf1518ace17c3828553 Mon Sep 17 00:00:00 2001 From: Carolyn Zech Date: Mon, 21 Apr 2025 11:27:00 -0400 Subject: [PATCH 3/6] https://github.com/rust-lang/rust/pull/139455 removed support for extern intrinsics --- tools/scanner/src/analysis.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tools/scanner/src/analysis.rs b/tools/scanner/src/analysis.rs index acd6a72e6fb8..db90d737d1a9 100644 --- a/tools/scanner/src/analysis.rs +++ b/tools/scanner/src/analysis.rs @@ -441,10 +441,8 @@ impl MirVisitor for BodyVisitor<'_> { let fn_sig = fn_def.fn_sig().skip_binder(); if fn_sig.safety == Safety::Unsafe { self.props.unsafe_call += 1; - if !matches!( - fn_sig.abi, - Abi::Rust | Abi::RustCold | Abi::RustCall | Abi::RustIntrinsic - ) && !fn_def.has_body() + if !matches!(fn_sig.abi, Abi::Rust | Abi::RustCold | Abi::RustCall) + && !fn_def.has_body() { self.props.extern_call += 1; } From 87ce0d6d5a2d80a35b83363242d9d1535ea7ac33 Mon Sep 17 00:00:00 2001 From: Carolyn Zech Date: Mon, 21 Apr 2025 11:39:18 -0400 Subject: [PATCH 4/6] remove unused FnStats field --- tools/scanner/src/analysis.rs | 7 ------- tools/scanner/src/call_graph.rs | 5 +++++ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/tools/scanner/src/analysis.rs b/tools/scanner/src/analysis.rs index db90d737d1a9..21f47df23801 100644 --- a/tools/scanner/src/analysis.rs +++ b/tools/scanner/src/analysis.rs @@ -36,12 +36,6 @@ struct FnStats { has_unsafe_ops: Option, has_unsupported_input: Option, has_loop_or_iterator: Option, - /// How many degrees of separation to unsafe code if any? - /// - `None` if this function is indeed safe. - /// - 0 if this function contains unsafe code (including invoking unsafe fns). - /// - 1 if this function calls a safe abstraction. - /// - 2+ if this function calls other functions that call safe abstractions. - unsafe_distance: Option, } impl FnStats { @@ -52,7 +46,6 @@ impl FnStats { has_unsafe_ops: None, has_unsupported_input: None, has_loop_or_iterator: None, - unsafe_distance: None, } } } diff --git a/tools/scanner/src/call_graph.rs b/tools/scanner/src/call_graph.rs index 0e2d1760492a..a8fa3baa3b4f 100644 --- a/tools/scanner/src/call_graph.rs +++ b/tools/scanner/src/call_graph.rs @@ -18,6 +18,11 @@ use std::path::PathBuf; impl OverallStats { /// Iterate over all functions defined in this crate and log any unsafe operation. + /// Distance indicates how many degrees of separation the function has to unsafe code, if any? + /// - `None` if this function is indeed safe. + /// - 0 if this function contains unsafe code (including invoking unsafe fns). + /// - 1 if this function calls a safe abstraction. + /// - 2+ if this function calls other functions that call safe abstractions. pub fn unsafe_distance(&mut self, filename: PathBuf) { let all_items = stable_mir::all_local_items(); let mut queue = From fa9b7e53d49f74e1912828bdae7cd2c4c1c9920a Mon Sep 17 00:00:00 2001 From: Carolyn Zech Date: Mon, 21 Apr 2025 12:08:30 -0400 Subject: [PATCH 5/6] clippy --- tools/scanner/src/analysis.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tools/scanner/src/analysis.rs b/tools/scanner/src/analysis.rs index 21f47df23801..95b529a530ca 100644 --- a/tools/scanner/src/analysis.rs +++ b/tools/scanner/src/analysis.rs @@ -50,6 +50,12 @@ impl FnStats { } } +impl Default for OverallStats { + fn default() -> Self { + Self::new() + } +} + impl OverallStats { pub fn new() -> OverallStats { let all_items = stable_mir::all_local_items(); From ae029768017b700ba547ff210bab7fce7bcc6546 Mon Sep 17 00:00:00 2001 From: Carolyn Zech Date: Tue, 22 Apr 2025 12:32:02 -0400 Subject: [PATCH 6/6] add unsafe distance results to test --- .../tool-scanner/scanner-test.expected | 11 +++++++++++ tests/script-based-pre/tool-scanner/scanner-test.sh | 6 ++++++ 2 files changed, 17 insertions(+) diff --git a/tests/script-based-pre/tool-scanner/scanner-test.expected b/tests/script-based-pre/tool-scanner/scanner-test.expected index 44c9704c3b98..92751988b06d 100644 --- a/tests/script-based-pre/tool-scanner/scanner-test.expected +++ b/tests/script-based-pre/tool-scanner/scanner-test.expected @@ -5,3 +5,14 @@ 3 test_scan_recursion.csv 9 test_scan_unsafe_distance.csv 6 test_scan_unsafe_ops.csv + +Unsafe Distance Results +call_external;0 +next_id;0 +external_function;0 +raw_to_ref;0 +with_for_loop;1 +check_outer_coercion;1 +with_iterator;2 +current_id;0 +generic;0 diff --git a/tests/script-based-pre/tool-scanner/scanner-test.sh b/tests/script-based-pre/tool-scanner/scanner-test.sh index 2cd5a33a3f8e..468bb61dcf0d 100755 --- a/tests/script-based-pre/tool-scanner/scanner-test.sh +++ b/tests/script-based-pre/tool-scanner/scanner-test.sh @@ -16,5 +16,11 @@ pushd $OUT_DIR cargo run -p scanner test.rs --crate-type lib wc -l *csv +# How to intepret these results: +# - If the function is "truly safe," i.e., there's no unsafe in its call graph, it will not show up in the output at all. +# - Otherwise, the count should match the rules described in scanner::call_graph::OverallStats::unsafe_distance. +echo "Unsafe Distance Results" +cat test_scan_unsafe_distance.csv + popd rm -rf ${OUT_DIR}