Skip to content

Commit 2334f2c

Browse files
Add out of bounds check for offset intrinsics (#3755)
Note that the `offset` intrinsics is lowered to `Rvalue::Offset`. Thus, we have to replace the entire statement. This PR does not address `ptr_offset_from` and `ptr_offset_from_unsigned`. I'll push those changes to a separate PR. Resolves #1233 By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 and MIT licenses. --------- Co-authored-by: Carolyn Zech <cmzech@amazon.com>
1 parent db1c5fe commit 2334f2c

File tree

37 files changed

+529
-118
lines changed

37 files changed

+529
-118
lines changed

kani-compiler/src/codegen_cprover_gotoc/codegen/assert.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ pub enum PropertyClass {
3838
/// Overflow panics that can be generated by Intrisics.
3939
/// NOTE: Not all uses of this are found by rust-analyzer because of the use of macros. Try grep instead.
4040
///
41-
/// SPECIAL BEHAVIOR: None TODO: Why should this exist?
41+
/// SPECIAL BEHAVIOR: Same as SafetyCheck. TODO: Replace this with `SafetyCheck`.
4242
ArithmeticOverflow,
4343
/// The Rust `assume` instrinsic is `assert`'d by Kani, and gets this property class.
4444
///
@@ -67,13 +67,13 @@ pub enum PropertyClass {
6767
/// That is, they do not depend on special instrumentation that Kani performs that wouldn't
6868
/// otherwise be observable.
6969
Assertion,
70-
/// Another instrinsic check.
70+
/// Another intrinsic check.
7171
///
72-
/// SPECIAL BEHAVIOR: None TODO: Why should this exist?
72+
/// SPECIAL BEHAVIOR: Same as SafetyCheck. TODO: Replace this with `SafetyCheck`.
7373
ExactDiv,
74-
/// Another instrinsic check.
74+
/// Another intrinsic check.
7575
///
76-
/// SPECIAL BEHAVIOR: None TODO: Why should this exist?
76+
/// SPECIAL BEHAVIOR: Same as SafetyCheck. TODO: Replace this with `SafetyCheck`.
7777
FiniteCheck,
7878
/// Checks added by Kani compiler to determine whether a property (e.g.
7979
/// `PropertyClass::Assertion` or `PropertyClass:Cover`) is reachable
@@ -82,6 +82,7 @@ pub enum PropertyClass {
8282
/// E.g., things that trigger UB or unstable behavior.
8383
///
8484
/// SPECIAL BEHAVIOR: Assertions that may not exist when running code normally (i.e. not under Kani)
85+
/// This is not caught by `#[should_panic]`.
8586
SafetyCheck,
8687
/// Checks to ensure that Kani's code generation is correct.
8788
///

kani-compiler/src/codegen_cprover_gotoc/codegen/intrinsic.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1015,7 +1015,7 @@ impl GotocCtx<'_> {
10151015
///
10161016
/// This function handles code generation for the `arith_offset` intrinsic.
10171017
/// <https://doc.rust-lang.org/std/intrinsics/fn.arith_offset.html>
1018-
/// According to the documenation, the operation is always safe.
1018+
/// According to the documentation, the operation is always safe.
10191019
fn codegen_arith_offset(&mut self, mut fargs: Vec<Expr>, p: &Place, loc: Location) -> Stmt {
10201020
let src_ptr = fargs.remove(0);
10211021
let offset = fargs.remove(0);

kani-compiler/src/codegen_cprover_gotoc/codegen/rvalue.rs

Lines changed: 8 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,7 @@ impl GotocCtx<'_> {
356356

357357
fn codegen_rvalue_binary_op(
358358
&mut self,
359-
ty: Ty,
359+
_ty: Ty,
360360
op: &BinOp,
361361
e1: &Operand,
362362
e2: &Operand,
@@ -405,42 +405,15 @@ impl GotocCtx<'_> {
405405
}
406406
// https://doc.rust-lang.org/std/primitive.pointer.html#method.offset
407407
BinOp::Offset => {
408+
// We don't need to check for UB since we handled user calls of offset already
409+
// via rustc_intrinsic transformation pass.
410+
//
411+
// This operation may still be used by other Kani instrumentation which should
412+
// ensure safety.
413+
// We should consider adding sanity checks if `debug_assertions` are enabled.
408414
let ce1 = self.codegen_operand_stable(e1);
409415
let ce2 = self.codegen_operand_stable(e2);
410-
411-
// Check that computing `offset` in bytes would not overflow
412-
let (offset_bytes, bytes_overflow_check) = self.count_in_bytes(
413-
ce2.clone().cast_to(Type::ssize_t()),
414-
pointee_type_stable(ty).unwrap(),
415-
Type::ssize_t(),
416-
"offset",
417-
loc,
418-
);
419-
420-
// Check that the computation would not overflow an `isize` which is UB:
421-
// https://doc.rust-lang.org/std/primitive.pointer.html#method.offset
422-
// These checks may allow a wrapping-around behavior in CBMC:
423-
// https://github.com/model-checking/kani/issues/1150
424-
// Note(std): We don't check that the starting or resulting pointer stay
425-
// within bounds of the object they point to. Doing so causes spurious
426-
// failures due to the usage of these intrinsics in the standard library.
427-
// See <https://github.com/model-checking/kani/issues/1233> for more details.
428-
// Note that this is one of the safety conditions for `offset`:
429-
// <https://doc.rust-lang.org/std/primitive.pointer.html#safety-2>
430-
431-
let overflow_res = ce1.clone().cast_to(Type::ssize_t()).add_overflow(offset_bytes);
432-
let overflow_check = self.codegen_assert_assume(
433-
overflow_res.overflowed.not(),
434-
PropertyClass::ArithmeticOverflow,
435-
"attempt to compute offset which would overflow",
436-
loc,
437-
);
438-
let res = ce1.clone().plus(ce2);
439-
Expr::statement_expression(
440-
vec![bytes_overflow_check, overflow_check, res.as_stmt(loc)],
441-
ce1.typ().clone(),
442-
loc,
443-
)
416+
ce1.clone().plus(ce2)
444417
}
445418
}
446419
}

kani-compiler/src/codegen_cprover_gotoc/overrides/hooks.rs

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -134,15 +134,46 @@ impl GotocHook for Assert {
134134

135135
let (msg, reach_stmt) = gcx.codegen_reachability_check(msg, span);
136136

137-
// Since `cond` might have side effects, assign it to a temporary
138-
// variable so that it's evaluated once, then assert and assume it
139-
// TODO: I don't think `cond` can have side effects, this is MIR, it's going to be temps
140-
let (tmp, decl) = gcx.decl_temp_variable(cond.typ().clone(), Some(cond), caller_loc);
141137
Stmt::block(
142138
vec![
143139
reach_stmt,
144-
decl,
145-
gcx.codegen_assert_assume(tmp, PropertyClass::Assertion, &msg, caller_loc),
140+
gcx.codegen_assert_assume(cond, PropertyClass::Assertion, &msg, caller_loc),
141+
Stmt::goto(bb_label(target), caller_loc),
142+
],
143+
caller_loc,
144+
)
145+
}
146+
}
147+
148+
struct UnsupportedCheck;
149+
impl GotocHook for UnsupportedCheck {
150+
fn hook_applies(&self, _tcx: TyCtxt, _instance: Instance) -> bool {
151+
unreachable!("{UNEXPECTED_CALL}")
152+
}
153+
154+
fn handle(
155+
&self,
156+
gcx: &mut GotocCtx,
157+
_instance: Instance,
158+
mut fargs: Vec<Expr>,
159+
_assign_to: &Place,
160+
target: Option<BasicBlockIdx>,
161+
span: Span,
162+
) -> Stmt {
163+
assert_eq!(fargs.len(), 2);
164+
let msg = fargs.pop().unwrap();
165+
let cond = fargs.pop().unwrap().cast_to(Type::bool());
166+
let msg = gcx.extract_const_message(&msg).unwrap();
167+
let target = target.unwrap();
168+
let caller_loc = gcx.codegen_caller_span_stable(span);
169+
Stmt::block(
170+
vec![
171+
gcx.codegen_assert_assume(
172+
cond,
173+
PropertyClass::UnsupportedConstruct,
174+
&msg,
175+
caller_loc,
176+
),
146177
Stmt::goto(bb_label(target), caller_loc),
147178
],
148179
caller_loc,
@@ -166,8 +197,8 @@ impl GotocHook for SafetyCheck {
166197
span: Span,
167198
) -> Stmt {
168199
assert_eq!(fargs.len(), 2);
169-
let cond = fargs.remove(0).cast_to(Type::bool());
170-
let msg = fargs.remove(0);
200+
let msg = fargs.pop().unwrap();
201+
let cond = fargs.pop().unwrap().cast_to(Type::bool());
171202
let msg = gcx.extract_const_message(&msg).unwrap();
172203
let target = target.unwrap();
173204
let caller_loc = gcx.codegen_caller_span_stable(span);
@@ -181,6 +212,7 @@ impl GotocHook for SafetyCheck {
181212
}
182213
}
183214

215+
// TODO: Remove this and replace occurrences with `SanityCheck`.
184216
struct Check;
185217
impl GotocHook for Check {
186218
fn hook_applies(&self, _tcx: TyCtxt, _instance: Instance) -> bool {
@@ -709,6 +741,7 @@ pub fn fn_hooks() -> GotocHooks {
709741
(KaniHook::IsAllocated, Rc::new(IsAllocated)),
710742
(KaniHook::PointerObject, Rc::new(PointerObject)),
711743
(KaniHook::PointerOffset, Rc::new(PointerOffset)),
744+
(KaniHook::UnsupportedCheck, Rc::new(UnsupportedCheck)),
712745
(KaniHook::UntrackedDeref, Rc::new(UntrackedDeref)),
713746
(KaniHook::InitContracts, Rc::new(InitContracts)),
714747
(KaniHook::FloatToIntInRange, Rc::new(FloatToIntInRange)),

kani-compiler/src/kani_middle/kani_functions.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ pub enum KaniModel {
8181
IsSliceChunkPtrInitialized,
8282
#[strum(serialize = "IsSlicePtrInitializedModel")]
8383
IsSlicePtrInitialized,
84+
#[strum(serialize = "OffsetModel")]
85+
Offset,
8486
#[strum(serialize = "RunContractModel")]
8587
RunContract,
8688
#[strum(serialize = "RunLoopContractModel")]
@@ -140,6 +142,8 @@ pub enum KaniHook {
140142
PointerOffset,
141143
#[strum(serialize = "SafetyCheckHook")]
142144
SafetyCheck,
145+
#[strum(serialize = "UnsupportedCheckHook")]
146+
UnsupportedCheck,
143147
#[strum(serialize = "UntrackedDerefHook")]
144148
UntrackedDeref,
145149
}

kani-compiler/src/kani_middle/transform/body.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,11 @@ impl MutableBody {
434434
) {
435435
self.blocks.get_mut(source_instruction.bb()).unwrap().terminator = new_term;
436436
}
437+
438+
/// Remove the given statement.
439+
pub fn remove_stmt(&mut self, bb: BasicBlockIdx, stmt: usize) {
440+
self.blocks[bb].statements.remove(stmt);
441+
}
437442
}
438443

439444
// TODO: Remove this enum, since we now only support kani's assert.

kani-compiler/src/kani_middle/transform/rustc_intrinsics.rs

Lines changed: 107 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,21 @@
88
99
use crate::intrinsics::Intrinsic;
1010
use crate::kani_middle::kani_functions::{KaniFunction, KaniModel};
11-
use crate::kani_middle::transform::body::{MutMirVisitor, MutableBody};
11+
use crate::kani_middle::transform::body::{
12+
InsertPosition, MutMirVisitor, MutableBody, SourceInstruction,
13+
};
1214
use crate::kani_middle::transform::{TransformPass, TransformationType};
1315
use crate::kani_queries::QueryDb;
1416
use rustc_middle::ty::TyCtxt;
17+
use rustc_smir::rustc_internal;
1518
use stable_mir::mir::mono::Instance;
16-
use stable_mir::mir::{Body, ConstOperand, LocalDecl, Operand, Terminator, TerminatorKind};
17-
use stable_mir::ty::{FnDef, MirConst, RigidTy, TyKind};
19+
use stable_mir::mir::{
20+
BasicBlockIdx, BinOp, Body, ConstOperand, LocalDecl, Operand, Rvalue, StatementKind,
21+
Terminator, TerminatorKind,
22+
};
23+
use stable_mir::ty::{
24+
FnDef, GenericArgKind, GenericArgs, IntTy, MirConst, RigidTy, Span, Ty, TyKind, UintTy,
25+
};
1826
use std::collections::HashMap;
1927
use tracing::debug;
2028

@@ -42,12 +50,14 @@ impl TransformPass for RustcIntrinsicsPass {
4250

4351
/// Transform the function body by inserting checks one-by-one.
4452
/// For every unsafe dereference or a transmute operation, we check all values are valid.
45-
fn transform(&mut self, _tcx: TyCtxt, body: Body, instance: Instance) -> (bool, Body) {
53+
fn transform(&mut self, tcx: TyCtxt, body: Body, instance: Instance) -> (bool, Body) {
4654
debug!(function=?instance.name(), "transform");
4755
let mut new_body = MutableBody::from(body);
48-
let mut visitor = ReplaceIntrinsicVisitor::new(&self.models, new_body.locals().to_vec());
56+
let mut visitor =
57+
ReplaceIntrinsicCallVisitor::new(&self.models, new_body.locals().to_vec());
4958
visitor.visit_body(&mut new_body);
50-
(visitor.changed, new_body.into())
59+
let changed = self.replace_lowered_intrinsics(tcx, &mut new_body);
60+
(visitor.changed || changed, new_body.into())
5161
}
5262
}
5363

@@ -63,21 +73,78 @@ impl RustcIntrinsicsPass {
6373
debug!(?models, "RustcIntrinsicsPass::new");
6474
RustcIntrinsicsPass { models }
6575
}
76+
77+
/// This function checks if we need to replace intrinsics that have been lowered.
78+
fn replace_lowered_intrinsics(&self, tcx: TyCtxt, body: &mut MutableBody) -> bool {
79+
// Do a reverse iteration on the instructions since we will replace Rvalues by a function
80+
// call, which will split the basic block.
81+
let mut changed = false;
82+
let orig_bbs = body.blocks().len();
83+
for bb in (0..orig_bbs).rev() {
84+
let num_stmts = body.blocks()[bb].statements.len();
85+
for stmt in (0..num_stmts).rev() {
86+
changed |= self.replace_offset(tcx, body, bb, stmt);
87+
}
88+
}
89+
changed
90+
}
91+
92+
/// Replace a lowered offset intrinsic.
93+
fn replace_offset(
94+
&self,
95+
tcx: TyCtxt,
96+
body: &mut MutableBody,
97+
bb: BasicBlockIdx,
98+
stmt: usize,
99+
) -> bool {
100+
let statement = &body.blocks()[bb].statements[stmt];
101+
let StatementKind::Assign(place, rvalue) = &statement.kind else {
102+
return false;
103+
};
104+
let Rvalue::BinaryOp(BinOp::Offset, op1, op2) = rvalue else { return false };
105+
let mut source = SourceInstruction::Statement { idx: stmt, bb };
106+
107+
// Double check input parameters of `offset` operation.
108+
let offset_ty = op2.ty(body.locals()).unwrap();
109+
let pointer_ty = op1.ty(body.locals()).unwrap();
110+
validate_offset(tcx, offset_ty, statement.span);
111+
validate_raw_ptr(tcx, pointer_ty, statement.span);
112+
tcx.dcx().abort_if_errors();
113+
114+
let pointee_ty = pointer_ty.kind().builtin_deref(true).unwrap().ty;
115+
// The model takes the following parameters (PointeeType, PointerType, OffsetType).
116+
let model = self.models[&KaniModel::Offset];
117+
let params = vec![
118+
GenericArgKind::Type(pointee_ty),
119+
GenericArgKind::Type(pointer_ty),
120+
GenericArgKind::Type(offset_ty),
121+
];
122+
let instance = Instance::resolve(model, &GenericArgs(params)).unwrap();
123+
body.insert_call(
124+
&instance,
125+
&mut source,
126+
InsertPosition::After,
127+
vec![op1.clone(), op2.clone()],
128+
place.clone(),
129+
);
130+
body.remove_stmt(bb, stmt);
131+
true
132+
}
66133
}
67134

68-
struct ReplaceIntrinsicVisitor<'a> {
135+
struct ReplaceIntrinsicCallVisitor<'a> {
69136
models: &'a HashMap<KaniModel, FnDef>,
70137
locals: Vec<LocalDecl>,
71138
changed: bool,
72139
}
73140

74-
impl<'a> ReplaceIntrinsicVisitor<'a> {
141+
impl<'a> ReplaceIntrinsicCallVisitor<'a> {
75142
fn new(models: &'a HashMap<KaniModel, FnDef>, locals: Vec<LocalDecl>) -> Self {
76-
ReplaceIntrinsicVisitor { models, locals, changed: false }
143+
ReplaceIntrinsicCallVisitor { models, locals, changed: false }
77144
}
78145
}
79146

80-
impl MutMirVisitor for ReplaceIntrinsicVisitor<'_> {
147+
impl MutMirVisitor for ReplaceIntrinsicCallVisitor<'_> {
81148
/// Replace the terminator for some rustc's intrinsics.
82149
///
83150
/// In some cases, we replace a function call to a rustc intrinsic by a call to the
@@ -117,3 +184,33 @@ impl MutMirVisitor for ReplaceIntrinsicVisitor<'_> {
117184
self.super_terminator(term);
118185
}
119186
}
187+
188+
/// Validate whether the offset type is valid, i.e., `isize` or `usize`.
189+
///
190+
/// This will emit an error if the type is wrong but not abort.
191+
/// Invoke `tcx.dcx().abort_if_errors()` to abort execution.
192+
fn validate_offset(tcx: TyCtxt, offset_ty: Ty, span: Span) {
193+
if !matches!(
194+
offset_ty.kind(),
195+
TyKind::RigidTy(RigidTy::Int(IntTy::Isize)) | TyKind::RigidTy(RigidTy::Uint(UintTy::Usize))
196+
) {
197+
tcx.dcx().span_err(
198+
rustc_internal::internal(tcx, span),
199+
format!("Expected `isize` or `usize` for offset type. Found `{offset_ty}` instead"),
200+
);
201+
}
202+
}
203+
204+
/// Validate that we have a raw pointer otherwise emit an error.
205+
///
206+
/// This will emit an error if the type is wrong but not abort.
207+
/// Invoke `tcx.dcx().abort_if_errors()` to abort execution.
208+
fn validate_raw_ptr(tcx: TyCtxt, ptr_ty: Ty, span: Span) {
209+
let pointer_ty_kind = ptr_ty.kind();
210+
if !pointer_ty_kind.is_raw_ptr() {
211+
tcx.dcx().span_err(
212+
rustc_internal::internal(tcx, span),
213+
format!("Expected raw pointer for pointer type. Found `{ptr_ty}` instead"),
214+
);
215+
}
216+
}

library/kani_core/src/lib.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,20 @@ macro_rules! kani_intrinsics {
308308
assert!(cond, "Safety check failed: {msg}");
309309
}
310310

311+
/// This should indicate that Kani does not support a certain operation.
312+
#[doc(hidden)]
313+
#[allow(dead_code)]
314+
#[kanitool::fn_marker = "UnsupportedCheckHook"]
315+
#[inline(never)]
316+
#[allow(clippy::diverging_sub_expression)]
317+
pub(crate) fn unsupported(msg: &'static str) -> ! {
318+
#[cfg(not(feature = "concrete_playback"))]
319+
return kani_intrinsic();
320+
321+
#[cfg(feature = "concrete_playback")]
322+
unimplemented!("Unsupported Kani operation: {msg}")
323+
}
324+
311325
/// An empty body that can be used to define Kani intrinsic functions.
312326
///
313327
/// A Kani intrinsic is a function that is interpreted by Kani compiler.

0 commit comments

Comments
 (0)