Skip to content

Commit 71ab36e

Browse files
authored
Support pointer arithmetic in quantifier predicates (#4583)
Lower wrapping pointer arithmetic intrinsics (`wrapping_add`, `wrapping_byte_offset`) directly to CBMC pointer `Plus` expressions in the pure expression inliner. These intrinsics have no GOTO body to inline, so they need special handling. ## Problem Quantifier predicates that dereference pointers with offsets — e.g., `*ptr.wrapping_byte_offset(i as isize)` — failed because the `inline_as_pure_expr` inliner couldn't resolve pointer arithmetic intrinsics. These functions have no body in the symbol table (they're compiler intrinsics), so the inliner returned the original expression unchanged, leaving unresolved function calls that CBMC rejected. ## Solution In `inline_call_as_pure_expr` (`goto_ctx.rs`), before attempting to look up the function body, check if the function name matches a known wrapping pointer arithmetic intrinsic (`wrapping_add`, `wrapping_byte_offset`). If so, directly emit `ptr.plus(offset)` — the CBMC expression for pointer arithmetic. A type guard (`arguments[0].typ().is_pointer()`) prevents misapplication to non-pointer functions. Non-wrapping variants (`offset`, `add`, `arith_offset`) are intentionally excluded because they trigger CBMC bounds checks inside quantifier bodies, which fail in the symbolic evaluation context. ## Example ```rust kani::forall!(|i in (0, len)| unsafe { *ptr.wrapping_byte_offset(i as isize) == 0 }) ``` ## Changes - `goto_ctx.rs`: Recognize wrapping pointer arithmetic intrinsics and lower to `ptr.plus(offset)` - `rfc/src/rfcs/0010-quantifiers.md`: Document pointer arithmetic intrinsic lowering in the Detailed Design section - `tests/kani/Quantifiers/pointer_arithmetic.rs`: 4 harnesses covering `wrapping_byte_offset` (forall + exists) and `wrapping_add` (u32 + u8) --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 and MIT licenses.
1 parent 047c6b6 commit 71ab36e

3 files changed

Lines changed: 154 additions & 0 deletions

File tree

kani-compiler/src/codegen_cprover_gotoc/context/goto_ctx.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,56 @@ impl GotocCtx<'_, '_> {
515515
return original_expr.clone();
516516
}
517517

518+
// Lower known pointer arithmetic intrinsics directly to CBMC expressions.
519+
// These intrinsics have no GOTO body to inline, so we handle them specially.
520+
// We check that the function name contains a pointer-type path segment to
521+
// avoid false-positives on user-defined functions with similar names.
522+
//
523+
// CBMC's pointer Plus scales the offset by the pointee size. So:
524+
// - For element-offset variants (wrapping_add, wrapping_sub, wrapping_offset),
525+
// we apply Plus directly on the original pointer type (count is in elements of T).
526+
// - For byte-offset variants (wrapping_byte_offset, wrapping_byte_add,
527+
// wrapping_byte_sub), we first cast the pointer to `*u8` so Plus scales by 1.
528+
// - Sub variants negate the offset before adding.
529+
//
530+
// TODO: Replace name-based heuristic with a proper intrinsic registry
531+
// (e.g., matching on DefId or a dedicated KaniIntrinsic enum) for
532+
// robustness against mangling variations.
533+
let fn_name = fn_id.to_string();
534+
let is_ptr_fn = fn_name.contains("::ptr::") || fn_name.contains("const_ptr");
535+
let ptr_op = if is_ptr_fn {
536+
if fn_name.contains("wrapping_byte_offset") || fn_name.contains("wrapping_byte_add") {
537+
Some((/*is_byte*/ true, /*is_sub*/ false))
538+
} else if fn_name.contains("wrapping_byte_sub") {
539+
Some((true, true))
540+
} else if fn_name.contains("wrapping_add") || fn_name.contains("wrapping_offset") {
541+
Some((false, false))
542+
} else if fn_name.contains("wrapping_sub") {
543+
Some((false, true))
544+
} else {
545+
None
546+
}
547+
} else {
548+
None
549+
};
550+
if let Some((is_byte, is_sub)) = ptr_op
551+
&& arguments.len() >= 2
552+
&& arguments[0].typ().is_pointer()
553+
{
554+
let original_ptr_typ = arguments[0].typ().clone();
555+
let ptr = self.inline_as_pure_expr(&arguments[0], visited);
556+
let offset = self.inline_as_pure_expr(&arguments[1], visited);
557+
let offset = if is_sub { offset.neg() } else { offset };
558+
// For byte-offset variants, cast the pointer to `*u8` so CBMC's Plus
559+
// scales the offset by 1 byte (matching the Rust semantics that
560+
// first casts `self.cast::<u8>()`), then cast the result back to the
561+
// original pointer type so the surrounding expression sees the
562+
// expected type.
563+
let base = if is_byte { ptr.cast_to(Type::unsigned_int(8).to_pointer()) } else { ptr };
564+
let result = base.plus(offset);
565+
return if is_byte { result.cast_to(original_ptr_typ) } else { result };
566+
}
567+
518568
let function_body = self.symbol_table.lookup(*fn_id).and_then(|sym| match &sym.value {
519569
SymbolValues::Stmt(stmt) => Some(stmt.clone()),
520570
_ => None,

rfc/src/rfcs/0010-quantifiers.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,19 @@ To solve this, Kani generates **pure expression trees** for quantifier bodies:
234234
are inlined by looking up the function body in the symbol table, resolving its
235235
return expression, and substituting parameters — producing a pure expression.
236236

237+
5. **Pointer arithmetic intrinsic lowering**: Wrapping pointer arithmetic functions
238+
(`wrapping_add`, `wrapping_sub`, `wrapping_offset`, `wrapping_byte_offset`,
239+
`wrapping_byte_add`, `wrapping_byte_sub`) are compiler intrinsics with no GOTO
240+
body to inline. The inliner recognizes these by name and lowers them directly to
241+
CBMC pointer `Plus` expressions. Since CBMC's pointer `Plus` scales the offset by
242+
the pointee size, byte-offset variants first cast the pointer to `*u8` so the
243+
scaling is by 1 byte, and `sub` variants negate the offset before adding.
244+
Non-wrapping variants (`offset`, `add`) are not supported because they trigger
245+
CBMC bounds checks inside quantifier bodies. Example:
246+
```rust
247+
kani::forall!(|i in (0, len)| unsafe { *ptr.wrapping_byte_offset(i as isize) == 0 })
248+
```
249+
237250
### Typed quantifier variables
238251

239252
The `forall!` and `exists!` macros support an optional type annotation:
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// Copyright Kani Contributors
2+
// SPDX-License-Identifier: Apache-2.0 OR MIT
3+
// kani-flags: -Z quantifiers
4+
5+
//! Tests for pointer arithmetic inside quantifier predicates.
6+
//! These exercise the intrinsic lowering in `inline_call_as_pure_expr`
7+
//! which converts wrapping_byte_offset/wrapping_add to CBMC Plus.
8+
//!
9+
//! Quantifier ranges are half-open: `|i in (lo, hi)|` means `lo <= i < hi`.
10+
//! All ranges below match the array lengths to stay in-bounds.
11+
12+
#[kani::proof]
13+
fn check_wrapping_byte_offset_forall() {
14+
let arr: [u8; 8] = [0; 8];
15+
let ptr = arr.as_ptr();
16+
unsafe {
17+
assert!(kani::forall!(|i in (0, 8)| *ptr.wrapping_byte_offset(i as isize) == 0));
18+
}
19+
}
20+
21+
#[kani::proof]
22+
fn check_wrapping_byte_offset_exists() {
23+
let arr: [u8; 8] = [0, 0, 0, 42, 0, 0, 0, 0];
24+
let ptr = arr.as_ptr();
25+
unsafe {
26+
assert!(kani::exists!(|i in (0, 8)| *ptr.wrapping_byte_offset(i as isize) == 42));
27+
}
28+
}
29+
30+
#[kani::proof]
31+
fn check_wrapping_add_forall() {
32+
let arr: [u32; 4] = [10, 20, 30, 40];
33+
let ptr = arr.as_ptr();
34+
unsafe {
35+
assert!(kani::forall!(|i in (0, 4)| *ptr.wrapping_add(i) >= 10));
36+
}
37+
}
38+
39+
/// Tests `ptr.wrapping_add(i)` with a different element type (u8 vs u32).
40+
#[kani::proof]
41+
fn check_wrapping_add_u8_forall() {
42+
let arr: [u8; 4] = [1, 2, 3, 4];
43+
let ptr = arr.as_ptr();
44+
unsafe {
45+
assert!(kani::forall!(|i in (0, 4)| *ptr.wrapping_add(i) > 0));
46+
}
47+
}
48+
49+
#[kani::proof]
50+
fn check_wrapping_add_exists() {
51+
let arr: [u32; 4] = [10, 20, 30, 40];
52+
let ptr = arr.as_ptr();
53+
unsafe {
54+
assert!(kani::exists!(|i in (0, 4)| *ptr.wrapping_add(i) == 30));
55+
}
56+
}
57+
58+
/// Tests `wrapping_byte_offset` on a non-`u8` pointee.
59+
/// Each `u32` is 4 bytes, so valid byte offsets are 0, 4, 8, 12.
60+
/// This catches a bug where the offset was scaled by the pointee size
61+
/// (would scale by 4) instead of by 1 (bytes).
62+
#[kani::proof]
63+
fn check_wrapping_byte_offset_u32() {
64+
let arr: [u32; 4] = [0x11111111, 0x22222222, 0x33333333, 0x44444444];
65+
let ptr = arr.as_ptr();
66+
unsafe {
67+
assert!(
68+
kani::forall!(|i in (0, 4)| *ptr.wrapping_byte_offset((i * 4) as isize) >= 0x11111111)
69+
);
70+
}
71+
}
72+
73+
#[kani::proof]
74+
fn check_wrapping_sub_forall() {
75+
let arr: [u32; 4] = [10, 20, 30, 40];
76+
let end_ptr = unsafe { arr.as_ptr().add(4) };
77+
unsafe {
78+
// end_ptr.wrapping_sub(i) for i in 1..=4 points to arr[4-i]
79+
assert!(kani::forall!(|i in (1, 5)| *end_ptr.wrapping_sub(i) >= 10));
80+
}
81+
}
82+
83+
#[kani::proof]
84+
fn check_wrapping_byte_sub_forall() {
85+
let arr: [u32; 4] = [10, 20, 30, 40];
86+
let end_ptr = unsafe { arr.as_ptr().add(4) };
87+
unsafe {
88+
// 4-byte stride: end_ptr.wrapping_byte_sub(i*4) for i in 1..=4
89+
assert!(kani::forall!(|i in (1, 5)| *end_ptr.wrapping_byte_sub(i * 4) >= 10));
90+
}
91+
}

0 commit comments

Comments
 (0)