Skip to content

Replace byte-inspection NonZero contracts with niche test and raw_eq - #624

Merged
feliperodri merged 1 commit into
model-checking:mainfrom
tautschnig:fix-nonzero-contract-cost
Aug 28, 2026
Merged

Replace byte-inspection NonZero contracts with niche test and raw_eq#624
feliperodri merged 1 commit into
model-checking:mainfrom
tautschnig:fix-nonzero-contract-cost

Conversation

@tautschnig

Copy link
Copy Markdown
Member

The requires/ensures clauses of NonZero::new_unchecked (and the requires of from_mut_unchecked) expressed "n is not zero" and "result equals n" by building a raw byte slice over the value with slice::from_raw_parts and iterating it. Under symbolic execution, every asserted occurrence of these clauses pays for pointer indirection, slice-iterator reasoning, and allocation tracking — and since new_unchecked sits beneath most NonZero operations, harnesses whose call graph contains NonZero constructions were dominated by this: with dependency contracts asserted (the Kani default since model-checking/kani#3802), num::nonzero::verify::check_mul_u32_small takes 59.6s of CBMC solve time, against 0.3s with --no-assert-contracts.

This PR expresses the same properties through operations the verifier resolves directly: NonZero::new(n).is_some() performs the canonical zero test via the niche layout (a transmute plus discriminant test), and intrinsics::raw_eq compares object representations without constructing slices. Neither requires additional trait bounds on T.

Measured with Kani 152c6a8c + CBMC 6.10.0:

  • check_mul_u32_small with contracts asserted: 59.6s → 0.6s solve time (103x).
  • All 56 harnesses matching nonzero_check_new_unchecked_for* / nonzero_check_from_mut_unchecked* / check_mul* pass both with and without --no-assert-contracts.

Part of the effort to make dropping --no-assert-contracts from run-kani.sh feasible (see also #622, #623): the byte-inspection clauses were the single largest per-call-site cost multiplier identified when asserting dependency contracts across a 125-harness sample.

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

The requires/ensures clauses of NonZero::new_unchecked (and the
requires of from_mut_unchecked) expressed "n is not zero" and "result
equals n" by building a raw byte slice over the value with
slice::from_raw_parts and iterating it. Under symbolic execution every
asserted occurrence of these clauses then pays for pointer indirection,
slice-iterator reasoning, and allocation tracking - and since
new_unchecked sits beneath most NonZero operations, harnesses whose
call graph contains NonZero constructions were dominated by this: with
dependency contracts asserted (the Kani default since
model-checking/kani#3802), num::nonzero::verify::check_mul_u32_small
takes 59.6s of CBMC solve time, against 0.3s with
--no-assert-contracts.

Express the same properties through operations the verifier resolves
directly: `NonZero::new(n).is_some()` performs the canonical zero test
via the niche layout (a transmute plus discriminant test), and
`intrinsics::raw_eq` compares object representations without
constructing slices. Neither requires additional trait bounds on T.

With contracts asserted, check_mul_u32_small drops from 59.6s to 0.6s
solve time (103x). All 56 harnesses matching
nonzero_check_new_unchecked_for / nonzero_check_from_mut_unchecked /
check_mul pass both with and without --no-assert-contracts
(Kani 152c6a8c + CBMC 6.10.0).

Co-authored-by: Kiro <kiro-agent@users.noreply.github.com>
@tautschnig
tautschnig requested a review from a team as a code owner August 3, 2026 16:54
Copilot AI review requested due to automatic review settings August 3, 2026 16:54

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Optimizes the Kani contract clauses for core::num::NonZero constructors to reduce symbolic-execution overhead when dependency contracts are asserted, without changing runtime semantics (the runtime contract macros are currently no-ops).

Changes:

  • Replaced byte-slice “non-zero” preconditions for NonZero::new_unchecked / from_mut_unchecked with a niche-layout check via NonZero::new(..).is_some().
  • Replaced byte-slice equality postcondition for new_unchecked with intrinsics::raw_eq to compare object representations directly.
  • Added explanatory comments documenting why these contract forms are cheaper for the verifier.

Comment thread library/core/src/num/nonzero.rs

@feliperodri feliperodri left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Static review (no local Kani run this time). The new #[requires(NonZero::new(n).is_some())] / #[ensures(raw_eq(&result.get(), &n))] are semantically equivalent to the previous byte-slice inspections but avoid symbolic pointer indirection and iterator reasoning: NonZero::new is intrinsics::transmute_unchecked (so no recursion with new_unchecked), new(n).is_some() is exactly the condition under which the body's match Self::new(n) takes Some, and raw_eq is bit-equivalent for these padding-free integer types. Both new_unchecked and from_mut_unchecked are covered by proof_for_contract. No concerns.

@feliperodri feliperodri removed their assignment Aug 27, 2026
@feliperodri
feliperodri requested a review from a team August 27, 2026 17:54
@feliperodri
feliperodri enabled auto-merge August 28, 2026 20:51
@feliperodri
feliperodri disabled auto-merge August 28, 2026 20:52
@feliperodri
feliperodri added this pull request to the merge queue Aug 28, 2026
Merged via the queue into model-checking:main with commit 215990e Aug 28, 2026
28 of 29 checks passed
DiuDiu777 pushed a commit to safer-rust/rapx-verify-rust-std that referenced this pull request Aug 29, 2026
…ing#627)

`NonNull::slice_from_raw_parts` is a safe function with no validity
requirements on `data`: per its documentation, it is safe to construct
the pointer, and only its *use* is subject to safety conditions. Its
postcondition however evaluated `unsafe { result.as_ref() }.len()`,
creating a reference to the pointed-to memory just to read the slice
length — undefined behavior when `data` is dangling or misaligned, and a
failing check when the contract is evaluated in such a context.

This surfaces with dependency contracts asserted (the Kani default since
model-checking/kani#3802):
`ptr::non_null::verify::non_null_check_as_uninit_slice_mut` constructs,
legitimately, a `NonNull` slice pointer whose span may exceed the
backing allocation; evaluating `slice_from_raw_parts`' postcondition
then fails with "misaligned pointer to reference cast" / "dereference
failure: pointer invalid" inside `NonNull::as_ref`. CI currently masks
this with `--no-assert-contracts`.

This PR reads the length from the wide-pointer metadata via
`NonNull::len` instead, which involves no dereference (and no unsafe
code) and is the property the clause is about in the first place.

Blame: the dereferencing clause dates to the original contracts in
07318df (model-checking#127).

Verified with Kani 152c6a8c + CBMC 6.10.0:
`non_null_check_as_uninit_slice_mut`,
`non_null_check_slice_from_raw_parts`, `non_null_check_as_uninit_slice`
and `non_null_check_len` pass both with and without
`--no-assert-contracts` (the first previously failed with contracts
asserted — the last remaining failure of that kind known on the
125-harness sample after model-checking#622, model-checking#623, model-checking#624, model-checking#625, model-checking#626).

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

Co-authored-by: Kiro <kiro-agent@users.noreply.github.com>
DiuDiu777 pushed a commit to safer-rust/rapx-verify-rust-std that referenced this pull request Aug 29, 2026
…-checking#625)

The requires/ensures clauses of the pointer arithmetic operations
(`<*mut T>::{offset,add,sub}`, `<*const T>::{offset,add,sub}`,
`NonNull::{add,sub}`, `NonNull::offset_from_unsigned`) demand
`same_allocation` unconditionally (modulo a ZST escape), although the
documented semantics explicitly permit zero-sized offsets on any
pointer, including dangling ones: only "if the computed offset is
non-zero, then self must be derived from a pointer to some allocated
object".

The stricter-than-documented clauses are violated by legitimate std
code: empty slices may be backed by dangling pointers
(`slice::from_raw_parts(ptr, 0)` for arbitrary aligned non-null `ptr` —
exactly what `slice::iter`'s own `any_slice` helper generates),
whereupon `Iter::new` computes `ptr.add(0)` and `len()` computes
`end.offset_from_unsigned(begin)` on two equal dangling pointers. With
dependency contracts asserted (the Kani default since
model-checking/kani#3802), the `slice::iter::verify::verify_tup`
harnesses fail on these clauses — and evaluating `same_allocation` on an
allocation-less pointer is additionally a Kani unsupported construct
("Kani does not support reasoning about pointer to unallocated memory").
CI currently masks this via `--no-assert-contracts`.

This PR adds the documented escape hatches: `count == 0 ||` ahead of the
same-allocation disjunct in offset/add/sub requires and ensures —
matching the precedent already present in `NonNull::offset` — and an
equal-address escape in `NonNull::offset_from_unsigned`, matching the
precedent in `<*const T>::offset_from`.

Blame: the unconditional clauses date back to the original contract PRs
model-checking#113 (014965a) and model-checking#93 (688b15b) and siblings.

Verified with Kani 152c6a8c + CBMC 6.10.0:
*
`slice::iter::verify::verify_tup::{check_next_back_unchecked,check_advance_back_by}`
now pass with contracts asserted;
* all 265 proof harnesses matching
`non_null_check_{add,sub,offset_from_unsigned}` and
`ptr::verify::check_{mut,const}_{add,sub,offset}` pass both with and
without `--no-assert-contracts`.

Together with model-checking#622, model-checking#623, model-checking#624 and model-checking/kani#4709/rust-lang#4710, this
resolves all verdict differences found on a 125-harness sample when
running without `--no-assert-contracts`, except
`non_null_check_from_raw_part_trait` (Kani's "unstable vtable comparison
'Eq'" limitation, reached by `as_ptr`'s postcondition on a `dyn Trait`
pointee — tracked separately).

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

Co-authored-by: Kiro <kiro-agent@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Maintenance Maintenance related issues for the challange

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants