Skip to main content

rapx/verify/vm/
memory.rs

1//! Symbolic memory model for the VM.
2
3#[cfg(rapx_const_ext)]
4use rustc_middle::ty::consts::ConstExt;
5use rustc_middle::{
6    mir::{Local, Place, ProjectionElem},
7    ty::{Ty, TyKind},
8};
9use z3::{Context, Sort, ast::{Array, Ast, Bool, Int}};
10
11use super::state::{AllocId, AllocKind, Allocation, MemoryContent, MemoryUnit, Provenance, ValueFacts, ValueSource, VmState, VmValue};
12
13impl<'z3, 'tcx> VmState<'z3, 'tcx> {
14    pub(crate) fn address_of_place(&mut self, place: &Place<'tcx>) -> Option<VmValue<'z3, 'tcx>> {
15        self.ensure_local_allocation(place.local);
16
17        let zero = Int::from_u64(self.z3_ctx, 0);
18
19        if place.projection.is_empty() {
20            let base_addr = self.local_address(place.local);
21            let ty = self.body().local_decls[place.local].ty;
22            // Prefer the local's value provenance over the stack-allocation
23            // provenance. For Box/Vec parameters, the value tracks the heap
24            // allocation while slots tracks the stack location.
25            let provenance = self
26                .local_value(place.local)
27                .and_then(|v| v.provenance.clone())
28                .or_else(|| {
29                    self.current_frame.local_alloc
30                        .get(&place.local)
31                        .copied()
32                        .map(|alloc_id| Provenance {
33                            alloc_id,
34                            offset: zero,
35                            offset_kind: None,
36                        })
37                });
38            return Some(VmValue {
39                z3_term: base_addr,
40                ty,
41                provenance,
42                facts: ValueFacts::default(),
43                source: ValueSource::None,
44            });
45        }
46
47        let mut term = self.local_address(place.local);
48        let mut provenance: Option<Provenance<'z3>> = self
49            .current_frame.local_alloc
50            .get(&place.local)
51            .copied()
52            .map(|alloc_id| Provenance {
53                alloc_id,
54                offset: zero.clone(),
55                offset_kind: None,
56            });
57        let mut current_ty = self.body().local_decls[place.local].ty;
58        let mut field_path: Vec<usize> = Vec::new();
59        let mut view_ty = current_ty;
60
61        for proj in place.projection.iter() {
62            let mut handled = false;
63            if let ProjectionElem::Index(local) = proj {
64                // The element stride is the *element* size, not the container
65                // size: peel `[T; N]` / `[T]` down to `T` (mirroring state.rs's
66                // `Index` arm).  Keep `max(1)` so the stride matches the array
67                // allocation's `elem_size` (`N·max(1)`), letting the SMT cancel
68                // the factor for `idx + 1 <= N`.
69                let elem_ty = match current_ty.kind() {
70                    TyKind::Array(e, _) | TyKind::Slice(e) => *e,
71                    _ => current_ty,
72                };
73                let elem_sz = Int::from_u64(self.z3_ctx, self.size_of_ty(elem_ty).max(1));
74                if let Some(val) = self.local_value(local) {
75                    if let Some(idx) = val.z3_term.simplify().as_u64() {
76                        let scaled = Int::mul(self.z3_ctx, &[&Int::from_u64(self.z3_ctx, idx), &elem_sz]);
77                        term = Int::add(self.z3_ctx, &[&term, &scaled]);
78                        if let Some(ref mut prov) = provenance {
79                            prov.offset = Int::add(self.z3_ctx, &[&prov.offset, &scaled]);
80                        }
81                        handled = true;
82                    }
83                }
84                if !handled {
85                    let idx = self.fresh_int("idx");
86                    let scaled = Int::mul(self.z3_ctx, &[&idx, &elem_sz]);
87                    term = Int::add(self.z3_ctx, &[&term, &scaled]);
88                    if let Some(ref mut prov) = provenance {
89                        prov.offset = Int::add(self.z3_ctx, &[&prov.offset, &scaled]);
90                    }
91                }
92                continue;
93            }
94            match proj.kind() {
95                ProjectionElem::Field(field_idx, _) => {
96                    let fidx = field_idx.as_usize();
97                    field_path.push(fidx);
98                    let field_offset = self.field_offset_in_bytes(current_ty, fidx);
99                    let field_off = Int::from_u64(self.z3_ctx, field_offset);
100                    // Advance `current_ty` to the field's type so that subsequent
101                    // projections resolve their offsets against the right layout.
102                    let field_ty = match current_ty.kind() {
103                        TyKind::Adt(adt_def, substs) => {
104                            let variant = adt_def.non_enum_variant();
105                            variant
106                                .fields
107                                .get(rustc_abi::FieldIdx::from_usize(fidx))
108                                .map(|f| crate::helpers::mir_utils::field_ty(self.tcx, f, substs))
109                                .unwrap_or(current_ty)
110                        }
111                        _ => current_ty,
112                    };
113                    // A DST slice field (e.g. `CStr { inner: [u8] }`) carries its
114                    // own allocation so its length stays symbolic.  Prefer that
115                    // allocation over the parent struct's byte-offset address,
116                    // otherwise `&raw const self.inner` collapses the slice length
117                    // to the struct's own (minimal) size.
118                    let field_replacement = match field_ty.kind() {
119                        TyKind::Slice(_) => self
120                            .field_value(place.local, &field_path)
121                            .and_then(|fv| fv.provenance.clone().map(|p| (fv.z3_term.clone(), p))),
122                        TyKind::Array(..) => {
123                            // An array field decomposed into its own allocation by
124                            // `decompose_pointee_fields` (e.g. `keys: [MaybeUninit<K>; N]`)
125                            // carries a concrete element count. Prefer that allocation
126                            // over the parent struct's byte-offset address so
127                            // `&(*leaf).keys` keeps `len = N` for downstream InBound.
128                            let alloc = provenance.as_ref().map(|p| p.alloc_id);
129                            alloc.and_then(|a| {
130                                self.units[a.0].content.values
131                                    .get(&(view_ty, field_path.clone()))
132                                    .and_then(|fv| {
133                                        fv.provenance.clone().map(|p| (fv.z3_term.clone(), p))
134                                    })
135                            })
136                        }
137                        _ => None,
138                    };
139                    match field_replacement {
140                        Some((fv_term, fv_prov)) => {
141                            term = fv_term;
142                            provenance = Some(fv_prov);
143                        }
144                        None => {
145                            term = Int::add(self.z3_ctx, &[&term, &field_off]);
146                            if let Some(ref mut prov) = provenance {
147                                prov.offset = Int::add(self.z3_ctx, &[&prov.offset, &field_off]);
148                            }
149                        }
150                    }
151                    current_ty = field_ty;
152                }
153                ProjectionElem::Deref => {
154                    field_path.clear();
155                    let pointed = self.local_value(place.local)?;
156                    term = pointed.z3_term.clone();
157                    provenance = pointed.provenance.clone();
158                    // For fat pointers (aggregates without provenance),
159                    // use the first field's provenance (the data pointer).
160                    if provenance.is_none() && matches!(pointed.ty.kind(), TyKind::RawPtr(..)) {
161                        if let Some(field0) = self.field_value(place.local, &[0]) {
162                            provenance = field0.provenance.clone();
163                        }
164                    }
165                    if let TyKind::Ref(_, deref_ty, _) = current_ty.kind() {
166                        current_ty = *deref_ty;
167                    } else if let TyKind::RawPtr(deref_ty, _) = current_ty.kind() {
168                        current_ty = *deref_ty;
169                    }
170                    view_ty = current_ty;
171                }
172                _ => {
173                    return None;
174                }
175            }
176        }
177
178        let ty = place.ty(self.body(), self.tcx).ty;
179        Some(VmValue {
180            z3_term: term,
181            ty,
182            provenance,
183            facts: ValueFacts::default(),
184            source: ValueSource::None,
185        })
186    }
187
188    /// Lazily create a stack allocation for a MIR local if one doesn't exist.
189    pub(crate) fn ensure_local_allocation(&mut self, local: Local) {
190        if self.current_frame.local_alloc.contains_key(&local) {
191            return;
192        }
193        let ty = self.body().local_decls[local].ty;
194        let align = self.align_sym(ty);
195        // Generate the base address symbol directly (the address lives only in
196        // `Allocation::base` now; `local_address` reads it back from there).
197        let name = format!("addr__{}", local.as_usize());
198        let base = Int::new_const(self.z3_ctx, name.as_str());
199        let id = AllocId(self.units.len());
200        // For arrays, track the element type (not the array type) so that
201        // len() computes `size / elem_size` correctly.  When the element size
202        // is unknown (a generic `T`), `size_of::<[T; N]>()` collapses to 0, so
203        // instead record the element count `N` as a symbolic term — this keeps
204        // `len() = size / elem_size` equal to `N`, letting downstream
205        // InBound checks (e.g. `get_unchecked_mut(idx)` where `idx < N`) be
206        // discharged against the loop's `idx < N` path condition.
207        let (size_term, element_ty, slice_len) = match ty.kind() {
208            TyKind::Array(elem, const_len) => {
209                // Concrete element size (`.max(1)` so a generic `T` collapses to
210                // 1 byte, keeping `len() = size / elem_size` equal to the
211                // symbolic element count `N`).  Deliberately *not* the symbolic
212                // `size_sym(elem)`: materializing `sizeof_MaybeUninit<T>` here
213                // would let `access_bytes` read it back as an unbounded access
214                // size, breaking `Allocated(&mut MaybeUninit<T>, T, 1)` against
215                // the iterator provenance (array_try_from_fn_ext).
216                let elem_size = self.size_of_ty(*elem).max(1);
217                let n_term = self.const_len_term(const_len);
218                let size = match n_term.as_u64() {
219                    Some(n) => Int::from_u64(self.z3_ctx, n.saturating_mul(elem_size)),
220                    None => Int::mul(self.z3_ctx, &[&n_term, &Int::from_u64(self.z3_ctx, elem_size)]),
221                };
222                (size, Some(*elem), Some(n_term))
223            }
224            _ => {
225                let size = self
226                    .struct_size_sym(ty)
227                    .unwrap_or_else(|| self.size_sym(ty));
228                (size, Some(ty), None)
229            }
230        };
231        let mut alloc = Allocation::new(base, size_term, align, element_ty, AllocKind::Object);
232        if let Some(len) = slice_len {
233            alloc.set_slice_len(len);
234        }
235        self.units.push(MemoryUnit {
236            allocation: alloc,
237            content: MemoryContent::default(),
238        });
239        self.current_frame.local_alloc.insert(local, id);
240    }
241
242    pub(crate) fn field_offset_in_bytes(&self, ty: Ty<'tcx>, field_idx: usize) -> u64 {
243        crate::helpers::mir_utils::field_offset_in_bytes(
244            self.tcx,
245            self.current_frame.current_def_id,
246            ty,
247            field_idx,
248        )
249    }
250
251    pub(crate) fn size_of_ty(&self, ty: Ty<'tcx>) -> u64 {
252        crate::helpers::mir_utils::layout_of_ty(self.tcx, self.current_frame.current_def_id, ty)
253            .map(|l| l.size.bytes())
254            .unwrap_or(0)
255    }
256
257    pub(crate) fn align_of_ty(&self, ty: Ty<'tcx>) -> u64 {
258        crate::helpers::mir_utils::layout_of_ty(self.tcx, self.current_frame.current_def_id, ty)
259            .map(|l| l.align.abi.bytes())
260            .unwrap_or(1)
261    }
262
263    pub(crate) fn allocation_size(&self, alloc_id: AllocId) -> &Int<'z3> {
264        &self.alloc(alloc_id).size
265    }
266
267    pub(crate) fn allocation_base(&self, alloc_id: AllocId) -> &Int<'z3> {
268        &self.alloc(alloc_id).base
269    }
270
271    /// Get the element size (in bytes) for a pointer type, peeling
272    /// through `*const T`, `*mut T`, `&T`, and `&[T]` to find `size_of(T)`.
273    pub(crate) fn pointee_elem_size(&self, ty: Ty<'tcx>) -> u64 {
274        let inner = match ty.kind() {
275            TyKind::RawPtr(inner_ty, _) | TyKind::Ref(_, inner_ty, _) => *inner_ty,
276            _ => ty,
277        };
278        match inner.kind() {
279            TyKind::Slice(elem) => self.size_of_ty(*elem),
280            _ => self.size_of_ty(inner),
281        }
282    }
283
284    /// Element size of `ty` as a symbolic Z3 term.  For concrete types this is
285    /// the constant byte size; for a generic type whose `size_of` is unknown
286    /// (an unconstrained `T`) it is a single reusable symbolic constant with
287    /// `>= 0` (so `T` may be a ZST).  Using the same constant everywhere (ptr
288    /// strides, access counts, allocation sizes) lets SMT cancel the factor in
289    /// `InBound`.
290    pub(crate) fn size_sym(&mut self, ty: Ty<'tcx>) -> Int<'z3> {
291        let ty = peel_slice_elem(ty);
292        let size = self.size_of_ty(ty);
293        if size > 0 || !crate::helpers::mir_utils::ty_has_type_param(ty) {
294            return Int::from_u64(self.z3_ctx, size);
295        }
296        if let Some(s) = self.constraints.term_caches.sizes.get(&ty) {
297            return s.clone();
298        }
299        let s = self.fresh_int(&format!("sizeof_{ty}"));
300        self.constraints.term_caches.sizes.insert(ty, s.clone());
301        let zero = Int::from_u64(self.z3_ctx, 0);
302        self.constraints.assertions.push(s.ge(&zero));
303        s
304    }
305
306    /// The array length `N` as a Z3 term (concrete value or symbolic const
307    /// generic).  The symbolic name mirrors `value_of_operand`'s formatting so it
308    /// is *identical* to the `const N` term appearing in path conditions.
309    fn const_len_term(&self, const_len: &rustc_middle::ty::Const<'tcx>) -> Int<'z3> {
310        match const_len.try_to_target_usize(self.tcx) {
311            Some(v) => Int::from_u64(self.z3_ctx, v),
312            None => {
313                let const_text = format!("Ty({:?}, {:?})", self.tcx.types.usize, const_len);
314                let name = format!("const_{}", const_text.replace([':', '#', ' '], "_"));
315                Int::new_const(self.z3_ctx, name.as_str())
316            }
317        }
318    }
319
320    /// Read-only sibling of [`size_sym`](Self::size_sym): returns the symbolic
321    /// size for `ty`, falling back to `1` when the symbolic constant has not
322    /// been created yet (e.g. a checker invoked before the exec phase created
323    /// it).  Non-ZST concrete types return their constant byte size; a concrete
324    /// ZST or a not-yet-created generic constant falls back to `1` — a non-zero
325    /// element size keeps `size / elem_size` derivations from dividing by zero.
326    pub(crate) fn size_sym_read(&self, ty: Ty<'tcx>) -> Int<'z3> {
327        let ty = peel_slice_elem(ty);
328        let size = self.size_of_ty(ty);
329        if size > 0 {
330            return Int::from_u64(self.z3_ctx, size);
331        }
332        self.constraints.term_caches.sizes
333            .get(&ty)
334            .cloned()
335            .unwrap_or_else(|| Int::from_u64(self.z3_ctx, 1))
336    }
337
338    /// The *symbolic* element-size term of `alloc_id`'s element type, when it is
339    /// a generic type parameter (so it may be `0` for a ZST or `≥ 1` for a
340    /// non-ZST).  Returns `None` for concrete element types, where the size is a
341    /// known constant and no case split is needed.
342    pub(crate) fn generic_elem_size(&self, alloc_id: AllocId) -> Option<Int<'z3>> {
343        let elem_ty = self.alloc(alloc_id).element_ty.as_ty()?;
344        if !crate::helpers::mir_utils::ty_has_type_param(elem_ty) {
345            return None;
346        }
347        let s = self.size_sym_read(elem_ty);
348        if s.simplify().as_u64().is_some() {
349            return None;
350        }
351        Some(s)
352    }
353
354    /// Alignment of `ty` as a symbolic Z3 term.  For a concrete type this is
355    /// the constant byte alignment; for a generic type it is a reusable
356    /// symbolic constant `align_T` with `>= 1`, lower-bounded by the trait
357    /// bounds' minimum alignment, and linked to the element size by the layout
358    /// constraint `sizeof_T % align_T == 0` (a type's size is always a multiple
359    /// of its alignment).  For a generic struct, its alignment is additionally
360    /// constrained to be a multiple of each field's alignment, so a field
361    /// pointer (`(*node).value`) inherits the container's alignment.
362    pub(crate) fn align_sym(&mut self, ty: Ty<'tcx>) -> Int<'z3> {
363        let ty = peel_slice_elem(ty);
364        // An array's alignment equals its element's alignment.
365        if let TyKind::Array(elem, _) = ty.kind() {
366            return self.align_sym(*elem);
367        }
368        let align = self.align_of_ty(ty);
369        if align > 1 || !crate::helpers::mir_utils::ty_has_type_param(ty) {
370            return Int::from_u64(self.z3_ctx, align);
371        }
372        if let Some(a) = self.constraints.term_caches.aligns.get(&ty) {
373            return a.clone();
374        }
375        let a = self.fresh_int(&format!("align_{ty}"));
376        self.constraints.term_caches.aligns.insert(ty, a.clone());
377        let one = Int::from_u64(self.z3_ctx, 1);
378        let zero = Int::from_u64(self.z3_ctx, 0);
379        self.constraints.assertions.push(a.ge(&one));
380        // Lower bound from the trait bounds (0 for an unconstrained `T`): any
381        // implementor is at least this aligned.
382        let min_a =
383            crate::helpers::mir_utils::min_align_of_generic_param(self.tcx, self.current_frame.current_def_id, ty);
384        if min_a > 1 {
385            self.constraints.assertions
386                .push(a.ge(&Int::from_u64(self.z3_ctx, min_a)));
387        }
388        // Upper bound from the trait bounds (0 for an unconstrained `T`): any
389        // implementor is at most this aligned, which is what lets a cross-cast
390        // from a *more* aligned source (`&[U]` -> `*const T`) be discharged.
391        let max_a =
392            crate::helpers::mir_utils::max_align_of_generic_param(self.tcx, self.current_frame.current_def_id, ty);
393        if max_a > 0 {
394            self.constraints.assertions
395                .push(a.le(&Int::from_u64(self.z3_ctx, max_a)));
396        }
397        // A struct's alignment is a multiple of each field's alignment (both
398        // are powers of two).  Pointer fields have a *concrete* alignment, so
399        // this terminates even for recursively-defined containers.
400        if let TyKind::Adt(adt_def, substs) = ty.kind() {
401            if !adt_def.is_enum() {
402                let variant = adt_def.non_enum_variant();
403                for field in variant.fields.iter() {
404                    let field_ty = crate::helpers::mir_utils::field_ty(self.tcx, field, substs);
405                    let field_align = self.align_sym(field_ty);
406                    self.constraints.assertions.push(a.rem(&field_align)._eq(&zero));
407                }
408                // A struct's size is at least the sum of its fields (padding may
409                // add more).  This relates the symbolic `sizeof_Struct` constant
410                // to the field-sum that `size_of::<Struct>()` lowers to (e.g.
411                // `SIZE<LeafNode> + SIZE<[...; 12]>` for `InternalNode`), so
412                // `Allocated(p, u8, layout.size)` is discharged against the
413                // allocation size.
414                if let Some(sum) = self.struct_size_sym(ty) {
415                    let size = self.size_sym(ty);
416                    self.constraints.assertions.push(size.ge(&sum));
417                }
418            }
419        }
420        // Layout invariant: a type's size is a multiple of its alignment.
421        let size = self.size_sym(ty);
422        self.constraints.assertions.push(size.rem(&a)._eq(&zero));
423        a
424    }
425
426    /// Read-only sibling of [`align_sym`](Self::align_sym): returns the
427    /// symbolic alignment for `ty`, falling back to the trait bounds' minimum
428    /// alignment when the constant has not been created yet (e.g. a generic `U`
429    /// that only appears in a cast/contract, never as an allocation element
430    /// type).  Concrete types return their constant alignment.
431    pub(crate) fn align_sym_read(&self, ty: Ty<'tcx>) -> Int<'z3> {
432        let ty = peel_slice_elem(ty);
433        // An array's alignment equals its element's alignment.
434        if let TyKind::Array(elem, _) = ty.kind() {
435            return self.align_sym_read(*elem);
436        }
437        let align = self.align_of_ty(ty);
438        if align > 1 {
439            return Int::from_u64(self.z3_ctx, align);
440        }
441        if let Some(a) = self.constraints.term_caches.aligns.get(&ty) {
442            return a.clone();
443        }
444        let min_a =
445            crate::helpers::mir_utils::min_align_of_generic_param(self.tcx, self.current_frame.current_def_id, ty);
446        Int::from_u64(self.z3_ctx, min_a.max(1))
447    }
448
449    /// Size of a struct/ADT as the *sum* of its fields' sizes (each via
450    /// [`size_sym`](Self::size_sym)).  This lower-bounds the real layout so a
451    /// field reference (`Allocated(&alloc)`) can be discharged against the
452    /// struct allocation (`sizeof_A <= 8 + 8 + sizeof_A`).  Returns `None` for
453    /// non-ADT or enum types.
454    pub(crate) fn struct_size_sym(&mut self, ty: Ty<'tcx>) -> Option<Int<'z3>> {
455        let TyKind::Adt(adt_def, substs) = ty.kind() else {
456            return None;
457        };
458        if adt_def.is_enum() {
459            return None;
460        }
461        let concrete = self.size_of_ty(ty);
462        if concrete > 0 {
463            return Some(Int::from_u64(self.z3_ctx, concrete));
464        }
465        let variant = adt_def.non_enum_variant();
466        let mut total = Int::from_u64(self.z3_ctx, 0);
467        for field in variant.fields.iter() {
468            let field_ty = crate::helpers::mir_utils::field_ty(self.tcx, field, substs);
469            let field_size = self
470                .struct_size_sym(field_ty)
471                .unwrap_or_else(|| self.size_sym(field_ty));
472            total = Int::add(self.z3_ctx, &[&total, &field_size]);
473        }
474        Some(total)
475    }
476
477    // ── Per-byte state (`MemoryContent::byte_array`) ───────────────────
478
479    /// The shared `UNINIT` sentinel (≥ 256, outside the `u8` range).
480    fn uninit_byte(&self) -> Int<'z3> {
481        self.constraints
482            .term_caches
483            .uninit_byte
484            .clone()
485            .expect("uninit_byte is initialized in VmState::new")
486    }
487
488    /// A fresh `Array<Int, Int>` whose every offset reads `UNINIT`.
489    fn fresh_byte_array(&self) -> Array<'z3> {
490        // `const_array(domain, value)` takes the *index* sort; the array sort is
491        // inferred as `Array<domain, value_sort>` (here `Array<Int, Int>`).
492        Array::const_array(self.z3_ctx, &Sort::int(self.z3_ctx), &self.uninit_byte())
493    }
494
495    /// Read `byte[i]` at a (possibly symbolic) offset; unwritten offsets read `UNINIT`.
496    ///
497    /// The result is simplified so a `select(store(…), i)` chain (built up by
498    /// repeated `byte_write` / `copy_byte_tracking`) collapses to its constant
499    /// byte value when `i` is concrete, rather than leaking the nested
500    /// `select`/`store` expression into downstream SMT obligations.
501    pub(crate) fn byte_read(&self, alloc_id: AllocId, i: &Int<'z3>) -> Int<'z3> {
502        match &self.units[alloc_id.0].content.byte_array {
503            Some(arr) => arr.select(i).as_int().expect("byte array range is Int").simplify(),
504            None => self.uninit_byte(),
505        }
506    }
507
508    /// Write `byte[i] = v`.
509    pub(crate) fn byte_write(&mut self, alloc_id: AllocId, i: &Int<'z3>, v: &Int<'z3>) {
510        let arr = self.units[alloc_id.0].content.byte_array.clone();
511        let updated = match arr {
512            Some(a) => a.store(i, v),
513            None => self.fresh_byte_array().store(i, v),
514        };
515        let unit = &mut self.units[alloc_id.0];
516        unit.content.byte_array = Some(updated);
517        if let Some(off) = i.as_u64() {
518            unit.content.byte_written.insert(off as usize);
519        }
520    }
521
522    /// Record a per-byte symbolic value at a concrete offset in an allocation.
523    pub(crate) fn record_byte_value(&mut self, alloc_id: AllocId, offset: usize, term: Int<'z3>) {
524        self.byte_write(alloc_id, &Int::from_u64(self.z3_ctx, offset as u64), &term);
525    }
526
527    /// Mark a byte as initialized (written) with an unknown value.
528    pub(crate) fn mark_byte_init(&mut self, alloc_id: AllocId, offset: usize) {
529        let unknown = self.fresh_int("byte_unknown");
530        self.byte_write(alloc_id, &Int::from_u64(self.z3_ctx, offset as u64), &unknown);
531    }
532
533    /// Whether a byte at a concrete offset was written (`select != UNINIT`).
534    pub(crate) fn is_byte_init(&self, alloc_id: AllocId, offset: usize) -> bool {
535        let v = self.byte_read(alloc_id, &Int::from_u64(self.z3_ctx, offset as u64));
536        !v.simplify().eq(&self.uninit_byte())
537    }
538
539    /// Whether a byte at a concrete offset is known NUL.
540    pub(crate) fn is_byte_nul(&self, alloc_id: AllocId, offset: usize) -> bool {
541        self.byte_read(alloc_id, &Int::from_u64(self.z3_ctx, offset as u64))
542            .simplify()
543            .as_u64()
544            == Some(0)
545    }
546
547    /// Whether a byte at a concrete offset is known non-NUL.
548    pub(crate) fn is_byte_non_nul(&self, alloc_id: AllocId, offset: usize) -> bool {
549        self.byte_read(alloc_id, &Int::from_u64(self.z3_ctx, offset as u64))
550            .simplify()
551            .as_u64()
552            .is_some_and(|v| v != 0)
553    }
554
555    /// Enumerate `(offset, term)` pairs for the *written* bytes of an allocation,
556    /// in ascending offset order.
557    pub(crate) fn alloc_byte_values(&self, alloc_id: AllocId) -> Vec<(usize, Int<'z3>)> {
558        let mut offs: Vec<usize> = self.units[alloc_id.0]
559            .content
560            .byte_written
561            .iter()
562            .copied()
563            .collect();
564        offs.sort_unstable();
565        offs.into_iter()
566            .map(|off| (off, self.byte_read(alloc_id, &Int::from_u64(self.z3_ctx, off as u64))))
567            .collect()
568    }
569
570    /// Offsets known to be NUL.
571    pub(crate) fn alloc_nul_offsets(&self, alloc_id: AllocId) -> Vec<usize> {
572        let mut offs: Vec<usize> = self.units[alloc_id.0]
573            .content
574            .byte_written
575            .iter()
576            .copied()
577            .filter(|&off| self.is_byte_nul(alloc_id, off))
578            .collect();
579        offs.sort_unstable();
580        offs
581    }
582
583    /// Offsets known to be non-NUL.
584    pub(crate) fn alloc_non_nul_offsets(&self, alloc_id: AllocId) -> Vec<usize> {
585        let mut offs: Vec<usize> = self.units[alloc_id.0]
586            .content
587            .byte_written
588            .iter()
589            .copied()
590            .filter(|&off| self.is_byte_non_nul(alloc_id, off))
591            .collect();
592        offs.sort_unstable();
593        offs
594    }
595
596    /// Copy the per-byte state (values + written-offset bound) of one allocation
597    /// to another, shifting by `src_offset` so `dst[i] = src[i + src_offset]`.
598    pub(crate) fn copy_byte_tracking(&mut self, src: AllocId, src_offset: usize, dst: AllocId) {
599        let written: Vec<usize> = self.units[src.0]
600            .content
601            .byte_written
602            .iter()
603            .copied()
604            .filter(|&off| off >= src_offset)
605            .collect();
606        for off in written {
607            let v = self.byte_read(src, &Int::from_u64(self.z3_ctx, off as u64));
608            self.byte_write(dst, &Int::from_u64(self.z3_ctx, (off - src_offset) as u64), &v);
609        }
610    }
611
612    // ── Per-allocation fields (`MemoryContent::values`) ──────────────────
613
614    /// The value at a field offset *within an allocation* viewed as `view_ty`.
615    ///
616    /// This is the memory-contents (typed-value) layer ([`MemoryContent::values`]): the
617    /// single source of truth for field values. [`Self::field_value`] is the
618    /// local-facing wrapper that resolves a MIR local's backing allocation and
619    /// declared type, then reads this same layer. The viewed type is part of the
620    /// key so reinterpret casts (`LeafNode` ↔ `InternalNode`) resolve to the
621    /// right field view.
622    pub(crate) fn load_value(
623        &self,
624        alloc_id: AllocId,
625        view_ty: Ty<'tcx>,
626        path: &[usize],
627    ) -> Option<&VmValue<'z3, 'tcx>> {
628        self.units[alloc_id.0]
629            .content
630            .values
631            .get(&(view_ty, path.to_vec()))
632    }
633
634    /// Store a value at a field offset *within an allocation* viewed as `view_ty`.
635    ///
636    /// This is the write counterpart to [`Self::load_value`] on the same
637    /// memory-contents layer ([`MemoryContent::values`]); the viewed type is part of the
638    /// key so reinterpret casts resolve to the right field view.
639    pub(crate) fn store_value(
640        &mut self,
641        alloc_id: AllocId,
642        view_ty: Ty<'tcx>,
643        path: Vec<usize>,
644        value: VmValue<'z3, 'tcx>,
645    ) {
646        self.units[alloc_id.0]
647            .content
648            .values
649            .insert((view_ty, path), value);
650    }
651
652    /// Encode the UTF-8 validity DFA over this allocation's tracked bytes.
653    /// Returns `None` when no bytes are tracked (validity is trivially
654    /// satisfied), so callers can short-circuit to "proved".
655    pub(crate) fn utf8_validity(&self, alloc_id: AllocId) -> Option<Bool<'z3>> {
656        let byte_pairs = self.alloc_byte_values(alloc_id);
657        if byte_pairs.is_empty() {
658            return None;
659        }
660        let bytes: Vec<Int<'z3>> = byte_pairs.into_iter().map(|(_, t)| t).collect();
661        Some(utf8_validity_dfa(self.z3_ctx, &bytes))
662    }
663}
664
665/// Build the boolean expression "`bytes` form a valid UTF-8 sequence".
666///
667/// Encodes the UTF-8 DFA over the per-byte Z3 terms: every byte is ASCII, a
668/// continuation byte, or a valid lead byte, and a `k`-byte lead must be
669/// followed by exactly `k-1` continuation bytes.  Value-range refinements
670/// reject overlong encodings, surrogates (U+D800..=U+DFFF), and code points
671/// above U+10FFFF.
672fn utf8_validity_dfa<'z3>(z3_ctx: &'z3 Context, bytes: &[Int<'z3>]) -> Bool<'z3> {
673    let zero = Int::from_u64(z3_ctx, 0);
674    let one = Int::from_u64(z3_ctx, 1);
675    let two = Int::from_u64(z3_ctx, 2);
676    let three = Int::from_u64(z3_ctx, 3);
677
678    let c_0x80 = Int::from_u64(z3_ctx, 0x80);
679    let c_0xc0 = Int::from_u64(z3_ctx, 0xC0);
680    let c_0xc2 = Int::from_u64(z3_ctx, 0xC2);
681    let c_0xe0 = Int::from_u64(z3_ctx, 0xE0);
682    let c_0xf0 = Int::from_u64(z3_ctx, 0xF0);
683    let c_0xf5 = Int::from_u64(z3_ctx, 0xF5);
684    let c_0xa0 = Int::from_u64(z3_ctx, 0xA0);
685    let c_0x90 = Int::from_u64(z3_ctx, 0x90);
686    let c_0xed = Int::from_u64(z3_ctx, 0xED);
687    let c_0xf4 = Int::from_u64(z3_ctx, 0xF4);
688
689    let mut valid = Bool::from_bool(z3_ctx, true);
690    let mut state = zero.clone();
691    let mut lead = zero.clone();
692
693    for b in bytes {
694        let is_ascii = b.lt(&c_0x80);
695        let is_cont = b.ge(&c_0x80) & b.lt(&c_0xc0);
696        let is_2lead = b.ge(&c_0xc2) & b.lt(&c_0xe0);
697        let is_3lead = b.ge(&c_0xe0) & b.lt(&c_0xf0);
698        let is_4lead = b.ge(&c_0xf0) & b.lt(&c_0xf5);
699
700        let refine_3 =
701            (lead._eq(&c_0xe0).not() | b.ge(&c_0xa0)) & (lead._eq(&c_0xed).not() | b.lt(&c_0xa0));
702        let refine_4 =
703            (lead._eq(&c_0xf0).not() | b.ge(&c_0x90)) & (lead._eq(&c_0xf4).not() | b.lt(&c_0x90));
704
705        let valid_s0 = is_ascii.clone() | is_2lead.clone() | is_3lead.clone() | is_4lead.clone();
706        let valid_s1 = is_cont.clone();
707        let valid_s2 = is_cont.clone() & refine_3;
708        let valid_s3 = is_cont.clone() & refine_4;
709
710        let state0 = state._eq(&zero);
711        let state1 = state._eq(&one);
712        let state2 = state._eq(&two);
713
714        let byte_valid = Bool::ite(
715            &state0,
716            &valid_s0,
717            &Bool::ite(
718                &state1,
719                &valid_s1,
720                &Bool::ite(&state2, &valid_s2, &valid_s3),
721            ),
722        );
723
724        let new_state_s0 = Bool::ite(
725            &is_ascii,
726            &zero,
727            &Bool::ite(&is_2lead, &one, &Bool::ite(&is_3lead, &two, &three)),
728        );
729        let new_state_cont = Bool::ite(&state1, &zero, &Bool::ite(&state2, &one, &two));
730        let new_state = Bool::ite(&state0, &new_state_s0, &new_state_cont);
731
732        valid = valid & byte_valid;
733        let is_lead34 = is_3lead | is_4lead;
734        lead = Bool::ite(&(state0 & is_lead34), b, &lead);
735        state = new_state;
736    }
737
738    valid = valid & state._eq(&zero);
739    valid
740}
741
742/// Peel a slice type `[T]` to its element `T` (other types unchanged).
743fn peel_slice_elem(ty: Ty<'_>) -> Ty<'_> {
744    match ty.kind() {
745        TyKind::Slice(elem) => *elem,
746        _ => ty,
747    }
748}