Skip to main content

rapx/verify/property_checker/
cstr.rs

1//! ValidCStr property checking for the symbolic VM.
2
3use rustc_middle::mir::{Local, Operand, Rvalue, StatementKind};
4use z3::{Solver, ast::{Ast, Int}};
5
6use crate::verify::{
7    contract::{ContractExpr, Property, PropertyArg},
8    report::CheckResult,
9};
10use crate::helpers::mir_scan::Checkpoint;
11use crate::verify::vm::state::{AllocId, VmState};
12
13use super::PropertyChecker;
14
15impl PropertyChecker {
16    // ── check_valid_cstr ───────────────────────────────────────
17
18    pub(super) fn check_valid_cstr<'ctx, 'tcx>(&self, vm_state: &VmState<'ctx, 'tcx>, solver: &Solver<'ctx>,
19        checkpoint: &Checkpoint<'tcx>, property: &Property<'tcx>) -> CheckResult
20    {
21        let value = self.target_value(vm_state, checkpoint, property)
22            .or_else(|| {
23                checkpoint.destination.and_then(|d| vm_state.local_value(d).cloned())
24            });
25        let Some(value) = value else { return CheckResult::Unknown };
26
27        // If we have provenace, check liveness and byte-level tracking
28        if let Some(alloc_id) = value.provenance_alloc_id() {
29            if vm_state.alloc(alloc_id).dead {
30                return CheckResult::Failed;
31            }
32
33            // The allocation was asserted to be a null-terminated C string via
34            // a `ValidCStr` contract fact / struct invariant. Any sub-slice of
35            // it is therefore nul-terminated (it ends at the same nul byte),
36            // so the property holds without further byte-level reasoning.
37            // Follow `parent` so a `from_raw_parts` / slice-index
38            // sub-allocation rooted in a nul-terminated buffer also passes.
39            let mut root_id = alloc_id;
40            while let Some(parent_id) = vm_state.alloc(root_id).parent {
41                root_id = parent_id;
42            }
43            if vm_state.alloc(alloc_id).nul_terminated
44                || vm_state.alloc(root_id).nul_terminated
45            {
46                return CheckResult::Proved;
47            }
48
49            let alloc_size = vm_state.allocation_size(alloc_id).cloned();
50
51            // The `ValidCStr(p, n)` length argument is the exact byte length of
52            // the nul-terminated buffer.  Prefer it over the allocation size
53            // (which may be larger, e.g. a `Vec` with spare capacity).  A
54            // `Const` length is the `1` placeholder used for raw pointers
55            // (`from_ptr`), whose true length is `strlen(ptr) + 1` and is not
56            // expressible in the contract, so it is ignored.
57            let n_term = property.args().get(1).and_then(|a| match a {
58                PropertyArg::Expr(ContractExpr::Const(_)) => None,
59                a => self.resolve_arg_term(vm_state, checkpoint, a),
60            });
61            let buffer_size = n_term.or(alloc_size);
62
63            // Starting offset within the allocation (for pointer arithmetic like .add(2))
64            let start_offset = value.provenance.as_ref()
65                .and_then(|p| p.offset.as_u64())
66                .map(|v| v as usize)
67                .unwrap_or(0);
68
69            // 1. Try fast-path: concrete byte-level check from known_nul / known_non_nul
70            if let Some(r) = self.check_valid_cstr_from_known_nul(vm_state, alloc_id, start_offset) {
71                return r;
72            }
73
74            // 2. Try byte_value-based symbolic check via SMT
75            if let Some(size) = buffer_size {
76                if let Some(r) = self.check_valid_cstr_from_byte_values(vm_state, solver, alloc_id, &size) {
77                    return r;
78                }
79            }
80        }
81
82        // 3. MIR-level fallback: scan the body for constant byte assignments
83        //    (mirrors the legacy checker's approach for promoted constants)
84        if let Some(r) = self.check_valid_cstr_from_mir_constants(vm_state, checkpoint, property) {
85            return r;
86        }
87
88        // 4. Fallback: if the constructor requires strict NUL-termination
89        //    (from_bytes_with_nul_unchecked, from_vec_with_nul_unchecked)
90        //    and we can't verify all bytes, return Unknown.
91        let is_strict = checkpoint.callee.as_ref().map_or(false, |callee| {
92            let name = vm_state.tcx.def_path_str(*callee);
93            crate::helpers::api_classify::is_cstr_strict_constructor(&name)
94        });
95        if is_strict {
96            CheckResult::Unknown
97        } else {
98            CheckResult::Proved
99        }
100    }
101
102    /// Fast-path: check NUL termination using per-byte NUL/non-NUL knowledge.
103    /// This handles constant byte strings like `b"hello\0"` and aggregate initializers
104    /// where all element operands are constants.
105    /// `start_offset` is the byte offset within the allocation where the C string begins
106    /// (non-zero when pointer arithmetic like `.add(n)` is used).
107    fn check_valid_cstr_from_known_nul<'ctx, 'tcx>(
108        &self,
109        vm_state: &VmState<'ctx, 'tcx>,
110        alloc_id: AllocId,
111        start_offset: usize,
112    ) -> Option<CheckResult> {
113        // Collect all concrete offsets where we know what the byte is
114        let known_offsets: Vec<usize> = vm_state.alloc_nul_offsets(alloc_id)
115            .into_iter()
116            .chain(vm_state.alloc_non_nul_offsets(alloc_id))
117            .collect();
118
119        if known_offsets.is_empty() {
120            return None; // no byte-level info
121        }
122
123        let max_known = known_offsets.iter().max().copied().unwrap_or(0);
124
125        // Find the NUL byte at or after start_offset
126        let nul_offsets: Vec<usize> = vm_state.alloc_nul_offsets(alloc_id)
127            .into_iter()
128            .filter(|off| *off >= start_offset && *off <= max_known)
129            .collect();
130
131        if nul_offsets.is_empty() {
132            // No NUL in tracked range — might be in untracked region.
133            if let Some(size) = vm_state.allocation_size(alloc_id) {
134                if let Some(size_val) = size.as_u64() {
135                    if max_known + 1 < size_val as usize {
136                        return None;
137                    }
138                }
139            }
140            return Some(CheckResult::Failed);
141        }
142
143        // Check if there's exactly one NUL at the end of the known range
144        let min_nul = nul_offsets.iter().min().copied().unwrap_or(0);
145
146        // All offsets between start_offset and min_nul must be known non-NUL
147        for off in start_offset..min_nul {
148            if vm_state.is_byte_nul(alloc_id, off) {
149                // Interior NUL found before the first NUL after start_offset
150                return Some(CheckResult::Failed);
151            }
152            if !vm_state.is_byte_non_nul(alloc_id, off) {
153                // Unknown byte — can't prove valid
154                return None;
155            }
156        }
157
158        // If multiple NUL offsets exist and the first NUL is not at the last
159        // tracked position, there is an interior NUL → invalid C string.
160        if nul_offsets.len() > 1 && min_nul < max_known {
161            return Some(CheckResult::Failed);
162        }
163
164        // All bytes between start_offset and the first NUL are known non-NUL,
165        // and the NUL itself is known. This is a valid C string for the tracked range.
166        Some(CheckResult::Proved)
167    }
168
169    /// Check NUL termination using per-byte symbolic values tracked in `bytes`.
170    /// Uses the SMT solver to verify that a NUL-terminated byte sequence is possible.
171    fn check_valid_cstr_from_byte_values<'ctx, 'tcx>(
172        &self,
173        vm_state: &VmState<'ctx, 'tcx>,
174        solver: &Solver<'ctx>,
175        alloc_id: AllocId,
176        alloc_size: &Int<'ctx>,
177    ) -> Option<CheckResult> {
178        let byte_pairs = vm_state.alloc_byte_values(alloc_id);
179        if byte_pairs.is_empty() {
180            return None;
181        }
182
183        let zero = Int::from_u64(vm_state.ctx, 0);
184        let size_u64 = alloc_size.as_u64();
185
186        if size_u64.is_none() {
187            return None; // symbolic-size allocations need different handling
188        }
189
190        for &(nul_off, nul_term) in &byte_pairs {
191            solver.push();
192            solver.assert(&nul_term._eq(&zero));
193
194            for &(off, term) in &byte_pairs {
195                if off < nul_off {
196                    solver.assert(&term._eq(&zero).not());
197                }
198            }
199
200            let r = solver.check();
201            solver.pop(1);
202
203            if r == z3::SatResult::Sat {
204                let mut interior_safe = true;
205                for &(off, term) in &byte_pairs {
206                    if off < nul_off {
207                        solver.push();
208                        solver.assert(&term._eq(&zero));
209                        let inner = solver.check();
210                        solver.pop(1);
211                        if inner != z3::SatResult::Unsat {
212                            interior_safe = false;
213                            break;
214                        }
215                    }
216                }
217                if interior_safe {
218                    return Some(CheckResult::Proved);
219                }
220            }
221        }
222
223        // If no valid NUL position found, check if the last byte is tracked
224        // and no NUL exists among tracked bytes
225        let has_nul_in_tracked = byte_pairs.iter().any(|&(_, term)| {
226            solver.push();
227            solver.assert(&term._eq(&zero));
228            let r = solver.check();
229            solver.pop(1);
230            r == z3::SatResult::Sat
231        });
232
233        if !has_nul_in_tracked {
234            let last_off = byte_pairs.last().map(|(off, _)| *off).unwrap_or(0);
235            if let Some(size) = size_u64 {
236                if last_off + 1 >= size as usize {
237                    return Some(CheckResult::Failed);
238                }
239            }
240        }
241
242        None
243    }
244
245    /// Scan MIR blocks for a single `0_u8` store into the target buffer.
246    /// When exactly one nul-store exists among all constant stores, we
247    /// can prove ValidCStr even without VM-level byte tracking.  This
248    /// mirrors the legacy `nul_store_before_checkpoint` logic.
249    fn check_valid_cstr_nul_store<'tcx>(
250        vm_state: &VmState<'_, 'tcx>,
251        checkpoint: &Checkpoint<'tcx>,
252    ) -> Option<CheckResult> {
253        let target_local = checkpoint.args.get(0).and_then(|op| match op {
254            Operand::Copy(p) | Operand::Move(p) if p.projection.is_empty() => Some(p.local),
255            _ => None,
256        })?;
257        let body = vm_state.body;
258        let tcx = vm_state.tcx;
259
260        // Build parent map (same as legacy)
261        let parents = crate::verify::valid_cstr_util::body_parents(tcx, body);
262        let root = crate::verify::valid_cstr_util::resolve_through_casts(
263            body,
264            crate::verify::valid_cstr_util::follow_parents(&parents, target_local),
265        );
266
267        let mut buffer_locals: rustc_hash::FxHashSet<Local> = rustc_hash::FxHashSet::default();
268        let mut seen = rustc_hash::FxHashSet::default();
269        let mut work = vec![root];
270        while let Some(local) = work.pop() {
271            if !seen.insert(local) {
272                continue;
273            }
274            for data in body.basic_blocks.iter() {
275                for stmt in &data.statements {
276                    let StatementKind::Assign(assign) = &stmt.kind else { continue };
277                    let (target, rvalue) = &**assign;
278                    if target.local != local || !target.projection.is_empty() { continue; }
279                    if let Rvalue::Ref(_, _, place) = rvalue {
280                        buffer_locals.insert(place.local);
281                    }
282                    #[cfg(rapx_rvalue_use_with_retag)]
283                    if let Rvalue::Use(Operand::Copy(p) | Operand::Move(p), _) = rvalue {
284                        work.push(p.local);
285                    }
286                    #[cfg(not(rapx_rvalue_use_with_retag))]
287                    if let Rvalue::Use(Operand::Copy(p) | Operand::Move(p)) = rvalue {
288                        work.push(p.local);
289                    }
290                    if let Rvalue::Cast(_, Operand::Copy(p) | Operand::Move(p), _) = rvalue {
291                        if p.projection.is_empty() { work.push(p.local); }
292                    }
293                }
294            }
295        }
296
297        let mut nul_store_count = 0u32;
298        for data in body.basic_blocks.iter() {
299            for stmt in &data.statements {
300                let StatementKind::Assign(assign) = &stmt.kind else { continue };
301                let (target, rvalue) = &**assign;
302                let target_root = crate::verify::valid_cstr_util::follow_parents(&parents, target.local);
303                if target_root != root && !buffer_locals.contains(&target_root) {
304                    continue;
305                }
306                if target.projection.is_empty() {
307                    continue;
308                }
309                #[cfg(rapx_rvalue_use_with_retag)]
310                let Rvalue::Use(Operand::Constant(c), _) = rvalue else { continue };
311                #[cfg(not(rapx_rvalue_use_with_retag))]
312                let Rvalue::Use(Operand::Constant(c)) = rvalue else { continue };
313                if c.const_.try_to_scalar_int()
314                    .map_or(false, |s| s.to_uint(s.size()) == 0)
315                {
316                    nul_store_count += 1;
317                }
318            }
319        }
320
321        if nul_store_count == 1 {
322            Some(CheckResult::Proved)
323        } else if nul_store_count > 1 {
324            Some(CheckResult::Failed)
325        } else {
326            None
327        }
328    }
329
330    /// Fallback: scan the MIR body for constant byte assignments to the target
331    /// pointer's root local. Uses worklist-based analysis (handles as_ptr chains
332    /// and branches), falling back to simple local chain for Aggregate cases.
333    fn check_valid_cstr_from_mir_constants<'ctx, 'tcx>(
334        &self,
335        vm_state: &VmState<'ctx, 'tcx>,
336        checkpoint: &Checkpoint<'tcx>,
337        _property: &Property<'tcx>,
338    ) -> Option<CheckResult> {
339        let target_local = checkpoint.args.get(0).and_then(|op| match op {
340            Operand::Copy(p) | Operand::Move(p) if p.projection.is_empty() => Some(p.local),
341            _ => None,
342        })?;
343
344        let body = vm_state.body;
345        let tcx = vm_state.tcx;
346
347        // 1. Use worklist-based analysis for as_ptr() chains and branch cases
348        let all_bytes = crate::verify::valid_cstr_util::collect_all_const_bytes_worklist(tcx, body, target_local);
349        if !all_bytes.is_empty() {
350            let any_invalid = all_bytes.iter().any(|bytes| {
351                !(bytes.last() == Some(&0) && !bytes[..bytes.len().saturating_sub(1)].contains(&0))
352            });
353            if any_invalid {
354                return Some(CheckResult::Failed);
355            }
356            let all_valid = all_bytes.iter().all(|bytes| {
357                bytes.last() == Some(&0) && !bytes[..bytes.len().saturating_sub(1)].contains(&0)
358            });
359            if all_valid {
360                return Some(CheckResult::Proved);
361            }
362        }
363
364        // 2. Fallback: simple constant byte chain for Aggregate locals
365        if let Some(bytes) = crate::verify::valid_cstr_util::const_bytes_for_local(tcx, body, target_local) {
366            let valid = bytes.last() == Some(&0) && !bytes[..bytes.len().saturating_sub(1)].contains(&0);
367            return if valid { Some(CheckResult::Proved) } else { Some(CheckResult::Failed) };
368        }
369
370        // 3. Scan MIR for a single 0_u8 store into the target buffer
371        //    (mirrors legacy nul_store_before_checkpoint logic)
372        if let Some(r) = Self::check_valid_cstr_nul_store(vm_state, checkpoint) {
373            return Some(r);
374        }
375
376        None
377    }
378}