rapx/analysis/safetyflow_analysis/
root.rs

1use crate::helpers::mir_scan::{collect_global_local_pairs, get_rawptr_deref, get_unsafe_callees};
2use rustc_hir::{BodyId, ItemKind, def_id::DefId};
3use rustc_middle::{mir::Local, ty::TyCtxt};
4use rustc_span::Symbol;
5use std::collections::HashSet;
6
7use super::hir_visitor::ContainsUnsafe;
8
9/// Kind of unsafe operation found in a function body.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum UnsafeOpKind {
12    CallsUnsafeFn,
13    DerefsRawPtr,
14    AccessesStaticMut,
15}
16
17/// A function that contains unsafe operations — an "unsafe root".
18///
19/// This is the unified entry point for both the safetyflow analysis and the
20/// verify module to determine whether a function needs safety verification.
21#[derive(Debug, Clone)]
22pub struct UnsafeRoot {
23    pub def_id: DefId,
24    pub kinds: Vec<UnsafeOpKind>,
25    pub unsafe_callees: HashSet<DefId>,
26    pub raw_ptr_locals: HashSet<Local>,
27    pub static_muts: HashSet<DefId>,
28}
29
30/// Fast HIR-level pre-check: does this function contain `unsafe` blocks
31/// or is it declared `unsafe fn`?
32///
33/// This is a cheap check that can quickly filter out functions that are
34/// entirely safe and have no unsafe operations of any kind.
35pub fn hir_contains_unsafe(tcx: TyCtxt<'_>, body_id: BodyId) -> bool {
36    let (fn_unsafe, block_unsafe) = ContainsUnsafe::contains_unsafe(tcx, body_id);
37    fn_unsafe || block_unsafe
38}
39
40/// Check if a struct has `#[rapx::invariant(...)]` annotations.
41///
42/// This is a cheap HIR attribute scan — it only checks for the presence of
43/// the attribute path, without parsing the annotation arguments.
44pub fn has_struct_invariant(tcx: TyCtxt<'_>, struct_def_id: DefId) -> bool {
45    let Some(local_def_id) = struct_def_id.as_local() else {
46        return false;
47    };
48    let rapx = Symbol::intern("rapx");
49    let invariant = Symbol::intern("invariant");
50    let attrs = tcx.hir_attrs(tcx.local_def_id_to_hir_id(local_def_id));
51    attrs.iter().any(|attr| {
52        #[cfg(rapx_rustc_ge_193)]
53        if attr.is_doc_comment().is_some() {
54            return false;
55        }
56        #[cfg(not(rapx_rustc_ge_193))]
57        if attr.is_doc_comment() {
58            return false;
59        }
60        let path = attr.path();
61        path.len() == 2 && path[0] == rapx && path[1] == invariant
62    })
63}
64
65/// Quick check: does this function's owning struct have invariants?
66pub fn function_has_struct_invariant(tcx: TyCtxt<'_>, def_id: DefId) -> bool {
67    let Some(assoc_item) = tcx.opt_associated_item(def_id) else {
68        return false;
69    };
70    let Some(impl_id) = assoc_item.impl_container(tcx) else {
71        return false;
72    };
73    let self_ty = tcx.type_of(impl_id).skip_binder();
74    match self_ty.kind() {
75        rustc_middle::ty::TyKind::Adt(adt_def, _) => has_struct_invariant(tcx, adt_def.did()),
76        _ => false,
77    }
78}
79
80/// Quick check: does this function's containing impl implement an `unsafe trait`?
81///
82/// This is a fast HIR-level pre-filter similar to [`function_has_struct_invariant`].
83pub fn function_has_trait_ensurance(tcx: TyCtxt<'_>, def_id: DefId) -> bool {
84    let Some(assoc_item) = tcx.opt_associated_item(def_id) else {
85        return false;
86    };
87    let Some(impl_id) = assoc_item.impl_container(tcx) else {
88        return false;
89    };
90
91    let trait_def_id = {
92        #[cfg(rapx_rustc_ge_193)]
93        {
94            tcx.impl_opt_trait_ref(impl_id)
95        }
96        #[cfg(not(rapx_rustc_ge_193))]
97        {
98            tcx.impl_trait_ref(impl_id)
99        }
100    };
101    let Some(trait_ref) = trait_def_id else {
102        return false;
103    };
104    let trait_def_id = trait_ref.skip_binder().def_id;
105
106    let Some(local_id) = trait_def_id.as_local() else {
107        return false;
108    };
109
110    // Check if the trait is declared `unsafe trait`
111    let item = tcx.hir_expect_item(local_id);
112    #[cfg(not(rapx_rustc_ge_198))]
113    if let ItemKind::Trait(_, _, unsafety, _, _, _, _) = &item.kind {
114        return matches!(unsafety, rustc_hir::Safety::Unsafe);
115    }
116    #[cfg(rapx_rustc_ge_198)]
117    if let ItemKind::Trait { safety, .. } = &item.kind {
118        return matches!(safety, rustc_hir::Safety::Unsafe);
119    }
120
121    false
122}
123
124/// Full MIR-level detection: scan the function body for all unsafe operations.
125///
126/// Returns `None` if the function has no unsafe callees, no raw pointer
127/// dereferences, and no static mutable accesses.
128pub fn scan_mir(tcx: TyCtxt<'_>, def_id: DefId) -> Option<UnsafeRoot> {
129    if !tcx.is_mir_available(def_id) {
130        return None;
131    }
132
133    let unsafe_callees = get_unsafe_callees(tcx, def_id);
134    let raw_ptr_locals = get_rawptr_deref(tcx, def_id);
135    let global_locals = collect_global_local_pairs(tcx, def_id);
136    let static_muts: HashSet<DefId> = global_locals.keys().copied().collect();
137
138    let global_locals_set: HashSet<Local> = global_locals.values().flatten().copied().collect();
139    let raw_ptr_locals: HashSet<Local> = raw_ptr_locals
140        .difference(&global_locals_set)
141        .copied()
142        .collect();
143
144    if unsafe_callees.is_empty() && raw_ptr_locals.is_empty() && static_muts.is_empty() {
145        return None;
146    }
147
148    let mut kinds = Vec::new();
149    if !unsafe_callees.is_empty() {
150        kinds.push(UnsafeOpKind::CallsUnsafeFn);
151    }
152    if !raw_ptr_locals.is_empty() {
153        kinds.push(UnsafeOpKind::DerefsRawPtr);
154    }
155    if !static_muts.is_empty() {
156        kinds.push(UnsafeOpKind::AccessesStaticMut);
157    }
158
159    Some(UnsafeRoot {
160        def_id,
161        kinds,
162        unsafe_callees,
163        raw_ptr_locals,
164        static_muts,
165    })
166}