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:

  1. 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.
  2. 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):

KindDetection MethodExample
CallsUnsafeFnMIR terminator scan for TerminatorKind::Call targeting unsafe fnptr::read, ptr::write
DerefsRawPtrMIR rvalue scan for Rvalue::RawPtr dereferences*raw_ptr
AccessesStaticMutScan for static mut items accessed in the function bodyMY_STATIC

A function is considered an unsafe root if it contains at least one of the above. Detection uses a two-stage filter:

  1. HIR pre-check (root.rs:hir_contains_unsafe): A fast check for unsafe fn or unsafe { } blocks. Functions that pass neither are skipped entirely.
  2. 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_callees traverses all TerminatorKind::Call terminators, checking whether each callee's FnDef has Safety::Unsafe.
  • get_rawptr_deref finds locals whose type is *const T or *mut T and that appear in assignment statements — indicating a raw pointer dereference.
  • collect_global_local_pairs maps static mut DefIds 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:

EdgeStyleMeaning
CallerToCalleesolid blackFunction calls an unsafe callee
ConsToMethoddotted blackConstructor initializes the struct for a method
MutToCallerdashed blueMutable 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.