This follows on from @feliperodri's comment in kani-verifier-blog#59, where the idea of automatically checking that assumptions are reachable came up. This is a case that check would not catch, and I think it is worth naming because it sits in the merged corpus today.
To be clear up front: this is not a defect report. Layout::for_value_raw is correct. The finding is about what its contract warrants, which turns out to be nothing.
The contract
library/core/src/alloc/layout.rs, its only postcondition:
#[ensures(|result| result.align().is_power_of_two())]
pub const unsafe fn for_value_raw<T: ?Sized>(t: *const T) -> Self
Layout's own type invariant guarantees align() is a power of two. It is enforced at construction, and Layout::from_size_align returns Err otherwise. So this clause holds for every possible implementation of for_value_raw, including one that ignores its argument.
Demonstrated by mutation
I replaced the body with one that ignores t entirely and returns a fixed layout, then ran the existing merged harness check_for_value_raw_i32 unchanged:
pub const unsafe fn for_value_raw<T: ?Sized>(t: *const T) -> Self {
let _ = t;
unsafe { Layout::from_size_align_unchecked(1, 1) }
}
For i32 the correct answer is size 4, align 4. This returns size 1, align 1, for every type.
|
correct implementation |
mutant that ignores t |
current align().is_power_of_two() |
1 verified, 0 failures |
1 verified, 0 failures |
| proposed (below) |
1 verified, 0 failures |
0 verified, 1 failures |
The current contract cannot distinguish the standard library's implementation from one that ignores its input.
The proposed form, verified
The strong form already exists forty lines up in the same file, on the sibling function Layout::for_value:
#[ensures(|result| result.align() == mem::align_of_val(t))]
Applying the same shape to for_value_raw:
#[ensures(|result| result.align() == unsafe { mem::align_of_val_raw(t) })]
#[ensures(|result| result.size() == unsafe { mem::size_of_val_raw(t) })]
This verifies against the real implementation and fails against the mutant, with Kani naming the responsible clause:
Failed Checks: |result| result.align() == unsafe { mem::align_of_val_raw(t) }
Complete - 0 successfully verified harnesses, 1 failures, 1 total.
So unsafe inside an #[ensures] works, and the stronger contract costs nothing beyond the change itself.
Why reachability checking will not find this
This is the part I think is interesting for the automation discussion.
There is nothing unreachable here. The assertion is reached, there is no assume, the proof is sound, and the verdict is correct. A cover-based check sees a completely healthy harness, because the harness is healthy. The weakness is in what the contract says, not in what the harness explores.
Vacuity and this are duals. One is a proof over the empty set. The other is a proof of something that holds over every set. Reachability analysis detects the first and is blind to the second. As far as I can tell, mutation is the only thing that separates them, which is why I raised it as verify-rust-std#617 Criterion 2.
Two others in the same shape
I have not mutated these, so treat them as candidates and not results.
Alignment::of in library/core/src/ptr/alignment.rs, where both clauses are tautologies:
#[requires(mem::align_of::<T>().is_power_of_two())] // align_of always returns a power of two
#[ensures(|result| result.as_usize().is_power_of_two())] // Alignment is an enum of powers of two
Its harness is currently commented out behind a FIXME citing kani#3905. Worth noting that when that unblocks, this harness will verify immediately and establish nothing, which is a slightly awkward failure mode because it will look like progress.
Layout::align_to carries ...align().is_power_of_two() alongside a genuinely meaningful clause, so it is less severe.
What I am offering
Happy to open a PR strengthening for_value_raw and Alignment::of along the lines above, with the mutants as evidence that each new clause has teeth. Also happy to leave it here if you would rather handle it internally, or if I have misread the intent of the weaker form.
Reproduced with Kani 0.67.0 / CBMC 6.8.0 against verify-rust-std at 2138bc6, using scripts/run-kani.sh --kani-args --harness check_for_value_raw_i32.
This follows on from @feliperodri's comment in kani-verifier-blog#59, where the idea of automatically checking that assumptions are reachable came up. This is a case that check would not catch, and I think it is worth naming because it sits in the merged corpus today.
To be clear up front: this is not a defect report.
Layout::for_value_rawis correct. The finding is about what its contract warrants, which turns out to be nothing.The contract
library/core/src/alloc/layout.rs, its only postcondition:Layout's own type invariant guaranteesalign()is a power of two. It is enforced at construction, andLayout::from_size_alignreturnsErrotherwise. So this clause holds for every possible implementation offor_value_raw, including one that ignores its argument.Demonstrated by mutation
I replaced the body with one that ignores
tentirely and returns a fixed layout, then ran the existing merged harnesscheck_for_value_raw_i32unchanged:For
i32the correct answer is size 4, align 4. This returns size 1, align 1, for every type.talign().is_power_of_two()1 verified, 0 failures1 verified, 0 failures1 verified, 0 failures0 verified, 1 failuresThe current contract cannot distinguish the standard library's implementation from one that ignores its input.
The proposed form, verified
The strong form already exists forty lines up in the same file, on the sibling function
Layout::for_value:#[ensures(|result| result.align() == mem::align_of_val(t))]Applying the same shape to
for_value_raw:This verifies against the real implementation and fails against the mutant, with Kani naming the responsible clause:
So
unsafeinside an#[ensures]works, and the stronger contract costs nothing beyond the change itself.Why reachability checking will not find this
This is the part I think is interesting for the automation discussion.
There is nothing unreachable here. The assertion is reached, there is no
assume, the proof is sound, and the verdict is correct. A cover-based check sees a completely healthy harness, because the harness is healthy. The weakness is in what the contract says, not in what the harness explores.Vacuity and this are duals. One is a proof over the empty set. The other is a proof of something that holds over every set. Reachability analysis detects the first and is blind to the second. As far as I can tell, mutation is the only thing that separates them, which is why I raised it as verify-rust-std#617 Criterion 2.
Two others in the same shape
I have not mutated these, so treat them as candidates and not results.
Alignment::ofinlibrary/core/src/ptr/alignment.rs, where both clauses are tautologies:Its harness is currently commented out behind a FIXME citing kani#3905. Worth noting that when that unblocks, this harness will verify immediately and establish nothing, which is a slightly awkward failure mode because it will look like progress.
Layout::align_tocarries...align().is_power_of_two()alongside a genuinely meaningful clause, so it is less severe.What I am offering
Happy to open a PR strengthening
for_value_rawandAlignment::ofalong the lines above, with the mutants as evidence that each new clause has teeth. Also happy to leave it here if you would rather handle it internally, or if I have misread the intent of the weaker form.Reproduced with Kani 0.67.0 / CBMC 6.8.0 against
verify-rust-stdat2138bc6, usingscripts/run-kani.sh --kani-args --harness check_for_value_raw_i32.