Skip to main content

rapx/verify/property_checker/
memory.rs

1//! Checkers for memory-shape properties: `Align`, `NonNull`, `Allocated`,
2//! `Init`, and `Alive`.
3//!
4//! These consume the VM's provenance/invariant facts (e.g. `align_n`,
5//! `in_bounds`, `non_null`) with fast paths, falling back to SMT over
6//! `value.z3_term` and allocation base/size.
7
8use crate::helpers::mir_scan::Checkpoint;
9use crate::verify::api_classify;
10use crate::verify::contract::{ContractExpr, Property, PropertyArg};
11use crate::verify::report::{CheckResult, UnknownReason};
12use crate::verify::vm::state::{AllocId, OffsetKind, VmState, VmValue};
13use rustc_hash::FxHashSet;
14use rustc_middle::mir::{Local, Operand, Rvalue, StatementKind};
15use rustc_middle::ty::TyKind;
16use z3::{
17    SatResult, Solver,
18    ast::{Ast, Int},
19};
20
21use super::PropertyChecker;
22use super::util::maybe_uninit_inner;
23
24impl PropertyChecker {
25    pub(super) fn check_align<'z3, 'tcx>(
26        &self,
27        vm_state: &VmState<'z3, 'tcx>,
28        checkpoint: &Checkpoint<'tcx>,
29        property: &Property<'tcx>,
30    ) -> CheckResult {
31        let Some(value) = self.target_value(vm_state, checkpoint, property) else {
32            return CheckResult::Unknown(UnknownReason::Unimplemented);
33        };
34
35        if self.zst_guard(vm_state, checkpoint, property) {
36            return CheckResult::ProvedByRule;
37        }
38        if self.is_concrete_zst(vm_state, value.ty) {
39            return CheckResult::ProvedByRule;
40        }
41        let ty_arg = Self::ty_arg(property, 1);
42        // Alignment term: the *symbolic* `align_T` for a generic `T` (bounded by
43        // the trait bounds' min/max), the concrete constant alignment for a
44        // concrete type, or `1` when the property carries no type.
45        let align = match ty_arg {
46            Some(ty) => {
47                let resolved = self.instantiate_callsite_ty(vm_state, checkpoint, ty);
48                vm_state.align_sym_read(resolved)
49            }
50            None => Int::from_u64(vm_state.z3_ctx, 1),
51        };
52        if align.simplify().as_u64() == Some(1) {
53            return CheckResult::ProvedByRule;
54        }
55        // A pointer whose term is a known local's *stack address* is aligned to
56        // that local's own type alignment, regardless of the value's provenance.
57        // A borrow of a `Box`/`Vec` local (`&mut _1` / `&raw mut (*&mut _1)`)
58        // carries the pointee's *heap* provenance, so the provenance-based
59        // alignment below would compare against the pointee's alignment; the
60        // pointer itself, however, is aligned to the stack slot's type
61        // (`align_of::<Box<i32>>() = 8`).  Require a provenance so a pointer
62        // local whose value fell back to its own stack-address default (and is
63        // really some unaligned offset) is not mistaken for a stack borrow.
64        if value.is_pointer() {
65            if let Some(local) = vm_state.find_local_by_address(&value.z3_term) {
66                let local_ty = vm_state.body().local_decls[local].ty;
67                let local_align = vm_state.align_sym_read(local_ty);
68                if let Some(local_align_u64) = local_align.simplify().as_u64() {
69                    if local_align_u64 != 1 {
70                        if let Some(align_u64) = align.simplify().as_u64() {
71                            if local_align_u64 >= align_u64 {
72                                return CheckResult::ProvedByRule;
73                            }
74                        }
75                    }
76                }
77            }
78        }
79        // Symbolic fast-path: if the value is known to be at least `align`-aligned
80        // (its effective alignment satisfies `align_n >= align`, both powers of
81        // two), the check holds without a modulo query — Z3 cannot discharge
82        // `% align_T == 0` for a symbolic divisor, but `align_n >= align` is a
83        // linear inequality it *can* decide given the tracked bounds.
84        //
85        // For a field-offset pointer (`base + offset_of!(Container, field)`) the
86        // effective alignment is the *field's* own type alignment, not the
87        // container's.
88        let effective_align_n = if value
89            .provenance
90            .as_ref()
91            .is_some_and(|prov| matches!(prov.offset_kind, Some(OffsetKind::Field)))
92        {
93            crate::helpers::mir_utils::pointee_ty(value.ty).map(|ty| vm_state.align_sym_read(ty))
94        } else {
95            value.facts.align_n.clone()
96        };
97        if let Some(known_align) = effective_align_n {
98            // Concrete fast-path: both alignments are powers of two, so
99            // `align_n >= align` is a plain integer comparison — decide
100            // structurally, no solver query.
101            if let (Some(known_u64), Some(align_u64)) =
102                (known_align.simplify().as_u64(), align.simplify().as_u64())
103            {
104                if known_u64 >= align_u64 {
105                    return CheckResult::ProvedByRule;
106                }
107            }
108            let solver = Solver::new(vm_state.z3_ctx);
109            solver.push();
110            vm_state.assert_all(&solver);
111            solver.assert(&known_align.lt(&align));
112            let r = solver.check();
113            solver.pop(1);
114            // `align_n` is only a *lower bound* on the value's alignment: even
115            // when `align_n >= align` is satisfiable (not implied), the pointer
116            // may still be `align`-aligned through a separate path condition
117            // (e.g. `ptr.align_offset(align)` guarantees
118            // `(ptr + off*elem) % align == 0`).  Fall through to the full
119            // modulo query rather than reporting `Failed` prematurely.
120            if matches!(r, SatResult::Unsat) {
121                return CheckResult::ProvedBySmt;
122            }
123        }
124        // `Align(container.iter(), T)` for_each: every element pointer is
125        // aligned to `align_of(T)`, so a pointer loaded from the container
126        // (whose provenance names the container allocation) is T-aligned.
127        if let Some(prov) = &value.provenance {
128            if let Some(aligned_ty) = vm_state.alloc(prov.alloc_id).facts.for_each.aligned_ty {
129                let fa = vm_state.align_sym_read(aligned_ty);
130                if let (Some(fa_u64), Some(align_u64)) =
131                    (fa.simplify().as_u64(), align.simplify().as_u64())
132                {
133                    if fa_u64 >= align_u64 {
134                        return CheckResult::ProvedByRule;
135                    }
136                }
137            }
138        }
139        // Check allocation base alignment with concrete offset
140        if let Some(ref prov) = value.provenance {
141            let alloc = vm_state.alloc(prov.alloc_id);
142            let off_u64 = prov
143                .offset
144                .as_u64()
145                .or_else(|| prov.offset.simplify().as_u64());
146            if let (Some(off), Some(align_u64), Some(alloc_align_u64)) = (
147                off_u64,
148                align.simplify().as_u64(),
149                alloc.align.simplify().as_u64(),
150            ) {
151                if alloc_align_u64 >= align_u64 {
152                    if off % align_u64 == 0 {
153                        return CheckResult::ProvedByRule;
154                    }
155                    if off % align_u64 != 0 {
156                        return CheckResult::Failed;
157                    }
158                }
159            }
160        }
161        // Packed-struct fast-path: if the allocation is less aligned than
162        // required, the concrete offset alone determines alignment.
163        if let Some(ref prov) = value.provenance {
164            let alloc = vm_state.alloc(prov.alloc_id);
165            if let (Some(alloc_align_u64), Some(align_u64)) =
166                (alloc.align.simplify().as_u64(), align.simplify().as_u64())
167            {
168                if alloc_align_u64 < align_u64 {
169                    if let Some(off) = prov.offset.as_u64() {
170                        if off % align_u64 != 0 {
171                            return CheckResult::Failed;
172                        }
173                    }
174                }
175            }
176        }
177        let align_term = align;
178        let zero = Int::from_u64(vm_state.z3_ctx, 0);
179        let local = Solver::new(vm_state.z3_ctx);
180        local.push();
181        if let Some(ref prov) = value.provenance {
182            let alloc = vm_state.alloc(prov.alloc_id);
183            local.assert(
184                &value
185                    .z3_term
186                    ._eq(&Int::add(vm_state.z3_ctx, &[&alloc.base, &prov.offset])),
187            );
188            local.assert(&alloc.base._eq(&zero).not());
189            local.assert(&alloc.base.ge(&zero));
190            if alloc.align.simplify().as_u64() != Some(1) {
191                local.assert(&alloc.base.rem(&alloc.align)._eq(&zero));
192            }
193        }
194        if let Some(known_align) = value.facts.align_n.as_ref() {
195            local.assert(&value.z3_term.rem(known_align)._eq(&zero));
196        }
197        for cond in &vm_state.constraints.assertions {
198            local.assert(cond);
199        }
200        let negated = value.z3_term.rem(&align_term)._eq(&zero).not();
201        local.assert(&negated);
202        let r = match local.check() {
203            z3::SatResult::Sat => CheckResult::Failed,
204            z3::SatResult::Unsat => CheckResult::ProvedBySmt,
205            z3::SatResult::Unknown => CheckResult::Unknown(UnknownReason::SmtTimeout),
206        };
207        local.pop(1);
208        if matches!(r, CheckResult::Failed) {
209            rap_debug!(
210                "align=Failed vterm={} align_n={:?} off={}",
211                value.z3_term.to_string(),
212                value.facts.align_n,
213                value
214                    .provenance
215                    .as_ref()
216                    .map(|p| p.offset.to_string())
217                    .unwrap_or_default()
218            );
219        }
220        r
221    }
222
223    pub(super) fn value_aligned_to<'z3, 'tcx>(
224        vm_state: &VmState<'z3, 'tcx>,
225        value: &VmValue<'z3, 'tcx>,
226        align: u64,
227    ) -> bool {
228        if align <= 1 {
229            return true;
230        }
231        if let Some(n) = value
232            .facts
233            .align_n
234            .as_ref()
235            .and_then(|n| n.simplify().as_u64())
236        {
237            if n >= align && n % align == 0 {
238                return true;
239            }
240        }
241        let solver = Solver::new(vm_state.z3_ctx);
242        solver.push();
243        let zero = Int::from_u64(vm_state.z3_ctx, 0);
244        if let Some(ref prov) = value.provenance {
245            let alloc = vm_state.alloc(prov.alloc_id);
246            solver.assert(
247                &value
248                    .z3_term
249                    ._eq(&Int::add(vm_state.z3_ctx, &[&alloc.base, &prov.offset])),
250            );
251            solver.assert(&alloc.base.ge(&zero));
252            if alloc.align.simplify().as_u64() != Some(1) {
253                solver.assert(&alloc.base.rem(&alloc.align)._eq(&zero));
254            }
255        }
256        for cond in &vm_state.constraints.assertions {
257            solver.assert(cond);
258        }
259        let align_term = Int::from_u64(vm_state.z3_ctx, align);
260        solver.assert(&value.z3_term.rem(&align_term)._eq(&zero).not());
261        let r = solver.check() == SatResult::Unsat;
262        solver.pop(1);
263        r
264    }
265
266    pub(super) fn check_non_null<'z3, 'tcx>(
267        &self,
268        vm_state: &VmState<'z3, 'tcx>,
269        checkpoint: &Checkpoint<'tcx>,
270        property: &Property<'tcx>,
271    ) -> CheckResult {
272        let Some(value) = self.target_value(vm_state, checkpoint, property) else {
273            return CheckResult::Unknown(UnknownReason::Unimplemented);
274        };
275        if value.facts.non_null {
276            return CheckResult::ProvedByRule;
277        }
278        if value.facts.in_bounds {
279            return CheckResult::ProvedByRule;
280        }
281        // Pointers with non-external provenance point into known stack/heap
282        // allocations whose base addresses are never zero.  Raw-pointer
283        // parameters get external provenance which may be null.
284        if let Some(ref prov) = value.provenance {
285            if !vm_state.alloc(prov.alloc_id).is_external() {
286                return CheckResult::ProvedByRule;
287            }
288        }
289        // For an external pointer, check nullability against the *path
290        // conditions* only.  `assert_all` also asserts every live value's
291        // derived `non_null` flag, but the `&T` produced by this very deref
292        // shares the source pointer's term and is marked non-null — using it
293        // would circularly "prove" `NonNull` on the pointer being dereferenced.
294        let zero = Int::from_u64(vm_state.z3_ctx, 0);
295        let local = Solver::new(vm_state.z3_ctx);
296        for cond in &vm_state.constraints.assertions {
297            local.assert(cond);
298        }
299        self.smt_check(&local, &value.z3_term._eq(&zero))
300    }
301
302    pub(super) fn check_null<'z3, 'tcx>(
303        &self,
304        vm_state: &VmState<'z3, 'tcx>,
305        checkpoint: &Checkpoint<'tcx>,
306        property: &Property<'tcx>,
307    ) -> CheckResult {
308        // `Null(p)` is the guard branch of `any(Null(p), ...)`.  It is Proved
309        // when `p` is null (or carries no allocation, i.e. not known non-null),
310        // making the guarded obligation vacuous; otherwise Failed, so the other
311        // disjunct decides the outcome.
312        let Some(place) = (match property.args().first() {
313            Some(PropertyArg::Expr(ContractExpr::Place(p))) => Some(p),
314            _ => None,
315        }) else {
316            return CheckResult::Unknown(UnknownReason::Unimplemented);
317        };
318        if self.is_null(vm_state, checkpoint, place) {
319            CheckResult::ProvedByRule
320        } else {
321            CheckResult::Failed
322        }
323    }
324
325    /// Whether `value` is known to be aligned: either `align_n` carries a
326    /// concrete alignment, or the value sits at the base of an allocation whose
327    /// `align` is not 1.
328    fn is_value_aligned<'z3, 'tcx>(
329        vm_state: &VmState<'z3, 'tcx>,
330        value: &VmValue<'z3, 'tcx>,
331    ) -> bool {
332        value.facts.align_n.is_some()
333            || value.provenance.as_ref().is_some_and(|p| {
334                p.offset_kind.is_none()
335                    && vm_state.alloc(p.alloc_id).align.simplify().as_u64() != Some(1)
336            })
337    }
338
339    /// Whether `value` is a `MaybeUninit`-typed pointer access into `alloc_id`.
340    ///
341    /// `assume_init_drop` / `as_mut_ptr` (and friends) legitimately consume an
342    /// initialized element from storage that may be going out of scope, so the
343    /// `Init`/`Allocated` requirement concerns the write, not the allocation's
344    /// live/dead flag.
345    fn is_maybe_uninit_ptr<'z3, 'tcx>(
346        vm_state: &VmState<'z3, 'tcx>,
347        value: &VmValue<'z3, 'tcx>,
348        alloc_id: AllocId,
349    ) -> bool {
350        value.facts.init
351            && value.facts.non_null
352            && Self::is_value_aligned(vm_state, value)
353            && (matches!(value.ty.kind(), TyKind::RawPtr(..))
354                || matches!(value.ty.kind(), TyKind::Ref(_, inner, _)
355                    if matches!(inner.kind(), TyKind::Adt(adt, _)
356                        if api_classify::is_maybe_uninit_type(adt.did()))))
357            && {
358                let a = vm_state.alloc(alloc_id);
359                !a.is_external()
360                    && a.element_ty.as_ty().is_some_and(|ty| {
361                        if let TyKind::Adt(adt, _) = ty.kind() {
362                            api_classify::is_maybe_uninit_type(adt.did())
363                        } else {
364                            false
365                        }
366                    })
367            }
368    }
369
370    pub(super) fn check_allocated<'z3, 'tcx>(
371        &self,
372        vm_state: &VmState<'z3, 'tcx>,
373        checkpoint: &Checkpoint<'tcx>,
374        property: &Property<'tcx>,
375    ) -> CheckResult {
376        let Some(value) = self.target_value(vm_state, checkpoint, property) else {
377            return CheckResult::Unknown(UnknownReason::Unimplemented);
378        };
379        let value = self.resolve_pointer_provenance(vm_state, value);
380
381        if self.zst_guard(vm_state, checkpoint, property) {
382            return CheckResult::ProvedByRule;
383        }
384        if self.is_concrete_zst(vm_state, value.ty) {
385            return CheckResult::ProvedByRule;
386        }
387
388        // Zero-element access (`Allocated(p, T, 0)`) is trivially satisfied:
389        // any pointer is valid for its 0-byte prefix, so this holds even when
390        // provenance has been lost through a cast.  Mirrors the `count == 0`
391        // fast-path in `check_in_bound` and covers `from_raw_parts(ptr, 0)`
392        // (e.g. `Option::as_slice` on `None`).
393        if self.count_is_zero(vm_state, checkpoint, property, 2) {
394            return CheckResult::ProvedByRule;
395        }
396
397        let Some(alloc_id) = value.provenance_alloc_id() else {
398            // `ManuallyDrop::drop(&mut slot)` on an *empty* container (`Vec::new`
399            // / `String::new`) has no heap allocation behind the reference: the
400            // container value is valid and there is nothing to free, so its
401            // `ValidPtr`/`Allocated` precondition holds vacuously.
402            if crate::verify::api_classify::is_manually_drop_drop(checkpoint.callee) {
403                return CheckResult::ProvedByRule;
404            }
405            // A null pointer (address term 0) is definitely not backed by any
406            // allocation — a confirmed violation, not an incomplete proof.
407            if value.z3_term.simplify().as_u64() == Some(0) {
408                return CheckResult::Failed;
409            }
410            // A pointer whose address is a compile-time constant (e.g.
411            // `NonNull::dangling`'s `align_of::<T>()`, an unevaluated `const_…`
412            // term) is likewise not backed by any allocation.
413            if value.z3_term.to_string().contains("const_") {
414                return CheckResult::Failed;
415            }
416            return CheckResult::Unknown(UnknownReason::Unimplemented);
417        };
418
419        if vm_state.alloc(alloc_id).facts.dead {
420            let reenter = vm_state.path_facts.reenter;
421            // A `ManuallyDrop::drop` frees the slot's allocation at *this*
422            // checkpoint, so its `ValidPtr`/`Allocated` precondition concerns
423            // the pre-drop (still-live) state.  A double free — an
424            // already-dead allocation reaching a *second* drop — must still
425            // fail, so the exemption is lifted (unless the repeated block is
426            // only a loop-unrolled iteration).
427            let dropped_here =
428                crate::verify::api_classify::is_manually_drop_drop(checkpoint.callee) && reenter;
429            if !dropped_here && !Self::is_maybe_uninit_ptr(vm_state, &value, alloc_id) {
430                let is_param_ref = vm_state.resolve_origin(&value).is_some_and(|origin| {
431                    origin.local.as_usize() <= vm_state.body().arg_count
432                        && origin.local != Local::from_usize(0)
433                });
434                if !is_param_ref {
435                    return CheckResult::Failed;
436                }
437            }
438        }
439
440        let required_ty = property
441            .args()
442            .get(1)
443            .and_then(|a| {
444                if let PropertyArg::Ty(ty) = a {
445                    Some(*ty)
446                } else {
447                    None
448                }
449            })
450            .map(|ty| self.instantiate_callsite_ty(vm_state, checkpoint, ty));
451
452        // `Allocated(container.iter(), T, n)` for_each: every element pointer
453        // backs `>= n` `T` elements, so a pointer loaded from the container
454        // (whose provenance names the container allocation) satisfies
455        // `Allocated(cur, T, n)`.
456        if let Some((t, n)) = &vm_state.alloc(alloc_id).facts.for_each.allocated {
457            let resolved_t = self.instantiate_callsite_ty(vm_state, checkpoint, *t);
458            if Some(resolved_t) == required_ty {
459                let req_count = property
460                    .args()
461                    .get(2)
462                    .and_then(|a| self.resolve_arg_term(vm_state, checkpoint, a));
463                let count_ok = match (
464                    n.simplify().as_u64(),
465                    req_count.as_ref().and_then(|c| c.simplify().as_u64()),
466                ) {
467                    (Some(fact_n), Some(req_n)) => fact_n >= req_n,
468                    _ => false,
469                };
470                if count_ok {
471                    return CheckResult::ProvedByRule;
472                }
473            }
474        }
475
476        let alloc = vm_state.alloc(alloc_id);
477        if let (Some(alloc_elem_ty), Some(req_ty)) = (alloc.element_ty.as_ty(), required_ty) {
478            if self.alloc_elem_is_array_of(alloc_elem_ty, req_ty) {
479                return CheckResult::ProvedByRule;
480            }
481            // `MaybeUninit<T>` is `#[repr(transparent)]` over a union, so it has
482            // exactly the size/alignment of `T`.  An allocation of
483            // `MaybeUninit<T>` is therefore a valid allocation of `T` (and vice
484            // versa).  This lets `assume_init_ref`/`assume_init_mut`
485            // (`&[MaybeUninit<T>]` → `&[T]`) and `assume_init_drop` discharge
486            // `Allocated(p, T, n)` against the `MaybeUninit<T>` allocation whose
487            // symbolic size would otherwise be a distinct constant.
488            if maybe_uninit_inner(alloc_elem_ty) == Some(req_ty)
489                || maybe_uninit_inner(req_ty) == Some(alloc_elem_ty)
490            {
491                return CheckResult::ProvedByRule;
492            }
493            // Cross-type generic fast-path: when allocation element type
494            // and required type are both generic params (e.g. T vs U),
495            // sizes are opaque. If the pointer is derived from the same
496            // function's slice parameter, the byte-level layout is
497            // compatible by Rust's type system.
498            if matches!(
499                (alloc_elem_ty.kind(), req_ty.kind()),
500                (TyKind::Param(_), TyKind::Param(_))
501            ) {
502                return CheckResult::ProvedByRule;
503            }
504            // Smart-pointer indirection: `Allocated(slot, Box<T>, n)` after
505            // provenance penetration (`&mut ManuallyDrop<Box<T>>` → the `T`
506            // allocation) declares the wrapper type while the allocation holds
507            // its pointee. Peeling the wrapper down to `T` discharges the check
508            // (only when the declared type *wraps* the allocation element, so a
509            // plain type mismatch still falls through to the size check).
510            if req_ty != alloc_elem_ty {
511                let mut peeled = req_ty;
512                loop {
513                    match super::util::smart_pointer_pointee(peeled) {
514                        Some(inner) if inner != peeled => {
515                            if inner == alloc_elem_ty {
516                                return CheckResult::ProvedByRule;
517                            }
518                            peeled = inner;
519                        }
520                        _ => break,
521                    }
522                }
523            }
524        }
525
526        // A live `&mut MaybeUninit<T>` (or `&MaybeUninit<T>`) reference points at
527        // a `MaybeUninit<T>` whose size/alignment equal `T`'s (`#[repr(transparent)]`
528        // over a union), so `Allocated(p, T, n)` holds regardless of the provenance
529        // the VM recorded for an iterator-deref pointer (`array_try_from_fn_ext`).
530        if let Some(req_ty) = required_ty {
531            let val_pointee = crate::helpers::mir_utils::pointee_ty(value.ty);
532            if val_pointee.and_then(maybe_uninit_inner) == Some(req_ty)
533                || maybe_uninit_inner(req_ty) == val_pointee
534            {
535                return CheckResult::ProvedByRule;
536            }
537        }
538
539        let base = vm_state.allocation_base(alloc_id).clone();
540        let size = vm_state.allocation_size(alloc_id).clone();
541
542        // An external allocation whose size is the `i64::MAX` "unbounded"
543        // sentinel (a Vec/slice buffer, or a materialized `Allocated` fact)
544        // auto-passes any `Allocated` access.  A *raw-pointer target* carries a
545        // symbolic "unknown" size instead, so its access falls through and must
546        // be proved (and otherwise fails).
547        if vm_state.alloc(alloc_id).is_external() && size.simplify().as_u64() == Some(i64::MAX as u64)
548        {
549            return CheckResult::ProvedByRule;
550        }
551
552        let access = self.access_bytes(vm_state, property, 1, 2, checkpoint, &value);
553
554        // A field-offset pointer (`byte_add(offset_of!())`) is allocated within
555        // the *field* it addresses: the accessed range must fit in the field's
556        // own size.  "The field lies inside its container" is a layout
557        // invariant that needs no proof here (and the container's generic
558        // layout may be unknown, e.g. `Option<T>`).
559        if value
560            .provenance
561            .as_ref()
562            .is_some_and(|prov| matches!(prov.offset_kind, Some(OffsetKind::Field)))
563        {
564            let field_size = crate::helpers::mir_utils::pointee_ty(value.ty)
565                .map(|ty| vm_state.size_sym_read(ty))
566                .unwrap_or_else(|| Int::from_u64(vm_state.z3_ctx, 1));
567            let solver = Solver::new(vm_state.z3_ctx);
568            solver.push();
569            vm_state.assert_all(&solver);
570            solver.assert(&access.le(&field_size).not());
571            let r = match solver.check() {
572                SatResult::Unsat => CheckResult::ProvedBySmt,
573                SatResult::Sat => CheckResult::Failed,
574                _ => CheckResult::Unknown(UnknownReason::SmtTimeout),
575            };
576            solver.pop(1);
577            return r;
578        }
579
580        // Concrete sizes and offset: direct byte-range comparison, no solver.
581        // The accessed range is `[offset, offset + access)`, which must fit in
582        // `[0, size)`.  The offset is a non-negative byte offset by provenance
583        // construction, so only the upper bound needs checking.
584        if let (Some(off), Some(size_val), Some(access_val)) = (
585            value
586                .provenance
587                .as_ref()
588                .and_then(|p| p.offset.simplify().as_u64()),
589            size.as_u64(),
590            access.as_u64(),
591        ) {
592            if off.saturating_add(access_val) > size_val {
593                return CheckResult::Failed;
594            }
595            return CheckResult::ProvedByRule;
596        }
597
598        // Generic element type: both size and access use max(1) fallback,
599        // making the check about element counts. When the pointer's offset
600        // cannot be determined concretely, the byte-level inequality
601        // "offset + count <= total_len" relies on facts (split_at, etc.)
602        // that may not be in path conditions. Fall back to Unknown rather
603        // than Failed for generic-element allocations.
604        let alloc_elem_is_generic = vm_state
605            .alloc(alloc_id)
606            .element_ty
607            .as_ty()
608            .is_some_and(|ty| matches!(ty.kind(), TyKind::Param(_)));
609        let elem_size = vm_state.generic_elem_size(alloc_id);
610        if alloc_elem_is_generic && !size.as_u64().is_some() && !access.as_u64().is_some() {
611            return Self::allocation_covers_access(
612                vm_state,
613                &value,
614                &access,
615                &base,
616                &size,
617                CheckResult::Unknown(UnknownReason::Unimplemented),
618                elem_size.as_ref(),
619            );
620        }
621
622        Self::allocation_covers_access(
623            vm_state,
624            &value,
625            &access,
626            &base,
627            &size,
628            CheckResult::Failed,
629            elem_size.as_ref(),
630        )
631    }
632
633    /// Prove that `value + access` fits within `[base, base + size)`.
634    ///
635    /// `on_sat` is the result when the overflow is satisfiable: `Failed` for
636    /// concrete sizes, `Unknown` for generic-element allocations whose byte
637    /// layout cannot be resolved.  `elem_size`, when present, is the *generic*
638    /// element-size term: the check is then discharged by a case split on `S = 0`
639    /// (ZST) vs `S ≥ 1` (non-ZST) rather than a single nonlinear query.
640    fn allocation_covers_access<'z3, 'tcx>(
641        vm_state: &VmState<'z3, 'tcx>,
642        value: &VmValue<'z3, 'tcx>,
643        access: &Int<'z3>,
644        base: &Int<'z3>,
645        size: &Int<'z3>,
646        on_sat: CheckResult,
647        elem_size: Option<&Int<'z3>>,
648    ) -> CheckResult {
649        let bound = Int::add(vm_state.z3_ctx, &[base, size]);
650        let covered = Int::add(vm_state.z3_ctx, &[&value.z3_term, access]);
651        let negated = covered.le(&bound).not();
652
653        if let Some(s) = elem_size {
654            return Self::smt_check_size_split(vm_state, s, &negated, on_sat);
655        }
656
657        let solver = Solver::new(vm_state.z3_ctx);
658        solver.push();
659        vm_state.assert_all(&solver);
660        solver.assert(&negated);
661        let r = match solver.check() {
662            SatResult::Unsat => CheckResult::ProvedBySmt,
663            SatResult::Sat => on_sat,
664            _ => CheckResult::Unknown(UnknownReason::SmtTimeout),
665        };
666        solver.pop(1);
667        r
668    }
669
670    pub(super) fn check_init<'z3, 'tcx>(
671        &self,
672        vm_state: &VmState<'z3, 'tcx>,
673        checkpoint: &Checkpoint<'tcx>,
674        property: &Property<'tcx>,
675    ) -> CheckResult {
676        if self.zst_guard(vm_state, checkpoint, property) {
677            return CheckResult::ProvedByRule;
678        }
679        let Some(value) = self.target_value(vm_state, checkpoint, property) else {
680            return CheckResult::Unknown(UnknownReason::Unimplemented);
681        };
682        if self.is_concrete_zst(vm_state, value.ty) {
683            return CheckResult::ProvedByRule;
684        }
685
686        // Zero elements: `Init(p, T, 0)` is vacuously satisfied (the empty
687        // range is trivially initialized, regardless of whether `p` is a
688        // dangling pointer), mirroring `check_allocated`'s fast-path.
689        let count_arg = if property.args().len() >= 3 { 2 } else { 1 };
690        if self.count_is_zero(vm_state, checkpoint, property, count_arg) {
691            return CheckResult::ProvedByRule;
692        }
693
694        // `Init(p, MaybeUninit<T>, n)` reduces to `Typed(p, MaybeUninit<T>)`:
695        // `MaybeUninit<T>` carries no validity invariant (any bit pattern is a
696        // valid `MaybeUninit<T>`), so there is nothing to "initialize" — the
697        // content need only be of type `MaybeUninit<T>`.  Mirrors the
698        // `ty_is_maybe_uninit` fast-path in `check_typed`; this is what lets a
699        // `&[MaybeUninit<T>]` slice satisfy `Init` without its contents being
700        // initialized.
701        if let Some(required_ty) = Self::ty_arg(property, 1) {
702            if Self::ty_is_maybe_uninit(required_ty) {
703                return CheckResult::ProvedByRule;
704            }
705        }
706
707        // Compute the required init range: count * sizeof(T) bytes.  The
708        // two-argument form `Init(self, n)` carries no `T`; `access_bytes`
709        // falls back to the target's pointee element type.
710        let access = if property.args().len() >= 3 {
711            Some(self.access_bytes(vm_state, property, 1, count_arg, checkpoint, &value))
712        } else if property.args().len() == 2 {
713            Some(self.access_bytes(vm_state, property, 0, count_arg, checkpoint, &value))
714        } else {
715            None
716        };
717
718        if let Some(id) = value.provenance_alloc_id() {
719            rap_debug!(
720                "check_init: alloc={} init_set={} access={:?}",
721                id.0,
722                vm_state.content(id).facts.initialized,
723                access.as_ref().and_then(|a| a.as_u64())
724            );
725            if vm_state.alloc(id).facts.dead {
726                // `assume_init_drop` (and other MaybeUninit drop/read ops)
727                // legitimately consume an initialized element from storage that
728                // may be going out of scope; the `Init` requirement concerns
729                // whether the element was written, not whether the allocation is
730                // still live. Mirror the `check_allocated` exception.
731                if !Self::is_maybe_uninit_ptr(vm_state, &value, id) {
732                    return CheckResult::Failed;
733                }
734            }
735            // Verify the entire access range is covered
736            if let Some(ref access_term) = access {
737                if let (Some(access_val), Some(prov)) = (access_term.as_u64(), &value.provenance) {
738                    if let Some(prov_off) = prov.offset.as_u64() {
739                        let end = prov_off + access_val;
740                        let all_init = (prov_off as usize..end as usize)
741                            .all(|off| vm_state.is_byte_init(id, off));
742                        if all_init && access_val > 0 {
743                            return CheckResult::ProvedByRule;
744                        }
745                    }
746                }
747            }
748            if vm_state.content(id).facts.initialized {
749                if let Some(ref access_term) = access {
750                    let size = vm_state.allocation_size(id);
751                    if let (Some(access_val), Some(size_val)) =
752                        (access_term.as_u64(), size.as_u64())
753                    {
754                        // `size_val == 0` means the element type is generic
755                        // (size unknown), so the required access can't exceed a
756                        // meaningful allocation size; skip the bound check.
757                        if size_val > 0 && access_val > size_val {
758                            return CheckResult::Failed;
759                        }
760                    }
761                }
762                return CheckResult::ProvedByRule;
763            }
764            // as_ptr/as_mut_ptr on MaybeUninit → write operations don't need pre-init.
765            if value.facts.init
766                && value.facts.non_null
767                && Self::is_value_aligned(vm_state, &value)
768                && matches!(value.ty.kind(), TyKind::RawPtr(..))
769                && !vm_state.alloc(id).facts.dead
770            {
771                if crate::verify::api_classify::is_mem_copy_or_write(checkpoint.callee) {
772                    return CheckResult::ProvedByRule;
773                }
774            }
775            // Check byte-level init: if all bytes in range are initialized
776            let size = vm_state.allocation_size(id).clone();
777            if let Some(size_val) = size.as_u64() {
778                let size_usize = (size_val as usize).min(4096);
779                let all_init = (0..size_usize).all(|off| vm_state.is_byte_init(id, off));
780                if all_init && size_val > 0 {
781                    return CheckResult::ProvedByRule;
782                }
783            }
784            // The allocation's `initialized` flag stayed `false` (nothing wrote
785            // it: `MaybeUninit::uninit` / `Box::new_uninit`), and this is not a
786            // write operation — the access reads uninitialized memory, a
787            // confirmed violation rather than an incomplete proof.
788            return CheckResult::Failed;
789        }
790        // Check field-level init for aggregate types
791        if let Some(origin_op) = checkpoint.args.first() {
792            let origin_val = vm_state.value_of_operand(origin_op);
793            if let Some(prov) = &origin_val.provenance {
794                if vm_state.content(prov.alloc_id).facts.initialized {
795                    if let Some(ref access_term) = access {
796                        let size = vm_state.allocation_size(prov.alloc_id);
797                        if let (Some(access_val), Some(size_val)) =
798                            (access_term.as_u64(), size.as_u64())
799                        {
800                            if access_val <= size_val {
801                                return CheckResult::ProvedByRule;
802                            }
803                            // Required bytes exceed allocation → not fully init
804                        } else {
805                            return CheckResult::ProvedByRule;
806                        }
807                    }
808                    // access=None: can't verify size, fall through
809                }
810            }
811            if let Operand::Copy(place) | Operand::Move(place) = origin_op {
812                for alloc_id in self.trace_alloc_ids(vm_state, place.local) {
813                    if vm_state.content(alloc_id).facts.initialized {
814                        if let Some(ref access_term) = access {
815                            let size = vm_state.allocation_size(alloc_id);
816                            if let (Some(access_val), Some(size_val)) =
817                                (access_term.as_u64(), size.as_u64())
818                            {
819                                if access_val <= size_val {
820                                    return CheckResult::ProvedByRule;
821                                }
822                            } else {
823                                return CheckResult::ProvedByRule;
824                            }
825                        }
826                    }
827                }
828            }
829            // No path proved init.  If the value traces to a known allocation
830            // that was never written (its `initialized` flag stayed `false`),
831            // reading it is a confirmed violation rather than an incomplete
832            // proof.
833            if let Operand::Copy(place) | Operand::Move(place) = origin_op {
834                let allocs = self.trace_alloc_ids(vm_state, place.local);
835                if !allocs.is_empty() && allocs.iter().all(|id| !vm_state.content(*id).facts.initialized) {
836                    return CheckResult::Failed;
837                }
838            }
839        }
840        // A path that evaluated an `Iterator::next` discriminant may be
841        // infeasible when the iterator was empty (e.g. `assume_init_drop` on the
842        // `Some` branch of `next()` that returned `None`). Check feasibility
843        // only for such paths so unrelated over-constrained paths aren't
844        // spuriously marked sound.
845        if vm_state.path_facts.saw_next_discriminant {
846            let local = Solver::new(vm_state.z3_ctx);
847            local.push();
848            for cond in &vm_state.constraints.assertions {
849                local.assert(cond);
850            }
851            if local.check() == SatResult::Unsat {
852                local.pop(1);
853                return CheckResult::ProvedByRule;
854            }
855            local.pop(1);
856        }
857        CheckResult::Unknown(UnknownReason::Unimplemented)
858    }
859
860    pub(super) fn trace_alloc_ids<'z3, 'tcx>(
861        &self,
862        vm_state: &VmState<'z3, 'tcx>,
863        local: Local,
864    ) -> Vec<AllocId> {
865        let mut result = Vec::new();
866        if let Some(id) = vm_state.current_frame.local_alloc.get(&local) {
867            result.push(*id);
868        }
869        let mut worklist = vec![local];
870        let mut visited = FxHashSet::default();
871        visited.insert(local);
872        while let Some(cur) = worklist.pop() {
873            for block in vm_state.body().basic_blocks.iter() {
874                for stmt in &block.statements {
875                    if let StatementKind::Assign(assign) = &stmt.kind {
876                        let (dest, rvalue) = &**assign;
877                        if dest.local != cur || !dest.projection.is_empty() {
878                            continue;
879                        }
880                        let src_local = match rvalue {
881                            #[cfg(rapx_rvalue_use_with_retag)]
882                            Rvalue::Use(Operand::Copy(p) | Operand::Move(p), _)
883                                if p.projection.is_empty() =>
884                            {
885                                Some(p.local)
886                            }
887                            #[cfg(not(rapx_rvalue_use_with_retag))]
888                            Rvalue::Use(Operand::Copy(p) | Operand::Move(p))
889                                if p.projection.is_empty() =>
890                            {
891                                Some(p.local)
892                            }
893                            Rvalue::CopyForDeref(p) if p.projection.is_empty() => Some(p.local),
894                            Rvalue::Cast(_, Operand::Copy(p) | Operand::Move(p), _)
895                                if p.projection.is_empty() =>
896                            {
897                                Some(p.local)
898                            }
899                            Rvalue::RawPtr(_, p) if p.projection.is_empty() => Some(p.local),
900                            _ => None,
901                        };
902                        if let Some(src) = src_local {
903                            if visited.insert(src) {
904                                if let Some(id) = vm_state.current_frame.local_alloc.get(&src) {
905                                    result.push(*id);
906                                }
907                                worklist.push(src);
908                            }
909                        }
910                    }
911                }
912            }
913        }
914        result
915    }
916
917    pub(super) fn check_alive<'z3, 'tcx>(
918        &self,
919        vm_state: &VmState<'z3, 'tcx>,
920        checkpoint: &Checkpoint<'tcx>,
921        property: &Property<'tcx>,
922    ) -> CheckResult {
923        let Some(value) = self.target_value(vm_state, checkpoint, property) else {
924            return CheckResult::Unknown(UnknownReason::Unimplemented);
925        };
926        let Some(id) = value.provenance_alloc_id() else {
927            return if value.facts.non_null || value.facts.init {
928                CheckResult::ProvedByRule
929            } else {
930                CheckResult::Unknown(UnknownReason::Unimplemented)
931            };
932        };
933
934        if vm_state.alloc(id).facts.dead {
935            if let Some(origin) = vm_state.resolve_origin(&value) {
936                let is_param = origin.local.as_usize() <= vm_state.body().arg_count
937                    && origin.local != Local::from_usize(0);
938                if is_param {
939                    return CheckResult::ProvedByRule;
940                }
941            }
942            return CheckResult::Failed;
943        }
944
945        // Classify by the *value's own type*, not `resolve_origin`'s kind: a raw
946        // pointer field can be misclassified when a derived temp (`&mut T` from a
947        // call) shares the allocation's provenance and reads back as `MutRef`.
948        // A raw pointer / `NonNull` carries no liveness guarantee, so `Alive`
949        // must be justified by an explicit assumption (an `Alive` precondition /
950        // struct invariant, materialized as `liveness`) or by provenance
951        // shared with a live reference parameter.
952        let is_raw_ptr = matches!(value.ty.kind(), TyKind::RawPtr(..))
953            || matches!(value.ty.kind(), TyKind::Adt(adt_def, _)
954                if crate::helpers::mir_utils::is_raw_ptr_wrapper(vm_state.tcx, adt_def.did()));
955
956        if is_raw_ptr {
957            let mut root_id = id;
958            while let Some(parent_id) = vm_state.alloc(root_id).parent {
959                root_id = parent_id;
960            }
961            // A non-external allocation (stack local, owned heap, or const
962            // materialization) is a real allocation whose liveness is tracked by
963            // `dead`, so "not dead" means alive.
964            if !vm_state.alloc(root_id).is_external() && !vm_state.alloc(root_id).facts.dead {
965                return CheckResult::ProvedByRule;
966            }
967            // An external allocation is a placeholder for arbitrary external
968            // memory (raw-pointer params/fields) and carries no liveness
969            // guarantee; it is alive only if explicitly assumed (`Alive`
970            // precondition / struct invariant), or grounded in a live reference.
971            if !vm_state.alloc(root_id).facts.dead {
972                match &vm_state.alloc(root_id).facts.liveness {
973                    Some(src_region) => {
974                        // The `Alive(p, 'r)` check demands the memory alive for
975                        // `'r`, while the assumption only guarantees `'a`; the
976                        // assumption covers the demand only when `'a: 'r`.
977                        //
978                        // A struct invariant / function `requires` binds its
979                        // region at parse time (`PropertyArg::Region`); a callee
980                        // contract carries either `'static` (concrete) or the
981                        // callee's *generic* return lifetime (`Ident`), which is
982                        // instantiated from the caller's return reference region.
983                        let check_region = property.args().get(1).and_then(|a| match a {
984                            PropertyArg::Region(r) => Some(*r),
985                            PropertyArg::Ident(name)
986                                if name == "static" || name == "static_lifetime" =>
987                            {
988                                Some(vm_state.tcx.lifetimes.re_static)
989                            }
990                            PropertyArg::Ident(_) => crate::verify::vm::region::fn_return_region(
991                                vm_state.tcx,
992                                checkpoint.caller,
993                            ),
994                            _ => None,
995                        });
996                        if let Some(r) = check_region {
997                            let outlives = crate::verify::vm::region::region_outlives(
998                                vm_state.tcx,
999                                checkpoint.caller,
1000                                *src_region,
1001                                r,
1002                            );
1003                            // A reference parameter `&'r Self<'a>` implies
1004                            // `'a: 'r` through its type (not a where-clause).
1005                            let implied = crate::verify::vm::region::fn_arg_ty(
1006                                vm_state.tcx,
1007                                checkpoint.caller,
1008                                0,
1009                            )
1010                            .is_some_and(|self_ty| {
1011                                crate::verify::vm::region::region_outlives_implied(
1012                                    vm_state.tcx,
1013                                    *src_region,
1014                                    self_ty,
1015                                )
1016                            });
1017                            if !outlives && !implied {
1018                                return CheckResult::Failed;
1019                            }
1020                        }
1021                        return CheckResult::ProvedByRule;
1022                    }
1023                    None => {}
1024                }
1025            }
1026            // A raw pointer derived from a live reference or owned (Box/Vec)
1027            // parameter is alive: the reference / ownership guarantees liveness.
1028            let body = vm_state.body();
1029            let matches_live_param = (1..=body.arg_count).any(|i| {
1030                let param_local = Local::from_usize(i);
1031                let param_ty = body.local_decls[param_local].ty;
1032                let guarantees = matches!(param_ty.kind(), TyKind::Ref(..))
1033                    || matches!(param_ty.kind(), TyKind::Adt(adt_def, _)
1034                        if api_classify::is_std_box(adt_def.did())
1035                            || api_classify::is_std_vec(adt_def.did()));
1036                if !guarantees {
1037                    return false;
1038                }
1039                vm_state
1040                    .local_value(param_local)
1041                    .and_then(|v| v.provenance_alloc_id())
1042                    .is_some_and(|pid| pid == root_id)
1043            });
1044            if matches_live_param {
1045                return CheckResult::ProvedByRule;
1046            }
1047            // No local origin: the allocation is not tied to any local, so it
1048            // outlives the function (e.g. `static` data not materialized as a
1049            // const byte array).
1050            if vm_state.resolve_origin(&value).is_none() {
1051                return CheckResult::ProvedByRule;
1052            }
1053            return CheckResult::Failed;
1054        }
1055
1056        // A reference or owned value guarantees its pointee is live.
1057        CheckResult::ProvedByRule
1058    }
1059}