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

Enable an #[safety_constraint(...)] attribute helper for the Arbitrary and Invariant macros #3283

Merged
merged 38 commits into from
Jul 22, 2024

Conversation

adpaco-aws
Copy link
Contributor

@adpaco-aws adpaco-aws commented Jun 21, 2024

This PR enables an #[safety_constraint(...)] attribute helper for the #[derive(Arbitrary)] and #[derive(Invariant)] macro.

For the Invariant derive macro, this allows users to derive more sophisticated invariants for their data types by annotating individual named fields with the #[safety_constraint(<cond>)] attribute, where <cond> represents the predicate to be evaluated for the corresponding field. In addition, the implementation always checks #field.is_safe() for each field.

For example, let's say we are working with the Point type from #3250

#[derive(kani::Invariant)]
struct Point<X, Y> {
    x: X,
    y: Y,
}

and we need to extend it to only allow positive values for both x and y.
With the [safety_constraint(...)] attribute, we can achieve this without explicitly writing the impl Invariant for ... as follows:

#[derive(kani::Invariant)]
struct PositivePoint {
    #[safety_constraint(*x >= 0)]
    x: i32,
    #[safety_constraint(*y >= 0)]
    y: i32,
}

For the Arbitrary derive macro, this allows users to derive more sophisticated kani::any() generators that respect the specified invariants. In other words, the kani::any() will assume any invariants specified through the #[safety_constraint(...)] attribute helper. Going back to the PositivePoint example, we'd expect this harness to be successful:

#[kani::proof]
fn check_invariant_helper_ok() {
    let pos_point: PositivePoint = kani::any();
    assert!(pos_point.x >= 0);
    assert!(pos_point.y >= 0);
}

The PR includes tests to ensure it's working as expected, in addition to UI tests checking for cases where the arguments provided to the macro are incorrect. Happy to add any other cases that you feel are missing.

Related #3095

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 and MIT licenses.

@adpaco-aws adpaco-aws requested a review from a team as a code owner June 21, 2024 21:13
@github-actions github-actions bot added the Z-BenchCI Tag a PR to run benchmark CI label Jun 21, 2024
@celinval
Copy link
Contributor

Quick question... wouldn't it make sense to also parse this helper in the #[derive(Arbitrary)] macro?

@adpaco-aws
Copy link
Contributor Author

I'm not sure about that. How would users create an unsafe value then? This isn't clear to me and I think it's a use-case we should continue to support.

To me, it'd make more sense to have another API kani::any_safe() which combines let x = kani::any()and kani::assume(x.is_safe()) (no need to replicate the parsing code). This way, the code would be explicit about the "safe values" assumption, which I don't think is clear otherwise.

@celinval
Copy link
Contributor

kani::any() is a safe function and it should only return safe values. Anything beyond that is incorrect.

@adpaco-aws adpaco-aws changed the title Enable an #[invariant(...)] attribute helper for the #[derive(Invariant)] macro Enable an #[invariant(...)] attribute helper for the Arbitrary and Invariant macros Jul 2, 2024
Copy link
Contributor

@celinval celinval left a comment

Choose a reason for hiding this comment

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

Nice! Can you please add a test where the invariant calls a function and one that tries to mutate the object? Thank you

tests/ui/derive-invariant/helper-empty/expected Outdated Show resolved Hide resolved
Copy link
Contributor

@zhassan-aws zhassan-aws left a comment

Choose a reason for hiding this comment

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

Thanks. I find it a bit unexpected that #[derive(Arbitrary)] takes invariants from #[invariant] into account but not those from impl Invariant.

@zhassan-aws
Copy link
Contributor

For example, what happens when the field type already has an Arbitrary implementation (e.g. char):

#[derive(Arbitrary)]
struct Foo {
    #[invariant(*c != 'X')]
    c: char,
    #[invariant(*i > 2)]
    i: i32,
}

Would the safety invariant of char still be maintained?

@zhassan-aws
Copy link
Contributor

I would still prefer keeping the Arbitrary implementation independent of the invariants to prevent surprises (e.g. vacuous proofs). I understand this would require manually adding .is_safe() in many places, but it seems safer to me from the soundness perspective.

@adpaco-aws adpaco-aws changed the title Enable an #[invariant(...)] attribute helper for the Arbitrary and Invariant macros Enable an #[safety_constaint(...)] attribute helper for the Arbitrary and Invariant macros Jul 17, 2024
@adpaco-aws adpaco-aws changed the title Enable an #[safety_constaint(...)] attribute helper for the Arbitrary and Invariant macros Enable an #[safety_constraint(...)] attribute helper for the Arbitrary and Invariant macros Jul 17, 2024
@adpaco-aws
Copy link
Contributor Author

Update: On a separate discussion, we agreed on keeping the implementation as is while we ensure that the documentation communicates the danger of over-constraining values generated with kani::any() when using the #[safety_constraint(...)] (yes, we also agreed on a new name because #[invariant(...)] may give the impression that only works for the Invariant trait).

For the documentation, I've added it to the derive functions so that it's visible in the crates documentation. We could add another section to the "Reference" section of our documentation with this, but I don't know how we could keep them sync'd. We also ensure now that is_safe() gets called for each field regardless of whether they have the attribute or not.

Copy link
Contributor

@zhassan-aws zhassan-aws left a comment

Choose a reason for hiding this comment

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

Thanks. Do we check anywhere (or need to check) that the annotated field matches with the variable used in the predicate?

For example, does Kani error out in the following case?

struct Foo {
    #[safety_constraint(*y == 5)]
    x: i32,
    y: i32,
}

If not, perhaps the safety constraints can be added anywhere inside the struct and not necessarily annotated on the individual fields?

@adpaco-aws
Copy link
Contributor Author

Thanks. Do we check anywhere (or need to check) that the annotated field matches with the variable used in the predicate?

We don't check the expressions for that. In my opinion, that'd be a nice feature to have but it's not needed at this point.

For example, does Kani error out in the following case?

struct Foo {
    #[safety_constraint(*y == 5)]
    x: i32,
    y: i32,
}

If not, perhaps the safety constraints can be added anywhere inside the struct and not necessarily annotated on the individual fields?

No, it doesn't error out. We could change it so the safety constraints can be added somewhere else but I don't know what's the advantage. Also, note that the feature in #3270 is expected to allow constraints relating multiple fields of the struct. But if we're talking about constraints associated to individual fields, I think this feature is more appropriate because the annotations are easier to read.

Copy link
Contributor

@celinval celinval left a comment

Choose a reason for hiding this comment

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

It looks good to me. Can you please add a test where all the attributes are guarded by kani configuration. I.e.: #[cfg_attr(kani, safety_attribute(/*content*/))].

Other tests that I would like to see is inadvertently specifying an expression with side effect, and mentioning other fields in the invariant expression. Thanks!

Copy link
Contributor

@celinval celinval left a comment

Choose a reason for hiding this comment

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

Thank you!!

@adpaco-aws adpaco-aws enabled auto-merge (squash) July 22, 2024 19:47
@adpaco-aws adpaco-aws merged commit 7ad4d1c into model-checking:main Jul 22, 2024
24 of 25 checks passed
adpaco-aws added a commit that referenced this pull request Jul 31, 2024
This PR defines a struct-level `#[safety_constraint(<pred>)]` macro
attribute that allows users to define the type invariant as the
predicate passed as an argument to the attribute. This safety constraint
is picked up when deriving `Arbitrary` and `Invariant` implementations
so that the values generated with `any()` or checked with `is_safe()`
satisfy the constraint.

This attribute is similar to the helper attribute introduced in #3283,
and they cannot be used together. It's preferable to use this attribute
when the constraint implies some relation between different fields. But,
at the moment, there's no practical difference between them because the
helper attribute allows users to refer to any fields. If we imposed that
restriction, this attribute would be the only way to specify struct-wide
invariants.

An example:
```rs
#[derive(kani::Arbitrary)]
#[derive(kani::Invariant)]
#[safety_constraint(*x == *y)]
struct SameCoordsPoint {
    x: i32,
    y: i32,
}

#[kani::proof]
fn check_invariant() {
    let point: SameCoordsPoint = kani::any();
    assert!(point.is_safe());
}
```

Resolves #3095
github-merge-queue bot pushed a commit that referenced this pull request Aug 9, 2024
## 0.54.0

### Major Changes
* We added support for slices in the `#[kani::modifies(...)]` clauses
when using function contracts.
* We introduce an `#[safety_constraint(...)]` attribute helper for the
`Arbitrary` and `Invariant` macros.
* We enabled support for concrete playback for harness that contains
stubs or function contracts.
* We added support for log2*, log10*, powif*, fma*, and sqrt*
intrisincs.

### Breaking Changes
* The `-Z ptr-to-ref-cast-checks` option has been removed, and pointer
validity checks when casting raw pointers to references are now run by
default.

## What's Changed
* Make Kani reject mutable pointer casts if padding is incompatible and
memory initialization is checked by @artemagvanian in
#3332
* Fix visibility of some Kani intrinsics by @artemagvanian in
#3323
* Function Contracts: Modify Slices by @pi314mm in
#3295
* Support for disabling automatically generated pointer checks to avoid
reinstrumentation by @artemagvanian in
#3344
* Add support for global transformations by @artemagvanian in
#3348
* Enable an `#[safety_constraint(...)]` attribute helper for the
`Arbitrary` and `Invariant` macros by @adpaco-aws in
#3283
* Fix contract handling of promoted constants and constant static by
@celinval in #3305
* Bump CBMC Viewer to 3.9 by @tautschnig in
#3373
* Update to CBMC version 6.1.1 by @tautschnig in
#2995
* Define a struct-level `#[safety_constraint(...)]` attribute by
@adpaco-aws in #3270
* Enable concrete playback for contract and stubs by @celinval in
#3389
* Add code scanner tool by @celinval in
#3120
* Enable contracts in associated functions by @celinval in
#3363
* Enable log2*, log10* intrinsics by @tautschnig in
#3001
* Enable powif* intrinsics by @tautschnig in
#2999
* Enable fma* intrinsics by @tautschnig in
#3002
* Enable sqrt* intrinsics by @tautschnig in
#3000
* Remove assigns clause for ZST pointers by @carolynzech in
#3417
* Instrumentation for delayed UB stemming from uninitialized memory by
@artemagvanian in #3374
* Unify kani library and kani core logic by @jaisnan in
#3333
* Stabilize pointer-to-reference cast validity checks by @artemagvanian
in #3426
* Rust toolchain upgraded to `nightly-2024-08-07` by @jaisnan @qinheping
@tautschnig @feliperodri

## New Contributors
* @carolynzech made their first contribution in
#3387

**Full Changelog**:
kani-0.53.0...kani-0.54.0

By submitting this pull request, I confirm that my contribution is made
under the terms of the Apache 2.0 and MIT licenses.

Signed-off-by: Felipe R. Monteiro <felisous@amazon.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Z-BenchCI Tag a PR to run benchmark CI
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants