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}