Skip to content

Commit 5eeb7cf

Browse files
author
Carolyn Zech
authored
[Breaking Change] Fail if stub verified doesn't have a contract harness (#4295)
Emit a compilation error if the target of a `stub_verified` attribute does not have a contract harness. Also require `-Z stubbing` for stub_verified. Both of these are breaking changes. The former I feel strongly should go in, since IMO it's a soundness hole. The latter I could be convinced to revert if we feel like it's unnecessary churn; it just felt odd to me to have an unstable stub feature that's not behind the unstable stub flag. Note that this compilation error is not really sufficient to ensure that the stub's contract holds, since a user can just pass `--harness` to skip the contract harness. A better design would be to insert the check automatically rather than requiring a separate harness. But this is better than nothing and less invasive to implement, so start with this for now. Resolves #4294 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 4da74f9 commit 5eeb7cf

40 files changed

+322
-83
lines changed

kani-compiler/src/kani_middle/attributes.rs

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
55
use std::collections::{BTreeMap, HashSet};
66

7+
use fxhash::FxHashMap;
78
use kani_metadata::{CbmcSolver, HarnessAttributes, HarnessKind, Stub};
89
use quote::ToTokens;
910
use rustc_ast::{LitKind, MetaItem, MetaItemKind};
@@ -118,6 +119,11 @@ impl KaniAttributeKind {
118119
pub fn demands_function_contract_use(self) -> bool {
119120
matches!(self, KaniAttributeKind::ProofForContract)
120121
}
122+
123+
/// Is this a stubbing attribute that requires the experimental stubbing feature?
124+
pub fn demands_stubbing_use(self) -> bool {
125+
matches!(self, KaniAttributeKind::Stub | KaniAttributeKind::StubVerified)
126+
}
121127
}
122128

123129
/// Bundles together common data used when evaluating the attributes of a given
@@ -300,11 +306,16 @@ impl<'tcx> KaniAttributes<'tcx> {
300306
}
301307

302308
/// Check that all attributes assigned to an item is valid.
309+
/// Returns a tuple of (stub_verified_targets_with_spans, proof_for_contract_targets).
303310
/// Errors will be added to the session. Invoke self.tcx.sess.abort_if_errors() to terminate
304311
/// the session and emit all errors found.
305-
pub(super) fn check_attributes(&self) {
312+
pub(super) fn check_attributes(&self) -> (FxHashMap<FnDefStable, Span>, HashSet<FnDefStable>) {
306313
// Check that all attributes are correctly used and well formed.
307314
let is_harness = self.is_proof_harness();
315+
316+
let mut contract_targets = HashSet::default();
317+
let mut stub_verified_targets = FxHashMap::default();
318+
308319
for (&kind, attrs) in self.map.iter() {
309320
let local_error = |msg| self.tcx.dcx().span_err(attrs[0].span(), msg);
310321

@@ -363,12 +374,19 @@ impl<'tcx> KaniAttributes<'tcx> {
363374
expect_single(self.tcx, kind, attrs);
364375
attrs.iter().for_each(|attr| {
365376
self.check_proof_attribute(kind, attr);
366-
let _ = self.parse_single_path_attr(attr);
377+
let res = self.parse_single_path_attr(attr);
378+
if let Ok(target) = res {
379+
contract_targets.insert(target.def().to_owned());
380+
}
367381
})
368382
}
369383
KaniAttributeKind::StubVerified => {
370384
attrs.iter().for_each(|attr| {
371385
self.check_stub_verified(attr);
386+
let res = self.parse_single_path_attr(attr);
387+
if let Ok(target) = res {
388+
stub_verified_targets.insert(target.def().to_owned(), attr.span());
389+
}
372390
});
373391
}
374392
KaniAttributeKind::FnMarker
@@ -393,6 +411,7 @@ impl<'tcx> KaniAttributes<'tcx> {
393411
}
394412
}
395413
}
414+
(stub_verified_targets, contract_targets)
396415
}
397416

398417
/// Get the value of an attribute if one exists.
@@ -433,6 +452,22 @@ impl<'tcx> KaniAttributes<'tcx> {
433452
}
434453
}
435454

455+
// If the `stubbing` unstable feature is not enabled then no
456+
// function should use any of those APIs.
457+
if !enabled_features.iter().any(|feature| feature == "stubbing") {
458+
for kind in self.map.keys().copied().filter(|a| a.demands_stubbing_use()) {
459+
let msg = format!(
460+
"Using the {} attribute requires activating the unstable `stubbing` feature",
461+
kind.as_ref()
462+
);
463+
if let Some(attr) = self.map.get(&kind).unwrap().first() {
464+
self.tcx.dcx().span_err(attr.span(), msg);
465+
} else {
466+
self.tcx.dcx().err(msg);
467+
}
468+
}
469+
}
470+
436471
if let Some(unstable_attrs) = self.map.get(&KaniAttributeKind::Unstable) {
437472
for attr in unstable_attrs {
438473
let unstable_attr = UnstableAttribute::try_from(*attr).unwrap();

kani-compiler/src/kani_middle/mod.rs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,16 @@ pub mod transform;
4141
/// error was found.
4242
pub fn check_crate_items(tcx: TyCtxt, ignore_asm: bool) {
4343
let krate = tcx.crate_name(LOCAL_CRATE);
44+
let mut all_stub_verified_targets = FxHashMap::default();
45+
let mut all_contract_targets = HashSet::new();
46+
4447
for item in tcx.hir_free_items() {
4548
let def_id = item.owner_id.def_id.to_def_id();
46-
KaniAttributes::for_item(tcx, def_id).check_attributes();
49+
let (stub_verified_targets, contract_targets) =
50+
KaniAttributes::for_item(tcx, def_id).check_attributes();
51+
all_stub_verified_targets.extend(stub_verified_targets);
52+
all_contract_targets.extend(contract_targets);
53+
4754
if tcx.def_kind(def_id) == DefKind::GlobalAsm {
4855
if !ignore_asm {
4956
let error_msg = format!(
@@ -59,6 +66,21 @@ pub fn check_crate_items(tcx: TyCtxt, ignore_asm: bool) {
5966
}
6067
}
6168
}
69+
70+
// Validate that all stub_verified targets have corresponding proof_for_contract harnesses
71+
for (stub_verified_target, span) in all_stub_verified_targets {
72+
if !all_contract_targets.contains(&stub_verified_target) {
73+
tcx.dcx().struct_span_err(
74+
span,
75+
format!(
76+
"stub verified target `{}` does not have a corresponding `#[proof_for_contract]` harness",
77+
stub_verified_target.name()
78+
),
79+
).with_help("verified stubs are meant to be sound abstractions for a function's behavior, so Kani enforces that proofs exist for the stub's contract")
80+
.emit();
81+
}
82+
}
83+
6284
tcx.dcx().abort_if_errors();
6385
}
6486

kani-driver/src/harness_runner.rs

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,6 @@ impl<'pr> HarnessRunner<'_, 'pr> {
5555
&self,
5656
harnesses: &'pr [&HarnessMetadata],
5757
) -> Result<Vec<HarnessResult<'pr>>> {
58-
self.check_stubbing(harnesses)?;
59-
6058
let sorted_harnesses = crate::metadata::sort_harnesses_by_loc(harnesses);
6159
let pool = {
6260
let mut builder = rayon::ThreadPoolBuilder::new();
@@ -114,33 +112,6 @@ impl<'pr> HarnessRunner<'_, 'pr> {
114112
}
115113
}
116114
}
117-
118-
/// Return an error if the user is trying to verify a harness with stubs without enabling the
119-
/// experimental feature.
120-
fn check_stubbing(&self, harnesses: &[&HarnessMetadata]) -> Result<()> {
121-
if !self.sess.args.is_stubbing_enabled() {
122-
let with_stubs: Vec<_> = harnesses
123-
.iter()
124-
.filter_map(|harness| {
125-
(!harness.attributes.stubs.is_empty()).then_some(harness.pretty_name.as_str())
126-
})
127-
.collect();
128-
match with_stubs.as_slice() {
129-
[] => { /* do nothing */ }
130-
[harness] => bail!(
131-
"Use of unstable feature 'stubbing' in harness `{}`.\n\
132-
To enable stubbing, pass option `-Z stubbing`",
133-
harness
134-
),
135-
harnesses => bail!(
136-
"Use of unstable feature 'stubbing' in harnesses `{}`.\n\
137-
To enable stubbing, pass option `-Z stubbing`",
138-
harnesses.join("`, `")
139-
),
140-
}
141-
}
142-
Ok(())
143-
}
144115
}
145116

146117
impl KaniSession {

tests/expected/function-contract/as-assertions/precedence.expected

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,4 @@ assertion\
3131
- Status: SUCCESS\
3232
- Description: "|_| old(*add_one_ptr + 1) == *add_one_ptr"
3333

34-
Complete - 3 successfully verified harnesses, 0 failures, 3 total.
34+
Complete - 4 successfully verified harnesses, 0 failures, 4 total.

tests/expected/function-contract/as-assertions/precedence.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Copyright Kani Contributors
22
// SPDX-License-Identifier: Apache-2.0 OR MIT
3-
// kani-flags: -Zfunction-contracts
3+
// kani-flags: -Zfunction-contracts -Zstubbing
44

55
// If a function is the target of a proof_for_contract or stub_verified, we should defer to the contract handling for those modes.
66

@@ -12,6 +12,7 @@ fn add_three(add_three_ptr: &mut u32) {
1212
}
1313

1414
#[kani::requires(*add_two_ptr < 101)]
15+
#[kani::modifies(add_two_ptr)]
1516
#[kani::ensures(|_| old(*add_two_ptr + 2) == *add_two_ptr)]
1617
fn add_two(add_two_ptr: &mut u32) {
1718
*add_two_ptr += 1;
@@ -59,3 +60,11 @@ fn stub_verified_takes_precedence() {
5960
let mut i = kani::any();
6061
add_three(&mut i);
6162
}
63+
64+
// The stub_verified contract mandates that we have a proof for the target of the verified stub
65+
#[kani::proof_for_contract(add_two)]
66+
fn check_add_two() {
67+
// 3 so that when add_one's contract is generated as an assertion, its precondition holds
68+
let mut x = 3;
69+
add_two(&mut x);
70+
}

tests/expected/function-contract/gcd_rec_replacement_pass.rs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Copyright Kani Contributors
22
// SPDX-License-Identifier: Apache-2.0 OR MIT
3-
// kani-flags: -Zfunction-contracts
3+
// kani-flags: -Zfunction-contracts -Zstubbing
44
type T = u8;
55

66
/// Euclid's algorithm for calculating the GCD of two numbers
@@ -47,13 +47,21 @@ impl Frac {
4747
}
4848
}
4949

50+
#[kani::proof_for_contract(gcd)]
51+
#[kani::unwind(64)]
52+
fn gcd_harness() {
53+
let x: T = kani::any();
54+
let y: T = kani::any();
55+
gcd(x, y);
56+
}
57+
5058
#[kani::proof]
5159
#[kani::stub_verified(gcd)]
5260
fn gcd_as_replace() {
53-
gcd_harness()
61+
gcd_test_harness()
5462
}
5563

56-
fn gcd_harness() {
64+
fn gcd_test_harness() {
5765
// Needed to avoid having `free` be removed as unused function. This is
5866
// because DFCC contract enforcement assumes that a definition for `free`
5967
// exists.

tests/expected/function-contract/gcd_replacement_fail.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Copyright Kani Contributors
22
// SPDX-License-Identifier: Apache-2.0 OR MIT
3-
// kani-flags: -Zfunction-contracts
3+
// kani-flags: -Zfunction-contracts -Zstubbing
44
type T = u8;
55

66
/// Euclid's algorithm for calculating the GCD of two numbers
@@ -54,6 +54,14 @@ impl Frac {
5454
}
5555
}
5656

57+
#[kani::proof_for_contract(gcd)]
58+
#[kani::unwind(64)]
59+
fn gcd_harness() {
60+
let x: T = kani::any();
61+
let y: T = kani::any();
62+
gcd(x, y);
63+
}
64+
5765
#[kani::proof]
5866
#[kani::stub_verified(gcd)]
5967
fn main() {

tests/expected/function-contract/gcd_replacement_pass.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Copyright Kani Contributors
22
// SPDX-License-Identifier: Apache-2.0 OR MIT
3-
// kani-flags: -Zfunction-contracts
3+
// kani-flags: -Zfunction-contracts -Zstubbing
44
type T = u8;
55

66
/// Euclid's algorithm for calculating the GCD of two numbers
@@ -54,6 +54,14 @@ impl Frac {
5454
}
5555
}
5656

57+
#[kani::proof_for_contract(gcd)]
58+
#[kani::unwind(64)]
59+
fn gcd_harness() {
60+
let x: T = kani::any();
61+
let y: T = kani::any();
62+
gcd(x, y);
63+
}
64+
5765
#[kani::proof]
5866
#[kani::stub_verified(gcd)]
5967
fn main() {

tests/expected/function-contract/history/stub.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// SPDX-License-Identifier: Apache-2.0 OR MIT
33
// This test consumes > 9 GB of memory with 16 object bits. Reducing the number
44
// of object bits to 8 to avoid running out of memory.
5-
// kani-flags: -Zfunction-contracts -Z unstable-options --cbmc-args --object-bits 8
5+
// kani-flags: -Zfunction-contracts -Zstubbing -Z unstable-options --cbmc-args --object-bits 8
66

77
#[kani::ensures(|result| old(*ptr + *ptr) == *ptr)]
88
#[kani::requires(*ptr < 100)]

tests/expected/function-contract/interior-mutability/api/cell_stub.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Copyright Kani Contributors
22
// SPDX-License-Identifier: Apache-2.0 OR MIT
3-
// kani-flags: -Zfunction-contracts --solver minisat
3+
// kani-flags: -Zfunction-contracts -Zstubbing --solver minisat
44

55
/// The objective of this test is to show that the contracts for double can be replaced as a stub within the contracts for quadruple.
66
/// This shows that we can generate `kani::any()` for Cell.

0 commit comments

Comments
 (0)