Skip to content

Challenge 26: Verify the safety of Rc and Weak in alloc::rc - #661

Open
kasimte wants to merge 1 commit into
model-checking:mainfrom
kasimte:challenge-26
Open

Challenge 26: Verify the safety of Rc and Weak in alloc::rc#661
kasimte wants to merge 1 commit into
model-checking:mainfrom
kasimte:challenge-26

Conversation

@kasimte

@kasimte kasimte commented Aug 27, 2026

Copy link
Copy Markdown

Towards #382. Solves Challenge 26: Verify reference-counted Cell implementation: safety contracts on all 12 required pub unsafe functions and Kani harnesses covering all 54 listed safe functions in alloc::rc. The suite lives in one mod verify: 101 harness macros expand to 1,293 harness functions, 205 of them proof_for_contract, plus 3 standalone harnesses. Every harness carries a kani::cover non-vacuity witness. Element types are primitives, slices, and small arrays (the challenge permits limiting generic T to primitives). Allocator coverage is Global, explicit and default. System lives in std and is not nameable from alloc's harnesses. All 1,293 harnesses pass via scripts/run-kani.sh.

Coverage: required unsafe functions (12/12)

Function Verified via
Rc<MaybeUninit<T>, A>::assume_init proof_for_contract (rc_check_assume_init, 13 types)
Rc<[MaybeUninit<T>], A>::assume_init proof_for_contract (rc_check_assume_init_slice, 12 types)
Rc<T>::from_raw proof_for_contract (rc_check_from_raw_{sized,unsized}, 18 instantiations)
Rc<T>::increment_strong_count proof_for_contract (sized + unsized, 18)
Rc<T>::decrement_strong_count proof_for_contract (sized + unsized, 18)
Rc<T, A>::from_raw_in proof_for_contract (sized + unsized, 18)
Rc<T, A>::increment_strong_count_in proof_for_contract (sized + unsized, 18)
Rc<T, A>::decrement_strong_count_in proof_for_contract (sized + unsized, 18)
Rc<T, A>::get_mut_unchecked proof_for_contract (sized + unsized, 18)
Rc<dyn Any, A>::downcast_unchecked proof_for_contract (13 types + 5 Vec payloads)
Weak<T>::from_raw proof_for_contract (sized + unsized, 18)
Weak<T, A>::from_raw_in proof_for_contract (sized + unsized, 18)

Coverage: safe functions (54/54)

All 54 functions from the challenge's table, grouped by API area; each entry names its harness family.

  • Constructors: new, new_uninit, new_zeroed, try_new, try_new_uninit, try_new_zeroed, new_uninit_in, new_zeroed_in, new_cyclic_in, try_new_in, try_new_uninit_in, try_new_zeroed_in, pin, pin_in (one rc_check_* family each; pin/pin_in include a !Unpin sentinel type).
  • Slice constructors: new_uninit_slice, new_zeroed_slice, new_uninit_slice_in, new_zeroed_slice_in, into_array (one family each, symbolic lengths under layout-validity assumptions).
  • Accessors and conversions: inner, into_inner_with_allocator, into_raw_with_allocator, as_ptr, get_mut (three-state), make_mut (three-state), downcast (success + failure), from_box_in, try_unwrap (three-state).
  • From/FromIterator/TryFrom: From<&str>, From<Vec<T>>, From<Rc<str>>, ToRcSlice::to_rc_slice (both specializations), TryFrom<Rc<[T]>> for Rc<[T; N]> (both arms, one cover per arm).
  • RcFromSlice: the T: Copy-era specialization (today's TrivialClone impl; rc_check_from_slice_copy, 13 types) and the T: Clone default (rc_check_from_slice_clone, via a manual-Clone wrapper — every primitive is TrivialClone, so no primitive can reach the default path).
  • Ownership and drop glue: Drop for Rc (three-state), Clone for Rc, Default (13 types + Vec payloads), Default for Rc<str>.
  • Weak: as_ptr (live + dangling), into_raw_with_allocator (live + dangling), upgrade (three-path), inner (both branches), Drop for Weak (three-path).
  • RcInnerPtr: inc_strong, inc_weak (non-overflow harnesses + the two should_panic overflow harnesses).
  • UniqueRc/UniqueRcUninit: into_rc, downgrade, Deref, DerefMut, Drop for UniqueRc (both branches), UniqueRcUninit::new, data_ptr, Drop for UniqueRcUninit.

Contracts

  • Contracts use safety::{requires, ensures}. The four modifies clauses use cfg_attr(kani, kani::modifies(...)) — the safety crate exposes no modifies wrapper.
  • All pointer-validity preconditions are pure provenance checks: rebuild the inner pointer from the data pointer, compare with ptr::addr_eq, check size and alignment via kani::mem::checked_size_of_raw/checked_align_of_raw, and require strong >= 1. No contract constructs or consumes an Rc internally.
  • assume_init harness comments state exactly what is constrained (the harness-generated bytes, before set_len) and why the postcondition assertion is not circular.

Proof soundness

  • One kani::cover witness per harness body (141 source lines), placed after the last assumption and before the operation under verification (in should_panic harnesses, before the panicking operation); every harness reports N of N cover properties satisfied.
  • Branch-state harnesses assert the constructed state's documented outcome (Some/None, Ok/Err, strong/weak count relations, pointer identity, post-drop upgrade() behavior, make_mut's clone-on-write and weak-disassociation effects); constructor harnesses assert value preservation.
  • The nondeterministic-input helpers carry their own covers, so every kani::assume in the suite has a reachability witness downstream of it; TryFrom additionally covers both match arms.
  • Branch-state families (unique/shared/weak_present, success/failure, live/strong_zero/dangling) construct each behavior-relevant state explicitly.
  • should_panic pairing: both reachable abort guards in the file (inc_strong/inc_weak refcount overflow) have #[kani::should_panic] harnesses. A sweep of the remaining assert!/panic!/unwrap sites found no other guard reachable under the documented preconditions: the Layout::array().unwrap() sites are unreachable under the harnesses' layout-validity assumptions, and the TrustedLen capacity overflow panic requires an iterator longer than usize::MAX.

Loop verification

from_iter_exact's element-writing loop is verified against the unmodified upstream for-loop by bounded unrolling (#[kani::unwind(5)], length-3 inputs, justified in-code). Unrolling is deliberate: a loop-contract formulation cannot carry the harnesses' element-value postconditions, because under invariant havoc no invariant relates an opaque impl Iterator's yielded prefix to the memory the harness asserts on. The file contains no cfg(kani) code substitutions: the code Kani verifies is the code that runs.

Bounds

The shared nondeterministic-vector helper bounds the symbolic length (sz <= 100). This is a CI tractability measure, not a safety precondition; sampled harnesses re-verified locally with the bound removed: clone_rc_vec_u8 22.4s, from_raw_vec_u8 40.5s, get_mut_vec_u8_shared_none 35.2s (all successful, covers satisfied). The assume_init_slice harnesses carry the same len <= 100 bound for CI-runner memory on wide element types: the unbounded form verifies on the Linux CI runners, but the widest variants exceed macOS runner memory. In-code comments carry the same statements. Fixed harness lengths (len 3, N = 100 in into_array) carry in-code justifications.

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

@kasimte
kasimte requested a review from a team as a code owner August 27, 2026 18:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant