Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions rfc/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@
- [0011-source-coverage](rfcs/0011-source-coverage.md)
- [0012-loop-contracts](rfcs/0012-loop-contracts.md)
- [0013-list](rfcs/0013-list.md)
- [0014-rust-ub-checks](rfcs/0014-rust-ub-checks.md)
187 changes: 187 additions & 0 deletions rfc/src/rfcs/0014-rust-ub-checks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
- **Feature Name:** Rust UB Checks (`rust-ub-checks`)
- **Feature Request Issue:** [#3089](https://github.com/model-checking/kani/issues/3089)
- **RFC PR:** [#3091](https://github.com/model-checking/kani/pull/3091)
- **Status:** Unstable
- **Version:** 0
- **Proof-of-concept:**

-------------------

## Summary

Specify a consistent framework for Undefined Behavior (UB) checks in Kani.

## User Impact

Provide a consistent user experience for all UB checks in Kani, while establishing the best mechanisms for
further instrumentation of undefined behavior in Kani.

## User Experience

A UB check should behave consistently independent on how and where it is implemented.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
A UB check should behave consistently independent on how and where it is implemented.
A UB check should behave consistently independent of how and where it is implemented.

The detection of an undefined behavior should impact the status of any assertion that may be reachable from the
detected undefined behavior.

For simplicity, we propose that all undefined behavior checks are modelled as assert-assume pairs.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Does this comment affect the validity of this approach at all?
(More generally, the PR description mentions that those two CBMC issues will affect the RFC. Since those issues are now closed/merged, could you update the PR description to instead explain how they affect the approach outlined here, if at all?)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ok, after finishing the RFC, I believe the demonic nondeterminism mention later addresses the soundness concerns from the linked comment.

We propose that the status of all passing assertions that may be affected by this check to be displayed as
`UNDETERMINED` if one or more UB checks fail.
For simplicity, Kani currently over-approximate this set by marking all passing assertions as `UNDETERMINED`.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
For simplicity, Kani currently over-approximate this set by marking all passing assertions as `UNDETERMINED`.
For simplicity, Kani currently over-approximates this set by marking all passing assertions as `UNDETERMINED`.

Comment on lines +26 to +28
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
We propose that the status of all passing assertions that may be affected by this check to be displayed as
`UNDETERMINED` if one or more UB checks fail.
For simplicity, Kani currently over-approximate this set by marking all passing assertions as `UNDETERMINED`.
Currently, if a UB check fails, Kani overapproximates and marks all passing assertions as `UNDETERMINED`.
We propose refining this overapproximation to only mark a passing assertion as `UNDETERMINED` if it is reachable from the failing UB check.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I suppose after looking at this comment, maybe "reachable" isn't the right word here--you said "downstream," and I take the difference to be that "downstream" just means "after" whereas "reachable" is "after, and also reachable." So perhaps this section can say that we propose an assertion be undetermined if it's "downstream" of the failing UB check, and then later, as an example of future pruning, you could give the example of only reachable passing assertions being undetermined.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

My overall motivation here is to try to be specific about what exactly we're proposing--we can always come back and update the RFC if we implement something different.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I actually don't want to be super specific at this point. Maybe let me change the proposal to say that we shall mark all passing assertions as UNDETERMINED, and mentioned that refining the set of assertions marked as UNDETERMINED is out of scope of this RFC.

Future pruning of this set can be done with further analysis, but it should not affect soundness.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can you give examples of other pruning that we could do / it would make sense for us to do?


All failing assertions will have their status preserved.
This may create spurious counter examples for cases where the failing UB check is implemented using "demonic"
non-determinism.[^demonic]
Comment on lines +32 to +33
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can you explain why?


[^demonic]: Some UB checks implementation use one tracker with non-deterministic assignment to track the
status of different objects.
This implementation relies on the fact that an assertion will fail if there is at least one possible
assignment to its tracker that violates the assertion.
Picking an assignment that fails can be seen as choosing the worst possible action, known as demonic nondeterminism.

See [Open questions section](0010-rust-ub-checks.md#open-questions) for alternatives to be considered.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
See [Open questions section](0010-rust-ub-checks.md#open-questions) for alternatives to be considered.
See the [Open questions section](0014-rust-ub-checks.md#open-questions) for alternatives to be considered.


### Concrete playback of UB checks

A counter example that triggers UB may require extra tooling for debugging, e.g.: MIRI or valgrind.
We propose that tests added by concrete playback in those cases should include a comment stating such limitation.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
We propose that tests added by concrete playback in those cases should include a comment stating such limitation.
We propose that tests added by concrete playback in those cases should include a comment stating this limitation.

Other possibilities are discussed in [0010-rust-ub-checks.md#open-questions].
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
Other possibilities are discussed in [0010-rust-ub-checks.md#open-questions].
Other possibilities are discussed in [0014-rust-ub-checks.md#open-questions].


### Delayed undefined behavior

In some cases, undefined behavior may occur in safe code that interacts with unsafe code.

For example, the code below shows how reading a boolean value in safe code may trigger UB due to an
invalid write:

```rust
#[kani::proof]
fn buggy_code() {
let mut value: bool = false;
let ptr: *mut bool = &mut value as *mut _;
let mut_ref = &mut value;
unsafe {
*ptr = true; // This assignment is safe if `mut_ref` is never used!
}
assert!(!*mut_ref); // This triggers UB ⚠️!
}
```

It may not be scalable for Kani to verify that every value produced is valid.
In cases like that, we may opt for implementing one of the following UB checks:

1. Perform static analysis to first identify which code may be affected by unsafe code, and only instrument those that
are reachable from the unsafe code.
2. Add eager UB detection in unsafe code that will fail verification if there are side effects that we may not be
able to detect efficiently. For example, we could fail if casting between types that have different value
requirements, i.e.: fail in the cast `*mut bool as *mut u8`.

For option 2, Kani may fail verification even if the code does not have UB.
In those cases, Kani check must be explicit about this failure being an over-approximation.
For that, we propose that the check message is clear about this limitation, and potentially its status
(see [0010-rust-ub-checks.md#open-questions]).
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
(see [0010-rust-ub-checks.md#open-questions]).
(see [0014-rust-ub-checks.md#open-questions]).


### Opt-out of a class of UB checks

Undefined behavior checks that have been stabilized should be added by default by Kani.
However, Kani should provide a fine-grained control for users to choose which checks to disable.

For every class of UB checks to be added, we shall add a command line argument to disable the check for all harnesses,
as well as an attribute that would allow users to opt-out a check for specific harnesses.

Command line arguments should follow the existing name schema:
`--no-[CHECK-NAME]-checks` where the CHECK-NAME is kebab case. There is no need to add `--[CHECK-NAME]-checks` since
they are enabled by default.

We propose to add the following attribute:

```rust
#[kani::proof]
#[kani::ignore(CHECK_NAMES)]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

"ignore" is a bit too general. Maybe use ignore_ub?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

+1

fn harness() {}
```

Where `CHECK_NAMES` is a list of check category names in snake case, such as `memory_safety`, `valid_values`.

For checks that are unstable, they should only be included if their respective unstable flag are passed, i.e.,
`-Z [CHECK-NAME]`.

## Software Design

*To be filled in the next iteration*

## Rationale and alternatives

Kani already has undefined behavior checks, some are implemented on the CBMC side, such as access out bounds, and
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
Kani already has undefined behavior checks, some are implemented on the CBMC side, such as access out bounds, and
Kani already has undefined behavior checks, some are implemented on the CBMC side, such as out-of-bounds access, and

arithmetic overflow.
While others are implemented on Kani's side, such as intrinsics validation checks, and some are a
result of instrumentation to the Rust standard library.

The behavior of these checks varies according to how they are added.
The ones on CBMC side do not interrupt the analysis of a path, while the ones on Kani / Rust std side will.

For example, memory bound checks are implemented in CBMC, and the following harness today will trigger 2 different
failures:

```rust
#[kani::proof]
pub fn is_zero() {
let var = [0u32; 4];
let ptr = &var as *const u32;
let idx: usize = 4;
assert_eq!(unsafe { ptr.add(idx).read() }, 0);
}
```

The failed checks are:

```plaintext
Failed Checks: assertion failed: unsafe { ptr.add(idx).read() } == 0
Failed Checks: dereference failure: pointer outside object bounds
```

While the following harness only fails the standard library check:

```rust
#[kani::proof]
pub fn is_zero() {
let var = [0; 4];
let idx: usize = 4;
assert_eq!(unsafe { *var.get_unchecked(idx) }, 0);
}
```

Failing due to an assumption check inside `get_unchecked` function.

```plaintext
Failed Checks: assumption failed
```
Comment on lines +153 to +157
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
Failing due to an assumption check inside `get_unchecked` function.
```plaintext
Failed Checks: assumption failed
```
Failing due to an [assumption check](https://github.com/rust-lang/rust/blob/a1d7676d6a8c6ff13f9165e98cc25eeec66cb592/library/core/src/slice/index.rs#L255) inside the `get_unchecked` function.
```plaintext
Failed Checks: assumption failed
File: "library/core/src/slice/index.rs", line 255, in <usize as std::slice::SliceIndex<[i32]>>::get_unchecked

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

(Just to make it a bit clearer that this is an assumption in the standard library itself, not some Kani model of get_unchecked).


We would like to keep taking advantage of existing instrumentation to maximize the UB check coverage.
The mechanism used to build the check, however, shouldn't influence in the user experience.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
The mechanism used to build the check, however, shouldn't influence in the user experience.
The mechanism used to build the check, however, shouldn't influence the user experience.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This section does a great job of explaining the current division of UB checks and how those manifest in different experiences for users, but I'm missing a call to action--what are you proposing we do about it? Should we add a model of get_unchecked to Kani so that we can catch this error ourselves and provide a better error message? Or not, because then we're not relying on existing instrumentation? We don't need to land on a precise answer now (I know the implementation section is empty), but can we give some options at least?
Also, I'm a bit confused about the scope of this RFC. The User Experience section made me think that we were just talking about whether a passing check should pass or be undetermined based on a failing UB check before it. Now it seems like we're also considering the UX of the UB checks themselves. I would recommend adding a sentence or two about consistent messaging for failing UB checks to the User Experience section if you intend for that to be part of the "consistent framework" you're proposing.


### Opt-out vs opt-in

The default behavior matches the one that we believe is the recommended for most users due to soundness.
In some cases, users may need to disable some checks for reasons such as spurious CEX or proof scalability.
Thus, we suggest that every category of checks should have an opt-out mechanism that can either be global or local;
i.e.: via command argument or harness attribute.

The main disadvantage of this model is that stabilizing a new check category is a non-backward compatible change.
I.e.: It may impact the status of a previously passing harness if a new check fails.

## Open questions

- Should we add a new status for potentially spurious counter examples? Such as:
- Counter examples detected together with a non-deterministic UB check.
- Over-approximating UB checks.
- How should UB checks affect concrete playback? Is a comment enough?
- Another possibility would be to add a `unreachable!()` statement at the end of the test case with a message
explaining why the statement should be unreachable.
- Should we deprecate the `--no-default-checks` and all `--[CHECK-NAME]-checks` checks.
- Should we add an attribute that allows us to opt-out some UB checks for a function or some code block?
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
- Should we add an attribute that allows us to opt-out some UB checks for a function or some code block?
- Should we add an attribute that allows us to opt-out of some UB checks for a function or some code block?

- I think we should at least consider adding that option for `kani` library code.
- We've been adding more logic that is used by UB checks, and we may want to skip some checks for them.

## Out of scope / Future Improvements

- **List enabled checks:** Kani could include a command that list all the categories of undefined behaviors supported.