rapx/verify/
call_summary.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.
11
12use std::collections::HashSet;
13use std::panic::{AssertUnwindSafe, catch_unwind};
14
15use rustc_hir::def_id::DefId;
16use rustc_middle::{
17    mir::{BasicBlock, Local, Operand, Rvalue, StatementKind, TerminatorKind},
18    ty::{ConstKind, GenericArgKind, PseudoCanonicalInput, Ty, TyCtxt, TyKind},
19};
20
21use crate::analysis::dataflow::{DataflowAnalysis, default::DataflowAnalyzer};
22use crate::analysis::path_analysis::graph::{PathEnumerator, PathGraph};
23
24use super::{primitive::PrimitiveCall, slicer::ForgetReason};
25
26/// Dependency summary consumed by the backward visitor.
27#[derive(Clone, Debug)]
28pub struct CallDependencySummary {
29    /// Callee definition when the call target is statically known.
30    pub callee: Option<DefId>,
31    /// Human-readable callee name.
32    pub name: String,
33    /// If the call destination is relevant, these call arguments are relevant.
34    pub return_depends_on_args: Vec<usize>,
35    /// Arguments that may be written or invalidated by the call.
36    pub may_write_args: Vec<usize>,
37    /// True when this summary is conservative rather than precise.
38    pub unsupported: bool,
39}
40
41impl CallDependencySummary {
42    /// Build a conservative summary that keeps all arguments relevant.
43    fn unknown(callee: Option<DefId>, name: String, arg_count: usize) -> Self {
44        Self {
45            callee,
46            name,
47            return_depends_on_args: (0..arg_count).collect(),
48            may_write_args: Vec::new(),
49            unsupported: true,
50        }
51    }
52}
53
54/// Effect summary consumed by the forward visitor.
55#[derive(Clone, Debug)]
56pub struct CallEffectSummary {
57    /// Callee definition when the call target is statically known.
58    pub callee: Option<DefId>,
59    /// Human-readable callee name.
60    pub name: String,
61    /// Destination local receiving the return value.
62    pub destination: Option<Local>,
63    /// Effects that can be applied to the path-local abstract state.
64    pub effects: Vec<CallEffect>,
65    /// True when this summary is conservative rather than precise.
66    pub unsupported: bool,
67}
68
69impl CallEffectSummary {
70    /// Build a conservative summary for an unsupported call.
71    fn unknown(callee: Option<DefId>, name: String, destination: Option<Local>) -> Self {
72        Self {
73            callee,
74            name,
75            destination,
76            effects: Vec::new(),
77            unsupported: true,
78        }
79    }
80}
81
82/// Path-local effect produced by a retained call.
83#[derive(Clone, Debug)]
84pub enum CallEffect {
85    /// The return value aliases or is a direct value flow from an argument.
86    ReturnAliasArg { arg: usize },
87    /// The return value is a pointer extracted from an aggregate/reference arg.
88    ReturnPointerFromArg { arg: usize },
89    /// The return value is `base + offset * stride`.
90    ReturnPointerAdd {
91        base_arg: usize,
92        offset_arg: usize,
93        stride: Option<u64>,
94    },
95    /// The return value is `base - offset * stride`.
96    ReturnPointerSub {
97        base_arg: usize,
98        offset_arg: usize,
99        stride: Option<u64>,
100    },
101    /// The return value is known to be non-zero.
102    ReturnNonZero,
103    /// The return value is known to satisfy a concrete alignment.
104    ReturnAligned { align: u64, ty_name: String },
105    /// The return value is a concrete layout/numeric constant.
106    ReturnConst { value: u64, label: String },
107    /// The call reads memory through an argument.
108    ReadMemory { arg: usize },
109    /// The call writes one initialized element through a pointer argument.
110    WriteMemory { pointer_arg: usize },
111    /// The return value is the length of an aggregate argument.
112    ReturnLengthOfArg { arg: usize },
113    /// A specific field of the returned tuple carries the length of a given
114    /// argument (e.g. split_at(mid) returns (left, right) where left.len() == mid).
115    ReturnTupleFieldLength { field: usize, from_arg: usize },
116    /// Facts about an argument must be forgotten conservatively.
117    ForgetArgFacts { arg: usize, reason: ForgetReason },
118}
119
120/// Return dependency information for a MIR call terminator.
121pub fn dependency_summary<'tcx>(
122    tcx: TyCtxt<'tcx>,
123    func: &Operand<'tcx>,
124    arg_count: usize,
125) -> CallDependencySummary {
126    let callee = callee_def_id(func);
127    let name = call_name(tcx, func);
128
129    let primitive = PrimitiveCall::classify(&name);
130
131    if primitive.is_some_and(PrimitiveCall::is_as_ptr_like) {
132        return CallDependencySummary {
133            callee,
134            name,
135            return_depends_on_args: vec![0],
136            may_write_args: Vec::new(),
137            unsupported: false,
138        };
139    }
140
141    if primitive == Some(PrimitiveCall::AsPtrRange)
142        || primitive == Some(PrimitiveCall::AsMutPtrRange)
143    {
144        return CallDependencySummary {
145            callee,
146            name,
147            return_depends_on_args: vec![0],
148            may_write_args: Vec::new(),
149            unsupported: false,
150        };
151    }
152
153    if primitive.is_some_and(PrimitiveCall::is_pointer_arithmetic) {
154        return CallDependencySummary {
155            callee,
156            name,
157            return_depends_on_args: vec![0, 1],
158            may_write_args: Vec::new(),
159            unsupported: false,
160        };
161    }
162
163    if primitive == Some(PrimitiveCall::PtrRead) {
164        return CallDependencySummary {
165            callee,
166            name,
167            return_depends_on_args: vec![0],
168            may_write_args: Vec::new(),
169            unsupported: false,
170        };
171    }
172
173    if primitive == Some(PrimitiveCall::PtrWrite) {
174        return CallDependencySummary {
175            callee,
176            name,
177            return_depends_on_args: Vec::new(),
178            may_write_args: vec![0],
179            unsupported: false,
180        };
181    }
182
183    if primitive == Some(PrimitiveCall::Len) {
184        return CallDependencySummary {
185            callee,
186            name,
187            return_depends_on_args: vec![0],
188            may_write_args: Vec::new(),
189            unsupported: false,
190        };
191    }
192
193    if primitive == Some(PrimitiveCall::MaybeUninitUninit) {
194        return CallDependencySummary {
195            callee,
196            name,
197            return_depends_on_args: Vec::new(),
198            may_write_args: Vec::new(),
199            unsupported: false,
200        };
201    }
202
203    if primitive.is_some_and(PrimitiveCall::is_layout_constant) {
204        return CallDependencySummary {
205            callee,
206            name,
207            return_depends_on_args: Vec::new(),
208            may_write_args: Vec::new(),
209            unsupported: false,
210        };
211    }
212
213    if is_from_trait_call(&name) {
214        return CallDependencySummary {
215            callee,
216            name,
217            return_depends_on_args: vec![0],
218            may_write_args: Vec::new(),
219            unsupported: false,
220        };
221    }
222
223    if let Some(callee) = callee {
224        // Skip interprocedural analysis for intrinsics and
225        // compiler-generated functions — their MIR can trigger
226        // worker-thread stack overflows during `optimized_mir`.
227        if name.contains("::intrinsics::")
228            || name.starts_with("intrinsics::")
229            || name.ends_with("::drop_in_place")
230        {
231            return CallDependencySummary::unknown(Some(callee), name, arg_count);
232        }
233        if let Some(must_write_args) = local_must_write_args(tcx, callee) {
234            if !must_write_args.is_empty() {
235                return CallDependencySummary {
236                    callee: Some(callee),
237                    name,
238                    return_depends_on_args: Vec::new(),
239                    may_write_args: must_write_args
240                        .into_iter()
241                        .filter(|index| *index < arg_count)
242                        .collect(),
243                    unsupported: false,
244                };
245            }
246        }
247        if let Some(return_deps) = local_return_dependencies(tcx, callee) {
248            return CallDependencySummary {
249                callee: Some(callee),
250                name,
251                return_depends_on_args: return_deps
252                    .into_iter()
253                    .filter(|index| *index < arg_count)
254                    .collect(),
255                may_write_args: Vec::new(),
256                unsupported: false,
257            };
258        }
259    }
260
261    CallDependencySummary::unknown(callee, name, arg_count)
262}
263
264/// Return effect information for a MIR call terminator.
265pub fn effect_summary<'tcx>(
266    tcx: TyCtxt<'tcx>,
267    caller: DefId,
268    func: &Operand<'tcx>,
269    destination: Local,
270) -> CallEffectSummary {
271    let callee = callee_def_id(func);
272    let name = call_name(tcx, func);
273    let destination = Some(destination);
274
275    let primitive = PrimitiveCall::classify(&name);
276
277    if primitive.is_some_and(PrimitiveCall::is_as_ptr_like) {
278        let mut effects = vec![
279            CallEffect::ReturnPointerFromArg { arg: 0 },
280            CallEffect::ReturnNonZero,
281        ];
282        if let Some((align, ty_name)) = destination_pointee_alignment(tcx, caller, destination) {
283            effects.push(CallEffect::ReturnAligned { align, ty_name });
284        }
285        return CallEffectSummary {
286            callee,
287            name,
288            destination,
289            effects,
290            unsupported: false,
291        };
292    }
293
294    if primitive == Some(PrimitiveCall::AsPtrRange)
295        || primitive == Some(PrimitiveCall::AsMutPtrRange)
296    {
297        let effects = vec![CallEffect::ReturnAliasArg { arg: 0 }];
298        return CallEffectSummary {
299            callee,
300            name,
301            destination,
302            effects,
303            unsupported: false,
304        };
305    }
306
307    if primitive.is_some_and(PrimitiveCall::is_pointer_add_like) {
308        let stride = if primitive.is_some_and(PrimitiveCall::is_byte_pointer_arithmetic) {
309            Some(1)
310        } else {
311            destination_stride(tcx, caller, destination)
312        };
313        return CallEffectSummary {
314            callee,
315            name,
316            destination,
317            effects: vec![CallEffect::ReturnPointerAdd {
318                base_arg: 0,
319                offset_arg: 1,
320                stride,
321            }],
322            unsupported: false,
323        };
324    }
325
326    if primitive.is_some_and(PrimitiveCall::is_pointer_sub_like) {
327        let stride = if primitive.is_some_and(PrimitiveCall::is_byte_pointer_arithmetic) {
328            Some(1)
329        } else {
330            destination_stride(tcx, caller, destination)
331        };
332        return CallEffectSummary {
333            callee,
334            name,
335            destination,
336            effects: vec![CallEffect::ReturnPointerSub {
337                base_arg: 0,
338                offset_arg: 1,
339                stride,
340            }],
341            unsupported: false,
342        };
343    }
344
345    if primitive == Some(PrimitiveCall::PtrRead) {
346        return CallEffectSummary {
347            callee,
348            name,
349            destination,
350            effects: vec![CallEffect::ReadMemory { arg: 0 }],
351            unsupported: false,
352        };
353    }
354
355    if primitive == Some(PrimitiveCall::PtrWrite) {
356        return CallEffectSummary {
357            callee,
358            name,
359            destination,
360            effects: vec![CallEffect::WriteMemory { pointer_arg: 0 }],
361            unsupported: false,
362        };
363    }
364
365    if primitive == Some(PrimitiveCall::Len) {
366        return CallEffectSummary {
367            callee,
368            name,
369            destination,
370            effects: vec![CallEffect::ReturnLengthOfArg { arg: 0 }],
371            unsupported: false,
372        };
373    }
374
375    if primitive == Some(PrimitiveCall::MaybeUninitUninit) {
376        return CallEffectSummary {
377            callee,
378            name,
379            destination,
380            effects: Vec::new(),
381            unsupported: false,
382        };
383    }
384
385    if primitive.is_some_and(PrimitiveCall::is_layout_constant) {
386        let effects = layout_constant_effect(tcx, caller, func, &name)
387            .into_iter()
388            .collect();
389        return CallEffectSummary {
390            callee,
391            name,
392            destination,
393            effects,
394            unsupported: false,
395        };
396    }
397
398    if let Some(prim) = primitive
399        && matches!(prim, PrimitiveCall::SplitAt | PrimitiveCall::SplitAtMut)
400    {
401        return CallEffectSummary {
402            callee,
403            name,
404            destination,
405            effects: vec![CallEffect::ReturnTupleFieldLength {
406                field: 0,
407                from_arg: 1,
408            }],
409            unsupported: false,
410        };
411    }
412
413    if let Some(prim) = primitive
414        && matches!(prim, PrimitiveCall::FromRawParts | PrimitiveCall::FromRawPartsMut)
415    {
416        let mut effects = vec![
417            CallEffect::ReturnAliasArg { arg: 0 },
418            CallEffect::ReturnNonZero,
419        ];
420        if let Some((align, ty_name)) = destination_pointee_alignment(tcx, caller, destination) {
421            effects.push(CallEffect::ReturnAligned { align, ty_name });
422        }
423        return CallEffectSummary {
424            callee,
425            name,
426            destination,
427            effects,
428            unsupported: false,
429        };
430    }
431
432    if is_from_trait_call(&name) && is_nonnull_destination(tcx, caller, destination) {
433        let mut effects = vec![
434            CallEffect::ReturnPointerFromArg { arg: 0 },
435            CallEffect::ReturnNonZero,
436        ];
437        if let Some((align, ty_name)) = destination_nonnull_alignment(tcx, caller, destination) {
438            effects.push(CallEffect::ReturnAligned { align, ty_name });
439        }
440        return CallEffectSummary {
441            callee,
442            name,
443            destination,
444            effects,
445            unsupported: false,
446        };
447    }
448
449    if let Some(callee) = callee {
450        // Skip interprocedural analysis for intrinsics and
451        // compiler-generated functions.
452        if name.contains("::intrinsics::")
453            || name.starts_with("intrinsics::")
454            || name.ends_with("::drop_in_place")
455        {
456            return CallEffectSummary::unknown(Some(callee), name, destination);
457        }
458        if let Some(must_write_args) = local_must_write_args(tcx, callee) {
459            let effects: Vec<_> = must_write_args
460                .into_iter()
461                .map(|arg| CallEffect::WriteMemory { pointer_arg: arg })
462                .collect();
463            if !effects.is_empty() {
464                return CallEffectSummary {
465                    callee: Some(callee),
466                    name,
467                    destination,
468                    effects,
469                    unsupported: false,
470                };
471            }
472        }
473        if let Some(effect) = try_pointer_arith_wrapper_effect(tcx, callee, destination) {
474            return CallEffectSummary {
475                callee: Some(callee),
476                name,
477                destination,
478                effects: vec![effect],
479                unsupported: false,
480            };
481        }
482        if let Some(return_deps) = local_return_dependencies(tcx, callee) {
483            return CallEffectSummary {
484                callee: Some(callee),
485                name,
486                destination,
487                effects: return_deps
488                    .into_iter()
489                    .map(|arg| CallEffect::ReturnAliasArg { arg })
490                    .collect(),
491                unsupported: false,
492            };
493        }
494    }
495
496    CallEffectSummary::unknown(callee, name, destination)
497}
498
499/// Return the static callee definition for a MIR call operand.
500pub fn callee_def_id(func: &Operand<'_>) -> Option<DefId> {
501    let Operand::Constant(func_constant) = func else {
502        return None;
503    };
504    let TyKind::FnDef(def_id, _) = func_constant.const_.ty().kind() else {
505        return None;
506    };
507    Some(*def_id)
508}
509
510/// Return a stable, human-readable name for a MIR call operand.
511pub fn call_name(tcx: TyCtxt<'_>, func: &Operand<'_>) -> String {
512    callee_def_id(func)
513        .map(|def_id| tcx.def_path_str(def_id))
514        .unwrap_or_else(|| format!("{func:?}"))
515}
516
517/// Return true for slice/string/vector pointer extraction calls.
518pub fn is_as_ptr_call(name: &str) -> bool {
519    PrimitiveCall::classify(name) == Some(PrimitiveCall::AsPtr)
520}
521
522/// Return true for mutable pointer extraction calls.
523pub fn is_as_mut_ptr_call(name: &str) -> bool {
524    PrimitiveCall::classify(name) == Some(PrimitiveCall::AsMutPtr)
525}
526
527/// Return true for typed pointer addition calls.
528pub fn is_pointer_add_call(name: &str) -> bool {
529    PrimitiveCall::classify(name).is_some_and(PrimitiveCall::is_pointer_add_like)
530}
531
532/// Return true for typed pointer subtraction calls.
533pub fn is_pointer_sub_call(name: &str) -> bool {
534    PrimitiveCall::classify(name).is_some_and(PrimitiveCall::is_pointer_sub_like)
535}
536
537/// Return true for typed pointer offset calls.
538pub fn is_pointer_offset_call(name: &str) -> bool {
539    PrimitiveCall::classify(name) == Some(PrimitiveCall::PtrOffset)
540}
541
542/// Return true for pointer reads.
543pub fn is_pointer_read_call(name: &str) -> bool {
544    PrimitiveCall::classify(name) == Some(PrimitiveCall::PtrRead)
545}
546
547/// Return true for pointer writes that initialize one element.
548pub fn is_pointer_write_call(name: &str) -> bool {
549    PrimitiveCall::classify(name) == Some(PrimitiveCall::PtrWrite)
550}
551
552/// Return true for slice/string/vector length queries.
553pub fn is_len_call(name: &str) -> bool {
554    PrimitiveCall::classify(name) == Some(PrimitiveCall::Len)
555}
556
557/// Return true for `MaybeUninit::<T>::uninit`.
558pub fn is_maybe_uninit_uninit_call(name: &str) -> bool {
559    PrimitiveCall::classify(name) == Some(PrimitiveCall::MaybeUninitUninit)
560}
561
562/// Return true for layout constant producers.
563pub fn is_layout_constant_call(name: &str) -> bool {
564    PrimitiveCall::classify(name).is_some_and(PrimitiveCall::is_layout_constant)
565}
566
567fn is_from_trait_call(name: &str) -> bool {
568    name == "std::convert::From::from" || name == "core::convert::From::from"
569}
570
571/// Return a concrete layout constant effect for `align_of::<T>()` or `size_of::<T>()`.
572fn layout_constant_effect<'tcx>(
573    tcx: TyCtxt<'tcx>,
574    caller: DefId,
575    func: &Operand<'tcx>,
576    name: &str,
577) -> Option<CallEffect> {
578    let ty = layout_call_ty(func)?;
579    let (align, size) = type_layout(tcx, caller, ty)?;
580    match PrimitiveCall::classify(name)? {
581        PrimitiveCall::AlignOf => Some(CallEffect::ReturnConst {
582            value: align,
583            label: format!("align_of::<{ty:?}>()"),
584        }),
585        PrimitiveCall::SizeOf => Some(CallEffect::ReturnConst {
586            value: size,
587            label: format!("size_of::<{ty:?}>()"),
588        }),
589        _ => None,
590    }
591}
592
593/// Return the type argument for a layout-producing call.
594fn layout_call_ty<'tcx>(func: &Operand<'tcx>) -> Option<Ty<'tcx>> {
595    let Operand::Constant(func_constant) = func else {
596        return None;
597    };
598    let TyKind::FnDef(_, args) = func_constant.const_.ty().kind() else {
599        return None;
600    };
601    args.iter().find_map(|arg| arg.as_type())
602}
603
604/// Trace backward from an operand (inner call arg) through Copy/Move/Cast
605/// assignments to the outer callee's argument local, returning its index.
606fn trace_to_callee_arg<'tcx>(
607    tcx: TyCtxt<'tcx>,
608    body: &rustc_middle::mir::Body<'tcx>,
609    operand: &Operand<'_>,
610) -> Option<usize> {
611    use std::collections::{HashSet, VecDeque};
612
613    let local = match operand {
614        Operand::Copy(place) | Operand::Move(place) => place.local,
615        _ => return None,
616    };
617    let idx = local.as_usize();
618    if idx >= 1 && idx <= body.arg_count {
619        return Some(idx - 1);
620    }
621    let mut queue = VecDeque::from([local]);
622    let mut seen = HashSet::from([local]);
623    while let Some(current) = queue.pop_front() {
624        let cidx = current.as_usize();
625        if cidx >= 1 && cidx <= body.arg_count {
626            return Some(cidx - 1);
627        }
628        for bb in body.basic_blocks.iter() {
629            for stmt in &bb.statements {
630                let StatementKind::Assign(assign) = &stmt.kind else {
631                    continue;
632                };
633                let dest = assign.0.local;
634                if dest != current {
635                    continue;
636                }
637                let source = match &assign.1 {
638                    Rvalue::Use(Operand::Copy(place), ..)
639                    | Rvalue::Use(Operand::Move(place), ..)
640                    | Rvalue::Cast(_, Operand::Copy(place), _)
641                    | Rvalue::Cast(_, Operand::Move(place), _)
642                    | Rvalue::Ref(_, _, place)
643                    | Rvalue::RawPtr(_, place)
644                    | Rvalue::CopyForDeref(place) => place.local,
645                    _ => continue,
646                };
647                if !seen.contains(&source) {
648                    seen.insert(source);
649                    queue.push_back(source);
650                }
651            }
652            let Some(terminator) = &bb.terminator else {
653                continue;
654            };
655            let TerminatorKind::Call {
656                func,
657                args,
658                destination,
659                ..
660            } = &terminator.kind
661            else {
662                continue;
663            };
664            if destination.local != current {
665                continue;
666            }
667            let primitive = PrimitiveCall::classify(&call_name(tcx, func));
668            if !primitive.is_some_and(PrimitiveCall::is_as_ptr_like) {
669                continue;
670            }
671            let Some(source) = args.first().and_then(|arg| match &arg.node {
672                Operand::Copy(place) | Operand::Move(place) => Some(place.local),
673                Operand::Constant(_) => None,
674                #[cfg(rapx_rustc_ge_196)]
675                Operand::RuntimeChecks(_) => None,
676            }) else {
677                continue;
678            };
679            if !seen.contains(&source) {
680                seen.insert(source);
681                queue.push_back(source);
682            }
683        }
684    }
685    None
686}
687
688/// Detect when a local callee wraps a pointer-arithmetic call (add/sub) and
689/// produce the correct `ReturnPointerAdd` / `ReturnPointerSub` effect.
690fn try_pointer_arith_wrapper_effect<'tcx>(
691    tcx: TyCtxt<'tcx>,
692    callee: DefId,
693    _destination: Option<Local>,
694) -> Option<CallEffect> {
695    use std::collections::{HashSet, VecDeque};
696
697    if !tcx.is_mir_available(callee) {
698        return None;
699    }
700
701    // Skip functions whose MIR is too large — they're unlikely to be
702    // simple pointer-arithmetic wrappers and can trigger worker-thread
703    // stack overflows during `optimized_mir`.
704    let body = tcx.optimized_mir(callee);
705    if body.basic_blocks.len() > 16 {
706        return None;
707    }
708    let ret = Local::from_usize(0);
709
710    for bb in body.basic_blocks.iter() {
711        let Some(terminator) = &bb.terminator else {
712            continue;
713        };
714        let TerminatorKind::Call {
715            func,
716            args,
717            destination: call_dest,
718            ..
719        } = &terminator.kind
720        else {
721            continue;
722        };
723
724        let name = call_name(tcx, func);
725        let primitive = PrimitiveCall::classify(&name);
726        let is_add = primitive.is_some_and(PrimitiveCall::is_pointer_add_like);
727        let is_sub = primitive.is_some_and(PrimitiveCall::is_pointer_sub_like);
728
729        // Also check if the inner callee is itself a pointer-arithmetic wrapper.
730        let inner_effect = if !is_add && !is_sub {
731            callee_def_id(func).and_then(|inner_callee| {
732                let inner_name = call_name(tcx, func);
733                if inner_name.contains("::intrinsics::")
734                    || inner_name.starts_with("intrinsics::")
735                    || inner_name.ends_with("::drop_in_place")
736                {
737                    return None;
738                }
739                try_pointer_arith_wrapper_effect(tcx, inner_callee, Some(call_dest.local))
740            })
741        } else {
742            None
743        };
744
745        if !is_add && !is_sub && inner_effect.is_none() {
746            continue;
747        }
748
749        // Check that the call result flows to the return value.
750        let mut queue = VecDeque::from([call_dest.local]);
751        let mut seen = HashSet::from([call_dest.local]);
752        let mut reaches_ret = false;
753        while let Some(current) = queue.pop_front() {
754            if current == ret {
755                reaches_ret = true;
756                break;
757            }
758            for bb2 in body.basic_blocks.iter() {
759                for stmt in &bb2.statements {
760                    let StatementKind::Assign(assign) = &stmt.kind else {
761                        continue;
762                    };
763                    let dest = assign.0.local;
764                    if seen.contains(&dest) {
765                        continue;
766                    }
767                    match &assign.1 {
768                        Rvalue::Use(Operand::Copy(place), ..)
769                        | Rvalue::Use(Operand::Move(place), ..) => {
770                            if place.local == current {
771                                queue.push_back(dest);
772                                seen.insert(dest);
773                            }
774                        }
775                        Rvalue::Cast(_, Operand::Copy(place), _)
776                        | Rvalue::Cast(_, Operand::Move(place), _) => {
777                            if place.local == current {
778                                queue.push_back(dest);
779                                seen.insert(dest);
780                            }
781                        }
782                        _ => {}
783                    }
784                }
785            }
786        }
787        if !reaches_ret {
788            continue;
789        }
790
791        // For indirect wrappers: remap inner call args to outer callee args.
792        if let Some(effect) = inner_effect {
793            match effect {
794                CallEffect::ReturnPointerAdd {
795                    base_arg: inner_base,
796                    offset_arg: inner_offset,
797                    stride,
798                }
799                | CallEffect::ReturnPointerSub {
800                    base_arg: inner_base,
801                    offset_arg: inner_offset,
802                    stride,
803                } => {
804                    let base_arg = trace_to_callee_arg(tcx, body, &args.get(inner_base)?.node)?;
805                    let offset_arg = trace_to_callee_arg(tcx, body, &args.get(inner_offset)?.node)?;
806                    return Some(match effect {
807                        CallEffect::ReturnPointerSub { .. } => CallEffect::ReturnPointerSub {
808                            base_arg,
809                            offset_arg,
810                            stride,
811                        },
812                        _ => CallEffect::ReturnPointerAdd {
813                            base_arg,
814                            offset_arg,
815                            stride,
816                        },
817                    });
818                }
819                _ => {}
820            }
821            continue;
822        }
823
824        // Map inner call args to callee argument indices by tracing back
825        // through Copy/Move assignments to the callee's parameter locals.
826        let base_arg = trace_to_callee_arg(tcx, body, &args[0].node)?;
827        let offset_arg = trace_to_callee_arg(tcx, body, &args[1].node)?;
828        // Use the inner call's destination to compute the byte stride,
829        // not the wrapper's return type (which may differ after a cast).
830        let stride = if primitive.is_some_and(PrimitiveCall::is_byte_pointer_arithmetic) {
831            Some(1)
832        } else {
833            destination_stride(tcx, callee, Some(call_dest.local))
834        };
835
836        return if is_sub {
837            Some(CallEffect::ReturnPointerSub {
838                base_arg,
839                offset_arg,
840                stride,
841            })
842        } else {
843            Some(CallEffect::ReturnPointerAdd {
844                base_arg,
845                offset_arg,
846                stride,
847            })
848        };
849    }
850
851    None
852}
853
854/// Use the existing dataflow graph to approximate local callee return deps.
855fn local_return_dependencies(tcx: TyCtxt<'_>, callee: DefId) -> Option<Vec<usize>> {
856    callee.as_local()?;
857    if !tcx.is_mir_available(callee) {
858        return None;
859    }
860    catch_unwind(AssertUnwindSafe(|| {
861        let mut analyzer = DataflowAnalyzer::new(tcx, false);
862        analyzer.build_graph(callee);
863        let deps = analyzer.get_fn_arg2ret(callee);
864        deps.iter_enumerated()
865            .filter_map(|(local, depends)| {
866                if *depends && local.as_usize() > 0 {
867                    Some(local.as_usize() - 1)
868                } else {
869                    None
870                }
871            })
872            .collect()
873    }))
874    .ok()
875}
876
877/// Return callee argument indices that are definitely written on every
878/// reachable return path.
879fn local_must_write_args(tcx: TyCtxt<'_>, callee: DefId) -> Option<Vec<usize>> {
880    callee.as_local()?;
881    if !tcx.is_mir_available(callee) {
882        return None;
883    }
884
885    catch_unwind(AssertUnwindSafe(|| {
886        let body = tcx.optimized_mir(callee);
887        let mut graph = PathGraph::new(tcx, callee);
888        graph.find_scc();
889        let mut enumerator = PathEnumerator::new(&graph);
890        let paths = enumerator.enumerate_paths_repeat(0);
891
892        let mut must_write: Option<HashSet<usize>> = None;
893        for path in paths.iter() {
894            if !path_ends_in_return(body, &path) {
895                continue;
896            }
897            let writes = write_args_on_path(tcx, body, &path);
898            must_write = Some(match must_write {
899                Some(current) => current.intersection(&writes).copied().collect(),
900                None => writes,
901            });
902        }
903
904        must_write
905            .unwrap_or_default()
906            .into_iter()
907            .collect::<Vec<_>>()
908    }))
909    .ok()
910}
911
912fn path_ends_in_return(body: &rustc_middle::mir::Body<'_>, path: &[usize]) -> bool {
913    path.last().is_some_and(|block| {
914        body.basic_blocks
915            .get(BasicBlock::from_usize(*block))
916            .and_then(|data| data.terminator.as_ref())
917            .is_some_and(|terminator| matches!(terminator.kind, TerminatorKind::Return))
918    })
919}
920
921fn write_args_on_path<'tcx>(
922    tcx: TyCtxt<'tcx>,
923    body: &rustc_middle::mir::Body<'tcx>,
924    path: &[usize],
925) -> HashSet<usize> {
926    let mut writes = HashSet::new();
927    for block in path {
928        let Some(data) = body.basic_blocks.get(BasicBlock::from_usize(*block)) else {
929            continue;
930        };
931        let Some(terminator) = data.terminator.as_ref() else {
932            continue;
933        };
934        let TerminatorKind::Call { func, args, .. } = &terminator.kind else {
935            continue;
936        };
937        let name = call_name(tcx, func);
938        if PrimitiveCall::classify(&name) != Some(PrimitiveCall::PtrWrite) {
939            continue;
940        }
941        if let Some(pointer_arg) = args
942            .first()
943            .and_then(|arg| trace_to_callee_arg(tcx, body, &arg.node))
944        {
945            writes.insert(pointer_arg);
946        }
947    }
948    writes
949}
950
951/// Return the byte stride for a pointer returned into `destination`.
952fn destination_stride<'tcx>(
953    tcx: TyCtxt<'tcx>,
954    caller: DefId,
955    destination: Option<Local>,
956) -> Option<u64> {
957    let destination = destination?;
958    let ty = tcx.optimized_mir(caller).local_decls[destination].ty;
959    let pointee = pointee_ty(ty)?;
960    type_layout(tcx, caller, pointee).map(|(_, size)| size)
961}
962
963/// Return pointee alignment for a pointer returned into `destination`.
964fn destination_pointee_alignment<'tcx>(
965    tcx: TyCtxt<'tcx>,
966    caller: DefId,
967    destination: Option<Local>,
968) -> Option<(u64, String)> {
969    let destination = destination?;
970    let ty = tcx.optimized_mir(caller).local_decls[destination].ty;
971    let pointee = pointee_ty(ty).or(Some(ty))?;
972    if let Some((align, _)) = type_layout(tcx, caller, pointee) {
973        return Some((align, format!("{pointee:?}")));
974    }
975    if let TyKind::Array(elem, _) = pointee.kind()
976        && let Some((align, _)) = type_layout(tcx, caller, *elem)
977    {
978        return Some((align, format!("{pointee:?}")));
979    }
980    Some((0, format!("{pointee:?}")))
981}
982
983/// Return pointee alignment when the destination is `NonNull<T>`.
984fn destination_nonnull_alignment<'tcx>(
985    tcx: TyCtxt<'tcx>,
986    caller: DefId,
987    destination: Option<Local>,
988) -> Option<(u64, String)> {
989    let destination = destination?;
990    let ty = tcx.optimized_mir(caller).local_decls[destination].ty;
991    let pointee = nonnull_inner_ty(tcx, ty)?;
992    type_layout(tcx, caller, pointee).map(|(align, _)| (align, format!("{pointee:?}")))
993}
994
995fn is_nonnull_destination<'tcx>(
996    tcx: TyCtxt<'tcx>,
997    caller: DefId,
998    destination: Option<Local>,
999) -> bool {
1000    let Some(destination) = destination else {
1001        return false;
1002    };
1003    let ty = tcx.optimized_mir(caller).local_decls[destination].ty;
1004    nonnull_inner_ty(tcx, ty).is_some() || format!("{ty:?}").contains("NonNull<")
1005}
1006
1007/// Return the pointee type of raw pointers and references.
1008fn pointee_ty<'tcx>(ty: Ty<'tcx>) -> Option<Ty<'tcx>> {
1009    match ty.kind() {
1010        TyKind::RawPtr(ty, _) | TyKind::Ref(_, ty, _) => Some(*ty),
1011        _ => None,
1012    }
1013}
1014
1015fn nonnull_inner_ty<'tcx>(tcx: TyCtxt<'tcx>, ty: Ty<'tcx>) -> Option<Ty<'tcx>> {
1016    let TyKind::Adt(def, args) = ty.kind() else {
1017        return None;
1018    };
1019    let path = tcx.def_path_str(def.did());
1020    if !path.contains("ptr::non_null::NonNull") {
1021        return None;
1022    }
1023    args.iter().find_map(|arg| arg.as_type())
1024}
1025
1026fn type_layout<'tcx>(tcx: TyCtxt<'tcx>, caller: DefId, ty: Ty<'tcx>) -> Option<(u64, u64)> {
1027    if ty_has_param_const(ty) {
1028        return None;
1029    }
1030    let typing_env = rustc_middle::ty::TypingEnv::post_analysis(tcx, caller);
1031    let input = PseudoCanonicalInput {
1032        typing_env,
1033        value: ty,
1034    };
1035    match tcx.layout_of(input) {
1036        Ok(layout) => Some((layout.align.abi.bytes(), layout.size.bytes())),
1037        Err(_) if matches!(ty.kind(), TyKind::Param(_)) => Some((0, 0)),
1038        Err(_) => None,
1039    }
1040}
1041
1042fn ty_has_param_const(ty: Ty<'_>) -> bool {
1043    for arg in ty.walk() {
1044        match arg.kind() {
1045            GenericArgKind::Const(c) if matches!(c.kind(), ConstKind::Param(_)) => return true,
1046            GenericArgKind::Type(inner_ty) if matches!(inner_ty.kind(), TyKind::Alias(..)) => {
1047                return true;
1048            }
1049            _ => {}
1050        }
1051    }
1052    false
1053}