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

Refactor overflow checks in intrinsics code #1131

Merged
merged 10 commits into from
May 3, 2022
79 changes: 47 additions & 32 deletions kani-compiler/src/codegen_cprover_gotoc/codegen/intrinsic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -523,7 +523,7 @@ impl<'tcx> GotocCtx<'tcx> {
"pref_align_of" => codegen_intrinsic_const!(),
"ptr_guaranteed_eq" => codegen_ptr_guaranteed_cmp!(eq),
"ptr_guaranteed_ne" => codegen_ptr_guaranteed_cmp!(neq),
"ptr_offset_from" => self.codegen_ptr_offset_from(instance, fargs, p, loc),
"ptr_offset_from" => self.codegen_ptr_offset_from(fargs, p, loc),
"raw_eq" => self.codegen_intrinsic_raw_eq(instance, fargs, p, loc),
"rintf32" => codegen_unimplemented_intrinsic!(
"https://github.com/model-checking/kani/issues/1025"
Expand Down Expand Up @@ -628,7 +628,7 @@ impl<'tcx> GotocCtx<'tcx> {
"wrapping_sub" => codegen_wrapping_op!(sub),
"write_bytes" => {
assert!(self.place_ty(p).is_unit());
self.codegen_write_bytes(instance, intrinsic, fargs, loc)
self.codegen_write_bytes(instance, fargs, loc)
}
// Unimplemented
_ => codegen_unimplemented_intrinsic!(
Expand Down Expand Up @@ -878,26 +878,22 @@ impl<'tcx> GotocCtx<'tcx> {
let src_ptr = fargs.remove(0);
let offset = fargs.remove(0);

// Check that computing `bytes` would not overflow
// Check that computing `offset` in bytes would not overflow
let ty = self.monomorphize(instance.substs.type_at(0));
let layout = self.layout_of(ty);
let size = Expr::int_constant(layout.size.bytes(), Type::ssize_t());
let bytes = offset.clone().mul_overflow(size);
let bytes_overflow_check = self.codegen_assert(
bytes.overflowed.not(),
PropertyClass::ArithmeticOverflow,
"attempt to compute offset in bytes which would overflow",
loc,
);
let (offset_bytes, bytes_overflow_check) =
self.count_in_bytes(offset.clone(), ty, Type::ssize_t(), "offset", loc);

// Check that the computation would not overflow an `isize`
let dst_ptr_of = src_ptr.clone().cast_to(Type::ssize_t()).add_overflow(bytes.result);
// These checks may allow a wrapping-around behavior in CBMC:
// https://github.com/model-checking/kani/issues/1150
let dst_ptr_of = src_ptr.clone().cast_to(Type::ssize_t()).add_overflow(offset_bytes);
let overflow_check = self.codegen_assert(
dst_ptr_of.overflowed.not(),
PropertyClass::ArithmeticOverflow,
"attempt to compute offset which would overflow",
loc,
);

// Re-compute `dst_ptr` with standard addition to avoid conversion
let dst_ptr = src_ptr.plus(offset);
let expr_place = self.codegen_expr_to_place(p, dst_ptr);
Expand All @@ -908,7 +904,6 @@ impl<'tcx> GotocCtx<'tcx> {
/// https://doc.rust-lang.org/std/intrinsics/fn.ptr_offset_from.html
fn codegen_ptr_offset_from(
&mut self,
instance: Instance<'tcx>,
mut fargs: Vec<Expr>,
p: &Place<'tcx>,
loc: Location,
Expand All @@ -919,17 +914,15 @@ impl<'tcx> GotocCtx<'tcx> {
// 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 = cast_dst_ptr.sub(cast_src_ptr);
let offset = cast_dst_ptr.sub_overflow(cast_src_ptr);

// Check that computing `offset_bytes` would not overflow an `isize`
let ty = self.monomorphize(instance.substs.type_at(0));
let layout = self.layout_of(ty);
let size = Expr::int_constant(layout.size.bytes(), Type::ssize_t());
let offset_bytes = offset.clone().mul_overflow(size);
// 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(
offset_bytes.overflowed.not(),
offset.overflowed.not(),
PropertyClass::ArithmeticOverflow,
"attempt to compute offset in bytes which would overflow",
"attempt to compute offset in bytes which would overflow an `isize`",
loc,
);

Expand Down Expand Up @@ -1209,7 +1202,6 @@ impl<'tcx> GotocCtx<'tcx> {
fn codegen_write_bytes(
&mut self,
instance: Instance<'tcx>,
intrinsic: &str,
mut fargs: Vec<Expr>,
loc: Location,
) -> Stmt {
Expand All @@ -1227,18 +1219,41 @@ impl<'tcx> GotocCtx<'tcx> {
loc,
);

// Check that computing `bytes` would not overflow
// Check that computing `count` in bytes would not overflow
let (count_bytes, overflow_check) =
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This catches the Rust definition of UB, but will still cause problems in CBMC if we overflow the index bits, correct?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, and there is now a separate test for CBMC's wrap-around behavior.

self.count_in_bytes(count, ty, Type::size_t(), "write_bytes", loc);

let memset_call = BuiltinFn::Memset.call(vec![dst, val, count_bytes], loc);
Stmt::block(vec![align_check, overflow_check, memset_call.as_stmt(loc)], loc)
}

/// Computes (multiplies) the equivalent of a memory-related number (e.g., an offset) in bytes.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice documentation!

/// Because this operation may result in an arithmetic overflow, it includes an overflow check.
/// Returns a tuple with:
/// * The result expression of the computation.
/// * An assertion statement to ensure the operation has not overflowed.
fn count_in_bytes(
&self,
count: Expr,
ty: Ty<'tcx>,
res_ty: Type,
intrinsic: &str,
loc: Location,
) -> (Expr, Stmt) {
assert!(res_ty.is_integer());
let layout = self.layout_of(ty);
let size = Expr::int_constant(layout.size.bytes(), Type::size_t());
let bytes = count.mul_overflow(size);
let overflow_check = self.codegen_assert(
bytes.overflowed.not(),
let size_of_elem = Expr::int_constant(layout.size.bytes(), res_ty);
let size_of_count_elems = count.mul_overflow(size_of_elem);
let message = format!(
"{}: attempt to compute number in bytes which would overflow",
intrinsic.to_string()
);
let assert_stmt = self.codegen_assert(
size_of_count_elems.overflowed.not(),
PropertyClass::ArithmeticOverflow,
format!("{}: attempt to compute `bytes` which would overflow", intrinsic).as_str(),
message.as_str(),
loc,
);

let memset_call = BuiltinFn::Memset.call(vec![dst, val, bytes.result], loc);
Stmt::block(vec![align_check, overflow_check, memset_call.as_stmt(loc)], loc)
(size_of_count_elems.result, assert_stmt)
}
}
2 changes: 1 addition & 1 deletion tests/expected/intrinsics/write_bytes/overflow/expected
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
FAILURE\
write_bytes: attempt to compute `bytes` which would overflow
write_bytes: attempt to compute number in bytes which would overflow
2 changes: 1 addition & 1 deletion tests/expected/offset-bytes-overflow/expected
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
FAILURE\
attempt to compute offset in bytes which would overflow
offset: attempt to compute number in bytes which would overflow
4 changes: 3 additions & 1 deletion tests/expected/offset-from-bytes-overflow/expected
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
FAILURE\
attempt to compute offset in bytes which would overflow
attempt to compute offset which would overflow
FAILURE\
attempt to compute offset in bytes which would overflow an `isize`
10 changes: 5 additions & 5 deletions tests/expected/offset-from-bytes-overflow/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ use std::convert::TryInto;

#[kani::proof]
fn main() {
let v: &[u128] = &[0; 200];
let v_100: *const u128 = &v[100];
let max_offset = usize::MAX / std::mem::size_of::<u128>();
let v: &[u128] = &[0; 10];
let v_0: *const u128 = &v[0];
let high_offset = usize::MAX / (std::mem::size_of::<u128>() * 2);
unsafe {
let v_wrap: *const u128 = v_100.add((max_offset + 1).try_into().unwrap());
let _ = v_wrap.offset_from(v_100) == 2;
let v_wrap: *const u128 = v_0.add(high_offset.try_into().unwrap());
adpaco-aws marked this conversation as resolved.
Show resolved Hide resolved
let _ = v_wrap.offset_from(v_0);
}
}
2 changes: 2 additions & 0 deletions tests/expected/offset-wraps-around/expected
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
FAILURE\
assertion failed: high_offset == wrapped_offset.try_into().unwrap()
35 changes: 35 additions & 0 deletions tests/expected/offset-wraps-around/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0 OR MIT

// Check that a high offset causes a "wrapping around" behavior in CBMC.

// This example can be really confusing. This program works fine in Rust and
// it's okay to assert that the value coming from `offset_from` is equal to
// `high_offset`. But CBMC's memory model is going to cause a "wrapping around"
// behavior in `v_wrap`, so any values that depend on it are going to show a
// strange behavior as well.
use std::convert::TryInto;

#[kani::proof]
fn main() {
let v: &[u128] = &[0; 10];
let v_0: *const u128 = &v[0];
let high_offset = usize::MAX / (std::mem::size_of::<u128>() * 4);
unsafe {
// Adding `high offset` to `v_0` is undefined behavior, but Kani's
// default behavior does not report it. This kind of operations
// are quite common in the standard library, and we disabled such
// checks in order to avoid spurious verification failures.
//
// Note that this instance of undefined behavior will be reported
// by `miri` and also by Kani with `--extra-pointer-checks`.
// Also, dereferencing the pointer will also be reported by Kani's
// default behavior.
let v_wrap: *const u128 = v_0.add(high_offset.try_into().unwrap());
let wrapped_offset = v_wrap.offset_from(v_0);
// Both offsets should be the same, but because of the "wrapping around"
// behavior in CBMC, `wrapped_offset` does not equal `high_offset`
// https://github.com/model-checking/kani/issues/1150
assert!(high_offset == wrapped_offset.try_into().unwrap());
}
}