rapx/verify/property_checker/
cstr.rs1use rustc_middle::mir::{Body, Local, Operand, Rvalue, StatementKind, TerminatorKind};
4use z3::{
5 Solver,
6 ast::{Ast, Int},
7};
8
9use crate::compat::FxHashMap;
10use crate::helpers::mir_scan::Checkpoint;
11use crate::helpers::mir_utils::dep_callee_def_id;
12use crate::verify::api_classify::is_as_ptr;
13use crate::verify::vm::state::{AllocId, VmState};
14use crate::verify::{
15 contract::{ContractExpr, Property, PropertyArg},
16 report::{CheckResult, UnknownReason},
17};
18
19use super::PropertyChecker;
20
21fn first_arg_local<'tcx>(checkpoint: &Checkpoint<'tcx>) -> Option<Local> {
24 checkpoint.args.get(0).and_then(|op| match op {
25 Operand::Copy(p) | Operand::Move(p) if p.projection.is_empty() => Some(p.local),
26 _ => None,
27 })
28}
29
30fn body_parents(body: &Body<'_>) -> FxHashMap<Local, Local> {
33 let mut parents: FxHashMap<Local, Local> = Default::default();
34 for data in body.basic_blocks.iter() {
35 for statement in &data.statements {
36 let StatementKind::Assign(assign) = &statement.kind else {
37 continue;
38 };
39 let (target, rvalue) = assign.as_ref();
40 let source = match rvalue {
41 Rvalue::Use(Operand::Copy(place) | Operand::Move(place), ..)
42 | Rvalue::Cast(_, Operand::Copy(place) | Operand::Move(place), _)
43 | Rvalue::Ref(_, _, place)
44 | Rvalue::RawPtr(_, place)
45 | Rvalue::CopyForDeref(place) => Some(place.local),
46 _ => None,
47 };
48 if let Some(source) = source {
49 parents.entry(target.local).or_insert(source);
50 }
51 }
52 let Some(terminator) = &data.terminator else {
53 continue;
54 };
55 let TerminatorKind::Call {
56 func,
57 args,
58 destination,
59 ..
60 } = &terminator.kind
61 else {
62 continue;
63 };
64 if !is_as_ptr(dep_callee_def_id(func)) {
65 continue;
66 }
67 let Some(source) = args.first().and_then(|arg| match &arg.node {
68 Operand::Copy(place) | Operand::Move(place) => Some(place.local),
69 _ => None,
70 }) else {
71 continue;
72 };
73 parents.entry(destination.local).or_insert(source);
74 }
75 parents
76}
77
78impl PropertyChecker {
79 pub(super) fn check_valid_cstr<'z3, 'tcx>(
82 &self,
83 vm_state: &VmState<'z3, 'tcx>,
84 solver: &Solver<'z3>,
85 checkpoint: &Checkpoint<'tcx>,
86 property: &Property<'tcx>,
87 ) -> CheckResult {
88 let value = self
89 .target_value(vm_state, checkpoint, property)
90 .or_else(|| {
91 checkpoint
92 .destination
93 .and_then(|d| vm_state.local_value(d).cloned())
94 });
95 let Some(value) = value else {
96 return CheckResult::Unknown(UnknownReason::Unimplemented);
97 };
98
99 if let Some(alloc_id) = value.provenance_alloc_id() {
101 let alloc_id = vm_state.data_alloc_of(alloc_id, value.ty).unwrap_or(alloc_id);
105 if vm_state.alloc(alloc_id).facts.dead {
106 return CheckResult::Failed;
107 }
108
109 let mut root_id = alloc_id;
116 while let Some(parent_id) = vm_state.alloc(root_id).parent {
117 root_id = parent_id;
118 }
119 if vm_state.is_cstr_trusted(alloc_id) || vm_state.is_cstr_trusted(root_id) {
120 return CheckResult::ProvedByRule;
121 }
122
123 let alloc_size = vm_state.allocation_size(alloc_id).clone();
124
125 let n_term = property.args().get(1).and_then(|a| match a {
132 PropertyArg::Expr(ContractExpr::Const(_)) => None,
133 a => self.resolve_arg_term(vm_state, checkpoint, a),
134 });
135 let buffer_size = n_term
136 .filter(|t| t.as_u64().is_some())
137 .or(Some(alloc_size));
138
139 let start_offset = value
141 .provenance
142 .as_ref()
143 .and_then(|p| p.offset.as_u64())
144 .map(|v| v as usize)
145 .unwrap_or(0);
146
147 if let Some(r) = self.check_valid_cstr_from_known_nul(vm_state, alloc_id, start_offset)
149 {
150 return r;
151 }
152
153 if let Some(size) = buffer_size {
155 if let Some(r) =
156 self.check_valid_cstr_from_byte_values(vm_state, solver, alloc_id, &size)
157 {
158 return r;
159 }
160 }
161 }
162
163 if let Some(r) = self.check_valid_cstr_from_mir_constants(vm_state, checkpoint) {
166 return r;
167 }
168
169 let is_strict =
173 crate::verify::api_classify::is_cstr_unchecked_constructor(checkpoint.callee);
174 if is_strict {
175 CheckResult::Unknown(UnknownReason::Unimplemented)
176 } else {
177 CheckResult::ProvedByRule
178 }
179 }
180
181 fn check_valid_cstr_from_known_nul<'z3, 'tcx>(
187 &self,
188 vm_state: &VmState<'z3, 'tcx>,
189 alloc_id: AllocId,
190 start_offset: usize,
191 ) -> Option<CheckResult> {
192 let nul_offsets_all = vm_state.alloc_nul_offsets(alloc_id);
194 let known_offsets: Vec<usize> = nul_offsets_all
195 .iter()
196 .copied()
197 .chain(vm_state.alloc_non_nul_offsets(alloc_id))
198 .collect();
199
200 if known_offsets.is_empty() {
201 return None; }
203
204 let max_known = known_offsets.iter().max().copied().unwrap_or(0);
205
206 let nul_offsets: Vec<usize> = nul_offsets_all
208 .into_iter()
209 .filter(|off| *off >= start_offset)
210 .collect();
211
212 if nul_offsets.is_empty() {
213 let size = vm_state.allocation_size(alloc_id);
215 if let Some(size_val) = size.as_u64() {
216 if max_known + 1 < size_val as usize {
217 return None;
218 }
219 }
220 return Some(CheckResult::Failed);
221 }
222
223 let min_nul = nul_offsets.iter().min().copied().unwrap_or(0);
225
226 for off in start_offset..min_nul {
228 if vm_state.is_byte_nul(alloc_id, off) {
229 return Some(CheckResult::Failed);
231 }
232 if !vm_state.is_byte_non_nul(alloc_id, off) {
233 return None;
235 }
236 }
237
238 if nul_offsets.len() > 1 && min_nul < max_known {
241 return Some(CheckResult::Failed);
242 }
243
244 Some(CheckResult::ProvedByRule)
247 }
248
249 fn check_valid_cstr_from_byte_values<'z3, 'tcx>(
252 &self,
253 vm_state: &VmState<'z3, 'tcx>,
254 solver: &Solver<'z3>,
255 alloc_id: AllocId,
256 alloc_size: &Int<'z3>,
257 ) -> Option<CheckResult> {
258 let byte_pairs = vm_state.alloc_byte_values(alloc_id);
259 if byte_pairs.is_empty() {
260 return None;
261 }
262
263 let zero = Int::from_u64(vm_state.z3_ctx, 0);
264 let size_u64 = alloc_size.as_u64();
265
266 if size_u64.is_none() {
267 if byte_pairs.len() >= 2 {
271 solver.push();
272 solver.assert(&byte_pairs[0].1._eq(&zero));
273 solver.assert(&byte_pairs[1].1._eq(&zero));
274 let r = solver.check();
275 solver.pop(1);
276 if r == z3::SatResult::Sat {
277 return Some(CheckResult::Failed);
278 }
279 }
280 return None; }
282
283 for (nul_off, nul_term) in &byte_pairs {
284 if let Some(size) = size_u64 {
287 if *nul_off + 1 != size as usize {
288 continue;
289 }
290 }
291 solver.push();
292 solver.assert(&nul_term._eq(&zero));
293
294 for (off, term) in &byte_pairs {
295 if *off < *nul_off {
296 solver.assert(&term._eq(&zero).not());
297 }
298 }
299
300 let r = solver.check();
301 solver.pop(1);
302
303 if r == z3::SatResult::Sat {
304 let mut interior_safe = true;
305 for (off, term) in &byte_pairs {
306 if *off < *nul_off {
307 solver.push();
308 solver.assert(&term._eq(&zero));
309 let inner = solver.check();
310 solver.pop(1);
311 if inner != z3::SatResult::Unsat {
312 interior_safe = false;
313 break;
314 }
315 }
316 }
317 if interior_safe {
318 return Some(CheckResult::ProvedByRule);
319 }
320 }
321 }
322
323 let has_nul_in_tracked = byte_pairs.iter().any(|(_, term)| {
326 solver.push();
327 solver.assert(&term._eq(&zero));
328 let r = solver.check();
329 solver.pop(1);
330 r == z3::SatResult::Sat
331 });
332
333 if !has_nul_in_tracked {
334 let last_off = byte_pairs.last().map(|(off, _)| *off).unwrap_or(0);
335 if let Some(size) = size_u64 {
336 if last_off + 1 >= size as usize {
337 return Some(CheckResult::Failed);
338 }
339 }
340 }
341
342 if let Some(size) = size_u64 {
346 if let Some((last_off, last_term)) = byte_pairs.last() {
347 if *last_off + 1 == size as usize {
348 solver.push();
349 solver.assert(&last_term._eq(&zero).not());
350 let last_not_nul = solver.check();
351 solver.pop(1);
352 if last_not_nul == z3::SatResult::Unsat {
353 for (off, term) in &byte_pairs {
354 if *off < *last_off {
355 solver.push();
356 solver.assert(&term._eq(&zero));
357 let r = solver.check();
358 solver.pop(1);
359 if r == z3::SatResult::Sat {
360 return Some(CheckResult::Failed);
361 }
362 }
363 }
364 }
365 }
366 }
367 }
368
369 None
370 }
371
372 fn check_valid_cstr_nul_store<'tcx>(
377 vm_state: &VmState<'_, 'tcx>,
378 checkpoint: &Checkpoint<'tcx>,
379 ) -> Option<CheckResult> {
380 let target_local = first_arg_local(checkpoint)?;
381 let body = vm_state.body();
382
383 let parents = body_parents(body);
385 let root = crate::helpers::mir_utils::resolve_through_casts(
386 body,
387 crate::helpers::mir_utils::follow_parents(&parents, target_local),
388 );
389
390 let mut buffer_locals: rustc_hash::FxHashSet<Local> = rustc_hash::FxHashSet::default();
391 let mut seen = rustc_hash::FxHashSet::default();
392 let mut work = vec![root];
393 while let Some(local) = work.pop() {
394 if !seen.insert(local) {
395 continue;
396 }
397 for data in body.basic_blocks.iter() {
398 for stmt in &data.statements {
399 let StatementKind::Assign(assign) = &stmt.kind else {
400 continue;
401 };
402 let (target, rvalue) = &**assign;
403 if target.local != local || !target.projection.is_empty() {
404 continue;
405 }
406 if let Rvalue::Ref(_, _, place) = rvalue {
407 buffer_locals.insert(place.local);
408 }
409 #[cfg(rapx_rvalue_use_with_retag)]
410 if let Rvalue::Use(Operand::Copy(p) | Operand::Move(p), _) = rvalue {
411 work.push(p.local);
412 }
413 #[cfg(not(rapx_rvalue_use_with_retag))]
414 if let Rvalue::Use(Operand::Copy(p) | Operand::Move(p)) = rvalue {
415 work.push(p.local);
416 }
417 if let Rvalue::Cast(_, Operand::Copy(p) | Operand::Move(p), _) = rvalue {
418 if p.projection.is_empty() {
419 work.push(p.local);
420 }
421 }
422 }
423 }
424 }
425
426 let mut nul_store_count = 0u32;
427 for data in body.basic_blocks.iter() {
428 for stmt in &data.statements {
429 let StatementKind::Assign(assign) = &stmt.kind else {
430 continue;
431 };
432 let (target, rvalue) = &**assign;
433 let target_root = crate::helpers::mir_utils::follow_parents(&parents, target.local);
434 if target_root != root && !buffer_locals.contains(&target_root) {
435 continue;
436 }
437 if target.projection.is_empty() {
438 continue;
439 }
440 #[cfg(rapx_rvalue_use_with_retag)]
441 let Rvalue::Use(Operand::Constant(c), _) = rvalue else {
442 continue;
443 };
444 #[cfg(not(rapx_rvalue_use_with_retag))]
445 let Rvalue::Use(Operand::Constant(c)) = rvalue else {
446 continue;
447 };
448 if c.const_
449 .try_to_scalar_int()
450 .is_some_and(|s| s.to_uint(s.size()) == 0)
451 {
452 nul_store_count += 1;
453 }
454 }
455 }
456
457 if nul_store_count == 1 {
458 Some(CheckResult::ProvedByRule)
459 } else if nul_store_count > 1 {
460 Some(CheckResult::Failed)
461 } else {
462 None
463 }
464 }
465
466 fn check_valid_cstr_from_mir_constants<'z3, 'tcx>(
470 &self,
471 vm_state: &VmState<'z3, 'tcx>,
472 checkpoint: &Checkpoint<'tcx>,
473 ) -> Option<CheckResult> {
474 let target_local = first_arg_local(checkpoint)?;
475
476 let body = vm_state.body();
477 let tcx = vm_state.tcx;
478
479 let all_bytes =
481 crate::helpers::mir_utils::collect_all_const_bytes_worklist(tcx, body, target_local);
482 if !all_bytes.is_empty() {
483 let any_invalid = all_bytes.iter().any(|bytes| {
484 !(bytes.last() == Some(&0) && !bytes[..bytes.len().saturating_sub(1)].contains(&0))
485 });
486 if any_invalid {
487 return Some(CheckResult::Failed);
488 }
489 let all_valid = all_bytes.iter().all(|bytes| {
490 bytes.last() == Some(&0) && !bytes[..bytes.len().saturating_sub(1)].contains(&0)
491 });
492 if all_valid {
493 return Some(CheckResult::ProvedByRule);
494 }
495 }
496
497 if let Some(bytes) =
499 crate::helpers::mir_utils::const_bytes_for_local(tcx, body, target_local)
500 {
501 let valid =
502 bytes.last() == Some(&0) && !bytes[..bytes.len().saturating_sub(1)].contains(&0);
503 return if valid {
504 Some(CheckResult::ProvedByRule)
505 } else {
506 Some(CheckResult::Failed)
507 };
508 }
509
510 if let Some(r) = Self::check_valid_cstr_nul_store(vm_state, checkpoint) {
513 return Some(r);
514 }
515
516 None
517 }
518}