Chapter 5.8. Safety-Flow Analysis
Safety-Flow Analysis tracks how unsafety propagates through a Rust crate. It builds a graph that connects safe callers to the unsafe operations they transitively depend on, revealing the full unsafety boundary of the crate. This module lives at rapx/src/analysis/safetyflow_analysis/.
Overview
Rust's safety model is compartmentalized: unsafe blocks and unsafe fn declarations isolate the surface area where UB can originate. However, safe functions that internally call unsafe functions still carry a verification burden — the safety of the safe caller depends on whether it correctly upholds the unsafe callee's preconditions.
Safety-Flow Analysis answers two questions:
- Which functions in my crate touch unsafe code? — including safe wrappers, methods on types with invariants, and functions that dereference raw pointers or access
static mut. - What is the unsafety dependency graph? — for each module, how do constructors, mutators, callers, and unsafe callees relate to each other?
Quick Usage
cargo rapx analyze safetyflow
To also render the unsafety propagation graph as PNG images (requires Graphviz):
cargo rapx analyze safetyflow --draw
For the Rust standard library:
cargo rapx analyze safetyflowstd
Unsafety Sources
The analysis classifies three kinds of unsafe operations in a function body (root.rs:UnsafeOpKind):
| Kind | Detection Method | Example |
|---|---|---|
CallsUnsafeFn | MIR terminator scan for TerminatorKind::Call targeting unsafe fn | ptr::read, ptr::write |
DerefsRawPtr | MIR rvalue scan for Rvalue::RawPtr dereferences | *raw_ptr |
AccessesStaticMut | Scan for static mut items accessed in the function body | MY_STATIC |
A function is considered an unsafe root if it contains at least one of the above. Detection uses a two-stage filter:
- HIR pre-check (
root.rs:hir_contains_unsafe): A fast check forunsafe fnorunsafe { }blocks. Functions that pass neither are skipped entirely. - MIR scan (
root.rs:scan_mir): For functions passing the HIR check, the MIR body is scanned for specific unsafe operations. Functions with zero unsafe callees, no raw pointer dereferences, and no static mut accesses are excluded.
Pipeline
FnCollector::collect(tcx)
└─ Collects all function BodyIds from the crate
│
├─ hir_contains_unsafe() ← HIR pre-filter
├─ scan_mir() ← MIR-level unsafe operation detection
│
└─ SafetyFlowUnit ← Per-function unsafety summary
│
├─ caller → callees (unsafe fn edges)
├─ constructors → caller
├─ mutable methods → caller
└─ raw_ptr / static_mut → caller
Function Collection
FnCollector (fn_collector.rs) uses tcx.hir_visit_all_item_likes_in_crate to collect all function bodies, grouped by module.
Root Detection
scan_mir (root.rs:82) performs a MIR-level scan for each function:
get_unsafe_calleestraverses allTerminatorKind::Callterminators, checking whether each callee'sFnDefhasSafety::Unsafe.get_rawptr_dereffinds locals whose type is*const Tor*mut Tand that appear in assignment statements — indicating a raw pointer dereference.collect_global_local_pairsmapsstatic mutDefIds to the MIR locals that reference them.
If all three sets are empty, the function is excluded from the safety flow graph.
SafetyFlowUnit
Each unsafe root produces a SafetyFlowUnit (safetyflow_unit.rs):
#![allow(unused)] fn main() { pub struct SafetyFlowUnit { pub caller: FnInfo, // The function under analysis pub callees: HashSet<FnInfo>, // Unsafe callees it calls pub raw_ptrs: HashSet<Local>, // Raw pointer dereference locals pub static_muts: HashSet<DefId>, // static mut accesses pub caller_cons: HashSet<FnInfo>,// Constructors of the caller's struct pub mut_methods: HashSet<DefId>, // Mutable methods on the caller's struct } }
Constructors are resolved via get_cons by checking which functions in the same impl block return Self. Mutable methods are resolved via get_muts by checking &mut self parameters.
A BasicUnitCounts structure in the same file classifies each unit into 13 categories (e.g., "safe function calling unsafe method", "unsafe method with unsafe constructor calling unsafe function") for statistical reporting.
Module-Level Graphs
SafetyFlowGraph (safetyflow_graph.rs) aggregates units per module and generates DOT-format graphs with three edge types:
| Edge | Style | Meaning |
|---|---|---|
CallerToCallee | solid black | Function calls an unsafe callee |
ConsToMethod | dotted black | Constructor initializes the struct for a method |
MutToCaller | dashed blue | Mutable method modifies the struct, affecting the caller |
Nodes are colored: red for unsafe functions, black for safe functions. Constructors are drawn as septagons, methods as ellipses, and free functions as boxes. Each struct's methods and constructors are grouped in a dashed subgraph cluster.
Standard Library Audit
When run in TargetCrate::Std mode (mod.rs:48), the analysis follows a different pipeline (std_analysis.rs) optimized for the standard library's structure. It produces unsafe call chains rather than per-module graphs (see chain.rs for the DFS-based chain extraction).
Output Format
The console output groups functions by module and shows:
SafetyFlow: my_module (3 function(s))
my_module::safe_wrapper [Safe]
-> core::ptr::read
-> core::ptr::write
*raw* ptr deref: _3, _7
+ constructor: my_module::MyStruct::new
~ mut_self: my_module::MyStruct::set_field
A final summary reports total counts:
SafetyFlow summary: 42 function(s), 87 call edge(s), 15 raw ptr deref(s), 2 static mut access(es)
Relationship to Verification
Safety-Flow Analysis is the upstream stage of the verification pipeline. The VerifyTargetCollector reuses hir_contains_unsafe and scan_mir from root.rs to identify verification targets. While Safety-Flow produces a module-level overview of unsafety propagation, the verification module drills into each function with path-sensitive SMT-based property checking.
Test Examples
The test suite at rapx/tests/analyze/safetyflow_raw_ptr contains tests for each unsafety source:
Safe Caller Calling Unsafe Callee (safe_caller)
// rapx/tests/analyze/safetyflow_safe_caller/src/main.rs fn main() { let mut s = String::from("a tmp string"); let ptr = s.as_mut_ptr(); let _v = unsafe { Vec::from_raw_parts(ptr, s.len(), s.len()) }; }
main is a safe function that calls Vec::from_raw_parts inside an unsafe block. The safety flow graph captures this as a CallerToCallee edge from a safe caller to an unsafe callee. This is the most common pattern in Rust codebases — safe wrappers around unsafe primitives.
Raw Pointer Dereference (raw_ptr)
// rapx/tests/analyze/safetyflow_raw_ptr/src/main.rs fn main() { let mut value = 0i32; let ptr: *mut i32 = &mut value as *mut i32; unsafe { *ptr = 1; } }
main contains a raw pointer dereference (*ptr = 1). The analysis records _ptr as a RawPtr local and connects it to a synthetic "raw ptr deref" node in the output graph. This pattern is typical in FFI code and low-level memory manipulation.
Struct Methods with Unsafe Operations (struct)
#![allow(unused)] fn main() { // rapx/tests/analyze/safetyflow_raw_ptr/src/main.rs (struct test) struct St1 { pub ptr: *mut u8, len: usize } struct St2 { pub ptr: *mut u8, pub len: usize } impl St1 { pub fn from(p: *mut u8, l: usize) -> St1 { St1 { ptr: p, len: l } } pub unsafe fn get(&self) -> &[u8] { slice::from_raw_parts(self.ptr, self.len) } pub unsafe fn set_len(&mut self, len: usize) { self.len = 1; } } // St2 has identical impls... }
Two structs (St1, St2) with unsafe methods. St1 has private fields, St2 has public fields. The safety flow graph distinguishes: for St2 (public fields), mutable field access is treated as an implicit mutator; for St1 (private fields), only explicit &mut self methods contribute. The output shows constructor → method edges and mutator → caller edges for each struct in its own subgraph cluster.
Expected Output
For the struct test, running cargo rapx analyze safetyflow produces output like:
SafetyFlow: struct (4 function(s))
St1::from [Safe]
+ constructor: St1::from
St1::get [Unsafe]
-> core::slice::raw::from_raw_parts
+ constructor: St1::from
St1::set_len [Unsafe]
+ constructor: St1::from
St2::get [Unsafe]
-> core::slice::raw::from_raw_parts
+ constructor: St2::from
~ mut_self: St2::len (public field)
This reveals the full unsafety surface: both St1 and St2 have unsafe methods calling slice::from_raw_parts, and St2 additionally has public-field mutation paths that could affect the safety of get.