rapx/verify/vm/state.rs
1//! Symbolic VM state types.
2//!
3//! The data structures that represent the symbolic execution state: symbolic
4//! values and their invariants, memory allocations, and the full execution
5//! state at a program point.
6
7use rustc_hir::def_id::DefId;
8use rustc_middle::{
9 mir::{Body, Local, Operand, Place, ProjectionElem},
10 ty::{Region, Ty, TyCtxt},
11};
12use z3::{
13 Context,
14 ast::{Array, Ast, Bool, Int},
15};
16
17use crate::compat::{FxHashMap, FxHashSet};
18use crate::verify::{def_use::PlaceKey, path_extractor::Path};
19
20/// Unique identifier for a heap or stack allocation.
21#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
22pub(crate) struct AllocId(pub usize);
23
24/// The structure of a pointer's byte offset, when it has one.
25///
26/// Each variant lets the verifier use a cheaper, more precise proof: `Field`
27/// carries the "field in-bounds" guarantee (`offset + size_of(field) <=
28/// size_of(container)`, e.g. `Option::as_slice`), and `Element(k)` tracks the
29/// element index so `InBound` checks `k + count <= slice_len` linearly instead
30/// of the non-linear byte form `(k+count)·S <= len·S` (undecidable in Z3 NIA
31/// for a generic `S`).
32#[derive(Clone, Debug)]
33pub(crate) enum OffsetKind<'z3> {
34 /// Compile-time field offset (`offset_of!`), including a first field at 0.
35 Field,
36 /// Element index from element-strided arithmetic (`ptr.add(k)`); the byte
37 /// offset is `element · S`.
38 Element(Int<'z3>),
39 /// A byte-strided or otherwise unclassifiable offset (`byte_add`).
40 Byte,
41}
42
43/// Pointer provenance: which allocation, at what byte offset, and (when known)
44/// what structure that offset has.
45#[derive(Clone, Debug)]
46pub(crate) struct Provenance<'z3> {
47 /// The allocation this pointer derives from.
48 pub alloc_id: AllocId,
49 /// Byte offset from the allocation base (always present). A freshly created
50 /// pointer to the base of an allocation has `offset = 0`.
51 pub offset: Int<'z3>,
52 /// The offset's structure. `None` means the pointer sits at the base
53 /// (`offset == 0`) with no further structure; `Some(..)` records whether the
54 /// offset is a compile-time field offset (`Field`), an element index from
55 /// element-strided arithmetic (`Element`), or a byte-strided offset (`Byte`).
56 pub offset_kind: Option<OffsetKind<'z3>>,
57}
58
59/// Value-level facts about a single symbolic value (nullness, alignment,
60/// bounds, and whether it has been written). These travel *with* the value — a
61/// [`VmValue`] leaves its allocation when passed as an operand or stashed in
62/// `InlineCtx::deferred_field_writes` — so they live on the value, not the
63/// allocation. The allocation-level counterpart to `init` is
64/// [`ContentFacts::initialized`], kept in sync by [`VmState::mark_initialized`].
65///
66/// `PartialEq`/`Eq` are deliberately *not* derived: `align_n` is a Z3 AST
67/// whose equality is structural (`Z3_is_eq_ast`), not semantic, so comparing
68/// two `ValueFacts` would silently report semantically-equal values as
69/// unequal.
70#[derive(Clone, Debug, Default)]
71pub(crate) struct ValueFacts<'z3> {
72 pub non_null: bool,
73 pub init: bool,
74 pub in_bounds: bool,
75 /// If Some(n), the value's term is known to satisfy `z3_term % n == 0`.
76 /// Set by alignment guards, Mul by power-of-two, and type alignment.
77 /// `n` is a Z3 term so that a generic type's alignment (a symbolic
78 /// `align_T`) can be carried the same way as a concrete alignment.
79 pub align_n: Option<Int<'z3>>,
80}
81
82/// A symbolic value tracked by the VM.
83///
84/// # Semantics of `z3_term`
85///
86/// - For pointer/reference types (`&T`, `*const T`, `*mut T`, `Box<T>`, etc.):
87/// `z3_term` represents the **address** in the VM's logical address space.
88/// - For scalar types (integers, `bool`, `char`): `z3_term` represents the **value**.
89/// - For aggregate types (struct, tuple, enum): `z3_term` is the base address of
90/// the stack allocation backing the aggregate.
91///
92/// When `provenance` is `Some`, the following relationship holds and is
93/// asserted into the solver at check time:
94/// `z3_term == alloc[provenance.alloc_id].base + provenance.offset`
95#[derive(Clone, Debug)]
96pub(crate) struct VmValue<'z3, 'tcx> {
97 /// The Z3 integer term (address or scalar value, see struct docs).
98 pub z3_term: Int<'z3>,
99 /// Rust type, for layout queries.
100 pub ty: Ty<'tcx>,
101 /// Which allocation this pointer derives from and at what offset.
102 pub provenance: Option<Provenance<'z3>>,
103 /// Known constraints on this value.
104 pub facts: ValueFacts<'z3>,
105 /// Extra semantics (field offset, discriminant, comparison, or binary-op
106 /// source); see [`ValueSource`].
107 pub source: ValueSource<'z3>,
108}
109
110impl<'z3, 'tcx> VmValue<'z3, 'tcx> {
111 pub(crate) fn new(term: Int<'z3>, ty: Ty<'tcx>) -> Self {
112 VmValue {
113 z3_term: term,
114 ty,
115 provenance: None,
116 facts: ValueFacts::default(),
117 source: ValueSource::None,
118 }
119 }
120
121 /// Convenience: extract the `AllocId` from provenance, if any.
122 pub(crate) fn provenance_alloc_id(&self) -> Option<AllocId> {
123 self.provenance.as_ref().map(|p| p.alloc_id)
124 }
125
126 /// Symbolic enum discriminant, if known.
127 pub(crate) fn discriminant(&self) -> Option<&Int<'z3>> {
128 match &self.source {
129 ValueSource::Discriminant(d) => Some(d),
130 _ => None,
131 }
132 }
133
134 /// Direct boolean condition of a comparison result, if any.
135 pub(crate) fn bool_cond(&self) -> Option<&Bool<'z3>> {
136 match &self.source {
137 ValueSource::Comparison { cond, .. } => Some(cond),
138 _ => None,
139 }
140 }
141
142 /// Whether this scalar is a compile-time `offset_of!` field offset.
143 pub(crate) fn is_field_offset(&self) -> bool {
144 matches!(self.source, ValueSource::FieldOffset)
145 }
146
147 /// Whether this value is a pointer (carries provenance).
148 pub(crate) fn is_pointer(&self) -> bool {
149 self.provenance.is_some()
150 }
151}
152
153/// The shape of an allocation: a single object, a slice/array buffer, or an
154/// external raw-pointer parameter. `element_ty` (typed vs untyped) and the
155/// `parent` sub-view edge stay separate fields.
156#[derive(Clone, Debug)]
157pub(crate) enum AllocKind<'z3> {
158 /// A single object: a `Box<T>` heap object, a struct, a scalar, or an
159 /// untyped raw buffer.
160 Object,
161 /// A slice/array buffer with a known element count.
162 Slice { len: Int<'z3> },
163 /// An external raw-pointer parameter. `size` is an unconstrained symbolic
164 /// term (the caller may pass any allocation); nullability is *not* stored
165 /// here — it is tracked via the pointer term (`term == 0`) path conditions.
166 External,
167}
168
169/// The element type of an allocation's contents, either a concrete [`Ty`] or
170/// symbolic (`Generic`).
171///
172/// `Typed` carries a concrete `Ty` — the element type of a slice, the object
173/// type of a `Box<T>`/struct, or `u8` for a raw byte buffer — while `Generic`
174/// marks a symbolic element type whose concrete `Ty` cannot be determined
175/// (e.g. `from_raw_parts::<T>`; its `size` uses the shared symbolic `sizeof_T`
176/// and its length must be materialized via `set_slice_len`).
177#[derive(Clone, Debug)]
178pub(crate) enum ElementTy<'tcx> {
179 Typed(Ty<'tcx>),
180 Generic,
181}
182
183impl<'tcx> ElementTy<'tcx> {
184 /// The concrete type, if known.
185 pub(crate) fn as_ty(&self) -> Option<Ty<'tcx>> {
186 match self {
187 ElementTy::Typed(t) => Some(*t),
188 ElementTy::Generic => None,
189 }
190 }
191
192 /// Whether the element type is symbolic/unknown (no concrete `Ty`).
193 pub(crate) fn is_generic(&self) -> bool {
194 matches!(self, ElementTy::Generic)
195 }
196}
197
198impl<'tcx> From<Option<Ty<'tcx>>> for ElementTy<'tcx> {
199 fn from(o: Option<Ty<'tcx>>) -> Self {
200 match o {
201 Some(t) => ElementTy::Typed(t),
202 None => ElementTy::Generic,
203 }
204 }
205}
206
207/// Uniform facts about the *pointer elements* of a container, established by
208/// `x.iter()` for_each invariants.
209///
210/// Each fact is a property every element pointer satisfies. The facts are
211/// anchored to the container's allocation (not the container value) because a
212/// pointer loaded from the container resolves its provenance through this
213/// allocation, so the checker finds the fact via the loaded pointer's
214/// provenance.
215#[derive(Clone, Debug, Default)]
216pub(crate) struct ForEachFacts<'z3, 'tcx> {
217 /// `Typed(iter(), T)`: every element pointer points at a valid `T`.
218 pub target_ty: Option<Ty<'tcx>>,
219 /// `Align(iter(), T)`: every element pointer is aligned to `align_of(T)`.
220 pub aligned_ty: Option<Ty<'tcx>>,
221 /// `Allocated(iter(), T, n)`: every element pointer backs `>= n` `T`
222 /// elements (`n` may be symbolic).
223 pub allocated: Option<(Ty<'tcx>, Int<'z3>)>,
224 /// `Owning(iter())`: every element pointer is the sole owner of its
225 /// pointee (mutually non-aliasing). Established by the trusted invariant;
226 /// the aliasing *check* of the invariant itself is done by the alias
227 /// analysis, not by this flag.
228 pub owning: bool,
229}
230
231/// Per-allocation *lifecycle* facts (`dead`/`liveness`/`for_each`), kept apart
232/// from the allocation's shape metadata and `parent` edge so identity/layout
233/// and facts are separated at the type level. The *content* facts
234/// (readability, C-string/UTF-8 trust) live on [`ContentFacts`] instead.
235#[derive(Clone, Debug, Default)]
236pub(crate) struct AllocFacts<'z3, 'tcx> {
237 /// Whether the allocation has been freed (StorageDead / Drop).
238 pub dead: bool,
239
240 /// The region this external allocation is assumed alive for, via an
241 /// `Alive(p, 'a)` contract/invariant (or `'static` for `ValidCStr`/`Allocated`
242 /// params/`'static` data). Only consulted for *external* allocations, whose
243 /// memory is owned by the caller (so `dead` carries no liveness guarantee):
244 /// the checker rejects a use that demands a longer region than this. `None`
245 /// means no assumption — an external allocation then fails `Alive` unless
246 /// grounded in a live reference; a VM-owned allocation is alive while
247 /// `!dead` and never sets this.
248 pub liveness: Option<Region<'tcx>>,
249
250 /// Uniform facts about this allocation's pointer elements, established by
251 /// `x.iter()` for_each invariants (`Typed`/`Align`/`Allocated`).
252 pub for_each: ForEachFacts<'z3, 'tcx>,
253}
254
255/// Per-allocation *content* facts: whether the allocation's contents are
256/// readable, and whether they were asserted to be a valid C string / UTF-8.
257/// These describe the stored bytes/values, so they live on [`MemoryContent`]
258/// next to that data, rather than on [`AllocFacts`] (lifecycle).
259#[derive(Clone, Debug, Default)]
260pub(crate) struct ContentFacts {
261 /// Whether the allocation's contents hold an initialized (readable) value:
262 /// a heap constructor (`Box::new`, `Vec`), a reference parameter's referent,
263 /// a callee return, a `write`, `ValidCStr`, or const/static byte data.
264 /// Stays `false` for uninitialized memory (`Box::new_uninit`,
265 /// `MaybeUninit::uninit`).
266 pub initialized: bool,
267
268 /// Whether this allocation was asserted valid C string via a `ValidCStr`
269 /// contract fact / struct invariant (the "no interior NUL + terminal NUL"
270 /// trust marker).
271 pub cstr_trusted: bool,
272
273 /// Whether this allocation was asserted valid UTF-8 via a `ValidString`
274 /// contract fact (the "bytes form a valid UTF-8 sequence" trust marker).
275 pub utf8_trusted: bool,
276}
277
278/// A memory allocation: a stack local, a heap object (`Box`/`Vec`), or an
279/// external raw-pointer placeholder.
280///
281/// The allocation is stored in a [`MemoryUnit`] at index `AllocId.0` (an
282/// `AllocId` is a monotonic counter that doubles as the vector index).
283#[derive(Clone, Debug)]
284pub(crate) struct Allocation<'z3, 'tcx> {
285 // ── Shape (always present) ──
286 /// Base address (fresh Z3 constant).
287 pub base: Int<'z3>,
288
289 /// Size in bytes (Z3 term, may be symbolic).
290 pub size: Int<'z3>,
291
292 /// Alignment in bytes (Z3 term, may be symbolic for a generic element
293 /// type).
294 pub align: Int<'z3>,
295
296 /// Type of the allocation's contents (concrete `Ty` or symbolic `Generic`).
297 pub element_ty: ElementTy<'tcx>,
298
299 /// The allocation shape (object vs slice vs external).
300 pub kind: AllocKind<'z3>,
301
302 // ── Facts (the allocation-level slice of the Facts layer) ──
303 /// Cross-cutting per-allocation facts (`dead`/`liveness`/`for_each`), kept
304 /// apart from the shape metadata above.
305 pub facts: AllocFacts<'z3, 'tcx>,
306
307 // ── Relationships (Option) ──
308 /// The allocation a sub-view was derived from: a slice view created by
309 /// `s[i..j]` / `s.get(range)`, `split_at` / `align_to` / `as_chunks`, or
310 /// `from_raw_parts`. Each view is a fresh `AllocId` but a window onto the
311 /// same memory as its source, so `root_alloc` follows this edge to group
312 /// aliasing views. The address linkage back to the source lives in the
313 /// pointer term (`parent_term + offset`) and provenance `offset`, not in
314 /// base arithmetic.
315 pub parent: Option<AllocId>,
316}
317
318impl<'z3, 'tcx> Allocation<'z3, 'tcx> {
319 /// Construct a fresh allocation with all live/dead/invariant flags in
320 /// their initial state.
321 pub(crate) fn new(
322 base: Int<'z3>,
323 size: Int<'z3>,
324 align: Int<'z3>,
325 element_ty: Option<Ty<'tcx>>,
326 kind: AllocKind<'z3>,
327 ) -> Self {
328 Allocation {
329 base,
330 size,
331 align,
332 element_ty: element_ty.into(),
333 kind,
334 facts: AllocFacts::default(),
335 parent: None,
336 }
337 }
338
339 /// Whether this allocation models an external raw-pointer parameter.
340 pub(crate) fn is_external(&self) -> bool {
341 matches!(self.kind, AllocKind::External)
342 }
343
344 /// The slice/array element count, if this allocation is slice data.
345 ///
346 /// Invariant: `size == len * sizeof(element_ty)` is maintained by the
347 /// callers that call [`Self::set_slice_len`] (they compute `size` from the
348 /// same `len` and push the equality as a path condition); it is *not*
349 /// enforced here. `slice_len_from_value` falls back to `size / elem_size`
350 /// when the length was never materialized.
351 pub(crate) fn slice_len(&self) -> Option<&Int<'z3>> {
352 match &self.kind {
353 AllocKind::Slice { len } => Some(len),
354 _ => None,
355 }
356 }
357
358 /// Mark this allocation as slice data with the given element count.
359 /// Callers must keep `size == len * sizeof(element_ty)` consistent.
360 pub(crate) fn set_slice_len(&mut self, len: Int<'z3>) {
361 self.kind = AllocKind::Slice { len };
362 }
363}
364
365/// Per-path facts, read afterwards by the property checker.
366///
367/// Most flags are latched at most once during path execution (a contract fact
368/// or a recognized discriminant / bounds check); `reenter` is instead derived
369/// from the input path in [`VmState::new`]. They are per-path state, not
370/// per-step: once set they are never cleared within a path. (`has_checked_bounds`
371/// is additionally accumulated *across checkpoints* by the engine, which reads
372/// it back into the next path's flags.)
373#[derive(Clone, Copy, Debug, Default)]
374pub(crate) struct PathFacts {
375 /// Whether the current path re-enters a block (loop-unrolled), which lets
376 /// the checker exempt the unrolled iteration's "second drop".
377 pub reenter: bool,
378 /// Whether a SplitTransmute contract was asserted by the caller.
379 pub split_transmute_asserted: bool,
380 /// Whether an `Alias` hazard was accepted via the caller's contract.
381 pub alias_hazard_accepted: bool,
382 /// Whether a ChecksIndexBoundsDisjoint call was processed in any
383 /// checkpoint of this function (accumulated across checkpoints).
384 pub has_checked_bounds: bool,
385 /// Set once the path evaluated an `Iterator::next` discriminant whose
386 /// variant was known symbolically.
387 pub saw_next_discriminant: bool,
388}
389
390/// Scratch state for the *recursive* inlined-callee mechanism
391/// ([`crate::verify::vm::call::exec_inline_call`]), which unwinds via the Rust
392/// call stack, is bounded by `inline_depth`, and stashes its per-call bindings
393/// in `arg_referents`/`deferred_field_writes`.
394///
395/// (The *path-replay* mechanism — `CalleeEntry`/`CalleeExit` items — keeps its
396/// saved caller frames on [`VmState::caller_frames`] instead, which lives next to
397/// [`VmState::current_frame`] to form the frame stack.)
398///
399/// # Deferring `&mut` writes across the inline frame
400///
401/// While the callee runs, the caller's `local_alloc` (the name → allocation
402/// binding) is parked in the saved [`FrameState`], so a write through a `&mut`
403/// argument cannot resolve the caller referent local by address and land in its
404/// field values immediately. `arg_referents` pre-resolves (before `save_frame`)
405/// which caller local each `&mut` argument points at, and `deferred_field_writes`
406/// collects the writes to replay once the caller is restored:
407///
408/// ```text
409/// struct Foo { field: i32 }
410/// fn bar(foo: &mut Foo) { foo.field = 1; } // inlined callee
411/// fn main() {
412/// let mut x = Foo { field: 0 };
413/// bar(&mut x); // inline `bar`
414/// }
415/// ```
416///
417/// 1. Enter `bar`: `save_frame` parks `main`'s local_alloc; `arg_referents[0] = x`.
418/// 2. Run `bar`: `foo.field = 1` (`(*foo).field`) can no longer resolve `x` by
419/// address (the caller's address map is gone), so it pushes `(x, [0], 1)`.
420/// 3. Exit `bar`: `restore_frame` brings `main`'s local_alloc back, then the deferred
421/// write is replayed, giving `x.field == 1`.
422#[derive(Default)]
423pub(crate) struct InlineCtx<'z3, 'tcx> {
424 /// Current inlining depth (nested inlined callees), bounded by
425 /// `MAX_INLINE_DEPTH`.
426 pub inline_depth: usize,
427 /// During `exec_inline_call`, maps each callee argument index to the
428 /// *caller* local its value points at (resolved from the reference's
429 /// address term before the caller's address map is saved away). Used by
430 /// `exec_assign` to resolve `(*self).field = val` writes through a `&mut
431 /// self` reborrow temp back to the caller's referent.
432 pub arg_referents: Vec<Option<Local>>,
433 /// Field writes through a `&mut` argument collected during
434 /// `exec_inline_call`, replayed against the caller's field values after
435 /// `restore_frame` (the caller's address map is parked while the callee
436 /// runs). Each entry is `(caller_local, field_path, value)`, where
437 /// `caller_local` comes from `arg_referents` — the caller local the `&mut`
438 /// argument points at, not the argument itself.
439 pub deferred_field_writes: Vec<(Local, Vec<usize>, VmValue<'z3, 'tcx>)>,
440}
441
442/// The extra semantics attached to a value, beyond its term/type/provenance.
443///
444/// A single value carries at most one of these: it is either a plain value, an
445/// `offset_of!` field offset, a symbolic enum discriminant, a comparison
446/// result, or a non-comparison binary-op result. The operands/operator are
447/// used for guard inference (tracing a switch/assert guard back to the pointer
448/// it null-checks/alignment-checks) and division-axiom injection (following
449/// dataflow edges to reach `Div`/`Rem` results).
450#[derive(Clone, Debug)]
451pub(crate) enum ValueSource<'z3> {
452 /// No extra semantics.
453 None,
454 /// A compile-time `offset_of!` field offset.
455 FieldOffset,
456 /// A symbolic enum discriminant (variant index).
457 Discriminant(Int<'z3>),
458 /// A comparison result (`Eq`/`Ne`/`Le`/`Lt`/`Ge`/`Gt`): the operands and
459 /// the direct boolean condition (`offset <= len`) carried alongside the
460 /// ite-encoded term.
461 Comparison {
462 lhs: Option<PlaceKey>,
463 rhs: Option<PlaceKey>,
464 op: rustc_middle::mir::BinOp,
465 cond: Bool<'z3>,
466 },
467 /// A non-comparison binary-op result (`Add`/`Sub`/…/`Div`/`Rem`): the
468 /// operands and operator.
469 BinaryOp {
470 lhs: Option<PlaceKey>,
471 rhs: Option<PlaceKey>,
472 op: rustc_middle::mir::BinOp,
473 },
474}
475
476impl<'z3> ValueSource<'z3> {
477 /// The `(lhs, rhs, op)` of a binary-op/comparison result, if this value is
478 /// one.
479 pub(crate) fn operands(&self) -> Option<(&Option<PlaceKey>, &Option<PlaceKey>, rustc_middle::mir::BinOp)> {
480 match self {
481 ValueSource::Comparison { lhs, rhs, op, .. } => Some((lhs, rhs, *op)),
482 ValueSource::BinaryOp { lhs, rhs, op } => Some((lhs, rhs, *op)),
483 _ => None,
484 }
485 }
486
487 /// Just the field-offset part of this source: `FieldOffset` if it is one,
488 /// otherwise `None`.
489 pub(crate) fn field_offset_only(&self) -> ValueSource<'z3> {
490 match self {
491 ValueSource::FieldOffset => ValueSource::FieldOffset,
492 _ => ValueSource::None,
493 }
494 }
495}
496
497/// A single allocation: its shape metadata ([`Allocation`]) plus its contents
498/// ([`MemoryContent`]). Splitting the two keeps the identity/layout facts apart
499/// from the mutable memory the values live in, while colocating them in one
500/// unit so no parallel table can drift out of sync. Facts live in three
501/// anchored layers — lifecycle ([`AllocFacts`]), content ([`ContentFacts`]),
502/// and value ([`ValueFacts`] on each [`VmValue`]) — see each type's doc.
503pub(crate) struct MemoryUnit<'z3, 'tcx> {
504 /// The allocation's shape and lifecycle facts.
505 pub(crate) allocation: Allocation<'z3, 'tcx>,
506 /// The allocation's contents (byte layer + typed-value layer) and content facts.
507 pub(crate) content: MemoryContent<'z3, 'tcx>,
508}
509
510/// The per-allocation contents: the byte layer, the typed-value layer, and the
511/// content facts.
512#[derive(Default)]
513pub(crate) struct MemoryContent<'z3, 'tcx> {
514 /// Byte value function: `byte[i] = select(array, i)` for any (possibly
515 /// symbolic) offset `i`. `None` when no byte has been written. Unwritten
516 /// offsets read back the `UNINIT` sentinel, so `init`/`nul` are derived
517 /// from `select`, not stored per byte.
518 pub(crate) byte_array: Option<Array<'z3>>,
519
520 /// The concrete byte offsets written to this allocation. Z3 arrays cannot
521 /// enumerate their stored indices, so the byte-level checkers iterate this
522 /// set directly instead of scanning the allocation's (possibly symbolic or
523 /// huge) `size` range. Only *concrete* writes are recorded: a symbolic
524 /// `byte_write` (e.g. a symbolic `ValidCStr` length) still updates the byte
525 /// array but not this set.
526 pub(crate) byte_written: FxHashSet<usize>,
527
528 /// The typed-value (Value) layer: (viewed_type, path) → value. `path == []`
529 /// is the allocation's *whole* value (the rvalue bound to a local) and
530 /// `path == [i, ..]` is field `i`, both viewed as `viewed_type`. The
531 /// `viewed_type` distinguishes reinterprets of the same allocation under
532 /// different ADTs (e.g. `LeafNode` vs `InternalNode` cast views), so field
533 /// index `1` resolves to `parent_idx` under `LeafNode` and `edges` under
534 /// `InternalNode` without colliding. This is the alloc-keyed counterpart to
535 /// the byte-level [`Self::byte_array`]; the Local-keyed
536 /// `local_value`/`set_local`/`field_value`/`set_field_value` resolve a
537 /// local's backing allocation and then read/write this layer.
538 pub(crate) values: FxHashMap<(Ty<'tcx>, Vec<usize>), VmValue<'z3, 'tcx>>,
539
540 /// Content facts (`initialized`/`cstr_trusted`/`utf8_trusted`); value-level
541 /// facts live on each [`VmValue::facts`].
542 pub(crate) facts: ContentFacts,
543}
544
545/// Accumulated solver state for the current path.
546///
547/// `assertions` is the assertion stream fed to Z3; `term_caches` holds the
548/// per-phenomenon term-provenance caches that keep expressions compact and
549/// linear. Everything is path-scoped and monotonic: it accumulates as the VM
550/// steps and is never reset within a path (or across inlined frames).
551#[derive(Default)]
552pub(crate) struct Constraints<'z3, 'tcx> {
553 /// Accumulated solver constraints along the current path: branch/guard
554 /// constraints (`SwitchInt`/`Assert`), API preconditions, and symbolic
555 /// layout facts (`sizeof_T`, `align_T`). Asserted into the solver by
556 /// [`VmState::assert_all`] and by the property checker's feasibility
557 /// queries.
558 pub(crate) assertions: Vec<Bool<'z3>>,
559
560 /// Term-provenance caches that shape terms into a form Z3 can solve.
561 pub(crate) term_caches: TermCaches<'z3, 'tcx>,
562}
563
564/// Term-provenance caches, one per phenomenon the VM must shape by hand.
565///
566/// Z3's nonlinear integer solver (NIA) cannot rewrite degree-3 products or
567/// deeply-nested pointer chains, so the VM records the *semantic meaning* of a
568/// symbol (what it divides, which iterator it indexes, …) and uses that
569/// provenance later to emit a compact, degree-≤2 term instead. Each cache is
570/// independent — they share only the path-scoped, monotonic lifetime.
571#[derive(Default)]
572pub(crate) struct TermCaches<'z3, 'tcx> {
573 /// `sizeof_T` for each generic type, one symbolic constant per type. Keeps
574 /// `ptr.add` strides, `access_bytes` element sizes, and allocation sizes
575 /// consistent so that SMT can cancel the `S` factor in `InBound`.
576 pub(crate) sizes: FxHashMap<Ty<'tcx>, Int<'z3>>,
577
578 /// `align_T` for each generic type, one symbolic constant per type; linked
579 /// to the size by the layout constraint `sizeof_T % align_T == 0`.
580 pub(crate) aligns: FxHashMap<Ty<'tcx>, Int<'z3>>,
581
582 /// Terms that are the result of a bitwise `Not` (two's-complement mask).
583 /// Used to recognize `x & !(align-1)` alignment patterns in BitAnd so we
584 /// can derive `align = -mask` and emit linear bounds for the result.
585 pub(crate) not_mask_terms: FxHashSet<Int<'z3>>,
586
587 /// `quotient → dividend` for each *exact* division (`lhs % rhs == 0`),
588 /// recording which size each `exact_div` symbol divides (e.g. `us` →
589 /// `sizeof_T`). Lets a later `us_len = (len / ts) * us` multiplication
590 /// emit the byte bound `us_len * sizeof_U <= len * sizeof_T`.
591 pub(crate) exact_div_roots: FxHashMap<Int<'z3>, Int<'z3>>,
592
593 /// `quotient → (lhs, rhs)` for each *non-exact* division, recovering the
594 /// `len` and divisor (`ts`) operands at a following `us_len = (len / ts) * us`.
595 pub(crate) div_roots: FxHashMap<Int<'z3>, (Int<'z3>, Int<'z3>)>,
596
597 /// Per-iterator element index, keyed by the *buffer* the iterator walks
598 /// (`end` field's provenance alloc id, which is frame-independent). The
599 /// value is `(offset, base_len)`: the current element index and the total
600 /// element count (the end field's `Element` offset, `None` when unknown).
601 /// Caching both keeps `next`/`len`/`is_empty` checks linear
602 /// (`base_len - offset`) instead of a deeply-nested `((base + S) + S) …`
603 /// pointer chain that Z3's NIA cannot reason about.
604 pub(crate) iter_ptr_offset: FxHashMap<AllocId, (Int<'z3>, Option<Int<'z3>>)>,
605
606 /// `UNINIT` sentinel byte value (≥ 256, outside the `u8` value range).
607 /// The default element of every byte array; `select(array, i) != UNINIT`
608 /// is how "byte `i` was written" is decided.
609 pub(crate) uninit_byte: Option<Int<'z3>>,
610}
611
612/// The frame-scoped subset of [`VmState`]: the name → allocation binding keyed
613/// by MIR `Local`, which the callee reuses, so it must be swapped out for the
614/// duration of an inlined callee and swapped back afterwards.
615pub(crate) struct FrameState {
616 /// The function whose body we execute (the MIR is derived via
617 /// [`VmState::body`]).
618 pub(crate) current_def_id: DefId,
619
620 /// The stack allocation backing each local's place (lvalue identity). A
621 /// local's whole value lives at `path == []` and its field values at
622 /// `path == [i, ..]` in that allocation (see [`MemoryContent::values`], keyed
623 /// `(AllocId, view_ty, path)` with the local's declared type as the view
624 /// type), so there is no local-keyed value table here.
625 pub(crate) local_alloc: FxHashMap<Local, AllocId>,
626}
627
628/// The full symbolic execution state at a program point.
629///
630/// Accumulates the name → allocation bindings, allocations, and solver
631/// constraints as the VM steps through retained MIR items. The Z3 context is
632/// borrowed so a single context can be reused across property checks.
633pub(crate) struct VmState<'z3, 'tcx> {
634 // ── Shared handles (passed in at run start; not execution state, but
635 // needed to create terms and query types during checking)
636 /// Shared Z3 context.
637 pub(crate) z3_ctx: &'z3 Context,
638
639 /// Compiler type context.
640 pub(crate) tcx: TyCtxt<'tcx>,
641
642 // ── Frame-scoped state (swapped on inline entry/exit; the exact set
643 // captured by [`Self::save_frame`])
644 pub(crate) current_frame: FrameState,
645
646 // ── Path-scoped state (accumulates across the whole path, including
647 // inlined frames)
648 /// Stack of saved caller frames for path-replay inlining
649 /// (`CalleeEntry`/`CalleeExit` items). Together with [`Self::current_frame`]
650 /// (the current frame) it forms the call stack: entering an inlined callee
651 /// moves the current frame here, exiting restores it.
652 pub(crate) caller_frames: Vec<FrameState>,
653
654 /// The object space: one [`MemoryUnit`] per allocation, indexed by
655 /// `AllocId`. Each unit carries its shape ([`Allocation`]) and its contents
656 /// (per-byte state + per-allocation typed values).
657 pub(crate) units: Vec<MemoryUnit<'z3, 'tcx>>,
658
659 /// Solver constraints and term caches accumulated along the current path.
660 pub(crate) constraints: Constraints<'z3, 'tcx>,
661
662 /// Recursive-inlining scratch state (depth and per-call temporary bindings
663 /// pushed/popped on inline entry/exit).
664 pub(crate) inline: InlineCtx<'z3, 'tcx>,
665
666 /// Per-path facts (latched while stepping, or derived in [`Self::new`]),
667 /// read by the property checker.
668 pub(crate) path_facts: PathFacts,
669}
670
671impl<'z3, 'tcx> VmState<'z3, 'tcx> {
672 /// Create a fresh VM state for executing a path.
673 pub(crate) fn new(
674 z3_ctx: &'z3 Context,
675 tcx: TyCtxt<'tcx>,
676 path: &Path,
677 caller_def_id: DefId,
678 ) -> Self {
679 // Derive the one path fact the checker needs after `run` (whether the
680 // path re-enters a block); the raw `Path` itself is not kept.
681 let reenter = path.reenters();
682 // The `UNINIT` sentinel is created once and shared by every byte array:
683 // an unwritten offset reads it back, so `select != UNINIT` decides
684 // whether a byte was written.
685 let mut constraints = Constraints::default();
686 let uninit = Int::fresh_const(z3_ctx, "uninit_byte");
687 constraints.term_caches.uninit_byte = Some(uninit.clone());
688 constraints
689 .assertions
690 .push(uninit.ge(&Int::from_u64(z3_ctx, 256)));
691 Self {
692 z3_ctx,
693 tcx,
694 current_frame: FrameState {
695 current_def_id: caller_def_id,
696 local_alloc: FxHashMap::default(),
697 },
698 caller_frames: Vec::default(),
699 units: Vec::default(),
700 inline: InlineCtx::default(),
701 constraints,
702 path_facts: PathFacts {
703 reenter,
704 ..PathFacts::default()
705 },
706 }
707 }
708
709 /// The MIR body of the current function, derived from `current_def_id`.
710 pub(crate) fn body(&self) -> &'tcx Body<'tcx> {
711 self.tcx.optimized_mir(self.current_frame.current_def_id)
712 }
713
714 /// Capture the frame-scoped state before switching to an inlined callee.
715 ///
716 /// This is the single source of truth for *what* is frame-scoped: the whole
717 /// [`FrameState`] (function identity, local bindings, and operand sources).
718 /// Both inline mechanisms (`handle_callee_entry` in path replay and
719 /// `exec_inline_call`) call this, so they can no longer drift apart.
720 pub(crate) fn save_frame(&mut self) -> FrameState {
721 FrameState {
722 current_def_id: self.current_frame.current_def_id,
723 local_alloc: std::mem::take(&mut self.current_frame.local_alloc),
724 }
725 }
726
727 /// Restore the frame-scoped state after an inlined callee returns.
728 pub(crate) fn restore_frame(&mut self, frame: FrameState) {
729 self.current_frame = frame;
730 }
731
732 /// Look up the whole value bound to a MIR local (its `path == []` slot in
733 /// the allocation backing the local).
734 pub(crate) fn local_value(&self, local: Local) -> Option<&VmValue<'z3, 'tcx>> {
735 let alloc_id = *self.current_frame.local_alloc.get(&local)?;
736 let view_ty = self.body().local_decls[local].ty;
737 self.load_value(alloc_id, view_ty, &[])
738 }
739
740 /// Bind a whole value to a MIR local (store it at `path == []` in the
741 /// allocation backing the local, allocating that stack slot on demand).
742 pub(crate) fn set_local(&mut self, local: Local, value: VmValue<'z3, 'tcx>) {
743 self.ensure_local_allocation(local);
744 let alloc_id = self.current_frame.local_alloc[&local];
745 let view_ty = self.body().local_decls[local].ty;
746 self.store_value(alloc_id, view_ty, vec![], value);
747 }
748
749 /// Mark a value as initialized (written), and sync its backing allocation's
750 /// [`ContentFacts::initialized`] fact.
751 ///
752 /// This is the single entry point that keeps the value-level
753 /// [`ValueFacts::init`] and the allocation-level [`ContentFacts::initialized`]
754 /// in sync; callers must set the pair through here rather than by hand so
755 /// the two facts cannot drift apart.
756 pub(crate) fn mark_initialized(&mut self, value: &mut VmValue<'z3, 'tcx>) {
757 value.facts.init = true;
758 if let Some(prov) = &value.provenance {
759 self.content_mut(prov.alloc_id).facts.initialized = true;
760 }
761 }
762
763 /// Get the symbolic address of a MIR local (its stack allocation's base).
764 pub(crate) fn local_address(&mut self, local: Local) -> Int<'z3> {
765 self.ensure_local_allocation(local);
766 let id = self.current_frame.local_alloc[&local];
767 self.units[id.0].allocation.base.clone()
768 }
769
770 /// Allocate a fresh symbolic object and return its ID and base address.
771 pub(crate) fn allocate(
772 &mut self,
773 size: Int<'z3>,
774 align: Int<'z3>,
775 element_ty: Option<Ty<'tcx>>,
776 ) -> (AllocId, Int<'z3>) {
777 self.allocate_internal(size, align, element_ty, AllocKind::Object)
778 }
779
780 /// Allocate a fresh external allocation (for raw-pointer parameters).
781 /// External allocations may be null and have unlimited size.
782 pub(crate) fn allocate_external(
783 &mut self,
784 size: Int<'z3>,
785 align: Int<'z3>,
786 element_ty: Option<Ty<'tcx>>,
787 ) -> (AllocId, Int<'z3>) {
788 self.allocate_internal(size, align, element_ty, AllocKind::External)
789 }
790
791 /// Allocate a slice/array data allocation with a known (possibly symbolic)
792 /// element count. Computes `size = len * elem_size` and materializes `len`
793 /// in one step, so `size` and the slice length can never diverge (the
794 /// `size == len * elem_size` invariant is established here instead of being
795 /// re-derived by every caller).
796 pub(crate) fn allocate_slice(
797 &mut self,
798 len: Int<'z3>,
799 elem_size: Int<'z3>,
800 align: Int<'z3>,
801 element_ty: Option<Ty<'tcx>>,
802 ) -> (AllocId, Int<'z3>) {
803 let size = Int::mul(self.z3_ctx, &[&len, &elem_size]);
804 let (id, base) = self.allocate(size, align, element_ty);
805 self.alloc_mut(id).set_slice_len(len);
806 (id, base)
807 }
808
809 fn allocate_internal(
810 &mut self,
811 size: Int<'z3>,
812 align: Int<'z3>,
813 element_ty: Option<Ty<'tcx>>,
814 kind: AllocKind<'z3>,
815 ) -> (AllocId, Int<'z3>) {
816 let id = AllocId(self.units.len());
817 let base = {
818 let name = format!(
819 "{}_{}",
820 if matches!(kind, AllocKind::External) {
821 "ext"
822 } else {
823 "heap"
824 },
825 id.0
826 );
827 Int::new_const(self.z3_ctx, name.as_str())
828 };
829 let alloc = Allocation::new(base.clone(), size, align, element_ty, kind);
830 self.units.push(MemoryUnit {
831 allocation: alloc,
832 content: MemoryContent::default(),
833 });
834 (id, base)
835 }
836
837 /// Indexed access to an allocation by its `AllocId` (the id is the index).
838 pub(crate) fn alloc(&self, id: AllocId) -> &Allocation<'z3, 'tcx> {
839 &self.units[id.0].allocation
840 }
841
842 /// Mutable indexed access to an allocation by its `AllocId`.
843 pub(crate) fn alloc_mut(&mut self, id: AllocId) -> &mut Allocation<'z3, 'tcx> {
844 &mut self.units[id.0].allocation
845 }
846
847 /// Indexed access to an allocation's contents by its `AllocId`.
848 pub(crate) fn content(&self, id: AllocId) -> &MemoryContent<'z3, 'tcx> {
849 &self.units[id.0].content
850 }
851
852 /// Mutable indexed access to an allocation's contents by its `AllocId`.
853 pub(crate) fn content_mut(&mut self, id: AllocId) -> &mut MemoryContent<'z3, 'tcx> {
854 &mut self.units[id.0].content
855 }
856
857 /// Whether `id` was asserted a valid C string via a `ValidCStr` contract
858 /// fact / struct invariant.
859 pub(crate) fn is_cstr_trusted(&self, id: AllocId) -> bool {
860 self.content(id).facts.cstr_trusted
861 }
862
863 /// Whether `id` was asserted valid UTF-8 via a `ValidString` contract fact.
864 pub(crate) fn is_utf8_trusted(&self, id: AllocId) -> bool {
865 self.content(id).facts.utf8_trusted
866 }
867
868 /// The ultimate root allocation, following `parent` chains (sub-allocations
869 /// created by `from_raw_parts` / `split_at` / `as_chunks`, whose `parent`
870 /// points at the allocation they were split from).
871 pub(crate) fn root_alloc(&self, id: AllocId) -> AllocId {
872 let mut cur = id;
873 let mut guard = 0;
874 while let Some(parent) = self.alloc(cur).parent {
875 cur = parent;
876 guard += 1;
877 // A parent chain as long as the total allocation count means a
878 // cycle; stop rather than loop forever.
879 if guard >= self.units.len() {
880 break;
881 }
882 }
883 cur
884 }
885
886 /// Create a fresh symbolic Z3 int constant (globally unique, even across
887 /// calls with the same prefix — `Z3_mk_fresh_const` auto-suffixes the name).
888 pub(crate) fn fresh_int(&self, prefix: &str) -> Int<'z3> {
889 Int::fresh_const(self.z3_ctx, prefix)
890 }
891
892 /// Get the value of a specific field within an aggregate local.
893 ///
894 /// Field values live in the allocation backing the local (the
895 /// memory-contents layer [`MemoryContent::values`]), keyed by the local's declared
896 /// type as the view type. Returns `None` if the local has no allocation
897 /// (the field was never materialized).
898 pub(crate) fn field_value(&self, local: Local, path: &[usize]) -> Option<&VmValue<'z3, 'tcx>> {
899 let alloc_id = *self.current_frame.local_alloc.get(&local)?;
900 let view_ty = self.body().local_decls[local].ty;
901 self.load_value(alloc_id, view_ty, path)
902 }
903
904 /// Enumerate the field paths materialized for a local: every non-empty
905 /// `path` for which [`Self::field_value`] currently returns a value (the
906 /// local's allocation's fields under its declared view type). The whole
907 /// value (`path == []`) is deliberately excluded — it is read via
908 /// [`Self::local_value`], not as a "field", so callers iterating fields do
909 /// not accidentally treat the whole value as a field projection.
910 pub(crate) fn field_paths(&self, local: Local) -> Vec<Vec<usize>> {
911 let Some(&alloc_id) = self.current_frame.local_alloc.get(&local) else {
912 return Vec::new();
913 };
914 let view_ty = self.body().local_decls[local].ty;
915 self.units[alloc_id.0]
916 .content
917 .values
918 .keys()
919 .filter(|(t, p)| *t == view_ty && !p.is_empty())
920 .map(|(_, p)| p.clone())
921 .collect()
922 }
923
924 /// The declared type of `local` in `frame`'s body.
925 fn frame_local_ty(&self, frame: &FrameState, local: Local) -> Ty<'tcx> {
926 self.tcx.optimized_mir(frame.current_def_id).local_decls[local].ty
927 }
928
929 /// Enumerate the field paths materialized for `local` in a saved caller
930 /// `frame`. Field values live in the path-scoped [`MemoryContent::values`], so they
931 /// are read via `frame`'s `local_alloc` and declared type.
932 pub(crate) fn frame_field_paths(
933 &self,
934 frame: &FrameState,
935 local: Local,
936 ) -> Vec<Vec<usize>> {
937 let Some(&alloc_id) = frame.local_alloc.get(&local) else {
938 return Vec::new();
939 };
940 let view_ty = self.frame_local_ty(frame, local);
941 self.units[alloc_id.0]
942 .content
943 .values
944 .keys()
945 .filter(|(t, p)| *t == view_ty && !p.is_empty())
946 .map(|(_, p)| p.clone())
947 .collect()
948 }
949
950 /// Read a field of `local` in a saved caller `frame`.
951 pub(crate) fn frame_field_value(
952 &self,
953 frame: &FrameState,
954 local: Local,
955 path: &[usize],
956 ) -> Option<&VmValue<'z3, 'tcx>> {
957 let alloc_id = *frame.local_alloc.get(&local)?;
958 let view_ty = self.frame_local_ty(frame, local);
959 self.load_value(alloc_id, view_ty, path)
960 }
961
962 /// Read the whole value of `local` in a saved caller `frame` (its
963 /// `path == []` slot). The value lives in the path-scoped [`MemoryContent::values`],
964 /// while the name → allocation binding lives in the saved `frame`.
965 pub(crate) fn frame_local_value(
966 &self,
967 frame: &FrameState,
968 local: Local,
969 ) -> Option<&VmValue<'z3, 'tcx>> {
970 let alloc_id = *frame.local_alloc.get(&local)?;
971 let view_ty = self.frame_local_ty(frame, local);
972 self.load_value(alloc_id, view_ty, &[])
973 }
974
975 /// Enumerate `(local, whole value)` for every local with a materialized
976 /// whole value (its `path == []` slot). This is the whole-value counterpart
977 /// to [`Self::field_paths`], used where code used to iterate `locals`.
978 pub(crate) fn all_local_values(&self) -> Vec<(Local, &VmValue<'z3, 'tcx>)> {
979 self.current_frame
980 .local_alloc
981 .keys()
982 .copied()
983 .filter_map(|local| self.local_value(local).map(|value| (local, value)))
984 .collect()
985 }
986
987 /// The buffer an `Iter`/`IterMut` at `local` walks: the provenance of its
988 /// `end` field. This is frame-independent (the buffer allocation is
989 /// path-scoped), unlike the `local` itself, so it keys the per-iterator
990 /// element index in [`TermCaches::iter_ptr_offset`].
991 pub(crate) fn iter_buffer(&self, local: Local) -> Option<AllocId> {
992 self.field_value(local, &[1])
993 .and_then(|end| end.provenance.as_ref())
994 .map(|ep| ep.alloc_id)
995 }
996
997 /// The byte buffer walked by an iterator at `local`, possibly wrapped in
998 /// adapter types (`Cloned`/`Rev`/…). `Rvalue::Aggregate` flattens nested
999 /// adapters, so the innermost `Iter`/`IterMut` `end` pointer is any tracked
1000 /// field whose path ends in `[1]`. Return its allocation and end offset
1001 /// (the end offset doubles as the byte length when the element is `u8`).
1002 pub(crate) fn iter_utf8_buffer(&self, local: Local) -> Option<(AllocId, Int<'z3>)> {
1003 let mut best: Option<(usize, AllocId, Int<'z3>)> = None;
1004 for path in self.field_paths(local) {
1005 if path.last() != Some(&1) {
1006 continue;
1007 }
1008 let Some(v) = self.field_value(local, &path) else {
1009 continue;
1010 };
1011 let Some(prov) = v.provenance.as_ref() else {
1012 continue;
1013 };
1014 if best.as_ref().map_or(true, |(depth, _, _)| path.len() < *depth) {
1015 best = Some((path.len(), prov.alloc_id, prov.offset.clone()));
1016 }
1017 }
1018 best.map(|(_, id, off)| (id, off))
1019 }
1020
1021 /// The field carrying an owned value's heap pointer (`Box`/`Vec`/`String`'s
1022 /// owning pointer field), located generically via
1023 /// [`Self::container_ptr_field`] (e.g. `Box` → `[0, 0]`, `Vec` → `[0, 0, 0]`).
1024 /// Falls back to the first materialized field with heap provenance for
1025 /// owners whose deep `NonNull` leaf was not explicitly materialized (e.g. a
1026 /// `Box` produced by `into_boxed_slice`, which only records the whole value).
1027 pub(crate) fn owner_ptr_field(&self, local: Local) -> Option<&VmValue<'z3, 'tcx>> {
1028 let ty = self.body().local_decls[local].ty;
1029 if let Some((path, _)) = self.container_ptr_field(ty) {
1030 if let Some(v) = self.field_value(local, &path) {
1031 if v.provenance_alloc_id().is_some() {
1032 return Some(v);
1033 }
1034 }
1035 }
1036 self.field_paths(local)
1037 .iter()
1038 .find_map(|path| {
1039 self.field_value(local, path)
1040 .filter(|v| v.provenance_alloc_id().is_some())
1041 })
1042 }
1043
1044 /// Invalidate `local`'s owner-field provenance (mark it moved-out after a
1045 /// whole-place move), so a later `Owning` check does not treat it as a
1046 /// second owner of the heap allocation it no longer owns.
1047 pub(crate) fn invalidate_owner_field(&mut self, local: Local) {
1048 let ty = self.body().local_decls[local].ty;
1049 let Some((path, _)) = self.container_ptr_field(ty) else {
1050 return;
1051 };
1052 let Some(mut fv) = self.field_value(local, &path).cloned() else {
1053 return;
1054 };
1055 if fv.provenance_alloc_id().is_some() {
1056 fv.provenance = None;
1057 self.set_field_value(local, path, fv);
1058 }
1059 }
1060
1061 /// Set the value of a specific field within an aggregate local.
1062 ///
1063 /// Ensures the local has a backing allocation, then stores into the
1064 /// memory-contents layer ([`MemoryContent::values`]) keyed by the local's declared
1065 /// type as the view type.
1066 pub(crate) fn set_field_value(
1067 &mut self,
1068 local: Local,
1069 path: Vec<usize>,
1070 value: VmValue<'z3, 'tcx>,
1071 ) {
1072 let view_ty = self.body().local_decls[local].ty;
1073 self.ensure_local_allocation(local);
1074 let alloc_id = self.current_frame.local_alloc[&local];
1075 self.store_value(alloc_id, view_ty, path, value);
1076 }
1077
1078 /// Assert path conditions and invariant constraints into a solver.
1079 pub(crate) fn assert_all(&self, solver: &z3::Solver<'z3>) {
1080 for cond in &self.constraints.assertions {
1081 solver.assert(cond);
1082 }
1083 let zero = Int::from_u64(self.z3_ctx, 0);
1084 for unit in &self.units {
1085 let alloc = &unit.allocation;
1086 if !alloc.is_external() {
1087 solver.assert(&alloc.base._eq(&zero).not());
1088 }
1089 solver.assert(&alloc.size.ge(&zero));
1090 if alloc.align.simplify().as_u64() != Some(1) {
1091 solver.assert(&alloc.base.rem(&alloc.align)._eq(&zero));
1092 }
1093 }
1094
1095 // Whole values (`path == []`) and field values (`path == [i, ..]`) both
1096 // live in the memory-contents layer. Field paths exclude the whole
1097 // value (see [`Self::field_paths`]), so assert the whole value and the
1098 // fields separately.
1099 let local_ids: Vec<Local> = self.current_frame.local_alloc.keys().copied().collect();
1100 for local in local_ids {
1101 if let Some(value) = self.local_value(local) {
1102 self.assert_value_constraints(solver, value);
1103 }
1104 for path in self.field_paths(local) {
1105 if let Some(value) = self.field_value(local, &path) {
1106 self.assert_value_constraints(solver, value);
1107 }
1108 }
1109 }
1110 }
1111
1112 /// Assert a single symbolic value's known invariant constraints.
1113 fn assert_value_constraints(&self, solver: &z3::Solver<'z3>, value: &VmValue<'z3, 'tcx>) {
1114 let zero = Int::from_u64(self.z3_ctx, 0);
1115 if value.facts.non_null {
1116 solver.assert(&value.z3_term._eq(&zero).not());
1117 }
1118 if let Some(ref prov) = value.provenance {
1119 let alloc = self.alloc(prov.alloc_id);
1120 let expected = Int::add(self.z3_ctx, &[&alloc.base, &prov.offset]);
1121 solver.assert(&value.z3_term._eq(&expected));
1122 }
1123 if matches!(
1124 value.ty.kind(),
1125 rustc_middle::ty::TyKind::Uint(_)
1126 | rustc_middle::ty::TyKind::Bool
1127 | rustc_middle::ty::TyKind::Char
1128 ) {
1129 solver.assert(&value.z3_term.ge(&zero));
1130 }
1131 if matches!(value.ty.kind(), rustc_middle::ty::TyKind::Bool) {
1132 let one = Int::from_u64(self.z3_ctx, 1);
1133 solver.assert(&value.z3_term.le(&one));
1134 }
1135 if matches!(value.ty.kind(), rustc_middle::ty::TyKind::Char) {
1136 let max = Int::from_u64(self.z3_ctx, 0x10FFFF);
1137 solver.assert(&value.z3_term.le(&max));
1138 }
1139 }
1140}
1141
1142impl std::fmt::Debug for VmState<'_, '_> {
1143 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1144 f.debug_struct("VmState")
1145 .field("locals_count", &self.current_frame.local_alloc.len())
1146 .field("allocations_count", &self.units.len())
1147 .field("assertions", &self.constraints.assertions.len())
1148 .finish()
1149 }
1150}
1151
1152// ── Shared value extraction ──────────────────────────────────────
1153
1154impl<'z3, 'tcx> VmState<'z3, 'tcx> {
1155 /// Extract a VmValue from a MIR operand.
1156 pub(crate) fn value_of_operand(&self, operand: &Operand<'tcx>) -> VmValue<'z3, 'tcx> {
1157 match operand {
1158 Operand::Copy(place) | Operand::Move(place) => self
1159 .value_of_place(place)
1160 .unwrap_or_else(|| self.unknown_value_for_place(place)),
1161 Operand::Constant(constant) => {
1162 let text = format!("{:?}", constant.const_);
1163 // `size_of::<T>()` / `align_of::<T>()` lower to the
1164 // `SizedTypeProperties::SIZE`/`::ALIGN` associated consts for a
1165 // generic `T`. Bind them to the shared symbolic `sizeof_T` /
1166 // `align_T` (created by `size_sym`/`align_sym` during
1167 // `init_parameters`) so they agree with allocation sizes and
1168 // pointer strides instead of being unrelated fresh constants.
1169 if let rustc_middle::mir::Const::Unevaluated(uneval, _) = constant.const_ {
1170 let def_name = self.tcx.def_path_str(uneval.def);
1171 let is_size = def_name.ends_with("SizedTypeProperties::SIZE");
1172 let is_align = def_name.ends_with("SizedTypeProperties::ALIGN");
1173 if (is_size || is_align) && !uneval.args.is_empty() {
1174 let ty = uneval.args.type_at(0);
1175 let term = if is_size {
1176 self.size_sym_read(ty)
1177 } else {
1178 self.align_sym_read(ty)
1179 };
1180 return VmValue::new(term, constant.const_.ty());
1181 }
1182 }
1183 let int_val = crate::helpers::mir_utils::eval_const_scalar_int(
1184 self.tcx,
1185 &constant.const_,
1186 &text,
1187 );
1188 let field_offset = int_val.is_none()
1189 && crate::helpers::mir_utils::offset_of_container(self.tcx, &constant.const_)
1190 .is_some();
1191 let term = if let Some(v) = int_val {
1192 if v < 0 {
1193 Int::from_i64(self.z3_ctx, v as i64)
1194 } else {
1195 Int::from_u64(self.z3_ctx, v as u64)
1196 }
1197 } else {
1198 // Create a deterministic name for const generics so
1199 // multiple uses of the same parameter share one term.
1200 let name = format!("const_{}", text.replace([':', '#', ' '], "_"));
1201 Int::new_const(self.z3_ctx, name.as_str())
1202 };
1203 let ty = constant.const_.ty();
1204 VmValue {
1205 z3_term: term,
1206 ty,
1207 provenance: None,
1208 facts: ValueFacts::default(),
1209 source: if field_offset {
1210 ValueSource::FieldOffset
1211 } else {
1212 ValueSource::None
1213 },
1214 }
1215 }
1216 #[cfg(rapx_ge_95)]
1217 Operand::RuntimeChecks(_) => VmValue::new(
1218 self.fresh_int("runtime_checks"),
1219 self.body().local_decls[Local::from_usize(0)].ty,
1220 ),
1221 }
1222 }
1223
1224 /// Read the byte at `offset` within a `repr(C)`-style ADT from its
1225 /// materialized scalar field values — the field→byte direction of the cast
1226 /// cross-view materialization. Returns `None` when `offset` does not land
1227 /// inside a concrete scalar field (so the caller falls back to the base).
1228 fn byte_from_field(&self, alloc_id: AllocId, ty: Ty<'tcx>, offset: usize) -> Option<Int<'z3>> {
1229 let rustc_middle::ty::TyKind::Adt(adt_def, substs) = ty.kind() else {
1230 return None;
1231 };
1232 let variant = adt_def.non_enum_variant();
1233 for (idx, field_def) in variant.fields.iter().enumerate() {
1234 let field_ty = crate::helpers::mir_utils::field_ty(self.tcx, field_def, substs);
1235 let field_off = self.field_offset_in_bytes(ty, idx) as usize;
1236 let field_size = self.size_of_ty(field_ty) as usize;
1237 if offset >= field_off && offset < field_off + field_size {
1238 let fv = self.load_value(alloc_id, ty, &[idx])?;
1239 let byte_idx = offset - field_off;
1240 // Fields wider than 8 bytes (e.g. `u128`) cannot be shifted by a
1241 // `u64` divisor; fall back to the base rather than overflow.
1242 if byte_idx >= 8 {
1243 return None;
1244 }
1245 let divisor = Int::from_u64(self.z3_ctx, 1u64 << (byte_idx * 8));
1246 let modulus = Int::from_u64(self.z3_ctx, 256);
1247 return Some(fv.z3_term.div(&divisor).rem(&modulus));
1248 }
1249 }
1250 None
1251 }
1252
1253 /// Look up the value stored at a MIR place.
1254 pub(crate) fn value_of_place(&self, place: &Place<'tcx>) -> Option<VmValue<'z3, 'tcx>> {
1255 if place.projection.is_empty() {
1256 return self.local_value(place.local).cloned();
1257 }
1258 let place_ty = place.ty(self.body(), self.tcx).ty;
1259
1260 // Collect field indices from projections
1261 let field_path: Vec<usize> = place
1262 .projection
1263 .iter()
1264 .filter_map(|proj| match proj.kind() {
1265 ProjectionElem::Field(field_idx, _) => Some(field_idx.as_usize()),
1266 _ => None,
1267 })
1268 .collect();
1269
1270 // If we have a pure field path (only Field / Downcast projections),
1271 // look up in the per-field value map first. For `Option`/`ControlFlow`,
1272 // the variant's data is stored under the same field index as the enum
1273 // field (the discriminant is tracked separately, not in the field map),
1274 // so `(x as Some).0` resolves to `x`'s field `[0]`.
1275 let is_pure_field = place.projection.iter().all(|p| {
1276 matches!(
1277 p.kind(),
1278 ProjectionElem::Field(..) | ProjectionElem::Downcast(..)
1279 )
1280 });
1281 let has_downcast = place
1282 .projection
1283 .iter()
1284 .any(|p| matches!(p.kind(), ProjectionElem::Downcast(..)));
1285 if !field_path.is_empty() && is_pure_field {
1286 if let Some(val) = self.field_value(place.local, &field_path).cloned() {
1287 return Some(val);
1288 }
1289 if !has_downcast {
1290 // Fallback: when the base local has provenance, propagate it
1291 // to field accesses. This handles pointer-wrapper types (Box,
1292 // Unique, NonNull) where accessing inner pointer fields yields
1293 // the same provenance as the container.
1294 if let Some(base_val) = self.local_value(place.local) {
1295 if let Some(ref prov) = base_val.provenance {
1296 return Some(VmValue {
1297 z3_term: base_val.z3_term.clone(),
1298 ty: place_ty,
1299 provenance: Some(prov.clone()),
1300 facts: base_val.facts.clone(),
1301 source: ValueSource::None,
1302 });
1303 }
1304 }
1305 return None;
1306 }
1307 // A Downcast without a materialized field falls through to the
1308 // Deref+Field / multi-element fallback below, which returns the
1309 // base local (preserving the pre-Downcast behavior instead of
1310 // forcing a fresh value).
1311 }
1312
1313 // For Deref+Field chains (e.g. (*self).ptr), strip the leading Deref
1314 // projection(s) and look up the field values with the remaining path.
1315 if !field_path.is_empty()
1316 && field_path.len() < place.projection.len()
1317 && place
1318 .projection
1319 .iter()
1320 .any(|p| matches!(p.kind(), ProjectionElem::Deref))
1321 {
1322 // Only Deref and Field projections — all non-Field must be Deref
1323 // (a full-range `Subslice` (`arr[..]`) is transparent: it converts
1324 // `[T; N]` → `[T]` without changing which field is selected, so
1325 // `(*ptr).edges[..]` still resolves to the `edges` field).
1326 let non_field_deref = place.projection.iter().all(|p| {
1327 matches!(
1328 p.kind(),
1329 ProjectionElem::Field(..)
1330 | ProjectionElem::Deref
1331 | ProjectionElem::Subslice { .. }
1332 )
1333 });
1334 if non_field_deref {
1335 if let Some(val) = self.field_value(place.local, &field_path).cloned() {
1336 return Some(val);
1337 }
1338 // Resolve a Deref+Field access through the pointee allocation's
1339 // per-allocation field tracking (e.g. `(*leaf).len` → the
1340 // `LeafNode.len` field value materialized by
1341 // `decompose_pointee_fields`). The viewed type (pointee) is part
1342 // of the key so reinterpret casts (e.g. `LeafNode` → `InternalNode`)
1343 // resolve to the right field view.
1344 if let Some(base_val) = self.local_value(place.local) {
1345 if let Some(alloc_id) = base_val.provenance_alloc_id() {
1346 let view_ty = crate::helpers::mir_utils::pointee_ty(base_val.ty)
1347 .unwrap_or(base_val.ty);
1348 if let Some(val) = self.load_value(alloc_id, view_ty, &field_path).cloned() {
1349 return Some(val);
1350 }
1351 }
1352 }
1353 }
1354 }
1355
1356 // Handle Deref + Field projections: follow the dereference chain to
1357 // get the pointee base, then apply field offsets.
1358 // E.g. `(*self).ptr` → Deref then Field(0).
1359 let mut base = self.local_value(place.local)?.clone();
1360 for proj in place.projection.iter() {
1361 match proj.kind() {
1362 ProjectionElem::Deref => {
1363 base.ty = place_ty;
1364 }
1365 ProjectionElem::Field(_field_idx, _) => {
1366 // Try to get the field value from the VM's field tracking
1367 if !field_path.is_empty() {
1368 if let Some(val) = self.field_value(place.local, &field_path).cloned() {
1369 return Some(val);
1370 }
1371 }
1372 // Fallback: return the base with updated type info
1373 base.ty = place_ty;
1374 }
1375 _ => {}
1376 }
1377 }
1378
1379 // Fall back to type-level resolution for an Index access whose prefix is
1380 // empty or only Deref projections (`arr[i]` / `(*slice)[i]`). The
1381 // projection is non-empty here (the empty case returned above).
1382 let proj = place.projection.last().expect("non-empty projection");
1383 let prefix_is_deref = place.projection[..place.projection.len() - 1]
1384 .iter()
1385 .all(|p| matches!(p.kind(), ProjectionElem::Deref));
1386 if let ProjectionElem::Index(local) = proj {
1387 if prefix_is_deref {
1388 if let Some(ref prov) = base.provenance {
1389 let alloc_id = prov.alloc_id;
1390 // The base's type may have been overwritten to the
1391 // element type by the Deref strip above; recover the
1392 // pointee element type from the base local's declared
1393 // type (`&[u8]` → `u8`, `&[T; N]` → `T`).
1394 let decl_ty = self.body().local_decls[place.local].ty;
1395 let inner_ty = match decl_ty.kind() {
1396 rustc_middle::ty::TyKind::Array(inner, _) => *inner,
1397 rustc_middle::ty::TyKind::Ref(_, inner, _) => {
1398 match inner.kind() {
1399 rustc_middle::ty::TyKind::Slice(e) => *e,
1400 _ => return Some(base.clone()),
1401 }
1402 }
1403 rustc_middle::ty::TyKind::Slice(e) => *e,
1404 _ => return Some(base.clone()),
1405 };
1406 let elem_sz = self.size_of_ty(inner_ty) as usize;
1407 let step = elem_sz.max(1);
1408 if let Some(index_val) = self.local_value(*local) {
1409 // `arr[i]` = the byte at `i * size_of(elem)`; the array
1410 // model resolves symbolic indices via `select` directly.
1411 let offset = Int::mul(
1412 self.z3_ctx,
1413 &[&index_val.z3_term, &Int::from_u64(self.z3_ctx, step as u64)],
1414 );
1415 // Byte-level tracking only exists once some byte of the
1416 // allocation has been written; otherwise fall through to
1417 // the field→byte materialization below. Even when the
1418 // allocation has a byte array, the byte may still read
1419 // `UNINIT` (e.g. a struct whose fields were written in
1420 // the value layer only), so also fall through to the
1421 // field→byte materialization in that case.
1422 if self.units[alloc_id.0].content.byte_array.is_some() {
1423 let term = self.byte_read(alloc_id, &offset);
1424 let is_uninit = offset
1425 .simplify()
1426 .as_u64()
1427 .map(|off| !self.is_byte_init(alloc_id, off as usize))
1428 .unwrap_or(false);
1429 if !is_uninit {
1430 return Some(VmValue {
1431 z3_term: term,
1432 ty: place_ty,
1433 provenance: None,
1434 facts: ValueFacts::default(),
1435 source: ValueSource::None,
1436 });
1437 }
1438 }
1439 // Field→byte direction of the cast cross-view
1440 // materialization: the buffer was reinterpreted from a
1441 // struct whose scalar fields were written in the value
1442 // layer, so read the byte back out of the field value.
1443 if let Some(off) = offset.simplify().as_u64() {
1444 if let Some(ty) = self.alloc(alloc_id).element_ty.as_ty() {
1445 if let Some(b) = self.byte_from_field(alloc_id, ty, off as usize) {
1446 return Some(VmValue {
1447 z3_term: b,
1448 ty: place_ty,
1449 provenance: None,
1450 facts: ValueFacts::default(),
1451 source: ValueSource::None,
1452 });
1453 }
1454 }
1455 }
1456 }
1457 }
1458 }
1459 return Some(base.clone());
1460 }
1461 // Any other trailing projection (Deref/Field/Downcast/…): the loop above
1462 // already traced Deref/Field and set `base.ty`, so return the base with
1463 // the place type to propagate provenance.
1464 let mut val = base;
1465 val.ty = place_ty;
1466 Some(val)
1467 }
1468
1469 /// Create an unknown value for a place.
1470 ///
1471 /// The value carries no `non_null` (or any other) assumption: a raw pointer
1472 /// whose provenance was lost may still be null, so assuming non-null here
1473 /// would let `NonNull`/null-guard checks pass unsoundly.
1474 pub(crate) fn unknown_value_for_place(&self, place: &Place<'tcx>) -> VmValue<'z3, 'tcx> {
1475 let ty = place.ty(self.body(), self.tcx).ty;
1476 VmValue::new(self.fresh_int("unknown"), ty)
1477 }
1478}