Skip to content

Verify safety of NonZero operations (Challenge 12) - #544

Open
jrey8343 wants to merge 7 commits into
model-checking:mainfrom
jrey8343:challenge-12-nonzero
Open

Verify safety of NonZero operations (Challenge 12)#544
jrey8343 wants to merge 7 commits into
model-checking:mainfrom
jrey8343:challenge-12-nonzero

Conversation

@jrey8343

@jrey8343 jrey8343 commented Feb 8, 2026

Copy link
Copy Markdown

Summary

Resolves #71 (Challenge 12: Safety of NonZero)

Adds 382 new Kani verification harnesses for all remaining NonZero functions specified in the challenge, covering:

  • Bit operations: count_ones, swap_bytes, reverse_bits, rotate_left, rotate_right (12 types each)
  • Byte order: from_be, from_le, to_be, to_le (12 types each)
  • Bitor: all 3 implementations — NonZero|NonZero, NonZero|T, T|NonZero (12 types each)
  • Checked/saturating arithmetic: checked_mul, saturating_mul, checked_add, saturating_add (unsigned-only for add)
  • Power operations: checked_pow, saturating_pow (12 types each)
  • Unsigned-only: checked_next_power_of_two, midpoint, isqrt
  • Signed-only: neg, abs, checked_abs, overflowing_abs, saturating_abs, wrapping_abs, unsigned_abs, checked_neg, overflowing_neg, wrapping_neg
  • Reference: from_mut

Changes

  • library/core/src/num/nonzero.rs: 382 new verification harnesses in mod verify — this is now the only file the PR touches. The #[safety::loop_invariant(true)] annotations on the primitive checked_pow (from Add loop_invariants for some Int power functions #327) are left untouched, per review feedback.

Interaction with the checked_pow loop contract (review follow-up)

Under -Z loop-contracts, the trivial #[safety::loop_invariant(true)] on the primitive checked_pow loop makes CBMC abstract the loop by havocking acc, so the abstracted primitive can return Some(0). That spuriously violates NonZero::new_unchecked's nonzero precondition inside the checked_pow/saturating_pow harnesses (the failure is the precondition check at nonzero.rs new_unchecked, reproducible with the pinned Kani).

As suggested in review, the workaround is scoped to the affected harnesses: the 24 pow harnesses carry #[kani::stub(<int>::checked_pow, stub_checked_pow_<int>)], where the stub is a verbatim copy of the primitive implementation (with try_opt! expanded) minus the loop-contract annotation. The stub introduces no nondeterminism and no kani::assume; with exp: u32 the loop runs at most 32 iterations, so #[kani::unwind(34)] fully unwinds it and the proofs are exhaustive over all inputs. The real NonZero methods under verification — including the new_unchecked safety condition the challenge targets — remain fully compiled and verified. (saturating_pow delegates to the primitive saturating_pow, which calls checked_pow, so the same stub covers it.)

The 128-bit pow harnesses remain bounded (exp <= 10, unwind(5)) to keep CBMC's 128-bit multiplication encoding tractable; all other pow harnesses are exhaustive.

Bounded verification for wide-type isqrt

The full-range isqrt harnesses for u64/usize/u128 exceeded CI's 10-minute per-harness timeout in the autoharness jobs (the Karatsuba isqrt implementation performs symbolic divisions/multiplications at half the operand width; full-range u32 already takes ~90s in CI). Following the interval pattern of the carrying_mul harnesses in num/mod.rs, u8/u16/u32 remain full-range and each wide type is checked with #[kani::solver(kissat)] on three bounded input windows: small ([1, 10], covering the result-zero boundary relevant to the NonZero safety property), large ([MAX - 10, MAX]), and mid-edge ([MAX/2 ± 10]). These are bounded, not unbounded, proofs — noted in the code comments.

Verification

Full-range harnesses are exhaustive over their input space; the explicitly-bounded ones are the 128-bit pow harnesses (exp <= 10) and the wide-type isqrt interval harnesses described above. Verified locally with the pinned Kani (415ca50) using CI-equivalent flags (--no-assert-contracts, --object-bits 12): all 24 pow harnesses and all 12 isqrt harnesses pass (longest ~40s); the remaining harnesses are unchanged from the previously green verification runs and are covered by CI.

Run with:

./scripts/run-kani.sh --kani-args --harness nonzero_check -p core --features core/kani

Add verification harnesses for all remaining NonZero functions:
bit operations (count_ones, swap_bytes, reverse_bits, rotate_left,
rotate_right), byte order (from_be, from_le, to_be, to_le), bitor
(all 3 impls), checked/saturating arithmetic (checked_mul,
saturating_mul, checked_add, saturating_add, checked_pow,
saturating_pow), power of two (checked_next_power_of_two), midpoint,
isqrt, signed operations (neg, abs, checked_abs, overflowing_abs,
saturating_abs, wrapping_abs, unsigned_abs, checked_neg,
overflowing_neg, wrapping_neg), and from_mut.

Remove trivial loop_invariant(true) annotations from primitive
checked_pow that caused CBMC assigns check interference with
NonZero::new_unchecked verification.

Total: 385 harnesses pass (376 new + 9 existing).
@jrey8343
jrey8343 requested a review from a team as a code owner February 8, 2026 03:37
The 128-bit saturating_pow harnesses with unbounded u32 exponents hit
CBMC's 10-minute timeout because 128-bit bitvector multiplication is
extremely expensive. Add a dedicated macro that constrains exp <= 10
with unwind(5), sufficient to cover both the non-saturating and
saturating code paths while keeping verification tractable.
…sion

Same fix as saturating_pow_128: use a dedicated macro with exp <= 10
and #[kani::unwind(5)] for the 128-bit checked_pow harnesses.
@jrey8343
jrey8343 force-pushed the challenge-12-nonzero branch from d41f3ea to 9d26114 Compare February 21, 2026 23:57
@jrey8343

Copy link
Copy Markdown
Author

CI is passing — ready for review.

@feliperodri feliperodri added the Challenge Used to tag a challenge label Mar 9, 2026
@feliperodri
feliperodri requested a review from Copilot March 30, 2026 20:22

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

This PR expands Kani-based verification coverage for core::num::NonZero* operations (Challenge 12), and adjusts checked_pow implementations to avoid CBMC/Kani interference from a trivial loop-invariant annotation.

Changes:

  • Add a large set of new Kani verification harnesses under core::num::nonzero::verify for many remaining NonZero operations (bit ops, byte-order, bitor, arithmetic, pow, signed/unsigned-only ops, and from_mut).
  • Remove #[safety::loop_invariant(true)] from checked_pow loops in both unsigned and signed integer macro implementations to prevent CBMC assigns-check interference during verification.
  • Add bounded-unwind / bounded-exponent variants for 128-bit pow harnesses to keep verification tractable.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
library/core/src/num/nonzero.rs Adds many new Kani harnesses for NonZero* APIs in mod verify.
library/core/src/num/uint_macros.rs Removes a trivial loop-invariant annotation from checked_pow’s loop.
library/core/src/num/int_macros.rs Removes the same trivial loop-invariant annotation from signed checked_pow.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.


nonzero_check_isqrt!(core::num::NonZeroU8, nonzero_check_isqrt_u8);
nonzero_check_isqrt!(core::num::NonZeroU16, nonzero_check_isqrt_u16);
nonzero_check_isqrt!(core::num::NonZeroU32, nonzero_check_isqrt_u32);

Copilot AI Mar 30, 2026

Copy link

Choose a reason for hiding this comment

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

NonZero::isqrt() is implemented for all unsigned nonzero types (u8/u16/u32/u64/u128/usize), but the verification harnesses here only cover u8/u16/u32. If the goal is to verify all remaining NonZero operations across all integer widths, add harnesses for NonZeroU64/NonZeroU128/NonZeroUsize as well (or document/encode an explicit bound or performance rationale for excluding the wider types).

Suggested change
nonzero_check_isqrt!(core::num::NonZeroU32, nonzero_check_isqrt_u32);
nonzero_check_isqrt!(core::num::NonZeroU32, nonzero_check_isqrt_u32);
nonzero_check_isqrt!(core::num::NonZeroU64, nonzero_check_isqrt_u64);
nonzero_check_isqrt!(core::num::NonZeroU128, nonzero_check_isqrt_u128);
nonzero_check_isqrt!(core::num::NonZeroUsize, nonzero_check_isqrt_usize);

Copilot uses AI. Check for mistakes.
@feliperodri feliperodri assigned jrey8343 and unassigned jrey8343 Apr 1, 2026
Add isqrt verification for the remaining unsigned types.
u128 uses checked_mul to avoid CBMC bitvector overflow concerns.

@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.

The NonZero verification here is solid work: the harnesses are real bounded proofs with meaningful assertions (e.g. checked_pow asserts result.get() != 0 on Some under #[kani::unwind(34)]; count_ones asserts both nonzero and functional equality with the primitive count_ones), symbolic inputs, and they cover the Challenge 12 Part 2 function list. No vacuity concerns — clean on cfg-usage, invariants, and contracts. That part is mergeable in substance.

I'm requesting changes on one out-of-scope, potentially-regressing change.

Removing #[safety::loop_invariant(true)] from checked_pow reverts merged PR #327

The PR deletes #[safety::loop_invariant(true)] from the checked_pow loop in both int_macros.rs and uint_macros.rs. Those annotations:

  • are present on main;
  • were added by merged PR #327 ("Add loop_invariants for some Int power functions"), which added only these annotations — int_macros.rs/uint_macros.rs contain no #[kani::proof] harnesses, so the loop contracts are consumed by the autoharness verification job for unbounded pow coverage.

Deleting them therefore:

  1. Reverts a merged contribution (#327) and is out of scope for Challenge 12 (NonZero).
  2. Changes verification of primitive checked_pow globally. With -Z loop-contracts always enabled, the annotation makes CBMC abstract the loop; without it the loop is unwound instead. This can silently reduce the autoharness coverage of checked_pow (it may now be bounded or skipped). A green CI run doesn't demonstrate that coverage was preserved — the Kani-Metrics coverage delta for checked_pow should be checked explicitly.
  3. Is a heavier fix than the problem requires. The stated motivation is that the loop contract's assigns clause interferes with the NonZero harnesses' assigns checking (new_unchecked). Since the harnesses already carry #[kani::unwind(34)], scoped alternatives that don't touch the shared annotation include: stubbing checked_pow within the affected NonZero harnesses, or reporting the loop-contracts/assigns interaction upstream to Kani.

Requested change

Please keep the NonZero harnesses, but remove the loop_invariant deletions from this PR. If the annotation genuinely needs to change, split that into a separate PR coordinated with #327's author and the maintainers, including Kani-Metrics evidence that checked_pow coverage is not regressed. Otherwise, scope the workaround to the NonZero harnesses (unwind is already present; stubbing checked_pow in those harnesses is an option).

Happy to help figure out the scoped workaround for the assigns-check interaction if useful.

Address review feedback: keep model-checking#327's #[safety::loop_invariant(true)]
annotations on the primitive checked_pow untouched (int_macros.rs and
uint_macros.rs are identical to main again) and instead scope the
loop-contracts workaround to the affected NonZero harnesses.

Under -Z loop-contracts, the trivial invariant makes CBMC abstract the
checked_pow loop by havocking acc, so the abstracted primitive can
return Some(0), spuriously reaching the unreachable zero arm of
NonZero::new_unchecked inside the pow harnesses. The 24 checked_pow/
saturating_pow harnesses now stub the primitive checked_pow with a
verbatim copy of its implementation (try_opt! expanded) minus the loop
contract: no nondeterminism, no assumptions, fully unwound by the
existing unwind bounds, so the proofs remain exhaustive over the real
NonZero code including the new_unchecked safety condition.

Verified with the pinned Kani (415ca50) using CI-equivalent flags:
all 24 pow harnesses pass; without the stub the harnesses fail on the
unreachable-code check, confirming the workaround is required.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jrey8343

Copy link
Copy Markdown
Author

Thanks for the detailed review, @feliperodri — and for suggesting the scoped alternatives. Both requested changes are in (a432b6e):

1. The #[safety::loop_invariant(true)] deletions are removed. int_macros.rs and uint_macros.rs are byte-identical to main again, so #327's annotations (and the autoharness coverage of the primitive checked_pow) are untouched. The PR now effectively only touches nonzero.rs.

2. The workaround is scoped to the affected harnesses via #[kani::stub], as you suggested. For the record, the precise failure mode after restoring the annotations (reproduced with the pinned Kani, CI-equivalent flags including --no-assert-contracts): the trivial invariant makes CBMC havoc acc in the loop abstraction, so the abstracted primitive checked_pow can return Some(0), which reaches the "unreachable" zero arm of NonZero::new_unchecked — the harnesses fail on that unreachable-code check. So it's the trivial-invariant over-approximation rather than the assigns clause specifically; either way the scoped fix is the same.

The 24 checked_pow/saturating_pow harnesses now stub the primitive checked_pow with a verbatim copy of its implementation (only try_opt! expanded) minus the loop-contract annotation — no kani::any(), no kani::assume, so nothing is assumed that the harnesses later assert. With exp: u32 the loop runs at most 32 iterations, so the existing #[kani::unwind(34)] fully unwinds the stub and those proofs stay exhaustive (the 128-bit harnesses remain bounded at exp <= 10 as before, noted in the code and PR description). The NonZero methods under verification — including the new_unchecked safety condition the challenge targets — are the real shipping code. A comment on the stub notes it must be kept in sync if the primitive implementation ever changes. saturating_pow is covered by the same stub because the primitive saturating_pow delegates to checked_pow.

Verified locally with the pinned Kani (415ca50) using the same flags as run-kani.sh: all 24 pow harnesses pass with the annotations restored; an A/B run without the stub fails on the unreachable-code check, confirming the workaround is required.

Happy to also file a follow-up (separate PR coordinated with the maintainers, per your note) proposing a strengthened invariant for #327's annotation — self == 0 || (base != 0 && acc != 0) holds inductively through the loop and would make the loop-contract abstraction strong enough for the NonZero proofs without any stubbing.

The full-range isqrt harnesses for u64, usize, and u128 exceeded CI's
10-minute per-harness timeout in both autoharness jobs: the Karatsuba
isqrt implementation performs symbolic divisions and multiplications at
half the operand width, which is intractable for CBMC's bitvector
encoding at widths >= 64 (full-range u32 already takes ~90s).

Following the interval pattern of the carrying_mul harnesses in
num/mod.rs, keep u8/u16/u32 full-range and check each wide type with
kissat on three bounded windows: small ([1, 10], covering the
result-zero boundary relevant to the NonZero safety property), large
([MAX - 10, MAX]), and mid-edge ([MAX/2 - 10, MAX/2 + 10]).

Verified with the pinned Kani (415ca50) and CI-equivalent flags: all 12
isqrt harnesses pass, the longest in ~40s.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jrey8343

Copy link
Copy Markdown
Author

Follow-up (049188b): the two autoharness CI jobs were failing on this branch before the review fixes as well — the full-range isqrt harnesses for u64/usize/u128 each exceeded the 10-minute per-harness timeout (Karatsuba isqrt does symbolic half-width divisions; full-range u32 already takes ~90s). Fixed by following the interval pattern of the carrying_mul harnesses in num/mod.rs: u8/u16/u32 stay full-range, and each wide type is now checked with kissat on three bounded windows — small ([1, 10], covering the result-zero boundary relevant to the NonZero safety property), large ([MAX - 10, MAX]), and mid-edge ([MAX/2 ± 10]) — clearly marked as bounded in the code and PR description. All 12 isqrt harnesses pass locally with the pinned Kani and CI flags, the longest in ~40s.

jrey8343 added a commit to jrey8343/verify-rust-std that referenced this pull request Aug 19, 2026
Per review on model-checking#537, this replaces the previous approach entirely:

- No cfg(kani) body swaps: pattern.rs product code is identical to main.
  CharSearcher::next_match/next_match_back run their real memchr/memrchr
  loops; next_reject/next_reject_back and all MultiCharEqSearcher
  methods are the real trait defaults.
- memchr/memrchr are stubbed per-harness with semantically identical
  naive first/last-occurrence scans (no kani::any, no kani::assume; the
  pattern accepted in model-checking#544), justified by Challenge 20 assumption 1
  (slice-module correctness), and the stubs are live at the real call
  sites.
- type_invariant_mces is a real invariant over the CharIndices state
  (subrange bounds, char boundaries, pointer identity) instead of true.
- Inputs are arbitrary UTF-8 haystacks of up to 5 symbolic bytes built
  constructively from symbolic chars (all four width classes), with
  symbolic char / [char; 2] needles. Boundary safety of every returned
  range is asserted, never assumed; inductive-step harnesses admit any
  C-satisfying state and re-assert C after the real methods run.
- All unwind bounds are justified by >=1-byte cursor progress per loop
  iteration.

All 17 harnesses verify with the pinned Kani (0.67.0, d4df833) under
CI's exact flags.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Challenge Used to tag a challenge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Challenge 12: Safety of NonZero

3 participants