Repository navigation
Conversation
There was a problem hiding this comment.
Copilot review overview
🔵 Needs a closer look
The soundness-sensitive MIR instrumentation and pointer-aliasing semantics warrant final human review despite comprehensive tests.
Review effort: Balanced
Findings: None
What changed in this PR
Adds opt-in autoharness modeling for caller-controlled aliasing between compatible shared-reference and raw-pointer arguments.
Changes:
- Adds
--alias-argumentsacross the driver and compiler. - Generates nondeterministic alias choices using a new
any_aliasmodel. - Documents and tests supported cases, contracts, exclusions, and
--std.
| File | Description |
|---|---|
tests/script-based-pre/cargo_autoharness_alias_arguments/src/lib.rs |
Defines aliasing test functions. |
tests/script-based-pre/cargo_autoharness_alias_arguments/config.yml |
Configures the regression test. |
tests/script-based-pre/cargo_autoharness_alias_arguments/Cargo.toml |
Defines the test crate. |
tests/script-based-pre/cargo_autoharness_alias_arguments/alias-arguments.sh |
Runs independent and aliasing modes. |
tests/script-based-pre/cargo_autoharness_alias_arguments/alias-arguments.expected |
Records expected verification results. |
tests/script-based-pre/autoharness_std_alias_arguments/verify_alias_arguments.rs |
Adds standard-library test targets. |
tests/script-based-pre/autoharness_std_alias_arguments/config.yml |
Configures the --std test. |
tests/script-based-pre/autoharness_std_alias_arguments/autoharness_std_alias_arguments.sh |
Exercises aliasing during --std verification. |
tests/script-based-pre/autoharness_std_alias_arguments/autoharness_std_alias_arguments.expected |
Records expected --std results. |
library/kani_core/src/arbitrary.rs |
Adds the nondeterministic alias model. |
kani-driver/src/autoharness/mod.rs |
Forwards the new compiler option. |
kani-driver/src/args/autoharness_args.rs |
Exposes the user-facing flag. |
kani-compiler/src/kani_middle/transform/automatic.rs |
Inserts compatible argument alias choices. |
kani-compiler/src/kani_middle/kani_functions.rs |
Registers the alias model marker. |
kani-compiler/src/args.rs |
Adds the internal compiler flag. |
docs/src/reference/experimental/autoharness.md |
Documents behavior and limitations. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
feliperodri
left a comment
There was a problem hiding this comment.
Thanks for this. I'm approving. The comments below are suggestions, nothing blocking.
The soundness question was whether the option could generate aliasing the type system forbids. It doesn't: only &T with &T and raw pointers of the same mutability alias, and &mut T and Box never do. I built it and ran your two tests. I also ran a probe crate with and without the flag covering *mut/*mut behind a contract, &self receivers with interior mutability, &mut next to two &T, &mut [u8], Box, &&mut u8, generic functions (::<i32> and ::<i32, i32>) and --constructor-args. Every result came out where Rust's rules say it should. The aliasing bugs show up as the intended assertion failure, not as an assigns or pointer check. Cost is modest: a six-slice function went from 14.6 s to 22.3 s, and a twelve-argument function with 66 alias pairs stayed under a second.
Two things I'd like your take on:
- The output doesn't say which harnesses actually explored aliasing. A
(aliasing)marker next to(bounded)and(ctor)would fix that (inline comment onmod.rs). *mut Twith*const Tis the most common shape for this kind of bug (dst/src), and it isn't covered yet. It would fit in this PR, but a follow-up under #4750 also works for me.
One doc fix outside the diff: the "Current limitations of the generated pointers" list in autoharness.md (line 222 on the head commit) still says "Distinct pointer arguments never alias each other". That should now point to --alias-arguments.
| ( | ||
| TyKind::RigidTy(RigidTy::RawPtr(earlier_pointee, earlier_mutability)), | ||
| TyKind::RigidTy(RigidTy::RawPtr(later_pointee, later_mutability)), | ||
| ) => earlier_pointee == later_pointee && earlier_mutability == later_mutability, |
There was a problem hiding this comment.
Suggestion: *const T and *mut T can alias in any caller without UB (f(p, p as *const T)), and (dst: *mut T, src: *const T) is the most common shape for an aliasing bug: it's the copy_nonoverlapping signature. I checked a probe with that signature:
#[kani::requires(kani::mem::can_dereference(dst) && kani::mem::can_dereference(src))]
#[kani::modifies(dst)]
pub unsafe fn add_one_mut_const(dst: *mut u32, src: *const u32) {
let s = *src;
*dst = s.wrapping_add(1);
assert!(*src == s);
}It verifies with --alias-arguments. The *mut u32, *mut u32 version fails as expected. Supporting it here looks small: accept raw pointers of either mutability with equal pointees, and when they differ, cast the earlier pointer with CastKind::PtrToPtr before passing it to any_alias. I'd keep &T with *mut T out, since a write through the *mut while the &T argument is live is UB under the aliasing model, so it gets more subtle. I'm fine with this landing as a follow-up under #4750 if you'd rather keep the PR as is.
There was a problem hiding this comment.
Done in 4641fd3. Raw pointers to the same pointee type now alias regardless of mutability, with a PtrToPtr cast of the earlier pointer when the two differ. Your add_one_mut_const probe went from Success to Failure with the flag, and I added it to the test. &T with a raw pointer stays out as you suggested, and ref_and_mut_ptr_distinct pins that down.
| args.push("--autoharness-check-invariants".to_string()); | ||
| } | ||
| if autoharness_args.alias_arguments { | ||
| args.push("--autoharness-alias-arguments".to_string()); |
There was a problem hiding this comment.
Suggestion: right now there's no way to tell from the output which harnesses explored aliasing. With the flag, every row still says #[kani::proof], so a user who sees Success for fn f(a: &u8, b: &u16) can't tell that aliasing was never considered for it. --bounded-arguments and --constructor-args already solve this with AutoHarnessCaveats in codegen_units.rs, a (bounded)/(ctor) suffix, and a note under the table. An is_aliasing caveat, set when at least one any_alias call is inserted, plus an (aliasing) suffix would make "the harness of a function that does not have at least two such arguments of the same type is unaffected" visible in the results instead of only in the docs.
There was a problem hiding this comment.
Good point! And also added in 4641fd3. A harness that gets at least one any_alias call is now marked (aliasing), with an is_aliasing field in the metadata and a note under the table, following how (ctor) was added in #4717. The marker and the harness generation both go through one function, aliasable_arg_pairs, so they can't disagree. In the test, different_types_distinct and muts_distinct stay unmarked, and I added two generic functions to check the marker matches after instantiation.
|
|
||
| The following limitations remain, so a successful harness is still an underapproximation with | ||
| respect to aliasing: | ||
| - Only arguments of exactly the same type alias. A `&T` does not alias a `*const T`, a `*const T` |
There was a problem hiding this comment.
Suggestion: could this bullet also say that smart pointers are never aliased? fn rcs(a: Rc<Cell<u8>>, b: Rc<Cell<u8>>) { assert!(!Rc::ptr_eq(&a, &b)); } verifies with --alias-arguments, but a caller passing Rc::clone(&x) twice breaks it. The bullet only talks about references and raw pointers, so a reader could assume Rc/Arc behave like &T.
There was a problem hiding this comment.
Added. The limitations list now says Rc<T> and Arc<T> arguments don't alias each other, and there's an rcs_distinct test for it. I also fixed the "Current limitations of the generated pointers" list you pointed out in the summary, so it now refers to --alias-arguments.
| status=$? | ||
| failed_checks=$(echo "${output}" | grep '^Failed Checks:') | ||
| failed=$(echo "${failed_checks}" | grep -c '^Failed Checks:') | ||
| other=$(echo "${failed_checks}" | grep '^Failed Checks:' | grep -vc ': assertion failed: ') |
There was a problem hiding this comment.
Praise: counting the failed checks that are not assertion failed: means a harness that starts failing for the wrong reason (a pointer check, an assigns failure in the contract cases) changes the expected output, even when its row still says Failure. The negative cases (muts_distinct, ref_and_mut_distinct, different_ptr_mutability_distinct, ref_and_ptr_distinct) pin down where aliasing must not be introduced, which matters more here than the positive ones.
There was a problem hiding this comment.
Thank you, that's really encouraging! One note on the negative cases: different_ptr_mutability_distinct is now const_and_mut_ptrs_distinct and fails with the flag, since *const T and *mut T alias after your other suggestion. ref_and_mut_ptr_distinct and rcs_distinct joined the negative cases in its place.
|
Thanks for the thorough review, and for running all those extra probes! I've addressed the four suggestions in 4641fd3 and replied inline. Could you take another look when you have a moment? @srivatsansamraj @Tianshu-Huang @wodex1nhaoIeng @acearyanarun I'd appreciate your eyes on this too if you have time. It adds an opt-in |
|
could you rebase this PR, @CYJ904? |
Autoharness generates every reference and pointer argument from its own storage, so two arguments never alias and a harness misses the case where a caller passes the same reference or pointer twice. Under the new opt-in --alias-arguments flag, a shared reference or raw pointer argument is nondeterministically either generated from its own storage or the same reference or pointer as an earlier argument of the same type. `&mut T` arguments never alias. Towards model-checking#4750
- Let raw pointers of different mutability alias each other (`*mut T` with `*const T`), casting the earlier pointer with PtrToPtr. - Mark harnesses that model aliasing with "(aliasing)" in the summary table and record it in the metadata as `is_aliasing`. The marker and the harness generation use the same list of argument pairs. - Docs: update the list of generated-pointer limitations, describe the marker, and state that `&T` does not alias raw pointers and that `Rc`/`Arc` arguments are not aliased. - Tests: cover `*mut T` with `*const T`, `&T` with `*mut T`, `Rc` arguments, and generic functions. Towards model-checking#4750
Add `harnesses[].is_aliasing` to the RFC 0015 export, next to `is_bounded`, mapped from `HarnessMetadata::is_aliasing`. A harness that does not model aliasing does not cover callers that pass the same reference or pointer for two arguments, so a passing result can prove less than it appears to. Following the `is_bounded` precedent and the spirit of RFC 0015's configuration policy, the export records which harnesses did model it. - RFC 0015: add the field to the example, the field reference and the presence matrix, and describe it under "Aliased arguments", including that `true` covers only the eligible argument pairs. - Schema template and fixtures: add `is_aliasing` as a mandatory boolean. - Unit test for the mapping. `schema_version` stays at 0.1.0. Adding a field is a minor change under the compatibility policy, but no release has shipped 0.1.0 yet. Towards model-checking#4750
358e4bd to
876e47c
Compare
|
Thanks, Felipe! Rebased onto the latest main. A few notes:
The CI workflows need your approval to run again. Thanks! |
This PR is a first step on #4750.
Towards #4750
Problem
Autoharness generates every reference and pointer argument from its own storage, so two arguments never alias. For example, the automatic harness for
succeeds today, even though
bump(&c, &c)violates the assertion.Change
This adds an opt-in flag,
--alias-arguments. With it, a shared reference (&T,&[T],&str) or raw pointer argument is nondeterministically either generated from its own storage, as before, or the same reference or pointer as an earlier argument of the same type.any_aliasmodel inkani_core(next toany_ptr) chooses between the two values.AutomaticHarnessPassinserts oneany_aliascall per pair of compatible arguments, after all arguments are generated. The calls are chained, so with three arguments of one type every grouping is covered.&mut Targuments never alias.#[kani::requires(!core::ptr::eq(a, b))]excludes the aliasing case.Not covered yet
These are documented as remaining limitations and still tracked in #4750:
&Twith*const T,*const Twith*mut T).Testing
cargo_autoharness_alias_argumentsruns 19 functions with and without the flag. It checks each result row, the exit status, and that every failure is the intended assertion.autoharness_std_alias_argumentsdoes the same check in the--stdflow.script-based-prepass locally, andkani-fmt.sh --checkand clippy are clean.&strarguments were checked manually (Success without the flag, Failure with it) but are not in the test, since a&strharness takes about 40 seconds with or without the flag.One note on the diff:
add_auto_harness_argsnow takes&CommonAutoharnessArgsinstead of individual flags, because one more parameter trippedclippy::too_many_arguments. Behavior is unchanged.I kept this opt-in as the issue suggested. Happy to rename the flag, change the default, or split the remaining cases into follow-up issues if you'd prefer. Thanks for taking a look!
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 and MIT licenses.