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