Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add UB checks for ptr_offset_from* intrinsics #3757

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
84 changes: 5 additions & 79 deletions kani-compiler/src/codegen_cprover_gotoc/codegen/intrinsic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@ use crate::codegen_cprover_gotoc::codegen::ty_stable::pointee_type_stable;
use crate::codegen_cprover_gotoc::{GotocCtx, utils};
use crate::intrinsics::Intrinsic;
use crate::unwrap_or_return_codegen_unimplemented_stmt;
use cbmc::goto_program::{
ArithmeticOverflowResult, BinaryOperator, BuiltinFn, Expr, Location, Stmt, Type,
};
use cbmc::goto_program::{BinaryOperator, BuiltinFn, Expr, Location, Stmt, Type};
use rustc_middle::ty::TypingEnv;
use rustc_middle::ty::layout::ValidityRequirement;
use rustc_smir::rustc_internal;
Expand Down Expand Up @@ -425,10 +423,6 @@ impl GotocCtx<'_> {
Intrinsic::PowIF64 => codegen_simple_intrinsic!(Powi),
Intrinsic::PrefAlignOf => codegen_intrinsic_const!(),
Intrinsic::PtrGuaranteedCmp => self.codegen_ptr_guaranteed_cmp(fargs, place, loc),
Intrinsic::PtrOffsetFrom => self.codegen_ptr_offset_from(fargs, place, loc),
Intrinsic::PtrOffsetFromUnsigned => {
self.codegen_ptr_offset_from_unsigned(fargs, place, loc)
}
Intrinsic::RawEq => self.codegen_intrinsic_raw_eq(instance, fargs, place, loc),
Intrinsic::RetagBoxToRaw => self.codegen_retag_box_to_raw(fargs, place, loc),
Intrinsic::RintF32 => codegen_simple_intrinsic!(Rintf),
Expand Down Expand Up @@ -546,7 +540,10 @@ impl GotocCtx<'_> {
assert!(self.place_ty_stable(place).kind().is_unit());
self.codegen_write_bytes(fargs, farg_types, loc)
}
Intrinsic::SizeOfVal | Intrinsic::MinAlignOfVal => {
Intrinsic::PtrOffsetFrom
| Intrinsic::PtrOffsetFromUnsigned
| Intrinsic::SizeOfVal
| Intrinsic::MinAlignOfVal => {
unreachable!("Intrinsic `{}` is handled before codegen", intrinsic_str)
}
// Unimplemented
Expand Down Expand Up @@ -1025,77 +1022,6 @@ impl GotocCtx<'_> {
self.codegen_expr_to_place_stable(p, dst_ptr, loc)
}

/// ptr_offset_from returns the offset between two pointers
/// <https://doc.rust-lang.org/std/intrinsics/fn.ptr_offset_from.html>
fn codegen_ptr_offset_from(&mut self, fargs: Vec<Expr>, p: &Place, loc: Location) -> Stmt {
let (offset_expr, offset_overflow) = self.codegen_ptr_offset_from_expr(fargs);

// Check that computing `offset` in bytes would not overflow an `isize`
// These checks may allow a wrapping-around behavior in CBMC:
// https://github.com/model-checking/kani/issues/1150
let overflow_check = self.codegen_assert_assume(
offset_overflow.overflowed.not(),
PropertyClass::ArithmeticOverflow,
"attempt to compute offset in bytes which would overflow an `isize`",
loc,
);

let offset_expr = self.codegen_expr_to_place_stable(p, offset_expr, loc);
Stmt::block(vec![overflow_check, offset_expr], loc)
}

/// `ptr_offset_from_unsigned` returns the offset between two pointers where the order is known.
/// The logic is similar to `ptr_offset_from` but the return value is a `usize`.
/// See <https://github.com/rust-lang/rust/issues/95892> for more details
fn codegen_ptr_offset_from_unsigned(
&mut self,
fargs: Vec<Expr>,
p: &Place,
loc: Location,
) -> Stmt {
let (offset_expr, offset_overflow) = self.codegen_ptr_offset_from_expr(fargs);

// Check that computing `offset` in bytes would not overflow an `isize`
// These checks may allow a wrapping-around behavior in CBMC:
// https://github.com/model-checking/kani/issues/1150
let overflow_check = self.codegen_assert_assume(
offset_overflow.overflowed.not(),
PropertyClass::ArithmeticOverflow,
"attempt to compute offset in bytes which would overflow an `isize`",
loc,
);

let non_negative_check = self.codegen_assert_assume(
offset_overflow.result.is_non_negative(),
PropertyClass::SafetyCheck,
"attempt to compute unsigned offset with negative distance",
loc,
);

let offset_expr =
self.codegen_expr_to_place_stable(p, offset_expr.cast_to(Type::size_t()), loc);
Stmt::block(vec![overflow_check, non_negative_check, offset_expr], loc)
}

/// Both `ptr_offset_from` and `ptr_offset_from_unsigned` return the offset between two pointers.
/// This function implements the common logic between them.
fn codegen_ptr_offset_from_expr(
&mut self,
mut fargs: Vec<Expr>,
) -> (Expr, ArithmeticOverflowResult) {
let dst_ptr = fargs.remove(0);
let src_ptr = fargs.remove(0);

// Compute the offset with standard substraction using `isize`
let cast_dst_ptr = dst_ptr.clone().cast_to(Type::ssize_t());
let cast_src_ptr = src_ptr.clone().cast_to(Type::ssize_t());
let offset_overflow = cast_dst_ptr.sub_overflow(cast_src_ptr);

// Re-compute the offset with standard substraction (no casts this time)
let ptr_offset_expr = dst_ptr.sub(src_ptr);
(ptr_offset_expr, offset_overflow)
}

/// A transmute is a bitcast from the argument type to the return type.
/// <https://doc.rust-lang.org/std/intrinsics/fn.transmute.html>
///
Expand Down
4 changes: 4 additions & 0 deletions kani-compiler/src/kani_middle/kani_functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ pub enum KaniModel {
IsSlicePtrInitialized,
#[strum(serialize = "OffsetModel")]
Offset,
#[strum(serialize = "PtrOffsetFromModel")]
PtrOffsetFrom,
#[strum(serialize = "PtrSubPtrModel")]
PtrSubPtr,
#[strum(serialize = "RunContractModel")]
RunContract,
#[strum(serialize = "RunLoopContractModel")]
Expand Down
2 changes: 2 additions & 0 deletions kani-compiler/src/kani_middle/transform/rustc_intrinsics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,8 @@ impl MutMirVisitor for ReplaceIntrinsicCallVisitor<'_> {
let model = match intrinsic {
Intrinsic::SizeOfVal => self.models[&KaniModel::SizeOfVal],
Intrinsic::MinAlignOfVal => self.models[&KaniModel::AlignOfVal],
Intrinsic::PtrOffsetFrom => self.models[&KaniModel::PtrOffsetFrom],
Intrinsic::PtrOffsetFromUnsigned => self.models[&KaniModel::PtrSubPtr],
// The rest is handled in codegen.
_ => {
return self.super_terminator(term);
Expand Down
1 change: 0 additions & 1 deletion kani-driver/src/cbmc_property_renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ static CBMC_ALT_DESCRIPTIONS: Lazy<CbmcAltDescriptions> = Lazy::new(|| {
("NaN on /", Some("NaN on division")),
("NaN on *", Some("NaN on multiplication")),
]);
map.insert("pointer", vec![("same object violation", None)]);
map.insert("pointer_arithmetic", vec![
("pointer relation: deallocated dynamic object", None),
("pointer relation: dead object", None),
Expand Down
33 changes: 33 additions & 0 deletions library/kani_core/src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,39 @@ macro_rules! generate_models {
}
}

#[kanitool::fn_marker = "PtrOffsetFromModel"]
pub unsafe fn ptr_offset_from<T>(ptr1: *const T, ptr2: *const T) -> isize {
kani::safety_check(
core::mem::size_of::<T>() > 0,
"Cannot compute offset of a ZST",
);
if ptr1 == ptr2 {
0
} else {
kani::safety_check(
kani::mem::same_allocation_internal(ptr1, ptr2),
"Offset result and original pointer should point to the same allocation",
);
// The offset must fit in isize since this represents the same allocation.
let offset_bytes = ptr1.addr().wrapping_sub(ptr2.addr()) as isize;
// We know `t_size` is a power of two, so avoid division.
let t_size = size_of::<T>() as isize;
kani::safety_check(
offset_bytes & (t_size - 1) == 0,
"Expected the distance between the pointers, in bytes, to be a
multiple of the size of `T`",
);
offset_bytes >> t_size.trailing_zeros()
}
}

#[kanitool::fn_marker = "PtrSubPtrModel"]
pub unsafe fn ptr_sub_ptr<T>(ptr1: *const T, ptr2: *const T) -> usize {
let offset = ptr_offset_from(ptr1, ptr2);
kani::safety_check(offset >= 0, "Expected non-negative distance between pointers");
offset as usize
}

/// An offset model that checks UB.
#[kanitool::fn_marker = "OffsetModel"]
pub fn offset<T, P: Ptr<T>, O: ToISize>(ptr: P, offset: O) -> P {
Expand Down
14 changes: 8 additions & 6 deletions tests/expected/arbitrary/ptrs/pointer_inbounds.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@
//! Kani will detect the usage of MaybeUninit and fail the verification.
extern crate kani;

use kani::PointerGenerator;

#[kani::proof]
fn check_inbounds() {
let mut generator = kani::pointer_generator::<char, 3>();
Expand Down Expand Up @@ -48,9 +46,13 @@ fn check_overlap() {
kani::cover!(ptr_1 == ptr_2, "Same");
kani::cover!(ptr_1 == unsafe { ptr_2.byte_add(1) }, "Overlap");

let distance = unsafe { ptr_1.offset_from(ptr_2) };
kani::cover!(distance > 0, "Greater");
kani::cover!(distance < 0, "Smaller");
// offset_from is only safe if the distance between the pointers in bytes is a multiple of size_of::<T>,
// which holds if either both ptr_1 and ptr_2 are aligned or neither are.
if ptr_1.is_aligned() == ptr_2.is_aligned() {
let distance = unsafe { ptr_1.offset_from(ptr_2) };
kani::cover!(distance > 0, "Greater");
kani::cover!(distance < 0, "Smaller");

assert!(distance >= -4 && distance <= 4, "Expected a maximum distance of 4 elements");
assert!(distance >= -4 && distance <= 4, "Expected a maximum distance of 4 elements");
}
}
3 changes: 1 addition & 2 deletions tests/expected/intrinsics/offset-same-object/expected
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
FAILURE\
"same object violation"
Failed Checks: Offset result and original pointer should point to the same allocation
Original file line number Diff line number Diff line change
@@ -1 +1 @@
Failed Checks: attempt to compute unsigned offset with negative distance
Failed Checks: Expected non-negative distance between pointers
6 changes: 6 additions & 0 deletions tests/expected/offset-bounds-check/offset_from.expected
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Failed Checks: assertion failed: 0 < pointee_size && pointee_size <= isize::MAX as usize
Failed Checks: Offset result and original pointer should point to the same allocation

Verification failed for - check_offset_from_diff_alloc
Verification failed for - check_offset_from_oob_ptr
Complete - 2 successfully verified harnesses, 2 failures, 4 total.
47 changes: 47 additions & 0 deletions tests/expected/offset-bounds-check/offset_from.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright Kani Contributors
// SPDX-License-Identifier: Apache-2.0 OR MIT
//! Check that Kani offset operations correctly detect out-of-bound access.

/// Verification should fail because safety violation is not a regular panic.
#[kani::proof]
#[kani::should_panic]
fn check_offset_from_oob_ptr() {
let val = 10u128;
let ptr: *const u128 = &val;
let ptr_oob: *const u128 = ptr.wrapping_add(10);
// SAFETY: This is not safe!
let _offset = unsafe { ptr_oob.offset_from(ptr) };
}

#[kani::proof]
fn check_offset_from_diff_alloc() {
let val1 = 10u128;
let val2 = 0u128;
let ptr1: *const u128 = &val1;
let ptr2: *const u128 = &val2;
// SAFETY: This is not safe!
let offset = unsafe { ptr1.offset_from(ptr2) };
assert!(offset != 0);
}

#[kani::proof]
#[kani::should_panic]
fn check_offset_from_unit_panic() {
let val1 = kani::any();
let val2 = kani::any();
let ptr1: *const () = &val1 as *const _ as *const ();
let ptr2: *const () = &val2 as *const _ as *const ();
// SAFETY: This is safe but will panic...
let _offset = unsafe { ptr1.offset_from(ptr2) };
}

#[kani::proof]
fn check_offset_from_same_dangling() {
let val = 10u128;
let ptr: *const u128 = &val;
let ptr_oob_1: *const u128 = ptr.wrapping_add(10);
let ptr_oob_2: *const u128 = ptr.wrapping_add(5).wrapping_add(5);
// SAFETY: This is safe since the pointer is the same!
let offset = unsafe { ptr_oob_1.offset_from(ptr_oob_2) };
assert_eq!(offset, 0);
}
29 changes: 29 additions & 0 deletions tests/expected/offset-bounds-check/sub_ptr.expected
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
Checking harness check_sub_ptr_same_dangling...
VERIFICATION:- SUCCESSFUL

Checking harness check_sub_ptr_unit_panic...
Failed Checks: assertion failed: 0 < pointee_size && pointee_size <= isize::MAX as usize
VERIFICATION:- SUCCESSFUL (encountered one or more panics as expected)

Checking harness check_sub_ptr_negative_result...
Failed Checks: Expected non-negative distance between pointers
VERIFICATION:- FAILED

Checking harness check_sub_ptr_diff_alloc...
Failed Checks: Offset result and original pointer should point to the same allocation
VERIFICATION:- FAILED

Checking harness check_sub_ptr_oob_ptr...
Failed Checks: Offset result and original pointer should point to the same allocation
VERIFICATION:- FAILED

Checking harness check_sub_ptr_self_oob...
Failed Checks: Offset result and original pointer should point to the same allocation
VERIFICATION:- FAILED

Summary:
Verification failed for - check_sub_ptr_negative_result
Verification failed for - check_sub_ptr_diff_alloc
Verification failed for - check_sub_ptr_oob_ptr
Verification failed for - check_sub_ptr_self_oob
Complete - 2 successfully verified harnesses, 4 failures, 6 total.
70 changes: 70 additions & 0 deletions tests/expected/offset-bounds-check/sub_ptr.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Copyright Kani Contributors
// SPDX-License-Identifier: Apache-2.0 OR MIT
//! Check that Kani can detect UB due to `sub_ptr` with out-of-bounds pointer or wrong order.

#![feature(ptr_sub_ptr)]

#[kani::proof]
fn check_sub_ptr_self_oob() {
let val = 10u128;
let ptr: *const u128 = &val;
let ptr_oob: *const u128 = ptr.wrapping_add(10);
// SAFETY: This is not safe!
let _offset = unsafe { ptr_oob.sub_ptr(ptr) };
}

#[kani::proof]
fn check_sub_ptr_oob_ptr() {
let val = 10u128;
let ptr: *const u128 = &val;
let ptr_oob: *const u128 = ptr.wrapping_sub(10);
// SAFETY: This is not safe!
let _offset = unsafe { ptr.sub_ptr(ptr_oob) };
}

#[kani::proof]
fn check_sub_ptr_diff_alloc() {
let val1 = kani::any();
let val2 = kani::any();
let ptr1: *const u128 = &val1;
let ptr2: *const u128 = &val2;
// SAFETY: This is not safe!
let _offset = unsafe { ptr1.sub_ptr(ptr2) };
}

#[kani::proof]
fn check_sub_ptr_negative_result() {
let val: [u8; 10] = kani::any();
let ptr_first: *const _ = &val[0];
let ptr_last: *const _ = &val[9];
// SAFETY: This is safe!
let offset_ok = unsafe { ptr_last.sub_ptr(ptr_first) };

// SAFETY: This is not safe!
let offset_not_ok = unsafe { ptr_first.sub_ptr(ptr_last) };

// Just use the result.
assert!(offset_ok != offset_not_ok);
}

#[kani::proof]
#[kani::should_panic]
fn check_sub_ptr_unit_panic() {
let val1 = kani::any();
let val2 = kani::any();
let ptr1: *const () = &val1 as *const _ as *const ();
let ptr2: *const () = &val2 as *const _ as *const ();
// SAFETY: This is safe but will panic...
let _offset = unsafe { ptr1.sub_ptr(ptr2) };
}

#[kani::proof]
fn check_sub_ptr_same_dangling() {
let val = 10u128;
let ptr: *const u128 = &val;
let ptr_oob_1: *const u128 = ptr.wrapping_add(10);
let ptr_oob_2: *const u128 = ptr.wrapping_add(5).wrapping_add(5);
// SAFETY: This is safe since the pointer is the same!
let offset = unsafe { ptr_oob_1.sub_ptr(ptr_oob_2) };
assert_eq!(offset, 0);
}
3 changes: 3 additions & 0 deletions tests/expected/offset-from-distance-check/expected
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Failed Checks: Expected the distance between the pointers, in bytes, to be a
multiple of the size of `T`
VERIFICATION:- FAILED
18 changes: 18 additions & 0 deletions tests/expected/offset-from-distance-check/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright Kani Contributors
// SPDX-License-Identifier: Apache-2.0 OR MIT
//
// kani-flags: -Z mem-predicates
//! Check that Kani detects UB for offset_from when the distance between the pointers is not a multiple of size_of::<T>
extern crate kani;

#[kani::proof]
fn check_offset_from_distance_ub() {
let mut generator = kani::pointer_generator::<u16, 5>();
let ptr_1 = generator.any_in_bounds::<u16>().ptr;
let ptr_2 = generator.any_in_bounds::<u16>().ptr;

// offset_from is only safe if the distance between the pointers in bytes is a multiple of size_of::<T>,
// which holds if either both ptr_1 and ptr_2 are aligned or neither are.
// However, any_in_bounds makes no guarantees about alignment, so the above condition may not hold and verification should fail.
unsafe { ptr_1.offset_from(ptr_2) };
}
1 change: 1 addition & 0 deletions tests/expected/offset-from-zst/expected
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
VERIFICATION:- FAILED
Loading
Loading