rapx/helpers/
mir_scan.rs

1use rustc_hir::{Safety, def_id::DefId};
2use rustc_middle::{
3    mir::{
4        BasicBlock, Body, Local, Operand, Place, ProjectionElem, Rvalue, StatementKind,
5        TerminatorKind,
6    },
7    ty::{self, Ty, TyCtxt, TyKind},
8};
9use rustc_span::Span;
10use std::collections::{HashMap, HashSet};
11
12use super::name::get_cleaned_def_path_name;
13
14/// Stable MIR location for a call terminator inside one function body.
15#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
16pub struct CheckpointLocation {
17    /// Function containing the call terminator.
18    pub caller: DefId,
19    /// Basic block whose terminator is the call.
20    pub block: BasicBlock,
21}
22
23/// Kind of an unsafe verification checkpoint inside a function body.
24#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
25pub enum CheckpointKind {
26    /// A real unsafe function call.
27    UnsafeCall,
28    /// A raw pointer dereference.
29    RawPtrDeref,
30    /// A mutable static variable access.
31    StaticMutAccess,
32}
33
34/// A verification checkpoint in one MIR body.
35///
36/// Unifies unsafe calls, raw-pointer dereferences, and mutable static
37/// accesses under a single type so they all flow through the same path
38/// extraction and SMT verification pipeline.
39#[derive(Clone, Debug)]
40pub struct Checkpoint<'tcx> {
41    pub caller: DefId,
42    pub callee: Option<DefId>,
43    pub block: BasicBlock,
44    pub span: Span,
45    pub args: Vec<Operand<'tcx>>,
46    pub kind: CheckpointKind,
47    pub is_ref: bool,
48}
49
50impl<'tcx> Checkpoint<'tcx> {
51    /// Return the MIR location that identifies this checkpoint inside the verifier.
52    pub fn location(&self) -> CheckpointLocation {
53        CheckpointLocation {
54            caller: self.caller,
55            block: self.block,
56        }
57    }
58
59    /// Return a human-readable label for diagnostics.
60    pub fn callee_name(&self, tcx: TyCtxt<'tcx>) -> String {
61        match self.callee {
62            Some(def_id) => get_cleaned_def_path_name(tcx, def_id),
63            None => match self.kind {
64                CheckpointKind::RawPtrDeref => "raw-ptr-deref".to_string(),
65                CheckpointKind::StaticMutAccess => "static-mut-access".to_string(),
66                CheckpointKind::UnsafeCall => "unknown-callee".to_string(),
67            },
68        }
69    }
70}
71
72/// Checks the safety of a function signature.
73pub fn check_safety(tcx: TyCtxt<'_>, def_id: DefId) -> Safety {
74    let poly_fn_sig = tcx.fn_sig(def_id);
75    let fn_sig = poly_fn_sig.skip_binder();
76    fn_sig.safety()
77}
78
79/// Helper checking if a [`Place`] involves raw pointer dereference.
80pub fn place_has_raw_deref<'tcx>(
81    _tcx: TyCtxt<'tcx>,
82    body: &Body<'tcx>,
83    place: &Place<'tcx>,
84) -> bool {
85    let local = place.local;
86    for proj in place.projection.iter() {
87        if let ProjectionElem::Deref = proj.kind() {
88            let ty = body.local_decls[local].ty;
89            if let TyKind::RawPtr(_, _) = ty.kind() {
90                return true;
91            }
92        }
93    }
94    false
95}
96
97/// Analyzes the MIR of the given function to collect all local variables
98/// that are involved in dereferencing raw pointers (`*const T` or `*mut T`).
99pub fn get_rawptr_deref(tcx: TyCtxt<'_>, def_id: DefId) -> HashSet<Local> {
100    let mut raw_ptrs = HashSet::new();
101    if tcx.is_mir_available(def_id) {
102        let body = tcx.optimized_mir(def_id);
103        for bb in body.basic_blocks.iter() {
104            for stmt in &bb.statements {
105                if let StatementKind::Assign(assign) = &stmt.kind {
106                    let (lhs, rhs) = &**assign;
107                    if place_has_raw_deref(tcx, &body, lhs) {
108                        raw_ptrs.insert(lhs.local);
109                    }
110                    if let Rvalue::Use(op, ..) = rhs {
111                        match op {
112                            Operand::Copy(place) | Operand::Move(place) => {
113                                if place_has_raw_deref(tcx, &body, place) {
114                                    raw_ptrs.insert(place.local);
115                                }
116                            }
117                            _ => {}
118                        }
119                    }
120                    if let Rvalue::Ref(_, _, place) = rhs {
121                        if place_has_raw_deref(tcx, &body, place) {
122                            raw_ptrs.insert(place.local);
123                        }
124                    }
125                }
126            }
127            if let Some(terminator) = &bb.terminator {
128                match &terminator.kind {
129                    rustc_middle::mir::TerminatorKind::Call { args, .. } => {
130                        for arg in args {
131                            match arg.node {
132                                Operand::Copy(place) | Operand::Move(place) => {
133                                    if place_has_raw_deref(tcx, &body, &place) {
134                                        raw_ptrs.insert(place.local);
135                                    }
136                                }
137                                _ => {}
138                            }
139                        }
140                    }
141                    _ => {}
142                }
143            }
144        }
145    }
146    raw_ptrs
147}
148
149/// Collects pairs of global static variables and their corresponding local variables
150/// within a function's MIR that are assigned from statics.
151pub fn collect_global_local_pairs(tcx: TyCtxt<'_>, def_id: DefId) -> HashMap<DefId, Vec<Local>> {
152    let mut globals: HashMap<DefId, Vec<Local>> = HashMap::new();
153
154    if !tcx.is_mir_available(def_id) {
155        return globals;
156    }
157
158    let body = tcx.optimized_mir(def_id);
159
160    for bb in body.basic_blocks.iter() {
161        for stmt in &bb.statements {
162            if let StatementKind::Assign(assign) = &stmt.kind {
163                let (lhs, rhs) = &**assign;
164                if let Rvalue::Use(Operand::Constant(c), ..) = rhs {
165                    if let Some(static_def_id) = c.check_static_ptr(tcx) {
166                        globals.entry(static_def_id).or_default().push(lhs.local);
167                    }
168                }
169            }
170        }
171    }
172
173    globals
174}
175
176/// Scans MIR for calls to unsafe functions and returns the set of callee DefIds.
177pub fn get_unsafe_callees(tcx: TyCtxt<'_>, def_id: DefId) -> HashSet<DefId> {
178    let mut unsafe_callees = HashSet::new();
179    if tcx.is_mir_available(def_id) {
180        let body = tcx.optimized_mir(def_id);
181        for bb in body.basic_blocks.iter() {
182            if let TerminatorKind::Call { func, .. } = &bb.terminator().kind {
183                if let Operand::Constant(func_constant) = func {
184                    if let ty::FnDef(callee_def_id, _) = func_constant.const_.ty().kind() {
185                        if check_safety(tcx, *callee_def_id) == Safety::Unsafe {
186                            unsafe_callees.insert(*callee_def_id);
187                        }
188                    }
189                }
190            }
191        }
192    }
193    unsafe_callees
194}
195
196/// Collect all unsafe MIR checkpoints in `def_id` with full per-checkpoint metadata.
197pub fn collect_unsafe_callsites<'tcx>(tcx: TyCtxt<'tcx>, def_id: DefId) -> Vec<Checkpoint<'tcx>> {
198    let mut checkpoints = Vec::new();
199    if !tcx.is_mir_available(def_id) {
200        return checkpoints;
201    }
202
203    let body = tcx.optimized_mir(def_id);
204    for (bb, data) in body.basic_blocks.iter_enumerated() {
205        let TerminatorKind::Call {
206            func,
207            args,
208            fn_span,
209            ..
210        } = &data.terminator().kind
211        else {
212            continue;
213        };
214
215        let Operand::Constant(func_constant) = func else {
216            continue;
217        };
218
219        let ty::FnDef(callee_def_id, callee_args) = func_constant.const_.ty().kind() else {
220            continue;
221        };
222
223        if check_safety(tcx, *callee_def_id) != Safety::Unsafe {
224            continue;
225        }
226
227        // Normalize a trait-method callee to the concrete impl method so that
228        // inline `#[rapx::requires]` contracts (which live on the impl, not the
229        // trait declaration) are found during contract lookup.
230        let resolved_callee =
231            resolve_callee_impl(tcx, def_id, *callee_def_id, callee_args).unwrap_or(*callee_def_id);
232
233        checkpoints.push(Checkpoint {
234            caller: def_id,
235            callee: Some(resolved_callee),
236            block: bb,
237            span: *fn_span,
238            args: args.iter().map(|arg| arg.node.clone()).collect(),
239            kind: CheckpointKind::UnsafeCall,
240            is_ref: false,
241        });
242    }
243
244    checkpoints
245}
246
247/// Resolve a (possibly trait-method) callee to the concrete impl method that
248/// will actually be dispatched, given the caller context and the callee's
249/// generic arguments.
250///
251/// This matters for contract lookup: `#[rapx::requires(...)]` annotations are
252/// attached to the impl method, but in a generic caller the MIR `FnDef` refers
253/// to the trait method declaration.  Resolving to the impl method lets the
254/// verifier find those inline contracts.  Returns `None` when the callee cannot
255/// be resolved to a distinct concrete item (e.g. still generic/virtual), in
256/// which case callers should keep the original DefId.
257fn resolve_callee_impl<'tcx>(
258    tcx: TyCtxt<'tcx>,
259    caller_def_id: DefId,
260    callee_def_id: DefId,
261    callee_args: ty::GenericArgsRef<'tcx>,
262) -> Option<DefId> {
263    // Only trait-associated methods need remapping; inherent/free functions
264    // already point at their concrete definition.
265    let assoc = tcx.opt_associated_item(callee_def_id)?;
266    if assoc.trait_container(tcx).is_none() {
267        return None;
268    }
269
270    let typing_env = ty::TypingEnv::post_analysis(tcx, caller_def_id);
271    let instance = ty::Instance::try_resolve(tcx, typing_env, callee_def_id, callee_args)
272        .ok()
273        .flatten()?;
274
275    let resolved = match instance.def {
276        ty::InstanceKind::Item(def_id) => def_id,
277        _ => return None,
278    };
279
280    if resolved == callee_def_id {
281        None
282    } else {
283        Some(resolved)
284    }
285}
286
287/// Metadata for a single raw pointer dereference operation found in MIR.
288#[derive(Clone, Debug)]
289pub struct RawPtrDerefInfo<'tcx> {
290    pub block: BasicBlock,
291    pub ptr_operand: Operand<'tcx>,
292    pub pointee_ty: Ty<'tcx>,
293    pub is_read: bool,
294    pub is_ref: bool,
295}
296
297/// Collect all raw pointer dereference operations in `def_id` as
298/// metadata records (block, pointer operand, pointee type, read-vs-write).
299pub fn collect_raw_ptr_deref_info<'tcx>(
300    tcx: TyCtxt<'tcx>,
301    def_id: DefId,
302) -> Vec<RawPtrDerefInfo<'tcx>> {
303    let mut infos = Vec::new();
304    if !tcx.is_mir_available(def_id) {
305        return infos;
306    }
307
308    let body = tcx.optimized_mir(def_id);
309    for (bb, data) in body.basic_blocks.iter_enumerated() {
310        for stmt in &data.statements {
311            let StatementKind::Assign(assign) = &stmt.kind else {
312                continue;
313            };
314            let (lhs, rhs) = &**assign;
315
316            let is_write = place_has_raw_deref(tcx, &body, lhs);
317            let (is_read, is_ref) = match rhs {
318                Rvalue::Use(Operand::Copy(place) | Operand::Move(place), ..) => {
319                    (place_has_raw_deref(tcx, &body, place), false)
320                }
321                Rvalue::Ref(_, _, place) => {
322                    (place_has_raw_deref(tcx, &body, place), true)
323                }
324                _ => (false, false),
325            };
326
327            if !is_write && !is_read {
328                continue;
329            }
330
331            let deref_place = if is_write {
332                lhs
333            } else {
334                match rhs {
335                    Rvalue::Use(Operand::Copy(place) | Operand::Move(place), ..)
336                    | Rvalue::Ref(_, _, place) => place,
337                    _ => continue,
338                }
339            };
340
341            let Some(ptr_operand) = ptr_operand_for_deref_place(deref_place) else {
342                continue;
343            };
344
345            let Some(pointee_ty) = deref_place_pointee_ty(&body, deref_place) else {
346                continue;
347            };
348
349            infos.push(RawPtrDerefInfo {
350                block: bb,
351                ptr_operand,
352                pointee_ty,
353                is_read,
354                is_ref,
355            });
356        }
357    }
358
359    infos
360}
361
362/// Return the pointee type of the raw pointer being dereferenced.
363fn deref_place_pointee_ty<'tcx>(body: &Body<'tcx>, place: &Place<'tcx>) -> Option<Ty<'tcx>> {
364    let ty = body.local_decls[place.local].ty;
365    match ty.kind() {
366        TyKind::RawPtr(inner, _) => Some(*inner),
367        _ => None,
368    }
369}
370
371/// Extract the pointer operand from a dereference place.
372fn ptr_operand_for_deref_place<'tcx>(place: &Place<'tcx>) -> Option<Operand<'tcx>> {
373    use rustc_middle::ty::List;
374
375    let first_deref_idx = place
376        .projection
377        .iter()
378        .position(|p| matches!(p.kind(), ProjectionElem::Deref));
379
380    if let Some(idx) = first_deref_idx && idx > 0 {
381        return None;
382    }
383
384    Some(Operand::Copy(Place {
385        local: place.local,
386        projection: List::empty(),
387    }))
388}
389
390/// Metadata for a `static mut` access found in MIR.
391#[derive(Clone, Debug)]
392pub struct StaticMutAccessInfo<'tcx> {
393    /// Basic block containing the access.
394    pub block: BasicBlock,
395    /// The mutable static being accessed.
396    pub static_def_id: DefId,
397    /// The pointee type (i.e. the type of the static itself, `T` in `static mut X: T`).
398    pub ty: Ty<'tcx>,
399    /// The MIR operand holding the pointer to the static.
400    pub ptr_operand: Operand<'tcx>,
401}
402
403/// Collect all basic blocks that reference mutable statics in `def_id`.
404///
405/// Mutable statics appear as `Constant` operands whose `check_static_ptr` points
406/// to a `static mut` item.  Both reads and writes are detected here; the
407/// conservative `Init` property will be checked regardless of direction.
408pub fn collect_static_mut_access_info<'tcx>(
409    tcx: TyCtxt<'tcx>,
410    def_id: DefId,
411) -> Vec<StaticMutAccessInfo<'tcx>> {
412    let mut infos = Vec::new();
413    if !tcx.is_mir_available(def_id) {
414        return infos;
415    }
416
417    let body = tcx.optimized_mir(def_id);
418    for (bb, data) in body.basic_blocks.iter_enumerated() {
419        for stmt in &data.statements {
420            if let StatementKind::Assign(assign) = &stmt.kind {
421                let (_lhs, rhs) = &**assign;
422                if let Rvalue::Use(op @ Operand::Constant(c), ..) = rhs {
423                    if let Some(static_id) = c.check_static_ptr(tcx) {
424                        if matches!(tcx.static_mutability(static_id), Some(m) if m.is_mut()) {
425                            let ty = tcx.type_of(static_id).skip_binder();
426                            infos.push(StaticMutAccessInfo {
427                                block: bb,
428                                static_def_id: static_id,
429                                ty,
430                                ptr_operand: op.clone(),
431                            });
432                        }
433                    }
434                }
435            }
436        }
437
438        if let Some(terminator) = &data.terminator {
439            if let TerminatorKind::Call { args, .. } = &terminator.kind {
440                for arg in args {
441                    match &arg.node {
442                        op @ Operand::Constant(c) => {
443                            if let Some(static_id) = c.check_static_ptr(tcx) {
444                                if matches!(tcx.static_mutability(static_id), Some(m) if m.is_mut())
445                                {
446                                    let ty = tcx.type_of(static_id).skip_binder();
447                                    infos.push(StaticMutAccessInfo {
448                                        block: bb,
449                                        static_def_id: static_id,
450                                        ty,
451                                        ptr_operand: op.clone(),
452                                    });
453                                }
454                            }
455                        }
456                        _ => {}
457                    }
458                }
459            }
460        }
461    }
462
463    infos
464}