Skip to content

Tautological postconditions: some merged contracts hold for any implementation (demonstrated by mutation) #630

Description

@KyleClouthier

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions