Skip to main content

rapx/verify/call_summary/
mod.rs

1//! Interprocedural call summaries for the staged verifier.
2//!
3//! The backward visitor needs dependency information: when a call result is
4//! relevant, which call arguments should become relevant too?  The forward
5//! visitor needs effect information: after a retained call, what facts about the
6//! return value or arguments can be added or forgotten?
7//!
8//! This module keeps those summaries in one place.  Standard unsafe/std APIs
9//! are summarized by name.  Local callees can additionally use the existing
10//! dataflow graph to approximate which arguments flow into the return value.
11pub mod fn_simulator;
12pub mod interprocedural;
13
14use rustc_hir::def_id::DefId;
15use rustc_middle::{
16    mir::{Local, Operand},
17    ty::{GenericArgKind, TyCtxt, TyKind},
18};
19
20use crate::helpers::mir_utils;
21
22/// Dependency summary consumed by the backward visitor.
23#[derive(Clone, Debug)]
24pub struct CallDependencySummary {
25    /// Callee definition when the call target is statically known.
26    pub callee: Option<DefId>,
27    /// Human-readable callee name.
28    pub name: String,
29    /// If the call destination is relevant, these call arguments are relevant.
30    pub return_depends_on_args: Vec<usize>,
31    /// Arguments that may be written or invalidated by the call.
32    pub may_write_args: Vec<usize>,
33    /// True when this summary is conservative rather than precise.
34    pub unsupported: bool,
35}
36
37impl CallDependencySummary {
38    /// Build a conservative summary that keeps all arguments relevant.
39    fn unknown(callee: Option<DefId>, name: String, arg_count: usize) -> Self {
40        Self {
41            callee,
42            name,
43            return_depends_on_args: (0..arg_count).collect(),
44            may_write_args: Vec::new(),
45            unsupported: true,
46        }
47    }
48}
49
50/// Effect summary consumed by the forward visitor.
51#[derive(Clone, Debug)]
52pub struct CallEffectSummary {
53    /// Callee definition when the call target is statically known.
54    pub callee: Option<DefId>,
55    /// Human-readable callee name.
56    pub name: String,
57    /// Destination local receiving the return value.
58    pub destination: Option<Local>,
59    /// Effects that can be applied to the path-local abstract state.
60    pub effects: Vec<CallEffect>,
61    /// True when this summary is conservative rather than precise.
62    pub unsupported: bool,
63}
64
65impl CallEffectSummary {
66    /// Build a conservative summary for an unsupported call.
67    fn unknown(callee: Option<DefId>, name: String, destination: Option<Local>) -> Self {
68        Self {
69            callee,
70            name,
71            destination,
72            effects: Vec::new(),
73            unsupported: true,
74        }
75    }
76}
77
78/// Path-local effect produced by a retained call.
79#[derive(Clone, Debug)]
80pub enum CallEffect {
81    /// The return value aliases or is a direct value flow from an argument.
82    ReturnAliasArg { arg: usize },
83    /// The return value is a pointer extracted from an aggregate/reference arg.
84    ReturnPointerFromArg { arg: usize },
85    /// The return value is `base + offset * stride`.
86    ReturnPointerAdd {
87        base_arg: usize,
88        offset_arg: usize,
89        stride: Option<u64>,
90    },
91    /// The return value is `base - offset * stride`.
92    ReturnPointerSub {
93        base_arg: usize,
94        offset_arg: usize,
95        stride: Option<u64>,
96    },
97    /// The return value is known to be non-zero.
98    ReturnNonZero,
99    /// The return value is known to satisfy a concrete alignment.
100    ReturnAligned { align: u64, ty_name: String },
101    /// The return value is a concrete layout/numeric constant.
102    ReturnConst { value: u64, label: String },
103    /// The call reads memory through an argument.
104    ReadMemory { arg: usize },
105    /// The call writes one initialized element through a pointer argument.
106    WriteMemory { pointer_arg: usize },
107    /// The return value is a pointer backed by a fresh allocation of
108    /// `size_arg` elements × `elem_size` bytes. The base address is taken
109    /// from `pointer_arg`. Used for `from_raw_parts(ptr, len)`.
110    ReturnFreshAllocation {
111        pointer_arg: usize,
112        size_arg: usize,
113        elem_size: u64,
114    },
115    /// The return value is the length of an aggregate argument.
116    ReturnLengthOfArg { arg: usize },
117    /// The return value is `1` iff the length of the aggregate argument is 0.
118    ReturnIsEmptyOfArg { arg: usize },
119    /// The return value is `min(lhs_arg, rhs_arg)`, satisfying
120    /// `return <= lhs_arg` and `return <= rhs_arg`.
121    ReturnMin { lhs_arg: usize, rhs_arg: usize },
122    /// The return value is `max(lhs_arg, rhs_arg)`.
123    ReturnMax { lhs_arg: usize, rhs_arg: usize },
124    /// The return value is `clamp(value_arg, min_arg, max_arg)`.
125    ReturnClamp {
126        value_arg: usize,
127        min_arg: usize,
128        max_arg: usize,
129    },
130    /// The return value is the absolute value of `arg` (`ite(arg >= 0, arg, -arg)`).
131    ReturnAbs { arg: usize },
132    /// The return value is the negation of `arg` (`-arg`).
133    ReturnNeg { arg: usize },
134    /// The return value is `lhs_arg + rhs_arg`.
135    ReturnAdd { lhs_arg: usize, rhs_arg: usize },
136    /// The return value is `lhs_arg - rhs_arg`.
137    ReturnSub { lhs_arg: usize, rhs_arg: usize },
138    /// The return value is `lhs_arg * rhs_arg`.
139    ReturnMul { lhs_arg: usize, rhs_arg: usize },
140    /// The call returns `Option<T>` whose `Some` payload is `lhs_arg + rhs_arg`
141    /// (models `checked_add`; the payload is non-zero whenever `lhs_arg` is).
142    ReturnOptionSomeAdd { lhs_arg: usize, rhs_arg: usize },
143    /// The call returns `Option<T>` whose `Some` payload is `lhs_arg * rhs_arg`
144    /// (models `checked_mul`; the payload is non-zero whenever both args are).
145    ReturnOptionSomeMul { lhs_arg: usize, rhs_arg: usize },
146    /// The return value is non-zero *iff* `arg` is non-zero (models bit-preserving
147    /// operations like `rotate_left`/`swap_bytes`/`count_ones`/`isqrt`, which map
148    /// `0` to `0` and non-zero to non-zero).
149    ReturnNonZeroIff { arg: usize },
150    /// The call returns `Option<T>` whose `Some` payload is non-zero *iff* `arg`
151    /// is non-zero (models `checked_pow`).
152    ReturnOptionSomeNonZeroIff { arg: usize },
153    /// A specific field of the returned tuple is known to be non-zero (e.g.
154    /// `overflowing_abs`/`overflowing_neg` return `(result, overflow)` where
155    /// `result != 0`). Used to discharge a downstream `ValidNum(result != 0)`.
156    ReturnTupleFieldNonZero { field: usize },
157    /// A specific field of the returned tuple carries the length of a given
158    /// argument (e.g. split_at(mid) returns (left, right) where left.len() == mid).
159    ReturnTupleFieldLength { field: usize, from_arg: usize },
160    /// The return value is a pointer backed by a fresh heap allocation of
161    /// `size_arg` elements × `elem_size` bytes. Unlike ReturnFreshAllocation
162    /// this does not require a pointer argument — used for constructors like
163    /// `Vec::from_elem(init, count)` that allocate fresh memory.
164    ReturnNewAllocation { size_arg: usize, elem_size: u64 },
165    /// Like ReturnNewAllocation but the length is carried by the argument
166    /// itself (a Box fat pointer) rather than a separate count argument.
167    /// Used for `into_vec` / `box_assume_init_into_vec_unsafe`.
168    ReturnNewAllocationFromBox { box_arg: usize },
169    /// `Allocator::allocate(self, layout)` / `allocate_zeroed` returns a
170    /// `Result<NonNull<[u8]>, AllocError>`. Model the `Ok` variant as a fresh
171    /// *external* (unbounded) allocation so downstream `NonNull`/`Allocated`
172    /// checks auto-pass regardless of the symbolic `layout.size()`. The
173    /// `Result` downcast (`((result as Ok).0)`) then propagates the provenance.
174    ReturnAllocBuffer,
175    /// The return value is a non-zero power of two (models `Layout::align`).
176    ReturnPowerOfTwo,
177    /// The call transfers a Vec's backing allocation into a Box (e.g.
178    /// `Vec::into_boxed_slice`). Looks up the current heap allocation from
179    /// the allocation's `slice_data` via the argument's stack provenance.
180    ReturnBoxFromVec { arg: usize },
181    /// The return value is known to own initialized memory of the type pointed
182    /// to by the indicated argument (e.g. `Box::from_raw(p)` owns one initialized
183    /// `T` element reached through `p`).
184    OwnsInitMemory { arg: usize },
185    /// The call validates that every element of the array argument `indices_arg`
186    /// is `< args[len_arg]` and that the elements are pairwise distinct, returning
187    /// `Err` otherwise.  On the `Ok` continuation the caller may assume
188    /// `InBound(index_access(slice_of(len_arg), indices_arg))` and
189    /// `NonOverlap(indices_arg)`.  (A trusted interprocedural summary, like the
190    /// std-primitive summaries — the validator's body is not re-proved here.)
191    ChecksIndexBoundsDisjoint { indices_arg: usize, len_arg: usize },
192    /// The call returns `Option<usize>` whose `Some` payload is a scan index
193    /// into the iterator argument `self_arg` (models `Iterator::position` /
194    /// `Iterator::find`): `Some(i)` satisfies `0 <= i < self.len()` where
195    /// `self` is the Iter/IterMut struct produced by `into_iter`/`iter`.
196    ReturnOptionSomeScanIndex { self_arg: usize },
197    /// The call returns the length of a nul-terminated string (models
198    /// `strlen`): `0 <= len < isize::MAX`, so `len + 1` (the byte length with
199    /// the terminator) fits in `isize::MAX` — discharging the
200    /// `from_raw_parts` `ValidNum(size_of(T)*(len+1) <= isize::MAX)` bound.
201    ReturnScanLength { ptr_arg: usize },
202    /// Remove the allocation's `slice_data` link for the argument's stack
203    /// alloc_id — used for `mem::forget` which prevents a drop cascade.
204    CleanSliceDataLinks { arg: usize },
205    /// Returns the element-count distance between two pointers with common
206    /// provenance: `(self_arg.addr() - origin_arg.addr()) / sizeof(T)`.
207    /// Models `NonNull::offset_from_unsigned` / `offset_from`.
208    ReturnOffsetFromUnsigned { self_arg: usize, origin_arg: usize },
209    /// `ptr.align_offset(align)` returns an offset such that
210    /// `(ptr + offset) % align == 0` and `0 <= offset < align` (or `usize::MAX`
211    /// when no such offset exists). Models `*const T::align_offset` /
212    /// `*mut T::align_offset` by recording the alignment path-condition so
213    /// downstream `ptr.add(offset)` dereferences can discharge `Align`.
214    ReturnAlignOffset { ptr_arg: usize, align_arg: usize },
215    /// A local `align_to`-style wrapper (`align_to_ext`/`align_to_mut_ext`)
216    /// returns `(prefix, body, suffix)` where `body` is `align_of::<U>()`-aligned.
217    /// Models the tuple by creating three sub-slices whose lengths/offsets obey
218    /// `prefix.len() = offset` and `len - suffix.len() = offset + k*size_of::<U>()`,
219    /// and records `(ptr + offset) % align_of::<U>() == 0` so downstream
220    /// `ptr.add(offset - k)` dereferences can discharge `Align`.
221    ReturnAlignTo { receiver_arg: usize },
222    /// `IntoIterator::into_iter` on `&[T]` / `&mut [T]` returns an
223    /// `Iter`/`IterMut` whose `ptr` (field 0) and `end_or_len` (field 1) share
224    /// the source slice's allocation. Models the constructor by materializing
225    /// those two pointer fields so downstream `Iterator::next` / `len` /
226    /// `is_empty` can resolve the iterator's provenance and element type.
227    ReturnIter { receiver_arg: usize },
228    /// `<ManuallyDrop<T> as Deref>::deref` / `MaybeDangling::as_ref` return a
229    /// reference to the inner value at the *same* address (transparent
230    /// wrappers).  The return aliases `arg` (a `&T` pointing at `arg`'s
231    /// pointee) and its pointee field values are the argument's field values
232    /// with the leading `peel` transparent field-0 hops stripped.
233    ReturnTransparentDeref { arg: usize, peel: usize },
234}
235
236/// Return dependency information for a MIR call terminator.
237pub fn dependency_summary<'tcx>(
238    tcx: TyCtxt<'tcx>,
239    func: &Operand<'tcx>,
240    arg_count: usize,
241) -> CallDependencySummary {
242    let callee = mir_utils::dep_callee_def_id(func);
243    let name = mir_utils::call_name(tcx, func);
244
245    if let Some(summary) = fn_simulator::lookup_dependency(callee, &name, arg_count) {
246        return summary;
247    }
248
249    // Interprocedural fallback for local callees.
250    if let Some(callee) = callee {
251        if name.contains("::intrinsics::")
252            || name.starts_with("intrinsics::")
253            || name.ends_with("::drop_in_place")
254        {
255            return CallDependencySummary::unknown(Some(callee), name, arg_count);
256        }
257        if let Some(must_write_args) = interprocedural::local_must_write_args(tcx, callee) {
258            if !must_write_args.is_empty() {
259                return CallDependencySummary {
260                    callee: Some(callee),
261                    name,
262                    return_depends_on_args: Vec::new(),
263                    may_write_args: must_write_args
264                        .into_iter()
265                        .filter(|index| *index < arg_count)
266                        .collect(),
267                    unsupported: false,
268                };
269            }
270        }
271        if let Some(return_deps) = interprocedural::local_return_dependencies(tcx, callee) {
272            return CallDependencySummary {
273                callee: Some(callee),
274                name,
275                return_depends_on_args: return_deps
276                    .into_iter()
277                    .filter(|index| *index < arg_count)
278                    .collect(),
279                may_write_args: Vec::new(),
280                unsupported: false,
281            };
282        }
283    }
284
285    CallDependencySummary::unknown(callee, name, arg_count)
286}
287
288/// Return effect information for a MIR call terminator.
289pub fn effect_summary<'tcx>(
290    tcx: TyCtxt<'tcx>,
291    caller: DefId,
292    func: &Operand<'tcx>,
293    destination: Local,
294) -> CallEffectSummary {
295    let callee = mir_utils::dep_callee_def_id(func);
296    let name = mir_utils::call_name(tcx, func);
297
298    if let Some(summary) = fn_simulator::lookup_effect(tcx, caller, callee, &name, func, destination) {
299        return summary;
300    }
301
302    // Transparent-wrapper deref: `<ManuallyDrop<T> as Deref>::deref` /
303    // `deref_mut` (and `MaybeDangling::as_ref`/`as_mut`) return a reference to
304    // the inner value at the same address.  The std MIR for these is
305    // unavailable cross-crate, so model them with field-value peeling.
306    if let Some(peel) = transparent_deref_peel(tcx, func) {
307        return CallEffectSummary {
308            callee,
309            name,
310            destination: Some(destination),
311            effects: vec![CallEffect::ReturnTransparentDeref { arg: 0, peel }],
312            unsupported: false,
313        };
314    }
315
316    // Interprocedural fallback for local callees.
317    if let Some(callee) = callee {
318        if name.contains("::intrinsics::")
319            || name.starts_with("intrinsics::")
320            || name.ends_with("::drop_in_place")
321        {
322            return CallEffectSummary::unknown(Some(callee), name, Some(destination));
323        }
324        if let Some(must_write_args) = interprocedural::local_must_write_args(tcx, callee) {
325            let effects: Vec<_> = must_write_args
326                .into_iter()
327                .map(|arg| CallEffect::WriteMemory { pointer_arg: arg })
328                .collect();
329            if !effects.is_empty() {
330                return CallEffectSummary {
331                    callee: Some(callee),
332                    name,
333                    destination: Some(destination),
334                    effects,
335                    unsupported: false,
336                };
337            }
338        }
339        if let Some(effect) = interprocedural::try_pointer_arith_wrapper_effect(tcx, callee, Some(destination)) {
340            return CallEffectSummary {
341                callee: Some(callee),
342                name,
343                destination: Some(destination),
344                effects: vec![effect],
345                unsupported: false,
346            };
347        }
348        if let Some(effect) = interprocedural::try_from_raw_parts_wrapper_effect(tcx, callee, Some(destination)) {
349            return CallEffectSummary {
350                callee: Some(callee),
351                name,
352                destination: Some(destination),
353                effects: vec![effect],
354                unsupported: false,
355            };
356        }
357        if let Some((indices_arg, len_arg)) = interprocedural::detect_index_disjoint_validator(tcx, callee)
358            .or_else(|| interprocedural::named_index_disjoint_validator(&name))
359        {
360            return CallEffectSummary {
361                callee: Some(callee),
362                name,
363                destination: Some(destination),
364                effects: vec![CallEffect::ChecksIndexBoundsDisjoint {
365                    indices_arg,
366                    len_arg,
367                }],
368                unsupported: false,
369            };
370        }
371        if let Some(return_deps) = interprocedural::local_return_dependencies(tcx, callee) {
372            // If the callee does pointer arithmetic, don't produce ReturnAliasArg
373            // since the offset might have been changed (e.g. wrapping_add(1)).
374            if !interprocedural::callee_contains_pointer_arithmetic(tcx, callee) {
375                // If the callee transitively calls functions that may write
376                // through &mut args, ReturnAliasArg alone is insufficient —
377                // the writes are lost. Mark as unsupported so CalleeEntry
378                // DFS can inline the full body.
379                let has_nested_calls = interprocedural::callee_calls_other_local(tcx, callee);
380                return CallEffectSummary {
381                    callee: Some(callee),
382                    name,
383                    destination: Some(destination),
384                    effects: return_deps
385                        .into_iter()
386                        .map(|arg| CallEffect::ReturnAliasArg { arg })
387                        .collect(),
388                    unsupported: has_nested_calls,
389                };
390            }
391        }
392    }
393
394    CallEffectSummary::unknown(callee, name, Some(destination))
395}
396
397/// Detect a transparent-wrapper deref whose receiver is `ManuallyDrop<T>` or
398/// `MaybeDangling<T>`, and return how many leading field-0 hops must be peeled
399/// to reach the inner `T`:
400///   * `ManuallyDrop<T> { value: MaybeDangling<T> }` → 2 (`value` → `MaybeDangling.0`)
401///   * `MaybeDangling<P>(P)` → 1.
402fn transparent_deref_peel<'tcx>(tcx: TyCtxt<'tcx>, func: &Operand<'tcx>) -> Option<usize> {
403    let Operand::Constant(c) = func else { return None };
404    let TyKind::FnDef(_, args) = c.const_.ty().kind() else { return None };
405    let self_ty = args.iter().find_map(|a| {
406        #[cfg(rapx_ge_99)] let a = a.skip_binder();
407        if let GenericArgKind::Type(t) = a.kind() { Some(t) } else { None }
408    })?;
409    let TyKind::Adt(adt_def, _) = self_ty.kind() else { return None };
410    let path = tcx.def_path_str(adt_def.did());
411    if path.contains("ManuallyDrop") {
412        Some(2)
413    } else if path.contains("MaybeDangling") {
414        Some(1)
415    } else {
416        None
417    }
418}
419