From 6dbebef518da2d06c8e500710ec6384402cd985c Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Tue, 16 Jun 2026 17:53:23 -0700 Subject: [PATCH 01/62] Verify iter adapter unsafe methods + safe abstractions (challenge #16) Kani harnesses for core::iter::adapters: - Unsafe (__iterator_get_unchecked / next_unchecked / get_unchecked): cloned, copied, enumerate, fuse, map, skip, zip. - Safe abstractions with internal unsafe: array_chunks::next_back_remainder, copied::spec_next_chunk, map_windows Buffer::{as_array_ref, as_uninit_array_mut, push, drop}, step_by::original_step, zip::{next, next_back}. Each harness takes a symbolic-length sub-slice of a fixed bounded array and is instantiated per representative element type ((), u8, char, (char, u8)) via macros. The generic unsafe trait methods use plain #[kani::proof] with the #[requires] precondition established by construction, since Kani cannot put contracts on generic trait methods (kani#1997). Signed-off-by: Onyeka Obi --- .../core/src/iter/adapters/array_chunks.rs | 40 ++++++++++ library/core/src/iter/adapters/cloned.rs | 59 +++++++++++++++ library/core/src/iter/adapters/copied.rs | 63 ++++++++++++++++ library/core/src/iter/adapters/enumerate.rs | 64 ++++++++++++++++ library/core/src/iter/adapters/fuse.rs | 40 ++++++++++ library/core/src/iter/adapters/map.rs | 66 ++++++++++++++++ library/core/src/iter/adapters/map_windows.rs | 70 +++++++++++++++++ library/core/src/iter/adapters/skip.rs | 42 +++++++++++ library/core/src/iter/adapters/step_by.rs | 43 +++++++++++ library/core/src/iter/adapters/zip.rs | 75 +++++++++++++++++++ 10 files changed, 562 insertions(+) diff --git a/library/core/src/iter/adapters/array_chunks.rs b/library/core/src/iter/adapters/array_chunks.rs index 8f1744fc5fbb7..3cb066bbd86f4 100644 --- a/library/core/src/iter/adapters/array_chunks.rs +++ b/library/core/src/iter/adapters/array_chunks.rs @@ -274,3 +274,43 @@ unsafe impl InPlaceIterable for A } }; } + +#[cfg(kani)] +#[unstable(feature = "kani", issue = "none")] +mod verify { + use super::*; + use crate::kani; + + fn any_slice(orig: &[T]) -> &[T] { + if kani::any() { + let last = kani::any_where(|i: &usize| *i <= orig.len()); + let first = kani::any_where(|i: &usize| *i <= last); + &orig[first..last] + } else { + let ptr = kani::any_where::(|v| *v != 0) as *const T; + kani::assume(ptr.is_aligned()); + unsafe { crate::slice::from_raw_parts(ptr, 0) } + } + } + + // `next_back_remainder` pulls the final `len % N` elements off the back via + // `rev().take(rem).next_chunk()`, then reverses them in place, relying on + // `unwrap_err_unchecked` (sound because `rem < N`). + macro_rules! check_next_back_remainder { + ($harness:ident, $elem_ty:ty, $n:expr, $max_len:expr) => { + #[kani::proof] + fn $harness() { + const N: usize = $n; + const MAX_LEN: usize = $max_len; + let array: [$elem_ty; MAX_LEN] = kani::any(); + let mut it: ArrayChunks<_, N> = ArrayChunks::new(any_slice(&array).iter()); + it.next_back_remainder(); + let _ = &it.remainder; + } + }; + } + check_next_back_remainder!(check_array_chunks_next_back_remainder_unit, (), 2, 8); + check_next_back_remainder!(check_array_chunks_next_back_remainder_u8, u8, 2, 8); + check_next_back_remainder!(check_array_chunks_next_back_remainder_char, char, 3, 9); + check_next_back_remainder!(check_array_chunks_next_back_remainder_tup, (char, u8), 2, 8); +} diff --git a/library/core/src/iter/adapters/cloned.rs b/library/core/src/iter/adapters/cloned.rs index 0f05260059880..79a900719dfe8 100644 --- a/library/core/src/iter/adapters/cloned.rs +++ b/library/core/src/iter/adapters/cloned.rs @@ -193,3 +193,62 @@ unsafe impl InPlaceIterable for Cloned { const EXPAND_BY: Option> = I::EXPAND_BY; const MERGE_BY: Option> = I::MERGE_BY; } + +#[cfg(kani)] +#[unstable(feature = "kani", issue = "none")] +mod verify { + use super::*; + use crate::kani; + + fn any_slice(orig_slice: &[T]) -> &[T] { + if kani::any() { + let last = kani::any_where(|idx: &usize| *idx <= orig_slice.len()); + let first = kani::any_where(|idx: &usize| *idx <= last); + &orig_slice[first..last] + } else { + let ptr = kani::any_where::(|val| *val != 0) as *const T; + kani::assume(ptr.is_aligned()); + unsafe { crate::slice::from_raw_parts(ptr, 0) } + } + } + + fn any_cloned_iter<'a, T: Clone>(orig_slice: &'a [T]) -> Cloned> { + Cloned::new(any_slice(orig_slice).iter()) + } + + macro_rules! check_get_unchecked { + ($harness:ident, $elem_ty:ty, $max_len:expr) => { + #[kani::proof] + fn $harness() { + const MAX_LEN: usize = $max_len; + let array: [$elem_ty; MAX_LEN] = kani::any(); + let mut it = any_cloned_iter::<$elem_ty>(&array); + let idx = kani::any_where(|i: &usize| *i < it.it.size_hint().0); + let _ = unsafe { it.__iterator_get_unchecked(idx) }; + } + }; + } + check_get_unchecked!(check_cloned_get_unchecked_unit, (), isize::MAX as usize); + check_get_unchecked!(check_cloned_get_unchecked_u8, u8, u32::MAX as usize); + check_get_unchecked!(check_cloned_get_unchecked_char, char, 50); + check_get_unchecked!(check_cloned_get_unchecked_tup, (char, u8), 50); + + // `next_unchecked` (UncheckedIterator): the precondition is that the iterator + // is non-empty; establish it by construction. + macro_rules! check_cloned_next_unchecked { + ($harness:ident, $elem_ty:ty, $max_len:expr) => { + #[kani::proof] + fn $harness() { + const MAX_LEN: usize = $max_len; + let array: [$elem_ty; MAX_LEN] = kani::any(); + let mut it = any_cloned_iter::<$elem_ty>(&array); + kani::assume(it.it.size_hint().0 > 0); + let _ = unsafe { it.next_unchecked() }; + } + }; + } + check_cloned_next_unchecked!(check_cloned_next_unchecked_unit, (), isize::MAX as usize); + check_cloned_next_unchecked!(check_cloned_next_unchecked_u8, u8, u32::MAX as usize); + check_cloned_next_unchecked!(check_cloned_next_unchecked_char, char, 50); + check_cloned_next_unchecked!(check_cloned_next_unchecked_tup, (char, u8), 50); +} diff --git a/library/core/src/iter/adapters/copied.rs b/library/core/src/iter/adapters/copied.rs index 0b9da45acaa06..f9a131e756ba4 100644 --- a/library/core/src/iter/adapters/copied.rs +++ b/library/core/src/iter/adapters/copied.rs @@ -278,3 +278,66 @@ unsafe impl InPlaceIterable for Copied { const EXPAND_BY: Option> = I::EXPAND_BY; const MERGE_BY: Option> = I::MERGE_BY; } + +#[cfg(kani)] +#[unstable(feature = "kani", issue = "none")] +mod verify { + use super::*; + use crate::kani; + + fn any_slice(orig_slice: &[T]) -> &[T] { + if kani::any() { + let last = kani::any_where(|idx: &usize| *idx <= orig_slice.len()); + let first = kani::any_where(|idx: &usize| *idx <= last); + &orig_slice[first..last] + } else { + let ptr = kani::any_where::(|val| *val != 0) as *const T; + kani::assume(ptr.is_aligned()); + unsafe { crate::slice::from_raw_parts(ptr, 0) } + } + } + + fn any_adapter_iter<'a, T>(orig_slice: &'a [T]) -> Copied> { + Copied::new(any_slice(orig_slice).iter()) + } + + macro_rules! check_get_unchecked { + ($harness:ident, $elem_ty:ty, $max_len:expr) => { + #[kani::proof] + fn $harness() { + const MAX_LEN: usize = $max_len; + let array: [$elem_ty; MAX_LEN] = kani::any(); + let mut it = any_adapter_iter::<$elem_ty>(&array); + let idx = kani::any_where(|i: &usize| *i < it.it.size_hint().0); + let _ = unsafe { it.__iterator_get_unchecked(idx) }; + } + }; + } + check_get_unchecked!(check_copied_get_unchecked_unit, (), isize::MAX as usize); + check_get_unchecked!(check_copied_get_unchecked_u8, u8, u32::MAX as usize); + check_get_unchecked!(check_copied_get_unchecked_char, char, 50); + check_get_unchecked!(check_copied_get_unchecked_tup, (char, u8), 50); + + // `spec_next_chunk` on the `slice::Iter` specialization bulk-copies `N` (or + // `len`) elements into a `MaybeUninit<[T; N]>` and then either + // `array_assume_init`s the full array or returns a `0..len` `IntoIter`; this + // proves the `copy_nonoverlapping` lengths and the init range stay in bounds. + // `N` is a trait generic, so it is pinned via the result type annotation. + macro_rules! check_spec_next_chunk { + ($harness:ident, $elem_ty:ty, $n:expr) => { + #[kani::proof] + fn $harness() { + const N: usize = $n; + const MAX_LEN: usize = 16; + let array: [$elem_ty; MAX_LEN] = kani::any(); + let mut it = any_slice(&array).iter(); + let _result: Result<[$elem_ty; N], crate::array::IntoIter<$elem_ty, N>> = + it.spec_next_chunk(); + } + }; + } + check_spec_next_chunk!(check_copied_spec_next_chunk_unit, (), 2); + check_spec_next_chunk!(check_copied_spec_next_chunk_u8, u8, 3); + check_spec_next_chunk!(check_copied_spec_next_chunk_char, char, 2); + check_spec_next_chunk!(check_copied_spec_next_chunk_tup, (char, u8), 2); +} diff --git a/library/core/src/iter/adapters/enumerate.rs b/library/core/src/iter/adapters/enumerate.rs index e7e18d178031f..553d1946bbdb3 100644 --- a/library/core/src/iter/adapters/enumerate.rs +++ b/library/core/src/iter/adapters/enumerate.rs @@ -321,3 +321,67 @@ impl Default for Enumerate { Enumerate::new(Default::default()) } } + +/// Verification harnesses for `Enumerate`'s `unsafe`/contract-bearing methods +/// (verify-rust-std challenge #16). +#[cfg(kani)] +#[unstable(feature = "kani", issue = "none")] +mod verify { + use super::*; + use crate::kani; + + /// An arbitrary-length sub-slice of `orig_slice` (mirrors + /// `slice::iter::verify::any_slice`). This is what makes the proof + /// *unbounded*: the backing array is a fixed Kani constant, but the slice + /// handed to the iterator has a symbolic length, so the proof covers every + /// shorter configuration at once. + fn any_slice(orig_slice: &[T]) -> &[T] { + if kani::any() { + let last = kani::any_where(|idx: &usize| *idx <= orig_slice.len()); + let first = kani::any_where(|idx: &usize| *idx <= last); + &orig_slice[first..last] + } else { + let ptr = kani::any_where::(|val| *val != 0) as *const T; + kani::assume(ptr.is_aligned()); + // SAFETY: `ptr` is non-null and aligned; length 0 makes the slice trivially valid. + unsafe { crate::slice::from_raw_parts(ptr, 0) } + } + } + + /// Wrap an arbitrary sub-slice in `Enumerate>`. We build + /// the inner `slice::Iter` via `(&[T]).iter()` because `slice::Iter::new` is + /// `pub(super)` to the `slice` module and unreachable from here. + fn any_enumerate_iter<'a, T>(orig_slice: &'a [T]) -> Enumerate> { + Enumerate::new(any_slice(orig_slice).iter()) + } + + /// One `proof_for_contract` harness per concrete element type; the contract + /// itself stays generic. `slice::Iter` is `TrustedRandomAccessNoCoerce` + /// for every `T`, satisfying the method's `Self: TrustedRandomAccessNoCoerce`. + // NOTE: `__iterator_get_unchecked` is a trait method on the *generic* impl + // `impl Iterator for Enumerate`, and Kani cannot attach a + // `proof_for_contract` to a generic trait method (kani#1997). So instead of + // the contract machinery we use a plain `#[kani::proof]` that establishes the + // method's precondition by construction (`idx < self.iter.size_hint().0`) and + // lets Kani prove the body introduces no UB -- the same safety property the + // contract expresses. + macro_rules! check_enumerate_get_unchecked { + ($harness:ident, $elem_ty:ty, $max_len:expr) => { + #[kani::proof] + fn $harness() { + const MAX_LEN: usize = $max_len; + let array: [$elem_ty; MAX_LEN] = kani::any(); + let mut enumerate = any_enumerate_iter::<$elem_ty>(&array); + // The method's precondition: `idx < self.iter.size_hint().0`. + let idx = kani::any_where(|i: &usize| *i < enumerate.iter.size_hint().0); + let _ = unsafe { enumerate.__iterator_get_unchecked(idx) }; + } + }; + } + + // Representative element types: ZST, byte, 4-byte-align niche type, composite. + check_enumerate_get_unchecked!(check_enumerate_get_unchecked_unit, (), isize::MAX as usize); + check_enumerate_get_unchecked!(check_enumerate_get_unchecked_u8, u8, u32::MAX as usize); + check_enumerate_get_unchecked!(check_enumerate_get_unchecked_char, char, 50); + check_enumerate_get_unchecked!(check_enumerate_get_unchecked_tup, (char, u8), 50); +} diff --git a/library/core/src/iter/adapters/fuse.rs b/library/core/src/iter/adapters/fuse.rs index fcad6168d85cd..11d607ba466fb 100644 --- a/library/core/src/iter/adapters/fuse.rs +++ b/library/core/src/iter/adapters/fuse.rs @@ -478,3 +478,43 @@ fn and_then_or_clear(opt: &mut Option, f: impl FnOnce(&mut T) -> Option } x } + +#[cfg(kani)] +#[unstable(feature = "kani", issue = "none")] +mod verify { + use super::*; + use crate::kani; + + fn any_slice(orig_slice: &[T]) -> &[T] { + if kani::any() { + let last = kani::any_where(|idx: &usize| *idx <= orig_slice.len()); + let first = kani::any_where(|idx: &usize| *idx <= last); + &orig_slice[first..last] + } else { + let ptr = kani::any_where::(|val| *val != 0) as *const T; + kani::assume(ptr.is_aligned()); + unsafe { crate::slice::from_raw_parts(ptr, 0) } + } + } + + fn any_fuse_iter<'a, T>(orig_slice: &'a [T]) -> Fuse> { + Fuse::new(any_slice(orig_slice).iter()) + } + + macro_rules! check_get_unchecked { + ($harness:ident, $elem_ty:ty, $max_len:expr) => { + #[kani::proof] + fn $harness() { + const MAX_LEN: usize = $max_len; + let array: [$elem_ty; MAX_LEN] = kani::any(); + let mut it = any_fuse_iter::<$elem_ty>(&array); + let idx = kani::any_where(|i: &usize| *i < it.iter.as_ref().unwrap().size_hint().0); + let _ = unsafe { it.__iterator_get_unchecked(idx) }; + } + }; + } + check_get_unchecked!(check_fuse_get_unchecked_unit, (), isize::MAX as usize); + check_get_unchecked!(check_fuse_get_unchecked_u8, u8, u32::MAX as usize); + check_get_unchecked!(check_fuse_get_unchecked_char, char, 50); + check_get_unchecked!(check_fuse_get_unchecked_tup, (char, u8), 50); +} diff --git a/library/core/src/iter/adapters/map.rs b/library/core/src/iter/adapters/map.rs index bf9f0c48fec3b..eb77668fd620b 100644 --- a/library/core/src/iter/adapters/map.rs +++ b/library/core/src/iter/adapters/map.rs @@ -245,3 +245,69 @@ unsafe impl InPlaceIterable for Map { const EXPAND_BY: Option> = I::EXPAND_BY; const MERGE_BY: Option> = I::MERGE_BY; } + +#[cfg(kani)] +#[unstable(feature = "kani", issue = "none")] +mod verify { + use super::*; + use crate::kani; + + fn any_slice(orig_slice: &[T]) -> &[T] { + if kani::any() { + let last = kani::any_where(|idx: &usize| *idx <= orig_slice.len()); + let first = kani::any_where(|idx: &usize| *idx <= last); + &orig_slice[first..last] + } else { + let ptr = kani::any_where::(|val| *val != 0) as *const T; + kani::assume(ptr.is_aligned()); + unsafe { crate::slice::from_raw_parts(ptr, 0) } + } + } + + // The mapping closure is irrelevant to buffer/UB safety; a simple + // `&T -> usize` fn pointer (nameable, unlike a closure) keeps the harness + // type concrete. `slice::Iter` yields `&T`, so `F: FnMut(&T) -> usize`. + fn map_fn(_: &T) -> usize { + 0 + } + + fn any_map_iter<'a, T>(orig_slice: &'a [T]) -> Map, fn(&T) -> usize> { + Map::new(any_slice(orig_slice).iter(), map_fn:: as fn(&T) -> usize) + } + + macro_rules! check_map_get_unchecked { + ($harness:ident, $elem_ty:ty, $max_len:expr) => { + #[kani::proof] + fn $harness() { + const MAX_LEN: usize = $max_len; + let array: [$elem_ty; MAX_LEN] = kani::any(); + let mut it = any_map_iter::<$elem_ty>(&array); + let idx = kani::any_where(|i: &usize| *i < it.iter.size_hint().0); + let _ = unsafe { it.__iterator_get_unchecked(idx) }; + } + }; + } + + macro_rules! check_map_next_unchecked { + ($harness:ident, $elem_ty:ty, $max_len:expr) => { + #[kani::proof] + fn $harness() { + const MAX_LEN: usize = $max_len; + let array: [$elem_ty; MAX_LEN] = kani::any(); + let mut it = any_map_iter::<$elem_ty>(&array); + kani::assume(it.iter.size_hint().0 > 0); + let _ = unsafe { it.next_unchecked() }; + } + }; + } + + check_map_get_unchecked!(check_map_get_unchecked_unit, (), isize::MAX as usize); + check_map_get_unchecked!(check_map_get_unchecked_u8, u8, u32::MAX as usize); + check_map_get_unchecked!(check_map_get_unchecked_char, char, 50); + check_map_get_unchecked!(check_map_get_unchecked_tup, (char, u8), 50); + + check_map_next_unchecked!(check_map_next_unchecked_unit, (), isize::MAX as usize); + check_map_next_unchecked!(check_map_next_unchecked_u8, u8, u32::MAX as usize); + check_map_next_unchecked!(check_map_next_unchecked_char, char, 50); + check_map_next_unchecked!(check_map_next_unchecked_tup, (char, u8), 50); +} diff --git a/library/core/src/iter/adapters/map_windows.rs b/library/core/src/iter/adapters/map_windows.rs index 536251a137da0..6980784d55396 100644 --- a/library/core/src/iter/adapters/map_windows.rs +++ b/library/core/src/iter/adapters/map_windows.rs @@ -297,3 +297,73 @@ impl Invariant for Buffer { self.start + N <= 2 * N } } + +#[cfg(kani)] +#[unstable(feature = "kani", issue = "none")] +mod verify { + use super::*; + use crate::kani; + + // Build a `Buffer` in a valid state: `start` in `[0, N]` (the type + // invariant) and the live window `[start, start + N)` fully initialized with + // arbitrary values; the remaining `N` slots stay uninitialized, exactly as + // the real buffer maintains them. + fn any_buffer() -> Buffer { + let start = kani::any_where(|s: &usize| *s <= N); + let mut buffer = Buffer { + buffer: [[const { MaybeUninit::uninit() }; N], [const { MaybeUninit::uninit() }; N]], + start, + }; + let items: [T; N] = kani::any(); + let base = buffer.buffer_mut_ptr(); + items.into_iter().enumerate().for_each(|(i, item)| { + // SAFETY: `start + i < start + N <= 2 * N`, in bounds of the `2 * N` buffer. + unsafe { (*base.add(start + i)).write(item) }; + }); + buffer + } + + // `as_array_ref` / `as_uninit_array_mut` reinterpret the live window as an + // array reference; `push` rotates the window (and wraps + compacts when + // `start == N`); `drop` drops the live window. Each must respect the + // initialized-window invariant and stay in bounds of the `2 * N` storage. + macro_rules! check_buffer { + ($module:ident, $elem_ty:ty, $n:expr) => { + mod $module { + use super::*; + const N: usize = $n; + + #[kani::proof] + fn check_as_array_ref() { + let buf = any_buffer::<$elem_ty, N>(); + let _ = buf.as_array_ref(); + kani::assert(buf.is_safe(), "buffer invariant holds"); + } + + #[kani::proof] + fn check_as_uninit_array_mut() { + let mut buf = any_buffer::<$elem_ty, N>(); + let _ = buf.as_uninit_array_mut(); + kani::assert(buf.is_safe(), "buffer invariant holds"); + } + + #[kani::proof] + fn check_push() { + let mut buf = any_buffer::<$elem_ty, N>(); + buf.push(kani::any()); + kani::assert(buf.is_safe(), "buffer invariant holds after push"); + } + + #[kani::proof] + fn check_drop() { + let buf = any_buffer::<$elem_ty, N>(); + drop(buf); + } + } + }; + } + check_buffer!(verify_map_windows_unit, (), 3); + check_buffer!(verify_map_windows_u8, u8, 3); + check_buffer!(verify_map_windows_char, char, 2); + check_buffer!(verify_map_windows_tup, (char, u8), 2); +} diff --git a/library/core/src/iter/adapters/skip.rs b/library/core/src/iter/adapters/skip.rs index ac3cc4c4f1152..499517c7e7678 100644 --- a/library/core/src/iter/adapters/skip.rs +++ b/library/core/src/iter/adapters/skip.rs @@ -293,3 +293,45 @@ where // I: TrustedLen would not. #[unstable(feature = "trusted_len", issue = "37572")] unsafe impl TrustedLen for Skip where I: Iterator + TrustedRandomAccess {} + +#[cfg(kani)] +#[unstable(feature = "kani", issue = "none")] +mod verify { + use super::*; + use crate::kani; + + fn any_slice(orig_slice: &[T]) -> &[T] { + if kani::any() { + let last = kani::any_where(|idx: &usize| *idx <= orig_slice.len()); + let first = kani::any_where(|idx: &usize| *idx <= last); + &orig_slice[first..last] + } else { + let ptr = kani::any_where::(|val| *val != 0) as *const T; + kani::assume(ptr.is_aligned()); + unsafe { crate::slice::from_raw_parts(ptr, 0) } + } + } + + fn any_skip_iter<'a, T>(orig_slice: &'a [T]) -> Skip> { + let slice = any_slice(orig_slice); + let n = kani::any_where(|offset: &usize| *offset <= slice.len()); + Skip::new(slice.iter(), n) + } + + macro_rules! check_get_unchecked { + ($harness:ident, $elem_ty:ty, $max_len:expr) => { + #[kani::proof] + fn $harness() { + const MAX_LEN: usize = $max_len; + let array: [$elem_ty; MAX_LEN] = kani::any(); + let mut it = any_skip_iter::<$elem_ty>(&array); + let idx = kani::any_where(|i: &usize| *i < it.iter.size_hint().0 - it.n); + let _ = unsafe { it.__iterator_get_unchecked(idx) }; + } + }; + } + check_get_unchecked!(check_skip_get_unchecked_unit, (), isize::MAX as usize); + check_get_unchecked!(check_skip_get_unchecked_u8, u8, u32::MAX as usize); + check_get_unchecked!(check_skip_get_unchecked_char, char, 50); + check_get_unchecked!(check_skip_get_unchecked_tup, (char, u8), 50); +} diff --git a/library/core/src/iter/adapters/step_by.rs b/library/core/src/iter/adapters/step_by.rs index 32604a07ca40c..1ac22d698a43e 100644 --- a/library/core/src/iter/adapters/step_by.rs +++ b/library/core/src/iter/adapters/step_by.rs @@ -589,3 +589,46 @@ spec_int_ranges_r!(u8 u16 u32 usize); spec_int_ranges!(u8 u16 usize); #[cfg(target_pointer_width = "16")] spec_int_ranges_r!(u8 u16 usize); + +#[cfg(kani)] +#[unstable(feature = "kani", issue = "none")] +mod verify { + use super::*; + use crate::kani; + + fn any_slice(orig: &[T]) -> &[T] { + if kani::any() { + let last = kani::any_where(|i: &usize| *i <= orig.len()); + let first = kani::any_where(|i: &usize| *i <= last); + &orig[first..last] + } else { + let ptr = kani::any_where::(|v| *v != 0) as *const T; + kani::assume(ptr.is_aligned()); + unsafe { crate::slice::from_raw_parts(ptr, 0) } + } + } + + // `original_step` reconstructs the configured step as `step_minus_one + 1` + // via `unchecked_add` and `NonZero::new_unchecked`. The type invariant + // (`step_minus_one < usize::MAX`) makes both operations sound; a valid + // `StepBy` is established by construction (`StepBy::new` requires `step != 0`). + macro_rules! check_original_step { + ($harness:ident, $elem_ty:ty, $max_len:expr) => { + #[kani::proof] + fn $harness() { + const MAX_LEN: usize = $max_len; + let array: [$elem_ty; MAX_LEN] = kani::any(); + let step = kani::any_where(|s: &usize| *s != 0); + let it = StepBy::new(any_slice(&array).iter(), step); + let result = it.original_step(); + kani::assert(result.get() == step, "original_step round-trips the configured step"); + } + }; + } + // `original_step` ignores the wrapped iterator, so a small backing array + // suffices; the proof is over the symbolic `step`, not the slice length. + check_original_step!(check_step_by_original_step_unit, (), 16); + check_original_step!(check_step_by_original_step_u8, u8, 16); + check_original_step!(check_step_by_original_step_char, char, 16); + check_original_step!(check_step_by_original_step_tup, (char, u8), 16); +} diff --git a/library/core/src/iter/adapters/zip.rs b/library/core/src/iter/adapters/zip.rs index e39f1535bd95d..caeb5970c8ff3 100644 --- a/library/core/src/iter/adapters/zip.rs +++ b/library/core/src/iter/adapters/zip.rs @@ -692,3 +692,78 @@ impl SpecFold for Zip { accum } } + +#[cfg(kani)] +#[unstable(feature = "kani", issue = "none")] +mod verify { + use super::*; + use crate::kani; + + fn any_slice(orig_slice: &[T]) -> &[T] { + if kani::any() { + let last = kani::any_where(|idx: &usize| *idx <= orig_slice.len()); + let first = kani::any_where(|idx: &usize| *idx <= last); + &orig_slice[first..last] + } else { + let ptr = kani::any_where::(|val| *val != 0) as *const T; + kani::assume(ptr.is_aligned()); + unsafe { crate::slice::from_raw_parts(ptr, 0) } + } + } + + fn any_zip_iter<'a, T, U>( + orig_slice_a: &'a [T], + orig_slice_b: &'a [U], + ) -> Zip, crate::slice::Iter<'a, U>> { + Zip::new(any_slice(orig_slice_a).iter(), any_slice(orig_slice_b).iter()) + } + + macro_rules! check_zip_get_unchecked { + ($harness:ident, $elem_ty_a:ty, $elem_ty_b:ty, $max_len:expr) => { + #[kani::proof] + fn $harness() { + const MAX_LEN: usize = $max_len; + let array_a: [$elem_ty_a; MAX_LEN] = kani::any(); + let array_b: [$elem_ty_b; MAX_LEN] = kani::any(); + let mut it = any_zip_iter::<$elem_ty_a, $elem_ty_b>(&array_a, &array_b); + let idx = kani::any_where(|i: &usize| *i < crate::iter::Iterator::size_hint(&it).0); + let _ = unsafe { it.__iterator_get_unchecked(idx) }; + } + }; + } + + check_zip_get_unchecked!(check_zip_get_unchecked_unit_unit, (), (), 50); + check_zip_get_unchecked!(check_zip_get_unchecked_u8_u8, u8, u8, u32::MAX as usize); + check_zip_get_unchecked!(check_zip_get_unchecked_char_u8, char, u8, 50); + check_zip_get_unchecked!(check_zip_get_unchecked_u8_char, u8, char, 50); + check_zip_get_unchecked!(check_zip_get_unchecked_tup_tup, (char, u8), (u32, i16), 50); + + // Safe `Zip` methods on `TrustedRandomAccess` sources drive the same + // `get_unchecked`-based machinery as `__iterator_get_unchecked`; these prove + // `next` / `next_back` keep their internal indexes in bounds. Bounded + // `MAX_LEN` because the methods iterate. + macro_rules! check_zip_safe { + ($a:ty, $b:ty, $next:ident, $back:ident) => { + #[kani::proof] + fn $next() { + const MAX_LEN: usize = 16; + let array_a: [$a; MAX_LEN] = kani::any(); + let array_b: [$b; MAX_LEN] = kani::any(); + let mut it = any_zip_iter::<$a, $b>(&array_a, &array_b); + let _ = crate::iter::Iterator::next(&mut it); + } + #[kani::proof] + fn $back() { + const MAX_LEN: usize = 16; + let array_a: [$a; MAX_LEN] = kani::any(); + let array_b: [$b; MAX_LEN] = kani::any(); + let mut it = any_zip_iter::<$a, $b>(&array_a, &array_b); + let _ = crate::iter::DoubleEndedIterator::next_back(&mut it); + } + }; + } + check_zip_safe!((), (), check_zip_next_unit, check_zip_next_back_unit); + check_zip_safe!(u8, u8, check_zip_next_u8, check_zip_next_back_u8); + check_zip_safe!(char, u8, check_zip_next_char_u8, check_zip_next_back_char_u8); + check_zip_safe!((char, u8), (u32, i16), check_zip_next_tup, check_zip_next_back_tup); +} From 17e9794a03457b7c216a5541024060357ce6729e Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Tue, 16 Jun 2026 23:36:54 -0700 Subject: [PATCH 02/62] Verify deferred iter adapter iterating methods (challenge #16) Adds Kani harnesses for the iterating / chunk-building iterator-adapter methods deferred from the initial #16 set for tractability: array_chunks::fold filter::next_chunk_dropless filter_map::next_chunk take::spec_fold / spec_for_each zip::nth / fold / spec_fold Each is instantiated over (), u8, char and a composite tuple type (32 harnesses total); all verify with 0 failures. These methods loop over a symbolic-length slice. Drafted without an explicit unwind bound, CBMC over-unwound the loop and timed out; adding assumption) plus a tightened MAX_LEN makes each verify in under a second. --- .../core/src/iter/adapters/array_chunks.rs | 22 ++++++++ library/core/src/iter/adapters/filter.rs | 49 +++++++++++++++++ library/core/src/iter/adapters/filter_map.rs | 50 +++++++++++++++++ library/core/src/iter/adapters/take.rs | 51 ++++++++++++++++++ library/core/src/iter/adapters/zip.rs | 54 ++++++++++++++++--- 5 files changed, 219 insertions(+), 7 deletions(-) diff --git a/library/core/src/iter/adapters/array_chunks.rs b/library/core/src/iter/adapters/array_chunks.rs index 3cb066bbd86f4..54e1b818fef7f 100644 --- a/library/core/src/iter/adapters/array_chunks.rs +++ b/library/core/src/iter/adapters/array_chunks.rs @@ -313,4 +313,26 @@ mod verify { check_next_back_remainder!(check_array_chunks_next_back_remainder_u8, u8, 2, 8); check_next_back_remainder!(check_array_chunks_next_back_remainder_char, char, 3, 9); check_next_back_remainder!(check_array_chunks_next_back_remainder_tup, (char, u8), 2, 8); + + // `fold` on a `TrustedRandomAccessNoCoerce` source builds each chunk with + // `array::from_fn` calling `__iterator_get_unchecked(i + local)` under an + // `inner_len - i >= N` guard; this proves those indexes stay in bounds. + macro_rules! check_fold { + ($harness:ident, $elem_ty:ty, $n:expr, $max_len:expr, $unwind:literal) => { + #[kani::proof] + #[kani::unwind($unwind)] + fn $harness() { + const N: usize = $n; + const MAX_LEN: usize = $max_len; + let array: [$elem_ty; MAX_LEN] = kani::any(); + let it: ArrayChunks<_, N> = ArrayChunks::new(any_slice(&array).iter()); + // Disambiguate from the in-scope internal `SpecFold::fold`. + let _ = crate::iter::Iterator::fold(it, 0usize, |acc, _chunk| acc.wrapping_add(1)); + } + }; + } + check_fold!(check_array_chunks_fold_unit, (), 2, 4, 5); + check_fold!(check_array_chunks_fold_u8, u8, 2, 4, 5); + check_fold!(check_array_chunks_fold_char, char, 3, 6, 7); + check_fold!(check_array_chunks_fold_tup, (char, u8), 2, 4, 5); } diff --git a/library/core/src/iter/adapters/filter.rs b/library/core/src/iter/adapters/filter.rs index dd08cd6f61c4c..936dbe2bedb59 100644 --- a/library/core/src/iter/adapters/filter.rs +++ b/library/core/src/iter/adapters/filter.rs @@ -214,3 +214,52 @@ unsafe impl InPlaceIterable for Filter { const EXPAND_BY: Option> = I::EXPAND_BY; const MERGE_BY: Option> = I::MERGE_BY; } + +#[cfg(kani)] +#[unstable(feature = "kani", issue = "none")] +mod verify { + use super::*; + use crate::kani; + + fn any_slice(orig: &[T]) -> &[T] { + if kani::any() { + let last = kani::any_where(|i: &usize| *i <= orig.len()); + let first = kani::any_where(|i: &usize| *i <= last); + &orig[first..last] + } else { + let ptr = kani::any_where::(|v| *v != 0) as *const T; + kani::assume(ptr.is_aligned()); + unsafe { crate::slice::from_raw_parts(ptr, 0) } + } + } + + // `Filter`'s predicate is `FnMut(&Self::Item)`; for `slice::Iter` that is + // `FnMut(&&T)`. A nondeterministic predicate exercises both the kept and the + // filtered branch. A named fn pointer keeps the helper return type nameable. + fn maybe_keep(_: &&T) -> bool { + kani::any() + } + + // `next_chunk_dropless` writes every element (branchlessly) into a + // `MaybeUninit<[_; N]>` and bumps `initialized` only for kept elements, + // breaking once `initialized == N`; this proves the `get_unchecked_mut(idx)` + // writes and the final `array_assume_init` / `IntoIter` range stay in bounds. + macro_rules! check_next_chunk_dropless { + ($harness:ident, $elem_ty:ty) => { + #[kani::proof] + #[kani::unwind(7)] + fn $harness() { + const MAX_LEN: usize = 6; + const N: usize = 4; + let array: [$elem_ty; MAX_LEN] = kani::any(); + let mut it = + Filter::new(any_slice(&array).iter(), maybe_keep::<$elem_ty> as fn(&&$elem_ty) -> bool); + let _ = it.next_chunk_dropless::(); + } + }; + } + check_next_chunk_dropless!(check_filter_next_chunk_dropless_unit, ()); + check_next_chunk_dropless!(check_filter_next_chunk_dropless_u8, u8); + check_next_chunk_dropless!(check_filter_next_chunk_dropless_char, char); + check_next_chunk_dropless!(check_filter_next_chunk_dropless_tup, (char, u8)); +} diff --git a/library/core/src/iter/adapters/filter_map.rs b/library/core/src/iter/adapters/filter_map.rs index 24ec6b1741ce1..f09cae914e6f5 100644 --- a/library/core/src/iter/adapters/filter_map.rs +++ b/library/core/src/iter/adapters/filter_map.rs @@ -211,3 +211,53 @@ unsafe impl InPlaceIterable for FilterMap { const EXPAND_BY: Option> = I::EXPAND_BY; const MERGE_BY: Option> = I::MERGE_BY; } + +#[cfg(kani)] +#[unstable(feature = "kani", issue = "none")] +mod verify { + use super::*; + use crate::kani; + + fn any_slice(orig: &[T]) -> &[T] { + if kani::any() { + let last = kani::any_where(|i: &usize| *i <= orig.len()); + let first = kani::any_where(|i: &usize| *i <= last); + &orig[first..last] + } else { + let ptr = kani::any_where::(|v| *v != 0) as *const T; + kani::assume(ptr.is_aligned()); + unsafe { crate::slice::from_raw_parts(ptr, 0) } + } + } + + // Maps `&T -> Option` nondeterministically to exercise both the + // "element kept" and "element filtered" paths of the chunk-fill loop. + fn maybe_map(_: &T) -> Option { + kani::any::().then_some(0) + } + + // `next_chunk` fills a `MaybeUninit<[B; N]>` via a `Guard`-protected loop + // that byte-copies each mapped value's payload and bumps `initialized` only + // when the map yields `Some`; this proves the copies, the `array_assume_init`, + // and the partial-fill `IntoIter` range all stay in bounds. + macro_rules! check_next_chunk { + ($harness:ident, $elem_ty:ty) => { + #[kani::proof] + #[kani::unwind(6)] + fn $harness() { + const MAX_LEN: usize = 5; + const N: usize = 3; + let array: [$elem_ty; MAX_LEN] = kani::any(); + let mut it = FilterMap::new( + any_slice(&array).iter(), + maybe_map::<$elem_ty> as fn(&$elem_ty) -> Option, + ); + let _ = it.next_chunk::(); + } + }; + } + check_next_chunk!(check_filter_map_next_chunk_unit, ()); + check_next_chunk!(check_filter_map_next_chunk_u8, u8); + check_next_chunk!(check_filter_map_next_chunk_char, char); + check_next_chunk!(check_filter_map_next_chunk_tup, (char, u8)); +} diff --git a/library/core/src/iter/adapters/take.rs b/library/core/src/iter/adapters/take.rs index b96335f415257..7924ccdc8a5d7 100644 --- a/library/core/src/iter/adapters/take.rs +++ b/library/core/src/iter/adapters/take.rs @@ -374,3 +374,54 @@ impl A, A> ExactSizeIterator for Take> self.n } } + +#[cfg(kani)] +#[unstable(feature = "kani", issue = "none")] +mod verify { + use super::*; + use crate::kani; + + fn any_slice(orig: &[T]) -> &[T] { + if kani::any() { + let last = kani::any_where(|i: &usize| *i <= orig.len()); + let first = kani::any_where(|i: &usize| *i <= last); + &orig[first..last] + } else { + let ptr = kani::any_where::(|v| *v != 0) as *const T; + kani::assume(ptr.is_aligned()); + unsafe { crate::slice::from_raw_parts(ptr, 0) } + } + } + + // On a `TrustedRandomAccess` source, `SpecTake::spec_fold` / `spec_for_each` + // drive a `0..end` loop calling `__iterator_get_unchecked(i)` where + // `end = self.n.min(self.iter.size())`; this proves those unchecked indexes + // stay in bounds. `MAX_LEN` is bounded because the methods iterate. + macro_rules! check_spec_take { + ($fold_harness:ident, $each_harness:ident, $elem_ty:ty) => { + #[kani::proof] + #[kani::unwind(6)] + fn $fold_harness() { + const MAX_LEN: usize = 5; + let array: [$elem_ty; MAX_LEN] = kani::any(); + let n = kani::any_where(|n: &usize| *n <= MAX_LEN); + let it = Take::new(any_slice(&array).iter(), n); + let _ = it.spec_fold(0usize, |acc, _| acc.wrapping_add(1)); + } + + #[kani::proof] + #[kani::unwind(6)] + fn $each_harness() { + const MAX_LEN: usize = 5; + let array: [$elem_ty; MAX_LEN] = kani::any(); + let n = kani::any_where(|n: &usize| *n <= MAX_LEN); + let it = Take::new(any_slice(&array).iter(), n); + it.spec_for_each(|_| {}); + } + }; + } + check_spec_take!(check_take_spec_fold_unit, check_take_spec_for_each_unit, ()); + check_spec_take!(check_take_spec_fold_u8, check_take_spec_for_each_u8, u8); + check_spec_take!(check_take_spec_fold_char, check_take_spec_for_each_char, char); + check_spec_take!(check_take_spec_fold_tup, check_take_spec_for_each_tup, (char, u8)); +} diff --git a/library/core/src/iter/adapters/zip.rs b/library/core/src/iter/adapters/zip.rs index caeb5970c8ff3..85faeb442c753 100644 --- a/library/core/src/iter/adapters/zip.rs +++ b/library/core/src/iter/adapters/zip.rs @@ -740,10 +740,10 @@ mod verify { // Safe `Zip` methods on `TrustedRandomAccess` sources drive the same // `get_unchecked`-based machinery as `__iterator_get_unchecked`; these prove - // `next` / `next_back` keep their internal indexes in bounds. Bounded - // `MAX_LEN` because the methods iterate. + // `next` / `nth` / `next_back` / `fold` / `spec_fold` keep their internal + // indexes in bounds. Bounded `MAX_LEN` because the methods iterate. macro_rules! check_zip_safe { - ($a:ty, $b:ty, $next:ident, $back:ident) => { + ($a:ty, $b:ty, $next:ident, $nth:ident, $back:ident, $fold:ident, $spec:ident) => { #[kani::proof] fn $next() { const MAX_LEN: usize = 16; @@ -753,6 +753,16 @@ mod verify { let _ = crate::iter::Iterator::next(&mut it); } #[kani::proof] + #[kani::unwind(6)] + fn $nth() { + const MAX_LEN: usize = 5; + let array_a: [$a; MAX_LEN] = kani::any(); + let array_b: [$b; MAX_LEN] = kani::any(); + let mut it = any_zip_iter::<$a, $b>(&array_a, &array_b); + let n = kani::any_where(|x: &usize| *x <= MAX_LEN); + let _ = crate::iter::Iterator::nth(&mut it, n); + } + #[kani::proof] fn $back() { const MAX_LEN: usize = 16; let array_a: [$a; MAX_LEN] = kani::any(); @@ -760,10 +770,40 @@ mod verify { let mut it = any_zip_iter::<$a, $b>(&array_a, &array_b); let _ = crate::iter::DoubleEndedIterator::next_back(&mut it); } + #[kani::proof] + #[kani::unwind(6)] + fn $fold() { + const MAX_LEN: usize = 5; + let array_a: [$a; MAX_LEN] = kani::any(); + let array_b: [$b; MAX_LEN] = kani::any(); + let it = any_zip_iter::<$a, $b>(&array_a, &array_b); + let _ = crate::iter::Iterator::fold(it, 0usize, |acc, _| acc.wrapping_add(1)); + } + #[kani::proof] + #[kani::unwind(6)] + fn $spec() { + const MAX_LEN: usize = 5; + let array_a: [$a; MAX_LEN] = kani::any(); + let array_b: [$b; MAX_LEN] = kani::any(); + let it = any_zip_iter::<$a, $b>(&array_a, &array_b); + let _ = SpecFold::spec_fold(it, 0usize, |acc, _| acc.wrapping_add(1)); + } }; } - check_zip_safe!((), (), check_zip_next_unit, check_zip_next_back_unit); - check_zip_safe!(u8, u8, check_zip_next_u8, check_zip_next_back_u8); - check_zip_safe!(char, u8, check_zip_next_char_u8, check_zip_next_back_char_u8); - check_zip_safe!((char, u8), (u32, i16), check_zip_next_tup, check_zip_next_back_tup); + check_zip_safe!( + (), (), check_zip_next_unit, check_zip_nth_unit, check_zip_next_back_unit, + check_zip_fold_unit, check_zip_spec_fold_unit + ); + check_zip_safe!( + u8, u8, check_zip_next_u8, check_zip_nth_u8, check_zip_next_back_u8, + check_zip_fold_u8, check_zip_spec_fold_u8 + ); + check_zip_safe!( + char, u8, check_zip_next_char_u8, check_zip_nth_char_u8, check_zip_next_back_char_u8, + check_zip_fold_char_u8, check_zip_spec_fold_char_u8 + ); + check_zip_safe!( + (char, u8), (u32, i16), check_zip_next_tup, check_zip_nth_tup, check_zip_next_back_tup, + check_zip_fold_tup, check_zip_spec_fold_tup + ); } From 9f5d372586c47ce33ff85866d34dde373aee1dfc Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Sun, 21 Jun 2026 19:52:53 -0700 Subject: [PATCH 03/62] Format challenge #16 harnesses to satisfy upstream rustfmt upstream_test runs ./x fmt --check with rust-lang/rust's rustfmt.toml (style_edition 2024, use_small_heuristics = Max). Reflow the check_zip_safe! invocations and the Filter::new call one argument per line. Formatting only. Signed-off-by: Onyeka Obi --- library/core/src/iter/adapters/filter.rs | 6 ++-- library/core/src/iter/adapters/zip.rs | 36 ++++++++++++++++++------ 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/library/core/src/iter/adapters/filter.rs b/library/core/src/iter/adapters/filter.rs index 936dbe2bedb59..230cef03de60b 100644 --- a/library/core/src/iter/adapters/filter.rs +++ b/library/core/src/iter/adapters/filter.rs @@ -252,8 +252,10 @@ mod verify { const MAX_LEN: usize = 6; const N: usize = 4; let array: [$elem_ty; MAX_LEN] = kani::any(); - let mut it = - Filter::new(any_slice(&array).iter(), maybe_keep::<$elem_ty> as fn(&&$elem_ty) -> bool); + let mut it = Filter::new( + any_slice(&array).iter(), + maybe_keep::<$elem_ty> as fn(&&$elem_ty) -> bool, + ); let _ = it.next_chunk_dropless::(); } }; diff --git a/library/core/src/iter/adapters/zip.rs b/library/core/src/iter/adapters/zip.rs index 85faeb442c753..90d2bbbd73d63 100644 --- a/library/core/src/iter/adapters/zip.rs +++ b/library/core/src/iter/adapters/zip.rs @@ -791,19 +791,39 @@ mod verify { }; } check_zip_safe!( - (), (), check_zip_next_unit, check_zip_nth_unit, check_zip_next_back_unit, - check_zip_fold_unit, check_zip_spec_fold_unit + (), + (), + check_zip_next_unit, + check_zip_nth_unit, + check_zip_next_back_unit, + check_zip_fold_unit, + check_zip_spec_fold_unit ); check_zip_safe!( - u8, u8, check_zip_next_u8, check_zip_nth_u8, check_zip_next_back_u8, - check_zip_fold_u8, check_zip_spec_fold_u8 + u8, + u8, + check_zip_next_u8, + check_zip_nth_u8, + check_zip_next_back_u8, + check_zip_fold_u8, + check_zip_spec_fold_u8 ); check_zip_safe!( - char, u8, check_zip_next_char_u8, check_zip_nth_char_u8, check_zip_next_back_char_u8, - check_zip_fold_char_u8, check_zip_spec_fold_char_u8 + char, + u8, + check_zip_next_char_u8, + check_zip_nth_char_u8, + check_zip_next_back_char_u8, + check_zip_fold_char_u8, + check_zip_spec_fold_char_u8 ); check_zip_safe!( - (char, u8), (u32, i16), check_zip_next_tup, check_zip_nth_tup, check_zip_next_back_tup, - check_zip_fold_tup, check_zip_spec_fold_tup + (char, u8), + (u32, i16), + check_zip_next_tup, + check_zip_nth_tup, + check_zip_next_back_tup, + check_zip_fold_tup, + check_zip_spec_fold_tup ); } From 4459557cdff7576583e862295d93611770882e13 Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Mon, 17 Aug 2026 15:14:39 -0700 Subject: [PATCH 04/62] Address review on challenge 16: unbounded loop-contract harnesses, direct get_unchecked, side-effect and drop-observing sources Review items (feliperodri, 2026-08-16): - F-B1 unbounded: attach Kani loop invariants to the iterating std loops in scope and drop the unwind bounds of the harnesses that drive them. take.rs spec_fold/spec_for_each (`kani::index <= end`), array_chunks.rs fold (`i <= inner_len`, frame pinned with `kani::loop_modifies(&accum, &i)` because the `from_fn` closure borrows `self`), zip.rs TrustedRandomAccess fold (`kani::index <= len`) and nth (`self.index <= end`). All attributes are additive and runtime no-ops (precedent: slice/mod.rs, str/pattern.rs, num/dec2flt/decimal_seq.rs). MAX_LEN follows the accessor menu: isize::MAX for the ZST, u32::MAX for u8, 10 for char and (char, u8) whose symbolic arrays are built element-wise. Harnesses whose loop lives outside the challenge scope (Filter/FilterMap next_chunk through the generic Iterator::try_fold default, the TrustedLen Zip::spec_fold inner loop over generic next(), the next_back trim loops over generic next_back()) stay bounded and say why. - F-doc: enumerate.rs comments no longer claim unbounded proofs. - F-N1: check_zip_get_unchecked_direct exercises ZipImpl::get_unchecked at a symbolic index > 0 on a struct-literal Zip. - F-N2: check_zip_side_effect (nth, next_back) and check_skip_get_unchecked_side_effect use Map-wrapped sources with MAY_HAVE_SIDE_EFFECT = true. - F-N3: map_windows harnesses add a DropToken element type. - F-N4: filter_map harnesses take an output type other than usize (char, (), (char, u8), DropToken). - F-N5: N = 0 stays excluded because Kani confirms an out-of-bounds write in Filter::next_chunk_dropless::<0> and FilterMap::next_chunk::<0> (upstream issue to follow). Kani also found that the debug assertion after the trim loops in the TrustedRandomAccess Zip::next_back fires when exactly one side has side effects and it is the shorter one; the two one-sided next_back harnesses exclude that shape and document it (upstream issue to follow). All 52 harnesses in take.rs, array_chunks.rs and zip.rs verify on fresh builds with the CI flags; rustfmt (nightly-2025-10-09, upstream config) is clean on the nine files. Signed-off-by: Onyeka Obi --- .../core/src/iter/adapters/array_chunks.rs | 25 +- library/core/src/iter/adapters/copied.rs | 15 +- library/core/src/iter/adapters/enumerate.rs | 17 +- library/core/src/iter/adapters/filter.rs | 34 ++- library/core/src/iter/adapters/filter_map.rs | 74 +++++- library/core/src/iter/adapters/map_windows.rs | 23 ++ library/core/src/iter/adapters/skip.rs | 23 ++ library/core/src/iter/adapters/take.rs | 37 ++- library/core/src/iter/adapters/zip.rs | 213 +++++++++++++++++- 9 files changed, 403 insertions(+), 58 deletions(-) diff --git a/library/core/src/iter/adapters/array_chunks.rs b/library/core/src/iter/adapters/array_chunks.rs index 54e1b818fef7f..82b32c2b63748 100644 --- a/library/core/src/iter/adapters/array_chunks.rs +++ b/library/core/src/iter/adapters/array_chunks.rs @@ -3,6 +3,8 @@ use crate::iter::adapters::SourceIter; use crate::iter::{ ByRefSized, FusedIterator, InPlaceIterable, TrustedFused, TrustedRandomAccessNoCoerce, }; +#[cfg(kani)] +use crate::kani; use crate::num::NonZero; use crate::ops::{ControlFlow, NeverShortCircuit, Try}; @@ -230,6 +232,14 @@ where let inner_len = self.iter.size(); let mut i = 0; // Use a while loop because (0..len).step_by(N) doesn't optimize well. + // Kani: the loop writes only `accum` and `i`, so `inner_len` keeps + // its entry value `self.iter.size()`; the invariant bounds `i`, which + // also guards the `inner_len - i` subtraction in the loop condition + // against underflow. The frame is stated explicitly because the + // `from_fn` closure borrows `self` mutably, and the inferred frame + // would otherwise havoc the whole adapter, `remainder` included. + #[safety::loop_invariant(i <= inner_len)] + #[cfg_attr(kani, kani::loop_modifies(&accum, &i))] while inner_len - i >= N { let chunk = crate::array::from_fn(|local| { // SAFETY: The method consumes the iterator and the loop condition ensures that @@ -317,10 +327,13 @@ mod verify { // `fold` on a `TrustedRandomAccessNoCoerce` source builds each chunk with // `array::from_fn` calling `__iterator_get_unchecked(i + local)` under an // `inner_len - i >= N` guard; this proves those indexes stay in bounds. + // The chunking loop carries a Kani loop invariant, so no unwind bound is + // necessary and `MAX_LEN` mirrors the accessor harness menu: `u32::MAX` + // for `u8`, `isize::MAX` for the ZST, moderate bounds for the wider + // element types. macro_rules! check_fold { - ($harness:ident, $elem_ty:ty, $n:expr, $max_len:expr, $unwind:literal) => { + ($harness:ident, $elem_ty:ty, $n:expr, $max_len:expr) => { #[kani::proof] - #[kani::unwind($unwind)] fn $harness() { const N: usize = $n; const MAX_LEN: usize = $max_len; @@ -331,8 +344,8 @@ mod verify { } }; } - check_fold!(check_array_chunks_fold_unit, (), 2, 4, 5); - check_fold!(check_array_chunks_fold_u8, u8, 2, 4, 5); - check_fold!(check_array_chunks_fold_char, char, 3, 6, 7); - check_fold!(check_array_chunks_fold_tup, (char, u8), 2, 4, 5); + check_fold!(check_array_chunks_fold_unit, (), 2, isize::MAX as usize); + check_fold!(check_array_chunks_fold_u8, u8, 2, u32::MAX as usize); + check_fold!(check_array_chunks_fold_char, char, 3, 10); + check_fold!(check_array_chunks_fold_tup, (char, u8), 2, 10); } diff --git a/library/core/src/iter/adapters/copied.rs b/library/core/src/iter/adapters/copied.rs index f9a131e756ba4..e059113e6a7c7 100644 --- a/library/core/src/iter/adapters/copied.rs +++ b/library/core/src/iter/adapters/copied.rs @@ -323,12 +323,15 @@ mod verify { // `array_assume_init`s the full array or returns a `0..len` `IntoIter`; this // proves the `copy_nonoverlapping` lengths and the init range stay in bounds. // `N` is a trait generic, so it is pinned via the result type annotation. + // The `MAX_LEN` menu mirrors `check_get_unchecked` above: the `u8`/ZST + // harnesses raise the bound very high (there is no per-iteration loop, only + // a bulk copy, so the cost stays flat), the wider types use a moderate one. macro_rules! check_spec_next_chunk { - ($harness:ident, $elem_ty:ty, $n:expr) => { + ($harness:ident, $elem_ty:ty, $n:expr, $max_len:expr) => { #[kani::proof] fn $harness() { const N: usize = $n; - const MAX_LEN: usize = 16; + const MAX_LEN: usize = $max_len; let array: [$elem_ty; MAX_LEN] = kani::any(); let mut it = any_slice(&array).iter(); let _result: Result<[$elem_ty; N], crate::array::IntoIter<$elem_ty, N>> = @@ -336,8 +339,8 @@ mod verify { } }; } - check_spec_next_chunk!(check_copied_spec_next_chunk_unit, (), 2); - check_spec_next_chunk!(check_copied_spec_next_chunk_u8, u8, 3); - check_spec_next_chunk!(check_copied_spec_next_chunk_char, char, 2); - check_spec_next_chunk!(check_copied_spec_next_chunk_tup, (char, u8), 2); + check_spec_next_chunk!(check_copied_spec_next_chunk_unit, (), 2, isize::MAX as usize); + check_spec_next_chunk!(check_copied_spec_next_chunk_u8, u8, 3, u32::MAX as usize); + check_spec_next_chunk!(check_copied_spec_next_chunk_char, char, 2, 50); + check_spec_next_chunk!(check_copied_spec_next_chunk_tup, (char, u8), 2, 50); } diff --git a/library/core/src/iter/adapters/enumerate.rs b/library/core/src/iter/adapters/enumerate.rs index 553d1946bbdb3..65e82bafef523 100644 --- a/library/core/src/iter/adapters/enumerate.rs +++ b/library/core/src/iter/adapters/enumerate.rs @@ -331,10 +331,12 @@ mod verify { use crate::kani; /// An arbitrary-length sub-slice of `orig_slice` (mirrors - /// `slice::iter::verify::any_slice`). This is what makes the proof - /// *unbounded*: the backing array is a fixed Kani constant, but the slice - /// handed to the iterator has a symbolic length, so the proof covers every - /// shorter configuration at once. + /// `slice::iter::verify::any_slice`). The slice handed to the iterator + /// has a symbolic length in `0..=MAX_LEN`, so one proof covers every + /// length up to the backing array's size at once. The proof is still + /// bounded by `MAX_LEN`: the `u8`/ZST harnesses raise `MAX_LEN` to + /// `u32::MAX`/`isize::MAX`, far beyond practical slice lengths, while the + /// wider element types use smaller bounds. fn any_slice(orig_slice: &[T]) -> &[T] { if kani::any() { let last = kani::any_where(|idx: &usize| *idx <= orig_slice.len()); @@ -355,9 +357,10 @@ mod verify { Enumerate::new(any_slice(orig_slice).iter()) } - /// One `proof_for_contract` harness per concrete element type; the contract - /// itself stays generic. `slice::Iter` is `TrustedRandomAccessNoCoerce` - /// for every `T`, satisfying the method's `Self: TrustedRandomAccessNoCoerce`. + /// One plain `#[kani::proof]` harness per concrete element type (the NOTE + /// below explains why this is not `proof_for_contract`). `slice::Iter` + /// is `TrustedRandomAccessNoCoerce` for every `T`, satisfying the method's + /// `Self: TrustedRandomAccessNoCoerce` bound. // NOTE: `__iterator_get_unchecked` is a trait method on the *generic* impl // `impl Iterator for Enumerate`, and Kani cannot attach a // `proof_for_contract` to a generic trait method (kani#1997). So instead of diff --git a/library/core/src/iter/adapters/filter.rs b/library/core/src/iter/adapters/filter.rs index 230cef03de60b..6cb83abc69a9f 100644 --- a/library/core/src/iter/adapters/filter.rs +++ b/library/core/src/iter/adapters/filter.rs @@ -244,13 +244,31 @@ mod verify { // `MaybeUninit<[_; N]>` and bumps `initialized` only for kept elements, // breaking once `initialized == N`; this proves the `get_unchecked_mut(idx)` // writes and the final `array_assume_init` / `IntoIter` range stay in bounds. + // + // Boundedness: the chunk fill iterates through the generic default + // `Iterator::try_fold` (a while-let loop that calls a generic closure in + // iterator.rs), so this adapter cannot attach a loop contract to it. A + // fixed `MAX_LEN` is still a complete state-space cover, not a truncation: + // every reachable value of `initialized` is in 0..=N for every slice + // length, so any `MAX_LEN >= N + 2` exercises every reachable + // configuration (empty source, saturation before exhaustion, and + // exhaustion before saturation). + // + // N = 0 is excluded on purpose. The current upstream implementation has a + // latent N = 0 defect: the closure writes through + // `array.get_unchecked_mut(idx)` before it compares `initialized < N`, so + // `next_chunk::<0>()` on a source that yields at least one element writes + // out of bounds into the zero-length array. Repo rules + // (doc/src/general-rules.md) do not permit a local change to the runtime + // logic, so the fix must land upstream. An upstream report is prepared. + // These harnesses cover N >= 1. macro_rules! check_next_chunk_dropless { - ($harness:ident, $elem_ty:ty) => { + ($harness:ident, $elem_ty:ty, $n:expr) => { #[kani::proof] #[kani::unwind(7)] fn $harness() { const MAX_LEN: usize = 6; - const N: usize = 4; + const N: usize = $n; let array: [$elem_ty; MAX_LEN] = kani::any(); let mut it = Filter::new( any_slice(&array).iter(), @@ -260,8 +278,12 @@ mod verify { } }; } - check_next_chunk_dropless!(check_filter_next_chunk_dropless_unit, ()); - check_next_chunk_dropless!(check_filter_next_chunk_dropless_u8, u8); - check_next_chunk_dropless!(check_filter_next_chunk_dropless_char, char); - check_next_chunk_dropless!(check_filter_next_chunk_dropless_tup, (char, u8)); + check_next_chunk_dropless!(check_filter_next_chunk_dropless_unit, (), 4); + check_next_chunk_dropless!(check_filter_next_chunk_dropless_u8, u8, 4); + check_next_chunk_dropless!(check_filter_next_chunk_dropless_char, char, 4); + check_next_chunk_dropless!(check_filter_next_chunk_dropless_tup, (char, u8), 4); + check_next_chunk_dropless!(check_filter_next_chunk_dropless_unit_n1, (), 1); + check_next_chunk_dropless!(check_filter_next_chunk_dropless_u8_n1, u8, 1); + check_next_chunk_dropless!(check_filter_next_chunk_dropless_char_n1, char, 1); + check_next_chunk_dropless!(check_filter_next_chunk_dropless_tup_n1, (char, u8), 1); } diff --git a/library/core/src/iter/adapters/filter_map.rs b/library/core/src/iter/adapters/filter_map.rs index f09cae914e6f5..6db600520e6ea 100644 --- a/library/core/src/iter/adapters/filter_map.rs +++ b/library/core/src/iter/adapters/filter_map.rs @@ -230,34 +230,84 @@ mod verify { } } - // Maps `&T -> Option` nondeterministically to exercise both the - // "element kept" and "element filtered" paths of the chunk-fill loop. - fn maybe_map(_: &T) -> Option { - kani::any::().then_some(0) + // Maps `&T -> Option` nondeterministically to exercise both the + // "element kept" and "element filtered" paths of the chunk-fill loop, with + // a nondeterministic payload of the parameterized output type `B`. + fn maybe_map_to(_: &T) -> Option { + kani::any::().then(|| kani::any()) + } + + // A drop-requiring output type: `needs_drop::()` is true, so + // the chunk loop's `Guard` compiles real drop glue instead of a no-op and + // the payload byte-copy plus `mem::forget` handling must keep every + // initialized slot live and in bounds. Kani models a panic as a + // verification failure and has no unwinding, so the panic-during-drop + // path itself is not expressible in a passing harness; the coverage this + // type adds is the non-trivial drop-glue code path. + struct DropToken(u8); + + impl Drop for DropToken { + fn drop(&mut self) { + let _ = crate::hint::black_box(self.0); + } + } + + impl kani::Arbitrary for DropToken { + fn any() -> Self { + DropToken(kani::any()) + } } // `next_chunk` fills a `MaybeUninit<[B; N]>` via a `Guard`-protected loop // that byte-copies each mapped value's payload and bumps `initialized` only // when the map yields `Some`; this proves the copies, the `array_assume_init`, - // and the partial-fill `IntoIter` range all stay in bounds. + // and the partial-fill `IntoIter` range all stay in bounds. The output menu + // covers `usize`, a validity niche (`char`), a ZST (`()`), a padded pair + // (`(char, u8)`), and a drop-requiring type (`DropToken`). + // + // Boundedness: the chunk fill iterates through the generic default + // `Iterator::try_fold` (a while-let loop that calls a generic closure in + // iterator.rs), so this adapter cannot attach a loop contract to it. A + // fixed `MAX_LEN` is still a complete state-space cover, not a truncation: + // every reachable value of `initialized` is in 0..=N for every slice + // length, so any `MAX_LEN >= N + 2` exercises every reachable + // configuration (empty source, saturation before exhaustion, and + // exhaustion before saturation). + // + // N = 0 is excluded on purpose. The current upstream implementation has a + // latent N = 0 defect: the closure does a one-element + // `copy_nonoverlapping` of the mapped payload into `guard.array` at + // `idx` before it compares `guard.initialized < N`, so `next_chunk::<0>()` + // on a source that yields at least one element writes out of bounds into + // the zero-capacity array. Repo rules (doc/src/general-rules.md) do not + // permit a local change to the runtime logic, so the fix must land + // upstream. An upstream report is prepared. These harnesses cover N >= 1. macro_rules! check_next_chunk { - ($harness:ident, $elem_ty:ty) => { + ($harness:ident, $elem_ty:ty, $out_ty:ty, $n:expr) => { #[kani::proof] #[kani::unwind(6)] fn $harness() { const MAX_LEN: usize = 5; - const N: usize = 3; + const N: usize = $n; let array: [$elem_ty; MAX_LEN] = kani::any(); let mut it = FilterMap::new( any_slice(&array).iter(), - maybe_map::<$elem_ty> as fn(&$elem_ty) -> Option, + maybe_map_to::<$elem_ty, $out_ty> as fn(&$elem_ty) -> Option<$out_ty>, ); let _ = it.next_chunk::(); } }; } - check_next_chunk!(check_filter_map_next_chunk_unit, ()); - check_next_chunk!(check_filter_map_next_chunk_u8, u8); - check_next_chunk!(check_filter_map_next_chunk_char, char); - check_next_chunk!(check_filter_map_next_chunk_tup, (char, u8)); + check_next_chunk!(check_filter_map_next_chunk_unit, (), usize, 3); + check_next_chunk!(check_filter_map_next_chunk_u8, u8, usize, 3); + check_next_chunk!(check_filter_map_next_chunk_char, char, usize, 3); + check_next_chunk!(check_filter_map_next_chunk_tup, (char, u8), usize, 3); + check_next_chunk!(check_filter_map_next_chunk_unit_n1, (), usize, 1); + check_next_chunk!(check_filter_map_next_chunk_u8_n1, u8, usize, 1); + check_next_chunk!(check_filter_map_next_chunk_char_n1, char, usize, 1); + check_next_chunk!(check_filter_map_next_chunk_tup_n1, (char, u8), usize, 1); + check_next_chunk!(check_filter_map_next_chunk_out_char, u8, char, 3); + check_next_chunk!(check_filter_map_next_chunk_out_unit, u8, (), 3); + check_next_chunk!(check_filter_map_next_chunk_out_tup, u8, (char, u8), 3); + check_next_chunk!(check_filter_map_next_chunk_out_drop, u8, DropToken, 3); } diff --git a/library/core/src/iter/adapters/map_windows.rs b/library/core/src/iter/adapters/map_windows.rs index 6980784d55396..558d2497eac1e 100644 --- a/library/core/src/iter/adapters/map_windows.rs +++ b/library/core/src/iter/adapters/map_windows.rs @@ -366,4 +366,27 @@ mod verify { check_buffer!(verify_map_windows_u8, u8, 3); check_buffer!(verify_map_windows_char, char, 2); check_buffer!(verify_map_windows_tup, (char, u8), 2); + + // A drop-requiring element type: `needs_drop::()` is true, so + // `push`'s `drop_in_place` and the `Buffer` `Drop` impl execute real drop + // glue instead of compiling to no-ops, and the destructor reads the + // payload, so the dropped element must be an in-bounds, live slot. Kani + // models a panic as a verification failure and has no unwinding, so the + // panic-during-drop path itself is not expressible in a passing harness; + // the coverage this type adds is the non-trivial drop-glue code path. + struct DropToken(u8); + + impl Drop for DropToken { + fn drop(&mut self) { + let _ = crate::hint::black_box(self.0); + } + } + + impl kani::Arbitrary for DropToken { + fn any() -> Self { + DropToken(kani::any()) + } + } + + check_buffer!(verify_map_windows_drop, DropToken, 2); } diff --git a/library/core/src/iter/adapters/skip.rs b/library/core/src/iter/adapters/skip.rs index 499517c7e7678..f9c8c97d5f8ec 100644 --- a/library/core/src/iter/adapters/skip.rs +++ b/library/core/src/iter/adapters/skip.rs @@ -334,4 +334,27 @@ mod verify { check_get_unchecked!(check_skip_get_unchecked_u8, u8, u32::MAX as usize); check_get_unchecked!(check_skip_get_unchecked_char, char, 50); check_get_unchecked!(check_skip_get_unchecked_tup, (char, u8), 50); + + fn bump(x: &u8) -> u8 { + x.wrapping_add(1) + } + + // `Map` has `MAY_HAVE_SIDE_EFFECT = true` (map.rs pins + // the constant to `true` for every `Map`), so `__iterator_get_unchecked` + // compiles in the `idx == 0` prefix-dropping branch that the plain + // `slice::Iter` harnesses compile out. `idx` stays symbolic, so the + // proof covers the branch both taken (`idx == 0`, dropping the `self.n` + // skipped items) and not taken. The prefix loop runs `self.n` times, so + // this harness is bounded by `MAX_LEN`. + #[kani::proof] + #[kani::unwind(7)] + fn check_skip_get_unchecked_side_effect() { + const MAX_LEN: usize = 5; + let array: [u8; MAX_LEN] = kani::any(); + let slice = any_slice(&array); + let n = kani::any_where(|offset: &usize| *offset <= slice.len()); + let mut it = Skip::new(slice.iter().map(bump as fn(&u8) -> u8), n); + let idx = kani::any_where(|i: &usize| *i < it.iter.size_hint().0 - it.n); + let _ = unsafe { it.__iterator_get_unchecked(idx) }; + } } diff --git a/library/core/src/iter/adapters/take.rs b/library/core/src/iter/adapters/take.rs index 7924ccdc8a5d7..ac0f231d6dca1 100644 --- a/library/core/src/iter/adapters/take.rs +++ b/library/core/src/iter/adapters/take.rs @@ -1,6 +1,8 @@ use crate::cmp; use crate::iter::adapters::SourceIter; use crate::iter::{FusedIterator, InPlaceIterable, TrustedFused, TrustedLen, TrustedRandomAccess}; +#[cfg(kani)] +use crate::kani; use crate::num::NonZero; use crate::ops::{ControlFlow, Try}; @@ -299,6 +301,11 @@ impl SpecTake for Take { { let mut acc = init; let end = self.n.min(self.iter.size()); + // Kani: the loop writes only `acc` and its counter, so `end` and + // `self.iter` keep their entry values and `end <= self.iter.size()` + // stays available to the body; the invariant only has to bound the + // counter. + #[safety::loop_invariant(kani::index <= end)] for i in 0..end { // SAFETY: i < end <= self.iter.size() and we discard the iterator at the end let val = unsafe { self.iter.__iterator_get_unchecked(i) }; @@ -310,6 +317,8 @@ impl SpecTake for Take { #[inline] fn spec_for_each(mut self, mut f: F) { let end = self.n.min(self.iter.size()); + // Kani: same frame as `spec_fold`; see the note there. + #[safety::loop_invariant(kani::index <= end)] for i in 0..end { // SAFETY: i < end <= self.iter.size() and we discard the iterator at the end let val = unsafe { self.iter.__iterator_get_unchecked(i) }; @@ -394,15 +403,17 @@ mod verify { } // On a `TrustedRandomAccess` source, `SpecTake::spec_fold` / `spec_for_each` - // drive a `0..end` loop calling `__iterator_get_unchecked(i)` where + // drive an `i < end` loop calling `__iterator_get_unchecked(i)` where // `end = self.n.min(self.iter.size())`; this proves those unchecked indexes - // stay in bounds. `MAX_LEN` is bounded because the methods iterate. + // stay in bounds. The loop carries a Kani loop invariant, so no unwind + // bound is necessary and `MAX_LEN` mirrors the accessor harness menu: + // `u32::MAX` for `u8`, `isize::MAX` for the ZST, moderate bounds for the + // wider element types. macro_rules! check_spec_take { - ($fold_harness:ident, $each_harness:ident, $elem_ty:ty) => { + ($fold_harness:ident, $each_harness:ident, $elem_ty:ty, $max_len:expr) => { #[kani::proof] - #[kani::unwind(6)] fn $fold_harness() { - const MAX_LEN: usize = 5; + const MAX_LEN: usize = $max_len; let array: [$elem_ty; MAX_LEN] = kani::any(); let n = kani::any_where(|n: &usize| *n <= MAX_LEN); let it = Take::new(any_slice(&array).iter(), n); @@ -410,9 +421,8 @@ mod verify { } #[kani::proof] - #[kani::unwind(6)] fn $each_harness() { - const MAX_LEN: usize = 5; + const MAX_LEN: usize = $max_len; let array: [$elem_ty; MAX_LEN] = kani::any(); let n = kani::any_where(|n: &usize| *n <= MAX_LEN); let it = Take::new(any_slice(&array).iter(), n); @@ -420,8 +430,13 @@ mod verify { } }; } - check_spec_take!(check_take_spec_fold_unit, check_take_spec_for_each_unit, ()); - check_spec_take!(check_take_spec_fold_u8, check_take_spec_for_each_u8, u8); - check_spec_take!(check_take_spec_fold_char, check_take_spec_for_each_char, char); - check_spec_take!(check_take_spec_fold_tup, check_take_spec_for_each_tup, (char, u8)); + check_spec_take!( + check_take_spec_fold_unit, + check_take_spec_for_each_unit, + (), + isize::MAX as usize + ); + check_spec_take!(check_take_spec_fold_u8, check_take_spec_for_each_u8, u8, u32::MAX as usize); + check_spec_take!(check_take_spec_fold_char, check_take_spec_for_each_char, char, 10); + check_spec_take!(check_take_spec_fold_tup, check_take_spec_for_each_tup, (char, u8), 10); } diff --git a/library/core/src/iter/adapters/zip.rs b/library/core/src/iter/adapters/zip.rs index 90d2bbbd73d63..b487ffb8a2511 100644 --- a/library/core/src/iter/adapters/zip.rs +++ b/library/core/src/iter/adapters/zip.rs @@ -28,6 +28,12 @@ impl Zip { ZipImpl::new(a, b) } fn super_nth(&mut self, mut n: usize) -> Option<(A::Item, B::Item)> { + // Kani: this loop stays bounded. The blocking construct is the + // `while let` over the generic `Iterator::next`: for a general `Zip` + // no size relation is available to a loop invariant. When the + // specialized `nth` calls this function, its contracted loop has + // already consumed `delta = min(n, len - index)` items, so this loop + // runs at most one iteration for any source length. while let Some(x) = Iterator::next(self) { if n == 0 { return Some(x); @@ -285,6 +291,11 @@ where { let mut accum = init; let len = ZipImpl::size_hint(&self).0; + // Kani: the loop writes only `accum` and its counter, so `len`, + // `self.index`, `self.a` and `self.b` keep their entry values and + // `self.index + len == self.len <= min(a.size(), b.size())` stays + // available to the body; the invariant only has to bound the counter. + #[safety::loop_invariant(kani::index <= len)] for i in 0..len { // SAFETY: since Self: TrustedRandomAccessNoCoerce we can trust the size-hint to // calculate the length and then use that to do unchecked iteration. @@ -334,6 +345,11 @@ where fn nth(&mut self, n: usize) -> Option { let delta = cmp::min(n, self.len - self.index); let end = self.index + delta; + // Kani: the loop writes only `self.index`, so `end`, `self.len`, + // `self.a` and `self.b` keep their entry values and + // `end <= self.len <= min(a.size(), b.size())` stays available to the + // body; the invariant only has to bound `self.index`. + #[safety::loop_invariant(self.index <= end)] while self.index < end { let i = self.index; // since get_unchecked executes code which can panic we increment the counters beforehand @@ -386,6 +402,13 @@ where // This condition can and must only be true on the first `next_back` call, // otherwise we will break the restriction on calls to `self.next_back()` // after calling `get_unchecked()`. + // Kani: the two adjust loops below stay bounded. The blocking + // construct is `next_back` on the generic inner iterators. It + // moves their private pointer state, and no public interface + // lets a loop invariant pin that state to its allocation, so + // a loop contract would havoc it into an unverifiable read. + // Harnesses that reach these loops keep a small MAX_LEN and + // an unwind bound sized to it. if sz_a != sz_b && (old_len == sz_a || old_len == sz_b) { if A::MAY_HAVE_SIDE_EFFECT && sz_a > old_len { for _ in 0..sz_a - old_len { @@ -655,6 +678,10 @@ impl SpecFold for Zip { F: FnMut(Acc, Self::Item) -> Acc, { let mut accum = init; + // Kani: this loop stays bounded. The blocking construct is the + // `while let` over the generic `ZipImpl::next`: for a general `Zip` + // no size relation is available to a loop invariant, and the generic + // `FnMut` closure state gives the havoc no expressible frame. while let Some(x) = ZipImpl::next(&mut self) { accum = f(accum, x); } @@ -669,6 +696,10 @@ impl SpecFold for Zip { F: FnMut(Acc, Self::Item) -> Acc, { let mut accum = init; + // Kani: the outer loop stays bounded. It runs one iteration for each + // `usize::MAX` chunk of a `TrustedLen` source. Every harnessed source + // reports an exact `Some` upper bound, so the loop body runs exactly + // once and a small unwind bound covers it. loop { let (upper, more) = if let Some(upper) = ZipImpl::size_hint(&self).1 { (upper, false) @@ -677,6 +708,12 @@ impl SpecFold for Zip { (usize::MAX, true) }; + // Kani: this loop stays bounded. The blocking construct is + // `next` on the generic inner iterators: it moves their private + // pointer state, and no public interface lets a loop invariant + // pin that state to its allocation, so a loop contract would + // havoc it into an unverifiable read. Harnesses that reach this + // loop keep a small MAX_LEN and an unwind bound sized to it. for _ in 0..upper { let pair = // SAFETY: TrustedLen guarantees that at least `upper` many items are available @@ -738,24 +775,80 @@ mod verify { check_zip_get_unchecked!(check_zip_get_unchecked_u8_char, u8, char, 50); check_zip_get_unchecked!(check_zip_get_unchecked_tup_tup, (char, u8), (u32, i16), 50); + // Direct proof for the separately listed `ZipImpl::get_unchecked` target, + // over an arbitrary valid `Zip` state instead of only a freshly built one. + // The `TrustedRandomAccess` specialization establishes `index == 0` and + // `len == min(a.size(), b.size())` at construction; `next` only increments + // `index` and `next_back` only decrements `len`, so every reachable state + // satisfies `index <= len <= min(a.size(), b.size())`. The caller + // contract for `get_unchecked(idx)` is `idx < size_hint().0`, that is + // `idx < len - index`. The body then computes `self.index + idx`, which + // this proof shows stays in bounds of both sources (and cannot overflow). + macro_rules! check_zip_get_unchecked_direct { + ($harness:ident, $elem_ty_a:ty, $elem_ty_b:ty, $max_len:expr) => { + #[kani::proof] + fn $harness() { + const MAX_LEN: usize = $max_len; + let array_a: [$elem_ty_a; MAX_LEN] = kani::any(); + let array_b: [$elem_ty_b; MAX_LEN] = kani::any(); + let slice_a = any_slice(&array_a); + let slice_b = any_slice(&array_b); + let bound = cmp::min(slice_a.len(), slice_b.len()); + let len = kani::any_where(|l: &usize| *l <= bound); + let index = kani::any_where(|i: &usize| *i <= len); + let mut it = Zip { a: slice_a.iter(), b: slice_b.iter(), index, len }; + let idx = kani::any_where(|i: &usize| *i < len - index); + let _ = unsafe { ZipImpl::get_unchecked(&mut it, idx) }; + } + }; + } + + check_zip_get_unchecked_direct!(check_zip_get_unchecked_direct_unit_unit, (), (), 50); + check_zip_get_unchecked_direct!( + check_zip_get_unchecked_direct_u8_u8, + u8, + u8, + u32::MAX as usize + ); + check_zip_get_unchecked_direct!(check_zip_get_unchecked_direct_char_u8, char, u8, 50); + check_zip_get_unchecked_direct!(check_zip_get_unchecked_direct_u8_char, u8, char, 50); + check_zip_get_unchecked_direct!( + check_zip_get_unchecked_direct_tup_tup, + (char, u8), + (u32, i16), + 50 + ); + // Safe `Zip` methods on `TrustedRandomAccess` sources drive the same // `get_unchecked`-based machinery as `__iterator_get_unchecked`; these prove // `next` / `nth` / `next_back` / `fold` / `spec_fold` keep their internal - // indexes in bounds. Bounded `MAX_LEN` because the methods iterate. + // indexes in bounds. The iteration loops in the specialized `nth`, the + // specialized `fold` and the `TrustedLen` `spec_fold` carry loop + // contracts, so `MAX_LEN` follows the accessor menu above instead of a + // bound the unwind limit must cover. The remaining unwind attributes + // cover only the loops that stay bounded: `super_nth` runs at most one + // iteration after the contracted `nth` loop, and the `spec_fold` outer + // loop runs exactly once for these sources. Both counts do not depend on + // `MAX_LEN`. macro_rules! check_zip_safe { - ($a:ty, $b:ty, $next:ident, $nth:ident, $back:ident, $fold:ident, $spec:ident) => { + ($a:ty, $b:ty, $max_len:expr, $spec_max_len:expr, $nth_unwind:literal, $next:ident, + $nth:ident, $back:ident, $fold:ident, $spec:ident) => { #[kani::proof] fn $next() { - const MAX_LEN: usize = 16; + const MAX_LEN: usize = $max_len; let array_a: [$a; MAX_LEN] = kani::any(); let array_b: [$b; MAX_LEN] = kani::any(); let mut it = any_zip_iter::<$a, $b>(&array_a, &array_b); let _ = crate::iter::Iterator::next(&mut it); } + // `nth` drives the contracted `while self.index < end` loop, so + // `$nth_unwind` only has to cover `super_nth` (at most one + // iteration) and the element-wise `kani::any` array construction + // of `MAX_LEN` items for the wider element types. #[kani::proof] - #[kani::unwind(6)] + #[kani::unwind($nth_unwind)] fn $nth() { - const MAX_LEN: usize = 5; + const MAX_LEN: usize = $max_len; let array_a: [$a; MAX_LEN] = kani::any(); let array_b: [$b; MAX_LEN] = kani::any(); let mut it = any_zip_iter::<$a, $b>(&array_a, &array_b); @@ -764,25 +857,24 @@ mod verify { } #[kani::proof] fn $back() { - const MAX_LEN: usize = 16; + const MAX_LEN: usize = $max_len; let array_a: [$a; MAX_LEN] = kani::any(); let array_b: [$b; MAX_LEN] = kani::any(); let mut it = any_zip_iter::<$a, $b>(&array_a, &array_b); let _ = crate::iter::DoubleEndedIterator::next_back(&mut it); } #[kani::proof] - #[kani::unwind(6)] fn $fold() { - const MAX_LEN: usize = 5; + const MAX_LEN: usize = $max_len; let array_a: [$a; MAX_LEN] = kani::any(); let array_b: [$b; MAX_LEN] = kani::any(); let it = any_zip_iter::<$a, $b>(&array_a, &array_b); let _ = crate::iter::Iterator::fold(it, 0usize, |acc, _| acc.wrapping_add(1)); } #[kani::proof] - #[kani::unwind(6)] + #[kani::unwind(7)] fn $spec() { - const MAX_LEN: usize = 5; + const MAX_LEN: usize = $spec_max_len; let array_a: [$a; MAX_LEN] = kani::any(); let array_b: [$b; MAX_LEN] = kani::any(); let it = any_zip_iter::<$a, $b>(&array_a, &array_b); @@ -793,6 +885,9 @@ mod verify { check_zip_safe!( (), (), + 50, + 5, + 3, check_zip_next_unit, check_zip_nth_unit, check_zip_next_back_unit, @@ -802,6 +897,9 @@ mod verify { check_zip_safe!( u8, u8, + u32::MAX as usize, + 5, + 3, check_zip_next_u8, check_zip_nth_u8, check_zip_next_back_u8, @@ -811,6 +909,9 @@ mod verify { check_zip_safe!( char, u8, + 10, + 5, + 12, check_zip_next_char_u8, check_zip_nth_char_u8, check_zip_next_back_char_u8, @@ -820,10 +921,102 @@ mod verify { check_zip_safe!( (char, u8), (u32, i16), + 10, + 5, + 12, check_zip_next_tup, check_zip_nth_tup, check_zip_next_back_tup, check_zip_fold_tup, check_zip_spec_fold_tup ); + + // `Map` is a `TrustedRandomAccess` source with + // `MAY_HAVE_SIDE_EFFECT = true` (map.rs pins the constant to `true` for + // every `Map`). Zipping one or both sides through `Map` compiles in the + // `A::MAY_HAVE_SIDE_EFFECT` / `B::MAY_HAVE_SIDE_EFFECT` branches of the + // specialized `nth` and `next_back` (including next_back's length-adjust + // loop), which the plain `slice::Iter` harnesses compile out. The two + // `any_slice` lengths are independent, so the `sz_a != sz_b` adjust path + // is reachable. + fn bump(x: &u8) -> u8 { + x.wrapping_add(1) + } + + fn side_effect_iter<'a>( + slice: &'a [u8], + ) -> crate::iter::Map, fn(&u8) -> u8> { + slice.iter().map(bump as fn(&u8) -> u8) + } + + fn plain_iter<'a>(slice: &'a [u8]) -> crate::slice::Iter<'a, u8> { + slice.iter() + } + + macro_rules! check_zip_side_effect { + ($nth:ident, $back:ident, $mk_a:ident, $mk_b:ident, $back_shape:expr) => { + // The contracted `nth` loop covers arbitrary lengths; the unwind + // bound covers only `super_nth`, which runs at most one + // iteration. + #[kani::proof] + #[kani::unwind(3)] + fn $nth() { + const MAX_LEN: usize = u32::MAX as usize; + let array_a: [u8; MAX_LEN] = kani::any(); + let array_b: [u8; MAX_LEN] = kani::any(); + let mut it = Zip::new($mk_a(any_slice(&array_a)), $mk_b(any_slice(&array_b))); + let n = kani::any_where(|x: &usize| *x <= MAX_LEN); + let _ = crate::iter::Iterator::nth(&mut it, n); + } + + // `next_back` reaches the bounded adjust loops in the + // specialized `next_back`, so this harness keeps a small + // `MAX_LEN` with an unwind bound sized to it. + // + // `$back_shape` restricts the source lengths. With exactly one + // side-effect side, a plain longer side is never trimmed (the + // trim loops guard on `MAY_HAVE_SIDE_EFFECT`), so upstream's + // `debug_assert_eq!(self.a.size(), self.b.size())` after the + // trim fires with debug assertions on: a debug-only false + // assertion in std, out of scope for this PR (upstream issue + // pending). The excluded shape is "the side-effect side is + // shorter than a plain side"; the `both` variant excludes + // nothing. + #[kani::proof] + #[kani::unwind(7)] + fn $back() { + const MAX_LEN: usize = 5; + let array_a: [u8; MAX_LEN] = kani::any(); + let array_b: [u8; MAX_LEN] = kani::any(); + let slice_a = any_slice(&array_a); + let slice_b = any_slice(&array_b); + let shape: fn(usize, usize) -> bool = $back_shape; + kani::assume(shape(slice_a.len(), slice_b.len())); + let mut it = Zip::new($mk_a(slice_a), $mk_b(slice_b)); + let _ = crate::iter::DoubleEndedIterator::next_back(&mut it); + } + }; + } + + check_zip_side_effect!( + check_zip_nth_side_effect_a, + check_zip_next_back_side_effect_a, + side_effect_iter, + plain_iter, + |len_a, len_b| len_a >= len_b + ); + check_zip_side_effect!( + check_zip_nth_side_effect_b, + check_zip_next_back_side_effect_b, + plain_iter, + side_effect_iter, + |len_a, len_b| len_b >= len_a + ); + check_zip_side_effect!( + check_zip_nth_side_effect_both, + check_zip_next_back_side_effect_both, + side_effect_iter, + side_effect_iter, + |_, _| true + ); } From 561997a32b4ae9f8500055cc364c1a56a754e1ec Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Sat, 12 Sep 2026 19:24:49 -0700 Subject: [PATCH 05/62] Clarify iterator proof limits and draft a generic VeriFast port Add the missing preconditions on cloned next_unchecked and specialized zip get_unchecked. Let zip nth harnesses use the full usize request domain. Add empty-source zero-capacity harnesses and link the upstream defect that still prevents nonempty-source coverage. Correct bounded-coverage claims. Prepare generic Buffer and StepBy contracts, ownership lemmas, exact source projections, and guarded remote verification/refinement commands. Mark the VeriFast port unvalidated and keep it out of established coverage claims. Add a dedicated hosted validation workflow with a 4 GiB process-group cap, no swap, and time limits so verification can run off the constrained Mac. Signed-off-by: Onyeka Obi --- .github/workflows/verifast-iter-adapters.yml | 74 ++ library/core/src/iter/adapters/cloned.rs | 1 + library/core/src/iter/adapters/filter.rs | 28 +- library/core/src/iter/adapters/filter_map.rs | 36 +- library/core/src/iter/adapters/zip.rs | 8 +- verifast-proofs/README.md | 4 + verifast-proofs/core/iter/adapters/README.md | 141 ++++ .../core/iter/adapters/check_sources.py | 164 +++++ .../core/iter/adapters/original/lib.rs | 13 + .../iter/adapters/original/map_windows.rs | 116 ++++ .../core/iter/adapters/original/step_by.rs | 36 + .../core/iter/adapters/source-map.json | 21 + .../core/iter/adapters/source/map_windows.rs | 392 +++++++++++ .../core/iter/adapters/source/step_by.rs | 634 ++++++++++++++++++ .../core/iter/adapters/test_sources.py | 95 +++ .../core/iter/adapters/verified/lib.rs | 13 + .../iter/adapters/verified/map_windows.rs | 271 ++++++++ .../core/iter/adapters/verified/step_by.rs | 38 ++ verifast-proofs/core/iter/adapters/verify.sh | 58 ++ 19 files changed, 2123 insertions(+), 20 deletions(-) create mode 100644 .github/workflows/verifast-iter-adapters.yml create mode 100644 verifast-proofs/core/iter/adapters/README.md create mode 100644 verifast-proofs/core/iter/adapters/check_sources.py create mode 100644 verifast-proofs/core/iter/adapters/original/lib.rs create mode 100644 verifast-proofs/core/iter/adapters/original/map_windows.rs create mode 100644 verifast-proofs/core/iter/adapters/original/step_by.rs create mode 100644 verifast-proofs/core/iter/adapters/source-map.json create mode 100644 verifast-proofs/core/iter/adapters/source/map_windows.rs create mode 100644 verifast-proofs/core/iter/adapters/source/step_by.rs create mode 100644 verifast-proofs/core/iter/adapters/test_sources.py create mode 100644 verifast-proofs/core/iter/adapters/verified/lib.rs create mode 100644 verifast-proofs/core/iter/adapters/verified/map_windows.rs create mode 100644 verifast-proofs/core/iter/adapters/verified/step_by.rs create mode 100644 verifast-proofs/core/iter/adapters/verify.sh diff --git a/.github/workflows/verifast-iter-adapters.yml b/.github/workflows/verifast-iter-adapters.yml new file mode 100644 index 0000000000000..04f93a07bf39e --- /dev/null +++ b/.github/workflows/verifast-iter-adapters.yml @@ -0,0 +1,74 @@ +name: VeriFast iterator adapters + +on: + workflow_dispatch: + merge_group: + pull_request: + branches: [main] + paths: + - '.github/workflows/verifast-iter-adapters.yml' + - 'verifast-proofs/core/iter/adapters/**' + - 'verifast-proofs/setup-verifast-home' + - 'verifast-proofs/verifast' + - 'verifast-proofs/refinement-checker' + - 'library/core/src/iter/adapters/map_windows.rs' + - 'library/core/src/iter/adapters/step_by.rs' + push: + branches: [main, 16-iter-adapters] + paths: + - '.github/workflows/verifast-iter-adapters.yml' + - 'verifast-proofs/core/iter/adapters/**' + - 'verifast-proofs/setup-verifast-home' + - 'verifast-proofs/verifast' + - 'verifast-proofs/refinement-checker' + - 'library/core/src/iter/adapters/map_windows.rs' + - 'library/core/src/iter/adapters/step_by.rs' + +permissions: + contents: read + +concurrency: + group: verifast-iter-adapters-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +defaults: + run: + shell: bash + +jobs: + verify-iter-adapters: + name: Verify generic iterator adapter contracts + runs-on: ubuntu-24.04 + timeout-minutes: 30 + steps: + - name: Check out proof inputs + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Check sources and contract selection + run: | + bash verifast-proofs/core/iter/adapters/verify.sh --static + python3 -I verifast-proofs/core/iter/adapters/test_sources.py + + - name: Verify contracts and source refinement + run: | + # A service cgroup limits the entire compiler/verifier process tree. + # --wait propagates failure, including timeout or memory exhaustion. + sudo --non-interactive systemd-run \ + --unit="verifast-iter-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" \ + --service-type=exec --wait --pipe --collect \ + --uid="$(id -u)" --gid="$(id -g)" \ + --working-directory="$PWD" \ + --setenv="PATH=$PATH" \ + --setenv="CARGO_HOME=${CARGO_HOME:-$HOME/.cargo}" \ + --setenv="RUSTUP_HOME=${RUSTUP_HOME:-$HOME/.rustup}" \ + --property=MemoryMax=4G \ + --property=MemorySwapMax=0 \ + --property=TasksMax=256 \ + --property=CPUQuota=200% \ + --property=OOMPolicy=kill \ + --property=KillMode=control-group \ + --property=RuntimeMaxSec=1500 \ + --property=TimeoutStopSec=15 \ + /usr/bin/bash verifast-proofs/core/iter/adapters/verify.sh --remote diff --git a/library/core/src/iter/adapters/cloned.rs b/library/core/src/iter/adapters/cloned.rs index 79a900719dfe8..096f7de14278c 100644 --- a/library/core/src/iter/adapters/cloned.rs +++ b/library/core/src/iter/adapters/cloned.rs @@ -152,6 +152,7 @@ where I: UncheckedIterator, T: Clone, { + #[requires(self.it.size_hint().0 > 0)] unsafe fn next_unchecked(&mut self) -> T { // SAFETY: `Cloned` is 1:1 with the inner iterator, so if the caller promised // that there's an element left, the inner iterator has one too. diff --git a/library/core/src/iter/adapters/filter.rs b/library/core/src/iter/adapters/filter.rs index 6cb83abc69a9f..de0a5dfddaa40 100644 --- a/library/core/src/iter/adapters/filter.rs +++ b/library/core/src/iter/adapters/filter.rs @@ -248,20 +248,22 @@ mod verify { // Boundedness: the chunk fill iterates through the generic default // `Iterator::try_fold` (a while-let loop that calls a generic closure in // iterator.rs), so this adapter cannot attach a loop contract to it. A - // fixed `MAX_LEN` is still a complete state-space cover, not a truncation: - // every reachable value of `initialized` is in 0..=N for every slice - // length, so any `MAX_LEN >= N + 2` exercises every reachable - // configuration (empty source, saturation before exhaustion, and - // exhaustion before saturation). + // fixed `MAX_LEN` only proves safety for sources up to that length. A + // predicate can reject arbitrarily many elements before accepting one; + // covering every value of `initialized` does not prove preservation of + // the buffer and iterator invariants across those extra iterations. + // These harnesses do not meet the challenge's unbounded requirement. // - // N = 0 is excluded on purpose. The current upstream implementation has a - // latent N = 0 defect: the closure writes through + // N = 0 with a nonempty source remains an upstream defect in this snapshot: + // the closure writes through // `array.get_unchecked_mut(idx)` before it compares `initialized < N`, so // `next_chunk::<0>()` on a source that yields at least one element writes // out of bounds into the zero-length array. Repo rules // (doc/src/general-rules.md) do not permit a local change to the runtime - // logic, so the fix must land upstream. An upstream report is prepared. - // These harnesses cover N >= 1. + // logic unless it has been incorporated upstream. The defect is tracked + // at https://github.com/rust-lang/rust/issues/153803, with a proposed fix + // at https://github.com/rust-lang/rust/pull/153813. + // The separate empty-source N = 0 harness below does not cover this defect. macro_rules! check_next_chunk_dropless { ($harness:ident, $elem_ty:ty, $n:expr) => { #[kani::proof] @@ -286,4 +288,12 @@ mod verify { check_next_chunk_dropless!(check_filter_next_chunk_dropless_u8_n1, u8, 1); check_next_chunk_dropless!(check_filter_next_chunk_dropless_char_n1, char, 1); check_next_chunk_dropless!(check_filter_next_chunk_dropless_tup_n1, (char, u8), 1); + + // With no source elements, the faulty write is unreachable. Exercise the + // zero-capacity result and its drop without claiming nonempty-source safety. + #[kani::proof] + fn check_filter_next_chunk_dropless_empty_n0() { + let mut it = Filter::new(crate::iter::empty::(), |_: &u8| kani::any::()); + let _ = it.next_chunk_dropless::<0>(); + } } diff --git a/library/core/src/iter/adapters/filter_map.rs b/library/core/src/iter/adapters/filter_map.rs index 6db600520e6ea..d8cca33eaafe1 100644 --- a/library/core/src/iter/adapters/filter_map.rs +++ b/library/core/src/iter/adapters/filter_map.rs @@ -268,20 +268,23 @@ mod verify { // Boundedness: the chunk fill iterates through the generic default // `Iterator::try_fold` (a while-let loop that calls a generic closure in // iterator.rs), so this adapter cannot attach a loop contract to it. A - // fixed `MAX_LEN` is still a complete state-space cover, not a truncation: - // every reachable value of `initialized` is in 0..=N for every slice - // length, so any `MAX_LEN >= N + 2` exercises every reachable - // configuration (empty source, saturation before exhaustion, and - // exhaustion before saturation). + // fixed `MAX_LEN` only proves safety for sources up to that length. A + // mapping can return `None` arbitrarily many times before yielding `Some`; + // covering every value of `initialized` does not prove preservation of + // the buffer and iterator invariants across those extra iterations. + // These harnesses do not meet the challenge's unbounded requirement. // - // N = 0 is excluded on purpose. The current upstream implementation has a - // latent N = 0 defect: the closure does a one-element + // N = 0 with a nonempty source remains an upstream defect in this snapshot: + // the closure does a one-element // `copy_nonoverlapping` of the mapped payload into `guard.array` at // `idx` before it compares `guard.initialized < N`, so `next_chunk::<0>()` // on a source that yields at least one element writes out of bounds into // the zero-capacity array. Repo rules (doc/src/general-rules.md) do not - // permit a local change to the runtime logic, so the fix must land - // upstream. An upstream report is prepared. These harnesses cover N >= 1. + // permit a local change to the runtime logic unless it has been + // incorporated upstream. The defect is tracked at + // https://github.com/rust-lang/rust/issues/153803, with a proposed fix at + // https://github.com/rust-lang/rust/pull/153813. + // The separate empty-source N = 0 harnesses below do not cover this defect. macro_rules! check_next_chunk { ($harness:ident, $elem_ty:ty, $out_ty:ty, $n:expr) => { #[kani::proof] @@ -310,4 +313,19 @@ mod verify { check_next_chunk!(check_filter_map_next_chunk_out_unit, u8, (), 3); check_next_chunk!(check_filter_map_next_chunk_out_tup, u8, (char, u8), 3); check_next_chunk!(check_filter_map_next_chunk_out_drop, u8, DropToken, 3); + + // Empty sources avoid the faulty payload copy. Check both dropless and + // drop-requiring outputs at zero capacity, including dropping the result. + #[kani::proof] + fn check_filter_map_next_chunk_empty_n0() { + let mut it = FilterMap::new(crate::iter::empty::(), |_| kani::any::>()); + let _ = it.next_chunk::<0>(); + } + + #[kani::proof] + fn check_filter_map_next_chunk_empty_drop_n0() { + let mut it = + FilterMap::new(crate::iter::empty::(), |_| kani::any::>()); + let _ = it.next_chunk::<0>(); + } } diff --git a/library/core/src/iter/adapters/zip.rs b/library/core/src/iter/adapters/zip.rs index b487ffb8a2511..f80e5564e4613 100644 --- a/library/core/src/iter/adapters/zip.rs +++ b/library/core/src/iter/adapters/zip.rs @@ -276,6 +276,7 @@ where } #[inline] + #[requires(idx < Iterator::size_hint(self).0)] #[cfg_attr(kani, kani::modifies(self))] unsafe fn get_unchecked(&mut self, idx: usize) -> ::Item { let idx = self.index + idx; @@ -852,7 +853,9 @@ mod verify { let array_a: [$a; MAX_LEN] = kani::any(); let array_b: [$b; MAX_LEN] = kani::any(); let mut it = any_zip_iter::<$a, $b>(&array_a, &array_b); - let n = kani::any_where(|x: &usize| *x <= MAX_LEN); + // Requests beyond the remaining length, including usize::MAX, + // must exhaust the iterator without overflowing or reading past it. + let n: usize = kani::any(); let _ = crate::iter::Iterator::nth(&mut it, n); } #[kani::proof] @@ -965,7 +968,8 @@ mod verify { let array_a: [u8; MAX_LEN] = kani::any(); let array_b: [u8; MAX_LEN] = kani::any(); let mut it = Zip::new($mk_a(any_slice(&array_a)), $mk_b(any_slice(&array_b))); - let n = kani::any_where(|x: &usize| *x <= MAX_LEN); + // The request can exceed either backing slice's length. + let n: usize = kani::any(); let _ = crate::iter::Iterator::nth(&mut it, n); } diff --git a/verifast-proofs/README.md b/verifast-proofs/README.md index 096d3e32e9061..6bf0716e51481 100644 --- a/verifast-proofs/README.md +++ b/verifast-proofs/README.md @@ -7,6 +7,10 @@ Specifically, it currently contains the following proofs: - Partial proof of [LinkedList](alloc/collections/linked_list.rs/) - Partial proof of [RawVec](alloc/raw_vec/mod.rs/) +A [generic iterator adapter port candidate](core/iter/adapters/) is also +available. It has not yet passed VeriFast or refinement validation and is +not included in the existing proof-coverage claims. + See each proof's accompanying README for a tour of the proof and applicable caveats. ## Maintaining the proofs diff --git a/verifast-proofs/core/iter/adapters/README.md b/verifast-proofs/core/iter/adapters/README.md new file mode 100644 index 0000000000000..12ee55bc79773 --- /dev/null +++ b/verifast-proofs/core/iter/adapters/README.md @@ -0,0 +1,141 @@ +# Generic iterator adapter proof port + +This is an **unvalidated VeriFast port candidate** for part of Challenge 16. +Neither VeriFast nor the refinement checker has been run on this revision. +The files under `verified/` are proof inputs, not evidence of a successful +proof. This port does not yet close Felipe's review requests on PR #602. + +The functions retain generic type parameters. `Buffer` also retains +symbolic `N`; there are no representative element types, fixed array sizes, +or unwind bounds. The target contracts are: + +| Target | Contract being checked | +| --- | --- | +| `StepBy::original_step` | Preserve the step field and return its nonzero successor, for any `I`. | +| `Buffer::as_array_ref` | Derive the active window's reference from its shared borrow and valid buffer bounds. | +| `Buffer::as_uninit_array_mut` | Derive a writable window from its exclusive storage borrow, without requiring initialized `T` values. | +| `Buffer::push` | Consume one new `T`, drop the old front, and preserve ownership of the shifted window on normal return. | +| `Buffer::drop` | Drop exactly the initialized window and recover its storage on normal return. | + +The two raw buffer pointer helpers also have contracts. `push` and `drop` +use the standard library specification of generic drop glue. Their unwind +postconditions return the thread token only. Unwind paths remain enabled, +but these contracts do not establish buffer-state recovery after a panicking +destructor. This is a remaining obligation for a full safe-abstraction proof. + +## Ownership and bounds + +`live` separates initialized slots from ownership of their values. Only the +active window carries `.own`. Inactive storage can contain stale copied +bytes, but carries no ownership of `T`. The wraparound copy moves the +survivors' logical ownership to the destination. It does not duplicate it. +`initialized_slots` and `wrap_slots` recursively relate `MaybeUninit` +storage to initialized `T` storage, without requiring `Copy`, `Clone`, or +a particular destructor. + +The preconditions require `0 < N`, `start <= N`, representable `2 * N`, +and the allocation/layout limits of the real buffer. These follow from the +intended `MapWindows` constructor and Rust layout restrictions. They are +explicit caller obligations here. Constructor preservation and the surrounding +`MapWindows` iterator implementation are outside this projection. + +The reference accessors require the corresponding lifetime borrow of the +window. Establishing those borrows from the full safe abstraction is also +outside this port. The mutable accessor deliberately accepts storage that +does not yet own initialized `T` values, as required by the clone path. + +## Source correspondence + +`source/` contains complete, hashed copies of the two std source files. +`source-map.json` records the exact lines projected into `original/`: +the actual struct declarations and selected method implementations. +Private core dependencies and unrelated iterator implementations are omitted. +The projections are not alternative iterator models. + +`check_sources.py` checks snapshot hashes, equality with the current std files, +exact projection contents, matching crate roots, and the presence of a +contract on every selected method. It also rejects common proof suppression +directives. This lexical guard does not parse or validate VeriFast proofs. +Only a successful verifier run can establish the contracts. + +The annotated version changes the two pointer helpers to raw field-address +expressions and names the returned references so ghost assertions can follow +their construction. `refinement-checker` must establish that these changes +preserve the projected Rust behavior. It is mandatory, even if VeriFast +passes. No compiler directives are ignored during refinement. + +To deliberately refresh an original projection after reviewing a source +update, update the snapshot, its hash, and its ranges, then run: + +```sh +python3 -I check_sources.py --generate-original +``` + +Normal checks never regenerate or silently accept changed sources. + +## Validation with GitHub Actions + +The [iterator adapter workflow](../../../../.github/workflows/verifast-iter-adapters.yml) +uses a temporary GitHub-hosted Ubuntu 24.04 runner. No separately maintained +machine is needed. It runs when these proof inputs change in a pull request, +or on pushes to `16-iter-adapters` and `main`, and for merge queue checks. +Actions must be enabled in the repository. Standard public-repository jobs use +[GitHub's free hosted runners](https://docs.github.com/en/actions/reference/runners/github-hosted-runners). + +The workflow checks the source snapshots and contract selection before +running the proof and refinement stages. A systemd service limits their +entire process tree to 4 GiB RAM, no swap, two CPUs, 256 tasks, and 25 minutes. +The job has a 30-minute timeout and cancels superseded runs. Tool errors, +proof failures, and resource-limit failures fail the job. Verification and +refinement stay sequential and retain the per-process limits below. + +This workflow has not yet run on the port. Adding a CI job does not establish +that the contracts or source refinement pass. Its result must be inspected +before this candidate can count as verified coverage. + +## Local static checks and optional manual verification + +The static check reads small files and starts no compiler or solver: + +```sh +bash verifast-proofs/core/iter/adapters/verify.sh --static +``` + +On a separate Linux machine with at least 8 GiB currently available RAM: + +```sh +bash verifast-proofs/core/iter/adapters/verify.sh --remote +``` + +The runner uses the repository's pinned VeriFast 25.11 wrappers and Rust +nightly 2025-10-09. The wrappers can download their toolchains. Each process +has a 2 GiB address-space limit, each verification stage has a ten-minute +wall limit, and the stages run sequentially. Address-space limits are per +process, not a combined memory cap. Use an otherwise idle remote worker with +sufficient headroom. The runner refuses proof execution on macOS. + +The command must pass all three gates: proof verification, refinement, and +source identity. `-skip_specless_fns` skips derived trait implementations +without contracts, while the source check requires contracts on every target. +The runner does not suppress unwind paths, overflow checks, or reference +creation checks, and does not allow assumed proof obligations. + +The existing VeriFast workflow is unchanged. The separate adapter job checks +this candidate without altering the existing LinkedList and RawVec checks. + +## Remaining work + +- Obtain actual verifier and refinement verdicts. In particular, check the + pinned frontend's handling of symbolic const sizes, `NonZero::new_unchecked` + and `unchecked_add`, reference creation, and generic slice drop glue. + The explicit slice assertions in `drop` must be proved by that memory + model; no local axiom has been added to make them pass. +- Complete the safe-abstraction and unwind-state obligations described above. +- Port the remaining Challenge 16 targets, including arbitrary-length filter, + filter-map, and zip iteration. This package has no proof of those loops. +- Reconcile the real nonempty-source `next_chunk::<0>()` defect in filter and + filter-map. The `MapWindows` constructor's exclusion of zero-sized windows + does not justify excluding that separate, valid filter input domain. + +Existing Kani results remain bounded where documented. Existing PR CI at +`4459557` predates this port and does not validate it. diff --git a/verifast-proofs/core/iter/adapters/check_sources.py b/verifast-proofs/core/iter/adapters/check_sources.py new file mode 100644 index 0000000000000..809ac82b6a1fa --- /dev/null +++ b/verifast-proofs/core/iter/adapters/check_sources.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python3 +"""Check proof inputs without invoking Rust, VeriFast, or a solver.""" + +import argparse +import hashlib +import json +from pathlib import Path +import re +import sys + + +PACKAGE = Path(__file__).resolve().parent +LIMIT = 1024 * 1024 +PROJECTION_HEADER = "// Source projection checked by check_sources.py. Do not edit manually.\n\n" + + +def read_file(path): + with path.open("rb") as source: + data = source.read(LIMIT + 1) + if len(data) > LIMIT: + raise ValueError(f"input exceeds {LIMIT} bytes: {path}") + return data + + +def inside(root, relative): + path = (root / relative).resolve() + if not path.is_relative_to(root.resolve()): + raise ValueError(f"path escapes its input directory: {relative}") + return path + + +def project(data, ranges): + lines = data.decode("utf-8").splitlines(keepends=True) + parts = [] + previous = 0 + for start, end in ranges: + if not 1 <= start <= end <= len(lines) or start <= previous: + raise ValueError(f"invalid or overlapping source range: {start}:{end}") + parts.append("".join(lines[start - 1:end])) + previous = end + return (PROJECTION_HEADER + "\n".join(parts)).encode("utf-8") + + +def mask_comments(text, annotations_only=False): + """Select Rust text or VeriFast comments, preserving source offsets.""" + masked = ["\n" if char == "\n" else " " for char in text] if annotations_only else list(text) + position = 0 + while position < len(text): + if text.startswith("//", position): + end = text.find("\n", position) + end = len(text) if end == -1 else end + elif text.startswith("/*", position): + end = position + 2 + depth = 1 + while depth and end < len(text): + if text.startswith("/*", end): + depth += 1 + end += 2 + elif text.startswith("*/", end): + depth -= 1 + end += 2 + else: + end += 1 + if depth: + raise ValueError("unterminated Rust block comment") + elif text[position] == '"': + position += 1 + while position < len(text) and text[position] != '"': + position += 2 if text[position] == "\\" else 1 + position += 1 + continue + else: + position += 1 + continue + annotation = text.startswith(("//@", "/*@"), position) + for offset in range(position, end): + if annotations_only and annotation: + masked[offset] = text[offset] + elif not annotations_only and text[offset] != "\n": + masked[offset] = " " + position = end + return "".join(masked) + + +def check_contracts(path, names): + text = read_file(path).decode("utf-8") + forbidden = ( + r"\bassume\s*\(", + r"\breq\s+false\s*;", + r"\b(?:allow_assume|ignore_unwind_paths|ignore_ref_creation|disable_overflow_check)\b", + r"#\s*\[\s*cfg(?:_attr)?\s*\(", + ) + if any(re.search(pattern, text) for pattern in forbidden): + raise ValueError(f"verification bypass in {path}") + rust = mask_comments(text) + annotations = mask_comments(text, annotations_only=True) + for name in names: + matches = list(re.finditer(r"\bfn\s+" + re.escape(name) + r"\b", rust)) + if len(matches) != 1: + raise ValueError(f"expected exactly one body for {name} in {path}") + begin = matches[0].end() + nesting = 0 + end = begin + while end < len(rust): + character = rust[end] + if character in "([": + nesting += 1 + elif character in ")]": + nesting -= 1 + elif nesting == 0 and character in "{;": + break + end += 1 + if end == len(rust) or rust[end] != "{": + raise ValueError(f"missing implementation for {name} in {path}") + clauses = annotations[begin:end] + for clause in ("req", "ens", "on_unwind_ens"): + if not re.search(r"\b" + clause + r"\s", clauses): + raise ValueError(f"missing {clause} for {name} in {path}") + + +def check(package=PACKAGE, repo=None, generate=False): + repo = package.parents[3] if repo is None else repo + manifest = json.loads(read_file(package / "source-map.json")) + if manifest.get("version") != 1 or len(manifest.get("sources", [])) != 2: + raise ValueError("unexpected source manifest") + for entry in manifest["sources"]: + snapshot = read_file(inside(package, entry["snapshot"])) + if hashlib.sha256(snapshot).hexdigest() != entry["sha256"]: + raise ValueError(f"snapshot hash changed: {entry['snapshot']}") + if snapshot != read_file(inside(repo, entry["upstream"])): + raise ValueError(f"std source differs from proof snapshot: {entry['upstream']}") + expected = project(snapshot, entry["ranges"]) + original = inside(package, entry["projection"]) + if generate: + original.parent.mkdir(parents=True, exist_ok=True) + original.write_bytes(expected) + elif read_file(original) != expected: + raise ValueError(f"original projection differs from selected std source: {original}") + if not generate: + check_contracts(package / "verified" / original.name, entry["contracts"]) + if not generate: + original_root = read_file(package / "original/lib.rs") + if original_root != read_file(package / "verified/lib.rs"): + raise ValueError("original and verified crate roots must match") + return len(manifest["sources"]) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--generate-original", action="store_true", + help="regenerate projections from unchanged, hash-checked snapshots") + args = parser.parse_args() + try: + count = check(generate=args.generate_original) + except (OSError, UnicodeError, ValueError, KeyError, TypeError) as error: + print(f"source check failed: {error}", file=sys.stderr) + return 1 + action = "generated" if args.generate_original else "checked" + print(f"{action} {count} source projections; no compiler or verifier was invoked") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/verifast-proofs/core/iter/adapters/original/lib.rs b/verifast-proofs/core/iter/adapters/original/lib.rs new file mode 100644 index 0000000000000..e1a6759dc6a28 --- /dev/null +++ b/verifast-proofs/core/iter/adapters/original/lib.rs @@ -0,0 +1,13 @@ +#![no_std] +#![crate_type = "lib"] +#![allow(dead_code, unused_imports, internal_features)] +#![feature(core_intrinsics, staged_api)] +#![stable(feature = "rust1", since = "1.0.0")] + +extern crate core as std; + +#[stable(feature = "rust1", since = "1.0.0")] +pub use std::{fmt, intrinsics, mem, num, ptr}; + +mod map_windows; +mod step_by; diff --git a/verifast-proofs/core/iter/adapters/original/map_windows.rs b/verifast-proofs/core/iter/adapters/original/map_windows.rs new file mode 100644 index 0000000000000..57d96be3cb0cc --- /dev/null +++ b/verifast-proofs/core/iter/adapters/original/map_windows.rs @@ -0,0 +1,116 @@ +// Source projection checked by check_sources.py. Do not edit manually. + +use crate::mem::MaybeUninit; + +use crate::{fmt, ptr}; + +struct Buffer { + // Invariant: `self.buffer[self.start..self.start + N]` is initialized, + // with all other elements being uninitialized. This also + // implies that `self.start <= N`. + buffer: [[MaybeUninit; N]; 2], + start: usize, +} + +impl Buffer { + + #[inline] + fn buffer_ptr(&self) -> *const MaybeUninit { + self.buffer.as_ptr().cast() + } + + #[inline] + fn buffer_mut_ptr(&mut self) -> *mut MaybeUninit { + self.buffer.as_mut_ptr().cast() + } + + #[inline] + fn as_array_ref(&self) -> &[T; N] { + debug_assert!(self.start + N <= 2 * N); + + // SAFETY: our invariant guarantees these elements are initialized. + unsafe { &*self.buffer_ptr().add(self.start).cast() } + } + + #[inline] + fn as_uninit_array_mut(&mut self) -> &mut MaybeUninit<[T; N]> { + debug_assert!(self.start + N <= 2 * N); + + // SAFETY: our invariant guarantees these elements are in bounds. + unsafe { &mut *self.buffer_mut_ptr().add(self.start).cast() } + } + + /// Pushes a new item `next` to the back, and pops the front-most one. + /// + /// All the elements will be shifted to the front end when pushing reaches + /// the back end. + fn push(&mut self, next: T) { + let buffer_mut_ptr = self.buffer_mut_ptr(); + debug_assert!(self.start + N <= 2 * N); + + let to_drop = if self.start == N { + // We have reached the end of our buffer and have to copy + // everything to the start. Example layout for N = 3. + // + // 0 1 2 3 4 5 0 1 2 3 4 5 + // ┌───┬───┬───┬───┬───┬───┐ ┌───┬───┬───┬───┬───┬───┐ + // │ - │ - │ - │ a │ b │ c │ -> │ b │ c │ n │ - │ - │ - │ + // └───┴───┴───┴───┴───┴───┘ └───┴───┴───┴───┴───┴───┘ + // ↑ ↑ + // start start + + // SAFETY: the two pointers are valid for reads/writes of N -1 + // elements because our array's size is semantically 2 * N. The + // regions also don't overlap for the same reason. + // + // We leave the old elements in place. As soon as `start` is set + // to 0, we treat them as uninitialized and treat their copies + // as initialized. + let to_drop = unsafe { + ptr::copy_nonoverlapping(buffer_mut_ptr.add(self.start + 1), buffer_mut_ptr, N - 1); + (*buffer_mut_ptr.add(N - 1)).write(next); + buffer_mut_ptr.add(self.start) + }; + self.start = 0; + to_drop + } else { + // SAFETY: `self.start` is < N as guaranteed by the invariant + // plus the check above. Even if the drop at the end panics, + // the invariant is upheld. + // + // Example layout for N = 3: + // + // 0 1 2 3 4 5 0 1 2 3 4 5 + // ┌───┬───┬───┬───┬───┬───┐ ┌───┬───┬───┬───┬───┬───┐ + // │ - │ a │ b │ c │ - │ - │ -> │ - │ - │ b │ c │ n │ - │ + // └───┴───┴───┴───┴───┴───┘ └───┴───┴───┴───┴───┴───┘ + // ↑ ↑ + // start start + // + let to_drop = unsafe { + (*buffer_mut_ptr.add(self.start + N)).write(next); + buffer_mut_ptr.add(self.start) + }; + self.start += 1; + to_drop + }; + + // SAFETY: the index is valid and this is element `a` in the + // diagram above and has not been dropped yet. + unsafe { ptr::drop_in_place(to_drop.cast_init()) }; + } +} + +impl Drop for Buffer { + fn drop(&mut self) { + // SAFETY: our invariant guarantees that N elements starting from + // `self.start` are initialized. We drop them here. + unsafe { + let initialized_part: *mut [T] = crate::ptr::slice_from_raw_parts_mut( + self.buffer_mut_ptr().add(self.start).cast(), + N, + ); + ptr::drop_in_place(initialized_part); + } + } +} diff --git a/verifast-proofs/core/iter/adapters/original/step_by.rs b/verifast-proofs/core/iter/adapters/original/step_by.rs new file mode 100644 index 0000000000000..17e69e16e4040 --- /dev/null +++ b/verifast-proofs/core/iter/adapters/original/step_by.rs @@ -0,0 +1,36 @@ +// Source projection checked by check_sources.py. Do not edit manually. + +use crate::intrinsics; + +use crate::num::NonZero; + +#[must_use = "iterators are lazy and do nothing unless consumed"] +#[stable(feature = "iterator_step_by", since = "1.28.0")] +#[derive(Clone, Debug)] +pub struct StepBy { + /// This field is guaranteed to be preprocessed by the specialized `SpecRangeSetup::setup` + /// in the constructor. + /// For most iterators that processing is a no-op, but for Range<{integer}> types it is lossy + /// which means the inner iterator cannot be returned to user code. + /// Additionally this type-dependent preprocessing means specialized implementations + /// cannot be used interchangeably. + iter: I, + /// This field is `step - 1`, aka the correct amount to pass to `nth` when iterating. + /// It MUST NOT be `usize::MAX`, as `unsafe` code depends on being able to add one + /// without the risk of overflow. (This is important so that length calculations + /// don't need to check for division-by-zero, for example.) + step_minus_one: usize, + first_take: bool, +} + +impl StepBy { + + /// The `step` that was originally passed to `Iterator::step_by(step)`, + /// aka `self.step_minus_one + 1`. + #[inline] + fn original_step(&self) -> NonZero { + // SAFETY: By type invariant, `step_minus_one` cannot be `MAX`, which + // means the addition cannot overflow and the result cannot be zero. + unsafe { NonZero::new_unchecked(intrinsics::unchecked_add(self.step_minus_one, 1)) } + } +} diff --git a/verifast-proofs/core/iter/adapters/source-map.json b/verifast-proofs/core/iter/adapters/source-map.json new file mode 100644 index 0000000000000..7a4d641ff3dbe --- /dev/null +++ b/verifast-proofs/core/iter/adapters/source-map.json @@ -0,0 +1,21 @@ +{ + "version": 1, + "sources": [ + { + "upstream": "library/core/src/iter/adapters/map_windows.rs", + "snapshot": "source/map_windows.rs", + "sha256": "2a3af923ad354d4c61ebcb9dc0fcbcac75ce3703420686d63a39d75289cbcf2d", + "projection": "original/map_windows.rs", + "ranges": [[2, 2], [4, 4], [41, 47], [108, 108], [116, 201], [224, 236]], + "contracts": ["buffer_ptr", "buffer_mut_ptr", "as_array_ref", "as_uninit_array_mut", "push", "drop"] + }, + { + "upstream": "library/core/src/iter/adapters/step_by.rs", + "snapshot": "source/step_by.rs", + "sha256": "5de19bd52fe39caf378b1d64e72d14220c67dab3f79ff485ade9b559be337e49", + "projection": "original/step_by.rs", + "ranges": [[1, 1], [5, 5], [16, 33], [42, 42], [50, 58]], + "contracts": ["original_step"] + } + ] +} diff --git a/verifast-proofs/core/iter/adapters/source/map_windows.rs b/verifast-proofs/core/iter/adapters/source/map_windows.rs new file mode 100644 index 0000000000000..558d2497eac1e --- /dev/null +++ b/verifast-proofs/core/iter/adapters/source/map_windows.rs @@ -0,0 +1,392 @@ +use crate::iter::FusedIterator; +use crate::mem::MaybeUninit; +use crate::ub_checks::Invariant; +use crate::{fmt, ptr}; + +/// An iterator over the mapped windows of another iterator. +/// +/// This `struct` is created by the [`Iterator::map_windows`]. See its +/// documentation for more information. +#[must_use = "iterators are lazy and do nothing unless consumed"] +#[unstable(feature = "iter_map_windows", reason = "recently added", issue = "87155")] +pub struct MapWindows { + f: F, + inner: MapWindowsInner, +} + +struct MapWindowsInner { + // We fuse the inner iterator because there shouldn't be "holes" in + // the sliding window. Once the iterator returns a `None`, we make + // our `MapWindows` iterator return `None` forever. + iter: Option, + // Since iterators are assumed lazy, i.e. it only yields an item when + // `Iterator::next()` is called, and `MapWindows` is not an exception. + // + // Before the first iteration, we keep the buffer `None`. When the user + // first call `next` or other methods that makes the iterator advance, + // we collect the first `N` items yielded from the inner iterator and + // put it into the buffer. + // + // When the inner iterator has returned a `None` (i.e. fused), we take + // away this `buffer` and leave it `None` to reclaim its resources. + // + // FIXME: should we shrink the size of `buffer` using niche optimization? + buffer: Option>, +} + +// `Buffer` uses two times of space to reduce moves among the iterations. +// `Buffer` is semantically `[MaybeUninit; 2 * N]`. However, due +// to limitations of const generics, we use this different type. Note that +// it has the same underlying memory layout. +struct Buffer { + // Invariant: `self.buffer[self.start..self.start + N]` is initialized, + // with all other elements being uninitialized. This also + // implies that `self.start <= N`. + buffer: [[MaybeUninit; N]; 2], + start: usize, +} + +impl MapWindows { + pub(in crate::iter) fn new(iter: I, f: F) -> Self { + assert!(N != 0, "array in `Iterator::map_windows` must contain more than 0 elements"); + + // Only ZST arrays' length can be so large. + if size_of::() == 0 { + assert!( + N.checked_mul(2).is_some(), + "array size of `Iterator::map_windows` is too large" + ); + } + + Self { inner: MapWindowsInner::new(iter), f } + } +} + +impl MapWindowsInner { + #[inline] + fn new(iter: I) -> Self { + Self { iter: Some(iter), buffer: None } + } + + fn next_window(&mut self) -> Option<&[I::Item; N]> { + let iter = self.iter.as_mut()?; + match self.buffer { + // It is the first time to advance. We collect + // the first `N` items from `self.iter` to initialize `self.buffer`. + None => self.buffer = Buffer::try_from_iter(iter), + Some(ref mut buffer) => match iter.next() { + None => { + // Fuse the inner iterator since it yields a `None`. + self.iter.take(); + self.buffer.take(); + } + // Advance the iterator. We first call `next` before changing our buffer + // at all. This means that if `next` panics, our invariant is upheld and + // our `Drop` impl drops the correct elements. + Some(item) => buffer.push(item), + }, + } + self.buffer.as_ref().map(Buffer::as_array_ref) + } + + fn size_hint(&self) -> (usize, Option) { + let Some(ref iter) = self.iter else { return (0, Some(0)) }; + let (lo, hi) = iter.size_hint(); + if self.buffer.is_some() { + // If the first `N` items are already yielded by the inner iterator, + // the size hint is then equal to the that of the inner iterator's. + (lo, hi) + } else { + // If the first `N` items are not yet yielded by the inner iterator, + // the first `N` elements should be counted as one window, so both bounds + // should subtract `N - 1`. + (lo.saturating_sub(N - 1), hi.map(|hi| hi.saturating_sub(N - 1))) + } + } +} + +impl Buffer { + fn try_from_iter(iter: &mut impl Iterator) -> Option { + let first_half = crate::array::iter_next_chunk(iter).ok()?; + let buffer = + [MaybeUninit::new(first_half).transpose(), [const { MaybeUninit::uninit() }; N]]; + Some(Self { buffer, start: 0 }) + } + + #[inline] + fn buffer_ptr(&self) -> *const MaybeUninit { + self.buffer.as_ptr().cast() + } + + #[inline] + fn buffer_mut_ptr(&mut self) -> *mut MaybeUninit { + self.buffer.as_mut_ptr().cast() + } + + #[inline] + fn as_array_ref(&self) -> &[T; N] { + debug_assert!(self.start + N <= 2 * N); + + // SAFETY: our invariant guarantees these elements are initialized. + unsafe { &*self.buffer_ptr().add(self.start).cast() } + } + + #[inline] + fn as_uninit_array_mut(&mut self) -> &mut MaybeUninit<[T; N]> { + debug_assert!(self.start + N <= 2 * N); + + // SAFETY: our invariant guarantees these elements are in bounds. + unsafe { &mut *self.buffer_mut_ptr().add(self.start).cast() } + } + + /// Pushes a new item `next` to the back, and pops the front-most one. + /// + /// All the elements will be shifted to the front end when pushing reaches + /// the back end. + fn push(&mut self, next: T) { + let buffer_mut_ptr = self.buffer_mut_ptr(); + debug_assert!(self.start + N <= 2 * N); + + let to_drop = if self.start == N { + // We have reached the end of our buffer and have to copy + // everything to the start. Example layout for N = 3. + // + // 0 1 2 3 4 5 0 1 2 3 4 5 + // ┌───┬───┬───┬───┬───┬───┐ ┌───┬───┬───┬───┬───┬───┐ + // │ - │ - │ - │ a │ b │ c │ -> │ b │ c │ n │ - │ - │ - │ + // └───┴───┴───┴───┴───┴───┘ └───┴───┴───┴───┴───┴───┘ + // ↑ ↑ + // start start + + // SAFETY: the two pointers are valid for reads/writes of N -1 + // elements because our array's size is semantically 2 * N. The + // regions also don't overlap for the same reason. + // + // We leave the old elements in place. As soon as `start` is set + // to 0, we treat them as uninitialized and treat their copies + // as initialized. + let to_drop = unsafe { + ptr::copy_nonoverlapping(buffer_mut_ptr.add(self.start + 1), buffer_mut_ptr, N - 1); + (*buffer_mut_ptr.add(N - 1)).write(next); + buffer_mut_ptr.add(self.start) + }; + self.start = 0; + to_drop + } else { + // SAFETY: `self.start` is < N as guaranteed by the invariant + // plus the check above. Even if the drop at the end panics, + // the invariant is upheld. + // + // Example layout for N = 3: + // + // 0 1 2 3 4 5 0 1 2 3 4 5 + // ┌───┬───┬───┬───┬───┬───┐ ┌───┬───┬───┬───┬───┬───┐ + // │ - │ a │ b │ c │ - │ - │ -> │ - │ - │ b │ c │ n │ - │ + // └───┴───┴───┴───┴───┴───┘ └───┴───┴───┴───┴───┴───┘ + // ↑ ↑ + // start start + // + let to_drop = unsafe { + (*buffer_mut_ptr.add(self.start + N)).write(next); + buffer_mut_ptr.add(self.start) + }; + self.start += 1; + to_drop + }; + + // SAFETY: the index is valid and this is element `a` in the + // diagram above and has not been dropped yet. + unsafe { ptr::drop_in_place(to_drop.cast_init()) }; + } +} + +impl Clone for Buffer { + fn clone(&self) -> Self { + let mut buffer = Buffer { + buffer: [[const { MaybeUninit::uninit() }; N], [const { MaybeUninit::uninit() }; N]], + start: self.start, + }; + buffer.as_uninit_array_mut().write(self.as_array_ref().clone()); + buffer + } +} + +impl Clone for MapWindowsInner +where + I: Iterator + Clone, + I::Item: Clone, +{ + fn clone(&self) -> Self { + Self { iter: self.iter.clone(), buffer: self.buffer.clone() } + } +} + +impl Drop for Buffer { + fn drop(&mut self) { + // SAFETY: our invariant guarantees that N elements starting from + // `self.start` are initialized. We drop them here. + unsafe { + let initialized_part: *mut [T] = crate::ptr::slice_from_raw_parts_mut( + self.buffer_mut_ptr().add(self.start).cast(), + N, + ); + ptr::drop_in_place(initialized_part); + } + } +} + +#[unstable(feature = "iter_map_windows", reason = "recently added", issue = "87155")] +impl Iterator for MapWindows +where + I: Iterator, + F: FnMut(&[I::Item; N]) -> R, +{ + type Item = R; + + fn next(&mut self) -> Option { + let window = self.inner.next_window()?; + let out = (self.f)(window); + Some(out) + } + + fn size_hint(&self) -> (usize, Option) { + self.inner.size_hint() + } +} + +// Note that even if the inner iterator not fused, the `MapWindows` is still fused, +// because we don't allow "holes" in the mapping window. +#[unstable(feature = "iter_map_windows", reason = "recently added", issue = "87155")] +impl FusedIterator for MapWindows +where + I: Iterator, + F: FnMut(&[I::Item; N]) -> R, +{ +} + +#[unstable(feature = "iter_map_windows", reason = "recently added", issue = "87155")] +impl ExactSizeIterator for MapWindows +where + I: ExactSizeIterator, + F: FnMut(&[I::Item; N]) -> R, +{ +} + +#[unstable(feature = "iter_map_windows", reason = "recently added", issue = "87155")] +impl fmt::Debug for MapWindows { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("MapWindows").field("iter", &self.inner.iter).finish() + } +} + +#[unstable(feature = "iter_map_windows", reason = "recently added", issue = "87155")] +impl Clone for MapWindows +where + I: Iterator + Clone, + F: Clone, + I::Item: Clone, +{ + fn clone(&self) -> Self { + Self { f: self.f.clone(), inner: self.inner.clone() } + } +} + +#[unstable(feature = "ub_checks", issue = "none")] +impl Invariant for Buffer { + fn is_safe(&self) -> bool { + self.start + N <= 2 * N + } +} + +#[cfg(kani)] +#[unstable(feature = "kani", issue = "none")] +mod verify { + use super::*; + use crate::kani; + + // Build a `Buffer` in a valid state: `start` in `[0, N]` (the type + // invariant) and the live window `[start, start + N)` fully initialized with + // arbitrary values; the remaining `N` slots stay uninitialized, exactly as + // the real buffer maintains them. + fn any_buffer() -> Buffer { + let start = kani::any_where(|s: &usize| *s <= N); + let mut buffer = Buffer { + buffer: [[const { MaybeUninit::uninit() }; N], [const { MaybeUninit::uninit() }; N]], + start, + }; + let items: [T; N] = kani::any(); + let base = buffer.buffer_mut_ptr(); + items.into_iter().enumerate().for_each(|(i, item)| { + // SAFETY: `start + i < start + N <= 2 * N`, in bounds of the `2 * N` buffer. + unsafe { (*base.add(start + i)).write(item) }; + }); + buffer + } + + // `as_array_ref` / `as_uninit_array_mut` reinterpret the live window as an + // array reference; `push` rotates the window (and wraps + compacts when + // `start == N`); `drop` drops the live window. Each must respect the + // initialized-window invariant and stay in bounds of the `2 * N` storage. + macro_rules! check_buffer { + ($module:ident, $elem_ty:ty, $n:expr) => { + mod $module { + use super::*; + const N: usize = $n; + + #[kani::proof] + fn check_as_array_ref() { + let buf = any_buffer::<$elem_ty, N>(); + let _ = buf.as_array_ref(); + kani::assert(buf.is_safe(), "buffer invariant holds"); + } + + #[kani::proof] + fn check_as_uninit_array_mut() { + let mut buf = any_buffer::<$elem_ty, N>(); + let _ = buf.as_uninit_array_mut(); + kani::assert(buf.is_safe(), "buffer invariant holds"); + } + + #[kani::proof] + fn check_push() { + let mut buf = any_buffer::<$elem_ty, N>(); + buf.push(kani::any()); + kani::assert(buf.is_safe(), "buffer invariant holds after push"); + } + + #[kani::proof] + fn check_drop() { + let buf = any_buffer::<$elem_ty, N>(); + drop(buf); + } + } + }; + } + check_buffer!(verify_map_windows_unit, (), 3); + check_buffer!(verify_map_windows_u8, u8, 3); + check_buffer!(verify_map_windows_char, char, 2); + check_buffer!(verify_map_windows_tup, (char, u8), 2); + + // A drop-requiring element type: `needs_drop::()` is true, so + // `push`'s `drop_in_place` and the `Buffer` `Drop` impl execute real drop + // glue instead of compiling to no-ops, and the destructor reads the + // payload, so the dropped element must be an in-bounds, live slot. Kani + // models a panic as a verification failure and has no unwinding, so the + // panic-during-drop path itself is not expressible in a passing harness; + // the coverage this type adds is the non-trivial drop-glue code path. + struct DropToken(u8); + + impl Drop for DropToken { + fn drop(&mut self) { + let _ = crate::hint::black_box(self.0); + } + } + + impl kani::Arbitrary for DropToken { + fn any() -> Self { + DropToken(kani::any()) + } + } + + check_buffer!(verify_map_windows_drop, DropToken, 2); +} diff --git a/verifast-proofs/core/iter/adapters/source/step_by.rs b/verifast-proofs/core/iter/adapters/source/step_by.rs new file mode 100644 index 0000000000000..1ac22d698a43e --- /dev/null +++ b/verifast-proofs/core/iter/adapters/source/step_by.rs @@ -0,0 +1,634 @@ +use crate::intrinsics; +use crate::iter::{TrustedLen, TrustedRandomAccess, from_fn}; +#[cfg(kani)] +use crate::kani; +use crate::num::NonZero; +use crate::ops::{Range, Try}; +use crate::ub_checks::Invariant; + +/// An iterator for stepping iterators by a custom amount. +/// +/// This `struct` is created by the [`step_by`] method on [`Iterator`]. See +/// its documentation for more. +/// +/// [`step_by`]: Iterator::step_by +/// [`Iterator`]: trait.Iterator.html +#[must_use = "iterators are lazy and do nothing unless consumed"] +#[stable(feature = "iterator_step_by", since = "1.28.0")] +#[derive(Clone, Debug)] +pub struct StepBy { + /// This field is guaranteed to be preprocessed by the specialized `SpecRangeSetup::setup` + /// in the constructor. + /// For most iterators that processing is a no-op, but for Range<{integer}> types it is lossy + /// which means the inner iterator cannot be returned to user code. + /// Additionally this type-dependent preprocessing means specialized implementations + /// cannot be used interchangeably. + iter: I, + /// This field is `step - 1`, aka the correct amount to pass to `nth` when iterating. + /// It MUST NOT be `usize::MAX`, as `unsafe` code depends on being able to add one + /// without the risk of overflow. (This is important so that length calculations + /// don't need to check for division-by-zero, for example.) + step_minus_one: usize, + first_take: bool, +} + +#[unstable(feature = "ub_checks", issue = "none")] +impl Invariant for StepBy { + fn is_safe(&self) -> bool { + self.step_minus_one < usize::MAX + } +} + +impl StepBy { + #[inline] + pub(in crate::iter) fn new(iter: I, step: usize) -> StepBy { + assert!(step != 0); + let iter = >::setup(iter, step); + StepBy { iter, step_minus_one: step - 1, first_take: true } + } + + /// The `step` that was originally passed to `Iterator::step_by(step)`, + /// aka `self.step_minus_one + 1`. + #[inline] + fn original_step(&self) -> NonZero { + // SAFETY: By type invariant, `step_minus_one` cannot be `MAX`, which + // means the addition cannot overflow and the result cannot be zero. + unsafe { NonZero::new_unchecked(intrinsics::unchecked_add(self.step_minus_one, 1)) } + } +} + +#[stable(feature = "iterator_step_by", since = "1.28.0")] +impl Iterator for StepBy +where + I: Iterator, +{ + type Item = I::Item; + + #[inline] + fn next(&mut self) -> Option { + self.spec_next() + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + self.spec_size_hint() + } + + #[inline] + fn nth(&mut self, n: usize) -> Option { + self.spec_nth(n) + } + + fn try_fold(&mut self, acc: Acc, f: F) -> R + where + F: FnMut(Acc, Self::Item) -> R, + R: Try, + { + self.spec_try_fold(acc, f) + } + + #[inline] + fn fold(self, acc: Acc, f: F) -> Acc + where + F: FnMut(Acc, Self::Item) -> Acc, + { + self.spec_fold(acc, f) + } +} + +impl StepBy +where + I: ExactSizeIterator, +{ + // The zero-based index starting from the end of the iterator of the + // last element. Used in the `DoubleEndedIterator` implementation. + fn next_back_index(&self) -> usize { + let rem = self.iter.len() % self.original_step(); + if self.first_take { if rem == 0 { self.step_minus_one } else { rem - 1 } } else { rem } + } +} + +#[stable(feature = "double_ended_step_by_iterator", since = "1.38.0")] +impl DoubleEndedIterator for StepBy +where + I: DoubleEndedIterator + ExactSizeIterator, +{ + #[inline] + fn next_back(&mut self) -> Option { + self.spec_next_back() + } + + #[inline] + fn nth_back(&mut self, n: usize) -> Option { + self.spec_nth_back(n) + } + + fn try_rfold(&mut self, init: Acc, f: F) -> R + where + F: FnMut(Acc, Self::Item) -> R, + R: Try, + { + self.spec_try_rfold(init, f) + } + + #[inline] + fn rfold(self, init: Acc, f: F) -> Acc + where + Self: Sized, + F: FnMut(Acc, Self::Item) -> Acc, + { + self.spec_rfold(init, f) + } +} + +// StepBy can only make the iterator shorter, so the len will still fit. +#[stable(feature = "iterator_step_by", since = "1.28.0")] +impl ExactSizeIterator for StepBy where I: ExactSizeIterator {} + +// SAFETY: This adapter is shortening. TrustedLen requires the upper bound to be calculated correctly. +// These requirements can only be satisfied when the upper bound of the inner iterator's upper +// bound is never `None`. I: TrustedRandomAccess happens to provide this guarantee while +// I: TrustedLen would not. +// This also covers the Range specializations since the ranges also implement TRA +#[unstable(feature = "trusted_len", issue = "37572")] +unsafe impl TrustedLen for StepBy where I: Iterator + TrustedRandomAccess {} + +trait SpecRangeSetup { + fn setup(inner: T, step: usize) -> T; +} + +impl SpecRangeSetup for T { + #[inline] + default fn setup(inner: T, _step: usize) -> T { + inner + } +} + +/// Specialization trait to optimize `StepBy>` iteration. +/// +/// # Safety +/// +/// Technically this is safe to implement (look ma, no unsafe!), but in reality +/// a lot of unsafe code relies on ranges over integers being correct. +/// +/// For correctness *all* public StepBy methods must be specialized +/// because `setup` drastically alters the meaning of the struct fields so that mixing +/// different implementations would lead to incorrect results. +unsafe trait StepByImpl { + type Item; + + fn spec_next(&mut self) -> Option; + + fn spec_size_hint(&self) -> (usize, Option); + + fn spec_nth(&mut self, n: usize) -> Option; + + fn spec_try_fold(&mut self, acc: Acc, f: F) -> R + where + F: FnMut(Acc, Self::Item) -> R, + R: Try; + + fn spec_fold(self, acc: Acc, f: F) -> Acc + where + F: FnMut(Acc, Self::Item) -> Acc; +} + +/// Specialization trait for double-ended iteration. +/// +/// See also: `StepByImpl` +/// +/// # Safety +/// +/// The specializations must be implemented together with `StepByImpl` +/// where applicable. I.e. if `StepBy` does support backwards iteration +/// for a given iterator and that is specialized for forward iteration then +/// it must also be specialized for backwards iteration. +unsafe trait StepByBackImpl { + type Item; + + fn spec_next_back(&mut self) -> Option + where + I: DoubleEndedIterator + ExactSizeIterator; + + fn spec_nth_back(&mut self, n: usize) -> Option + where + I: DoubleEndedIterator + ExactSizeIterator; + + fn spec_try_rfold(&mut self, init: Acc, f: F) -> R + where + I: DoubleEndedIterator + ExactSizeIterator, + F: FnMut(Acc, Self::Item) -> R, + R: Try; + + fn spec_rfold(self, init: Acc, f: F) -> Acc + where + I: DoubleEndedIterator + ExactSizeIterator, + F: FnMut(Acc, Self::Item) -> Acc; +} + +unsafe impl StepByImpl for StepBy { + type Item = I::Item; + + #[inline] + default fn spec_next(&mut self) -> Option { + let step_size = if self.first_take { 0 } else { self.step_minus_one }; + self.first_take = false; + self.iter.nth(step_size) + } + + #[inline] + default fn spec_size_hint(&self) -> (usize, Option) { + #[inline] + fn first_size(step: NonZero) -> impl Fn(usize) -> usize { + move |n| if n == 0 { 0 } else { 1 + (n - 1) / step } + } + + #[inline] + fn other_size(step: NonZero) -> impl Fn(usize) -> usize { + move |n| n / step + } + + let (low, high) = self.iter.size_hint(); + + if self.first_take { + let f = first_size(self.original_step()); + (f(low), high.map(f)) + } else { + let f = other_size(self.original_step()); + (f(low), high.map(f)) + } + } + + #[inline] + default fn spec_nth(&mut self, mut n: usize) -> Option { + if self.first_take { + self.first_take = false; + let first = self.iter.next(); + if n == 0 { + return first; + } + n -= 1; + } + // n and self.step_minus_one are indices, we need to add 1 to get the amount of elements + // When calling `.nth`, we need to subtract 1 again to convert back to an index + let mut step = self.original_step().get(); + // n + 1 could overflow + // thus, if n is usize::MAX, instead of adding one, we call .nth(step) + if n == usize::MAX { + self.iter.nth(step - 1); + } else { + n += 1; + } + + // overflow handling + loop { + let mul = n.checked_mul(step); + { + if intrinsics::likely(mul.is_some()) { + return self.iter.nth(mul.unwrap() - 1); + } + } + let div_n = usize::MAX / n; + let div_step = usize::MAX / step; + let nth_n = div_n * n; + let nth_step = div_step * step; + let nth = if nth_n > nth_step { + step -= div_n; + nth_n + } else { + n -= div_step; + nth_step + }; + self.iter.nth(nth - 1); + } + } + + default fn spec_try_fold(&mut self, mut acc: Acc, mut f: F) -> R + where + F: FnMut(Acc, Self::Item) -> R, + R: Try, + { + #[inline] + fn nth( + iter: &mut I, + step_minus_one: usize, + ) -> impl FnMut() -> Option + '_ { + move || iter.nth(step_minus_one) + } + + if self.first_take { + self.first_take = false; + match self.iter.next() { + None => return try { acc }, + Some(x) => acc = f(acc, x)?, + } + } + from_fn(nth(&mut self.iter, self.step_minus_one)).try_fold(acc, f) + } + + default fn spec_fold(mut self, mut acc: Acc, mut f: F) -> Acc + where + F: FnMut(Acc, Self::Item) -> Acc, + { + #[inline] + fn nth( + iter: &mut I, + step_minus_one: usize, + ) -> impl FnMut() -> Option + '_ { + move || iter.nth(step_minus_one) + } + + if self.first_take { + self.first_take = false; + match self.iter.next() { + None => return acc, + Some(x) => acc = f(acc, x), + } + } + from_fn(nth(&mut self.iter, self.step_minus_one)).fold(acc, f) + } +} + +unsafe impl StepByBackImpl for StepBy { + type Item = I::Item; + + #[inline] + default fn spec_next_back(&mut self) -> Option { + self.iter.nth_back(self.next_back_index()) + } + + #[inline] + default fn spec_nth_back(&mut self, n: usize) -> Option { + // `self.iter.nth_back(usize::MAX)` does the right thing here when `n` + // is out of bounds because the length of `self.iter` does not exceed + // `usize::MAX` (because `I: ExactSizeIterator`) and `nth_back` is + // zero-indexed + let n = n.saturating_mul(self.original_step().get()).saturating_add(self.next_back_index()); + self.iter.nth_back(n) + } + + default fn spec_try_rfold(&mut self, init: Acc, mut f: F) -> R + where + F: FnMut(Acc, Self::Item) -> R, + R: Try, + { + #[inline] + fn nth_back( + iter: &mut I, + step_minus_one: usize, + ) -> impl FnMut() -> Option + '_ { + move || iter.nth_back(step_minus_one) + } + + match self.next_back() { + None => try { init }, + Some(x) => { + let acc = f(init, x)?; + from_fn(nth_back(&mut self.iter, self.step_minus_one)).try_fold(acc, f) + } + } + } + + #[inline] + default fn spec_rfold(mut self, init: Acc, mut f: F) -> Acc + where + Self: Sized, + F: FnMut(Acc, I::Item) -> Acc, + { + #[inline] + fn nth_back( + iter: &mut I, + step_minus_one: usize, + ) -> impl FnMut() -> Option + '_ { + move || iter.nth_back(step_minus_one) + } + + match self.next_back() { + None => init, + Some(x) => { + let acc = f(init, x); + from_fn(nth_back(&mut self.iter, self.step_minus_one)).fold(acc, f) + } + } + } +} + +/// For these implementations, `SpecRangeSetup` calculates the number +/// of iterations that will be needed and stores that in `iter.end`. +/// +/// The various iterator implementations then rely on that to not need +/// overflow checking, letting loops just be counted instead. +/// +/// These only work for unsigned types, and will need to be reworked +/// if you want to use it to specialize on signed types. +/// +/// Currently these are only implemented for integers up to `usize` due to +/// correctness issues around `ExactSizeIterator` impls on 16bit platforms. +/// And since `ExactSizeIterator` is a prerequisite for backwards iteration +/// and we must consistently specialize backwards and forwards iteration +/// that makes the situation complicated enough that it's not covered +/// for now. +macro_rules! spec_int_ranges { + ($($t:ty)*) => ($( + + const _: () = assert!(usize::BITS >= <$t>::BITS); + + impl SpecRangeSetup> for Range<$t> { + #[inline] + fn setup(mut r: Range<$t>, step: usize) -> Range<$t> { + let inner_len = r.size_hint().0; + // If step exceeds $t::MAX, then the count will be at most 1 and + // thus always fit into $t. + let yield_count = inner_len.div_ceil(step); + // Turn the range end into an iteration counter + r.end = yield_count as $t; + r + } + } + + unsafe impl StepByImpl> for StepBy> { + #[inline] + fn spec_next(&mut self) -> Option<$t> { + // if a step size larger than the type has been specified fall back to + // t::MAX, in which case remaining will be at most 1. + let step = <$t>::try_from(self.original_step().get()).unwrap_or(<$t>::MAX); + let remaining = self.iter.end; + if remaining > 0 { + let val = self.iter.start; + // this can only overflow during the last step, after which the value + // will not be used + self.iter.start = val.wrapping_add(step); + self.iter.end = remaining - 1; + Some(val) + } else { + None + } + } + + #[inline] + fn spec_size_hint(&self) -> (usize, Option) { + let remaining = self.iter.end as usize; + (remaining, Some(remaining)) + } + + // The methods below are all copied from the Iterator trait default impls. + // We have to repeat them here so that the specialization overrides the StepByImpl defaults + + #[inline] + fn spec_nth(&mut self, n: usize) -> Option { + self.advance_by(n).ok()?; + self.next() + } + + #[inline] + fn spec_try_fold(&mut self, init: Acc, mut f: F) -> R + where + F: FnMut(Acc, Self::Item) -> R, + R: Try + { + let mut accum = init; + while let Some(x) = self.next() { + accum = f(accum, x)?; + } + try { accum } + } + + #[inline] + fn spec_fold(self, init: Acc, mut f: F) -> Acc + where + F: FnMut(Acc, Self::Item) -> Acc + { + // if a step size larger than the type has been specified fall back to + // t::MAX, in which case remaining will be at most 1. + let step = <$t>::try_from(self.original_step().get()).unwrap_or(<$t>::MAX); + let remaining = self.iter.end; + let mut acc = init; + let mut val = self.iter.start; + for _ in 0..remaining { + acc = f(acc, val); + // this can only overflow during the last step, after which the value + // will no longer be used + val = val.wrapping_add(step); + } + acc + } + } + )*) +} + +macro_rules! spec_int_ranges_r { + ($($t:ty)*) => ($( + const _: () = assert!(usize::BITS >= <$t>::BITS); + + unsafe impl StepByBackImpl> for StepBy> { + + #[inline] + fn spec_next_back(&mut self) -> Option { + let step = self.original_step().get() as $t; + let remaining = self.iter.end; + if remaining > 0 { + let start = self.iter.start; + self.iter.end = remaining - 1; + Some(start + step * (remaining - 1)) + } else { + None + } + } + + // The methods below are all copied from the Iterator trait default impls. + // We have to repeat them here so that the specialization overrides the StepByImplBack defaults + + #[inline] + fn spec_nth_back(&mut self, n: usize) -> Option { + if self.advance_back_by(n).is_err() { + return None; + } + self.next_back() + } + + #[inline] + fn spec_try_rfold(&mut self, init: Acc, mut f: F) -> R + where + F: FnMut(Acc, Self::Item) -> R, + R: Try + { + let mut accum = init; + while let Some(x) = self.next_back() { + accum = f(accum, x)?; + } + try { accum } + } + + #[inline] + fn spec_rfold(mut self, init: Acc, mut f: F) -> Acc + where + F: FnMut(Acc, Self::Item) -> Acc + { + let mut accum = init; + while let Some(x) = self.next_back() { + accum = f(accum, x); + } + accum + } + } + )*) +} + +#[cfg(target_pointer_width = "64")] +spec_int_ranges!(u8 u16 u32 u64 usize); +// DoubleEndedIterator requires ExactSizeIterator, which isn't implemented for Range +#[cfg(target_pointer_width = "64")] +spec_int_ranges_r!(u8 u16 u32 usize); + +#[cfg(target_pointer_width = "32")] +spec_int_ranges!(u8 u16 u32 usize); +#[cfg(target_pointer_width = "32")] +spec_int_ranges_r!(u8 u16 u32 usize); + +#[cfg(target_pointer_width = "16")] +spec_int_ranges!(u8 u16 usize); +#[cfg(target_pointer_width = "16")] +spec_int_ranges_r!(u8 u16 usize); + +#[cfg(kani)] +#[unstable(feature = "kani", issue = "none")] +mod verify { + use super::*; + use crate::kani; + + fn any_slice(orig: &[T]) -> &[T] { + if kani::any() { + let last = kani::any_where(|i: &usize| *i <= orig.len()); + let first = kani::any_where(|i: &usize| *i <= last); + &orig[first..last] + } else { + let ptr = kani::any_where::(|v| *v != 0) as *const T; + kani::assume(ptr.is_aligned()); + unsafe { crate::slice::from_raw_parts(ptr, 0) } + } + } + + // `original_step` reconstructs the configured step as `step_minus_one + 1` + // via `unchecked_add` and `NonZero::new_unchecked`. The type invariant + // (`step_minus_one < usize::MAX`) makes both operations sound; a valid + // `StepBy` is established by construction (`StepBy::new` requires `step != 0`). + macro_rules! check_original_step { + ($harness:ident, $elem_ty:ty, $max_len:expr) => { + #[kani::proof] + fn $harness() { + const MAX_LEN: usize = $max_len; + let array: [$elem_ty; MAX_LEN] = kani::any(); + let step = kani::any_where(|s: &usize| *s != 0); + let it = StepBy::new(any_slice(&array).iter(), step); + let result = it.original_step(); + kani::assert(result.get() == step, "original_step round-trips the configured step"); + } + }; + } + // `original_step` ignores the wrapped iterator, so a small backing array + // suffices; the proof is over the symbolic `step`, not the slice length. + check_original_step!(check_step_by_original_step_unit, (), 16); + check_original_step!(check_step_by_original_step_u8, u8, 16); + check_original_step!(check_step_by_original_step_char, char, 16); + check_original_step!(check_step_by_original_step_tup, (char, u8), 16); +} diff --git a/verifast-proofs/core/iter/adapters/test_sources.py b/verifast-proofs/core/iter/adapters/test_sources.py new file mode 100644 index 0000000000000..7c270009ef498 --- /dev/null +++ b/verifast-proofs/core/iter/adapters/test_sources.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +"""Exercise stale-source and omitted-contract failures without a compiler.""" + +import importlib.util +import json +from pathlib import Path +import sys +import tempfile +import unittest + + +PACKAGE = Path(__file__).resolve().parent +sys.dont_write_bytecode = True +SPEC = importlib.util.spec_from_file_location("check_sources", PACKAGE / "check_sources.py") +CHECKER = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(CHECKER) + + +class SourceGateTests(unittest.TestCase): + def setUp(self): + self.temporary = tempfile.TemporaryDirectory(prefix="vrs602-source-check-") + self.addCleanup(self.temporary.cleanup) + self.repo = Path(self.temporary.name) + self.package = self.repo / "verifast-proofs/core/iter/adapters" + self.manifest = json.loads(CHECKER.read_file(PACKAGE / "source-map.json")) + paths = ["source-map.json", "original/lib.rs", "verified/lib.rs"] + for entry in self.manifest["sources"]: + paths.extend((entry["snapshot"], entry["projection"], + "verified/" + Path(entry["projection"]).name)) + upstream = self.repo / entry["upstream"] + upstream.parent.mkdir(parents=True, exist_ok=True) + upstream.write_bytes(CHECKER.read_file(PACKAGE / entry["snapshot"])) + for relative in paths: + destination = self.package / relative + destination.parent.mkdir(parents=True, exist_ok=True) + destination.write_bytes(CHECKER.read_file(PACKAGE / relative)) + + def check(self): + return CHECKER.check(package=self.package, repo=self.repo) + + def alter(self, relative, before, after): + path = self.package / relative + text = path.read_text() + self.assertIn(before, text) + path.write_text(text.replace(before, after, 1)) + + def test_current_inputs_and_array_return_signatures(self): + self.assertEqual(self.check(), 2) + + def test_upstream_drift(self): + path = self.repo / self.manifest["sources"][0]["upstream"] + path.write_bytes(path.read_bytes() + b"\n") + with self.assertRaisesRegex(ValueError, "std source differs"): + self.check() + + def test_snapshot_drift(self): + path = self.package / self.manifest["sources"][0]["snapshot"] + path.write_bytes(path.read_bytes() + b"\n") + with self.assertRaisesRegex(ValueError, "snapshot hash changed"): + self.check() + + def test_projection_drift(self): + self.alter("original/step_by.rs", "self.step_minus_one, 1", "self.step_minus_one, 2") + with self.assertRaisesRegex(ValueError, "original projection differs"): + self.check() + + def test_ordinary_comment_does_not_count_as_contract(self): + self.alter("verified/step_by.rs", "//@ req", "// req") + with self.assertRaisesRegex(ValueError, "missing req for original_step"): + self.check() + + def test_missing_unwind_clause(self): + self.alter("verified/step_by.rs", "//@ on_unwind_ens false;", "") + with self.assertRaisesRegex(ValueError, "missing on_unwind_ens"): + self.check() + + def test_suppressed_method(self): + self.alter("verified/step_by.rs", " #[inline]", " #[cfg(any())]\n #[inline]") + with self.assertRaisesRegex(ValueError, "verification bypass"): + self.check() + + def test_assumption(self): + self.alter("verified/step_by.rs", "//@ on_unwind_ens false;", + "//@ on_unwind_ens false;\n //@ assume(false);") + with self.assertRaisesRegex(ValueError, "verification bypass"): + self.check() + + def test_changed_crate_root(self): + self.alter("verified/lib.rs", "mod step_by;", "") + with self.assertRaisesRegex(ValueError, "crate roots must match"): + self.check() + + +if __name__ == "__main__": + unittest.main() diff --git a/verifast-proofs/core/iter/adapters/verified/lib.rs b/verifast-proofs/core/iter/adapters/verified/lib.rs new file mode 100644 index 0000000000000..e1a6759dc6a28 --- /dev/null +++ b/verifast-proofs/core/iter/adapters/verified/lib.rs @@ -0,0 +1,13 @@ +#![no_std] +#![crate_type = "lib"] +#![allow(dead_code, unused_imports, internal_features)] +#![feature(core_intrinsics, staged_api)] +#![stable(feature = "rust1", since = "1.0.0")] + +extern crate core as std; + +#[stable(feature = "rust1", since = "1.0.0")] +pub use std::{fmt, intrinsics, mem, num, ptr}; + +mod map_windows; +mod step_by; diff --git a/verifast-proofs/core/iter/adapters/verified/map_windows.rs b/verifast-proofs/core/iter/adapters/verified/map_windows.rs new file mode 100644 index 0000000000000..c9461b0634e44 --- /dev/null +++ b/verifast-proofs/core/iter/adapters/verified/map_windows.rs @@ -0,0 +1,271 @@ +// Generic proof candidate. See ../README.md for validation status and scope. + +use crate::mem::MaybeUninit; +use crate::{fmt, ptr}; + +struct Buffer { + // Invariant: `self.buffer[self.start..self.start + N]` is initialized, + // with all other elements being uninitialized. This also + // implies that `self.start <= N`. + buffer: [[MaybeUninit; N]; 2], + start: usize, +} + +/*@ + +fix width() -> usize { usize_of_const(typeid(N)) } + +fix base(b: *Buffer) -> *MaybeUninit { + &(*b).buffer as *MaybeUninit +} + +// These are constructor/layout restrictions, not a finite proof bound. +pred bounds(b: *Buffer; start: usize) = + (*b).start |-> start &*& 0 < width::() &*& + width::() <= usize::MAX / 2 &*& start <= width::() &*& + 2 * width::() * std::mem::size_of::() <= isize::MAX &*& + pointer_within_limits(base(b)) == true &*& + pointer_within_limits(base(b) + 2 * width::()) == true; + +// Inactive slots may retain copied bytes. They carry no ownership of T. +pred live(t: thread_id_t, b: *Buffer; start: usize, values: list) = + bounds(b, start) &*& length(values) == width::() &*& + base(b)[..start] |-> ?prefix &*& + (base(b) + start)[..width::()] |-> map(MaybeUninit::new, values) &*& + (base(b) + start + width::())[..width::() - start] |-> ?suffix &*& + foreach(values, (.own)(t)); + +pred storage(b: *Buffer; start: usize) = + bounds(b, start) &*& base(b)[..2 * width::()] |-> ?slots; + +// Unwrap initialized slots without requiring T: Copy or duplicating T.own. +lem initialized_slots(p: *MaybeUninit, values: list) + req p[..length(values)] |-> map(MaybeUninit::new, values); + ens (p as *T)[..length(values)] |-> values; +{ + open array(p, length(values), _); + match values { + nil => { + close array(p as *T, 0, nil); + } + cons(value, tail) => { + std::mem::open_MaybeUninit(p); + close points_to(p as *T, value); + initialized_slots(p + 1, tail); + close array(p as *T, length(values), values); + } + } +} + +lem wrap_slots(p: *T, values: list) + req p[..length(values)] |-> values; + ens (p as *MaybeUninit)[..length(values)] |-> map(MaybeUninit::new, values); +{ + open array(p, length(values), values); + match values { + nil => { + close array(p as *MaybeUninit, 0, nil); + } + cons(value, tail) => { + std::mem::close_MaybeUninit(p as *MaybeUninit); + wrap_slots(p + 1, tail); + close array(p as *MaybeUninit, length(values), _); + } + } +} + +// The caller supplies a borrow of the window. It can originate from a live +// initialized array (as_array_ref), or writable storage (as_uninit_array_mut). +// Proving the surrounding safe abstraction and its constructors is separate. + +@*/ + +impl Buffer { + #[inline] + fn buffer_ptr(&self) -> *const MaybeUninit +//@ req true; + //@ ens result == base(self); + //@ on_unwind_ens false; + { + (&raw const self.buffer).cast() + } + + #[inline] + fn buffer_mut_ptr(&mut self) -> *mut MaybeUninit +//@ req true; + //@ ens result == base(self); + //@ on_unwind_ens false; + { + (&raw mut self.buffer).cast() + } + + #[inline] + fn as_array_ref<'a>(&'a self) -> &'a [T; N] +/*@ + req [?f]bounds(self, ?start) &*& [?q]lifetime_token('a) &*& + [_](<[T; N]>.share)('a, ?t, (base(self) + start) as *[T; N]); + ens [f]bounds(self, start) &*& [q]lifetime_token('a) &*& + result == (base(self) + start) as *[T; N] &*& + [_](<[T; N]>.share)('a, t, result); + on_unwind_ens false; + @*/ { + //@ open [f]bounds(self, start); + debug_assert!(self.start + N <= 2 * N); + + // SAFETY: our invariant guarantees these elements are initialized. + let result = unsafe { &*self.buffer_ptr().add(self.start).cast() }; + //@ close [f]bounds(self, start); + result + } + + #[inline] + fn as_uninit_array_mut<'a>(&'a mut self) -> &'a mut MaybeUninit<[T; N]> +/*@ + req bounds(self, ?start) &*& [?q]lifetime_token('a) &*& + full_borrow('a, >.full_borrow_content(?t, + (base(self) + start) as *MaybeUninit<[T; N]>)); + ens bounds(self, start) &*& [q]lifetime_token('a) &*& + result == (base(self) + start) as *MaybeUninit<[T; N]> &*& + full_borrow('a, >.full_borrow_content(t, result)); + on_unwind_ens false; + @*/ { + //@ open bounds(self, start); + debug_assert!(self.start + N <= 2 * N); + + // SAFETY: our invariant guarantees these elements are in bounds. + let result = unsafe { &mut *self.buffer_mut_ptr().add(self.start).cast() }; + //@ close bounds(self, start); + result + } + + /// Pushes a new item `next` to the back, and pops the front-most one. + /// + /// All the elements will be shifted to the front end when pushing reaches + /// the back end. + fn push(&mut self, next: T) + /*@ + req thread_token(?t) &*& live(t, self, ?start, ?values) &*& .own(t, next); + ens thread_token(t) &*& live(t, self, start == width::() ? 0 : start + 1, + append(tail(values), cons(next, nil))); + on_unwind_ens thread_token(t); + @*/ + { + //@ open live(t, self, start, values); + //@ open bounds(self, start); + //@ open foreach(values, (.own)(t)); + //@ open array(base(self) + start, width::(), _); + let buffer_mut_ptr = self.buffer_mut_ptr(); + debug_assert!(self.start + N <= 2 * N); + + let to_drop = if self.start == N { + // We have reached the end of our buffer and have to copy + // everything to the start. Example layout for N = 3. + // + // 0 1 2 3 4 5 0 1 2 3 4 5 + // ┌───┬───┬───┬───┬───┬───┐ ┌───┬───┬───┬───┬───┬───┐ + // │ - │ - │ - │ a │ b │ c │ -> │ b │ c │ n │ - │ - │ - │ + // └───┴───┴───┴───┴───┴───┘ └───┴───┴───┴───┴───┴───┘ + // ↑ ↑ + // start start + + // SAFETY: the two pointers are valid for reads/writes of N -1 + // elements because our array's size is semantically 2 * N. The + // regions also don't overlap for the same reason. + // + // We leave the old elements in place. As soon as `start` is set + // to 0, we treat them as uninitialized and treat their copies + // as initialized. + let to_drop = unsafe { + //@ array_split(buffer_mut_ptr, width::() - 1); + //@ open array(buffer_mut_ptr + width::() - 1, 1, _); + ptr::copy_nonoverlapping(buffer_mut_ptr.add(self.start + 1), buffer_mut_ptr, N - 1); + (*buffer_mut_ptr.add(N - 1)).write(next); + //@ close array(buffer_mut_ptr + width::() - 1, 1, cons(MaybeUninit::new(next), nil)); + //@ array_join(buffer_mut_ptr); + buffer_mut_ptr.add(self.start) + }; + self.start = 0; + to_drop + } else { + // SAFETY: `self.start` is < N as guaranteed by the invariant + // plus the check above. Even if the drop at the end panics, + // the invariant is upheld. + // + // Example layout for N = 3: + // + // 0 1 2 3 4 5 0 1 2 3 4 5 + // ┌───┬───┬───┬───┬───┬───┐ ┌───┬───┬───┬───┬───┬───┐ + // │ - │ a │ b │ c │ - │ - │ -> │ - │ - │ b │ c │ n │ - │ + // └───┴───┴───┴───┴───┴───┘ └───┴───┴───┴───┴───┴───┘ + // ↑ ↑ + // start start + // + let to_drop = unsafe { + //@ open array(buffer_mut_ptr + start + width::(), width::() - start, _); + (*buffer_mut_ptr.add(self.start + N)).write(next); + //@ close array(buffer_mut_ptr + start + width::() + 1, 0, nil); + //@ close array(buffer_mut_ptr + start + width::(), 1, cons(MaybeUninit::new(next), nil)); + //@ array_join(buffer_mut_ptr + start + 1); + buffer_mut_ptr.add(self.start) + }; + self.start += 1; + to_drop + }; + + // SAFETY: the index is valid and this is element `a` in the + // diagram above and has not been dropped yet. + //@ close foreach(nil, (.own)(t)); + //@ close foreach(cons(next, nil), (.own)(t)); + //@ foreach_append(tail(values), cons(next, nil)); + //@ std::mem::open_MaybeUninit(to_drop); + //@ close points_to(to_drop as *T, head(values)); + unsafe { ptr::drop_in_place(to_drop.cast_init()) }; + //@ std::mem::close_MaybeUninit(to_drop); + /*@ + if start == width::() { + close array(to_drop, width::(), _); + close array(buffer_mut_ptr, 0, nil); + } else { + close array(to_drop + 1, 0, nil); + close array(to_drop, 1, _); + array_join(buffer_mut_ptr); + } + close bounds(self, start == width::() ? 0 : start + 1); + close live(t, self, start == width::() ? 0 : start + 1, + append(tail(values), cons(next, nil))); + @*/ + } +} + +impl Drop for Buffer { + fn drop(&mut self) + /*@ + req thread_token(?t) &*& live(t, self, ?start, ?values); + ens thread_token(t) &*& storage(self, start); + on_unwind_ens thread_token(t); + @*/ + { + //@ open live(t, self, start, values); + //@ open bounds(self, start); + // SAFETY: our invariant guarantees that N elements starting from + // `self.start` are initialized. We drop them here. + unsafe { + let initialized_part: *mut [T] = crate::ptr::slice_from_raw_parts_mut( + self.buffer_mut_ptr().add(self.start).cast(), + N, + ); + //@ initialized_slots(base(self) + start, values); + // This assertion must be discharged by the Rust slice memory model. + // No additional axiom or assumed drop contract is supplied here. + //@ assert *initialized_part |-> slice_of_elems(values); + //@ close <[T]>.own(t, slice_of_elems(values)); + ptr::drop_in_place(initialized_part); + //@ assert (initialized_part as *T)[..width::()] |-?-> ?remaining; + //@ std::mem::array__to_array_MaybeUninit(initialized_part as *T); + } + //@ array_join(base(self)); + //@ array_join(base(self)); + //@ close bounds(self, start); + //@ close storage(self, start); + } +} diff --git a/verifast-proofs/core/iter/adapters/verified/step_by.rs b/verifast-proofs/core/iter/adapters/verified/step_by.rs new file mode 100644 index 0000000000000..90a1f9eaea9f8 --- /dev/null +++ b/verifast-proofs/core/iter/adapters/verified/step_by.rs @@ -0,0 +1,38 @@ +// Generic proof candidate. See ../README.md for validation status and scope. + +use crate::intrinsics; +use crate::num::NonZero; + +#[must_use = "iterators are lazy and do nothing unless consumed"] +#[stable(feature = "iterator_step_by", since = "1.28.0")] +#[derive(Clone, Debug)] +pub struct StepBy { + /// This field is guaranteed to be preprocessed by the specialized `SpecRangeSetup::setup` + /// in the constructor. + /// For most iterators that processing is a no-op, but for Range<{integer}> types it is lossy + /// which means the inner iterator cannot be returned to user code. + /// Additionally this type-dependent preprocessing means specialized implementations + /// cannot be used interchangeably. + iter: I, + /// This field is `step - 1`, aka the correct amount to pass to `nth` when iterating. + /// It MUST NOT be `usize::MAX`, as `unsafe` code depends on being able to add one + /// without the risk of overflow. (This is important so that length calculations + /// don't need to check for division-by-zero, for example.) + step_minus_one: usize, + first_take: bool, +} + +impl StepBy { + /// The `step` that was originally passed to `Iterator::step_by(step)`, + /// aka `self.step_minus_one + 1`. + #[inline] + fn original_step(&self) -> NonZero +//@ req [?f]self.step_minus_one |-> ?step &*& step < usize::MAX; + //@ ens [f]self.step_minus_one |-> step &*& result.get() == step + 1 &*& 0 < result.get(); + //@ on_unwind_ens false; + { + // SAFETY: By type invariant, `step_minus_one` cannot be `MAX`, which + // means the addition cannot overflow and the result cannot be zero. + unsafe { NonZero::new_unchecked(intrinsics::unchecked_add(self.step_minus_one, 1)) } + } +} diff --git a/verifast-proofs/core/iter/adapters/verify.sh b/verifast-proofs/core/iter/adapters/verify.sh new file mode 100644 index 0000000000000..8b83a56eec77d --- /dev/null +++ b/verifast-proofs/core/iter/adapters/verify.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +set -euo pipefail + +proof_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +cd "$proof_dir" + +case "${1:-}" in + --static) + exec python3 -I check_sources.py + ;; + --remote) + ;; + *) + echo 'Usage: bash verify.sh --static | --remote' >&2 + echo '--remote runs compilers and solvers on a prepared Linux machine.' >&2 + exit 2 + ;; +esac + +# Do not invoke the repository wrappers on the memory-constrained Mac. +# The wrappers may install VeriFast and its pinned Rust toolchain. +if [[ "$(uname -s)" != Linux ]]; then + echo 'Proof execution requires a separate Linux machine. Use --static here.' >&2 + exit 2 +fi + +python3 -I check_sources.py + +# This is a per-process address-space limit, not an aggregate process-tree cap. +# Require headroom for the verifier, Rust frontend, solver, and operating system. +python3 -I - <<'PY' +from pathlib import Path +import sys + +fields = dict(line.split(":", 1) for line in Path("/proc/meminfo").read_text().splitlines()) +available_kib = int(fields["MemAvailable"].split()[0]) +if available_kib < 8 * 1024 * 1024: + sys.exit("Proof execution requires at least 8 GiB currently available RAM.") +print(f"Remote preflight: {available_kib // 1024} MiB available RAM") +PY + +command -v timeout >/dev/null +ulimit -c 0 +ulimit -v 2097152 +ulimit -t 600 +export VFVERSION=25.11 +export CARGO_BUILD_JOBS=1 +export RAYON_NUM_THREADS=1 +export PATH="$proof_dir/../../..:$PATH" + +# Keep every stage sequential and fail on any unsuccessful proof or refinement. +# No assumption, unwind, reference-creation, or overflow suppression flags. +timeout --signal=TERM --kill-after=10s 600s \ + verifast -rustc_args '--edition 2024' -skip_specless_fns verified/lib.rs +timeout --signal=TERM --kill-after=10s 600s \ + refinement-checker --rustc-args '--edition 2024' original/lib.rs verified/lib.rs +python3 -I check_sources.py +echo 'PASS: generic adapter contracts, source refinement, and source identity' From 9e95bb7e83553d0e47035821eaecf4be030cab6b Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Sat, 12 Sep 2026 19:39:48 -0700 Subject: [PATCH 06/62] Enable MaybeUninit pointer conversion in the VeriFast port Signed-off-by: Onyeka Obi --- verifast-proofs/core/iter/adapters/original/lib.rs | 2 +- verifast-proofs/core/iter/adapters/verified/lib.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/verifast-proofs/core/iter/adapters/original/lib.rs b/verifast-proofs/core/iter/adapters/original/lib.rs index e1a6759dc6a28..73a46822695da 100644 --- a/verifast-proofs/core/iter/adapters/original/lib.rs +++ b/verifast-proofs/core/iter/adapters/original/lib.rs @@ -1,7 +1,7 @@ #![no_std] #![crate_type = "lib"] #![allow(dead_code, unused_imports, internal_features)] -#![feature(core_intrinsics, staged_api)] +#![feature(cast_maybe_uninit, core_intrinsics, staged_api)] #![stable(feature = "rust1", since = "1.0.0")] extern crate core as std; diff --git a/verifast-proofs/core/iter/adapters/verified/lib.rs b/verifast-proofs/core/iter/adapters/verified/lib.rs index e1a6759dc6a28..73a46822695da 100644 --- a/verifast-proofs/core/iter/adapters/verified/lib.rs +++ b/verifast-proofs/core/iter/adapters/verified/lib.rs @@ -1,7 +1,7 @@ #![no_std] #![crate_type = "lib"] #![allow(dead_code, unused_imports, internal_features)] -#![feature(core_intrinsics, staged_api)] +#![feature(cast_maybe_uninit, core_intrinsics, staged_api)] #![stable(feature = "rust1", since = "1.0.0")] extern crate core as std; From 199e4b4266c5fd6fc27c60a3b0949e9d11b2d8e5 Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Sat, 12 Sep 2026 19:51:11 -0700 Subject: [PATCH 07/62] Record the hosted VeriFast frontend blocker Signed-off-by: Onyeka Obi --- verifast-proofs/core/iter/adapters/README.md | 26 ++++++++++++++------ 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/verifast-proofs/core/iter/adapters/README.md b/verifast-proofs/core/iter/adapters/README.md index 12ee55bc79773..ca22d16736033 100644 --- a/verifast-proofs/core/iter/adapters/README.md +++ b/verifast-proofs/core/iter/adapters/README.md @@ -1,7 +1,8 @@ # Generic iterator adapter proof port This is an **unvalidated VeriFast port candidate** for part of Challenge 16. -Neither VeriFast nor the refinement checker has been run on this revision. +Hosted verification is blocked in the pinned VeriFast Rust frontend; +the refinement checker has not run. See the hosted results below. The files under `verified/` are proof inputs, not evidence of a successful proof. This port does not yet close Felipe's review requests on PR #602. @@ -89,9 +90,19 @@ The job has a 30-minute timeout and cancels superseded runs. Tool errors, proof failures, and resource-limit failures fail the job. Verification and refinement stay sequential and retain the per-process limits below. -This workflow has not yet run on the port. Adding a CI job does not establish -that the contracts or source refinement pass. Its result must be inspected -before this candidate can count as verified coverage. +Hosted run [34733601368](https://github.com/MavenRain/verify-rust-std/actions/runs/34733601368) +at commit `9e95bb7e83553d0e47035821eaecf4be030cab6b` passed the source checks +and their nine tests, then failed before proof checking. The Rust MIR exporter +encountered `AddUnchecked` in `StepBy::original_step` and panicked with +`not yet implemented` at `src/lib.rs:2455`. The earlier missing +`cast_maybe_uninit` feature flag was fixed in that commit. + +The [25.11 exporter](https://github.com/verifast/verifast/blob/25.11/src/rust_frontend/vf_mir_exporter/src/lib.rs#L2436-L2455) +has no `AddUnchecked` case. Static inspection of the +[26.01 exporter](https://github.com/verifast/verifast/blob/26.01/src/rust_frontend/vf_mir_exporter/src/lib.rs#L2445-L2464) +found the same omission, so that release alone does not address this failure. +Neither the generic contracts nor source refinement have a passing verdict. +The runner continues to fail on this error; no target or check is suppressed. ## Local static checks and optional manual verification @@ -125,9 +136,10 @@ this candidate without altering the existing LinkedList and RawVec checks. ## Remaining work -- Obtain actual verifier and refinement verdicts. In particular, check the - pinned frontend's handling of symbolic const sizes, `NonZero::new_unchecked` - and `unchecked_add`, reference creation, and generic slice drop glue. +- Resolve the frontend's confirmed `AddUnchecked` limitation without changing + the original source projection or introducing an assumed contract. Then + obtain verifier and refinement verdicts, including checks of symbolic const + sizes, `NonZero::new_unchecked`, reference creation, and generic slice drop glue. The explicit slice assertions in `drop` must be proved by that memory model; no local axiom has been added to make them pass. - Complete the safe-abstraction and unwind-state obligations described above. From 2f120f6da0550f8d92dd1552cc37322c92194bf1 Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Sun, 13 Sep 2026 00:01:59 -0700 Subject: [PATCH 08/62] Support checked verification of unchecked addition in the hosted frontend Signed-off-by: Onyeka Obi --- .github/workflows/verifast-iter-adapters.yml | 5 +++ verifast-proofs/core/iter/adapters/README.md | 28 +++++++++++---- .../iter/adapters/backend/add-overflow.rs | 11 ++++++ .../iter/adapters/backend/add-unchecked.patch | 12 +++++++ .../core/iter/adapters/backend/add-valid.rs | 11 ++++++ .../core/iter/adapters/backend/prepare.sh | 30 ++++++++++++++++ verifast-proofs/core/iter/adapters/verify.sh | 35 +++++++++++++++++-- 7 files changed, 123 insertions(+), 9 deletions(-) create mode 100644 verifast-proofs/core/iter/adapters/backend/add-overflow.rs create mode 100644 verifast-proofs/core/iter/adapters/backend/add-unchecked.patch create mode 100644 verifast-proofs/core/iter/adapters/backend/add-valid.rs create mode 100644 verifast-proofs/core/iter/adapters/backend/prepare.sh diff --git a/.github/workflows/verifast-iter-adapters.yml b/.github/workflows/verifast-iter-adapters.yml index 04f93a07bf39e..a71ff87d5dd93 100644 --- a/.github/workflows/verifast-iter-adapters.yml +++ b/.github/workflows/verifast-iter-adapters.yml @@ -51,6 +51,11 @@ jobs: bash verifast-proofs/core/iter/adapters/verify.sh --static python3 -I verifast-proofs/core/iter/adapters/test_sources.py + - name: Install frontend schema compiler + run: | + sudo --non-interactive apt-get update -qq + sudo --non-interactive apt-get install -y --no-install-recommends capnproto + - name: Verify contracts and source refinement run: | # A service cgroup limits the entire compiler/verifier process tree. diff --git a/verifast-proofs/core/iter/adapters/README.md b/verifast-proofs/core/iter/adapters/README.md index ca22d16736033..35101050549a7 100644 --- a/verifast-proofs/core/iter/adapters/README.md +++ b/verifast-proofs/core/iter/adapters/README.md @@ -1,8 +1,9 @@ # Generic iterator adapter proof port This is an **unvalidated VeriFast port candidate** for part of Challenge 16. -Hosted verification is blocked in the pinned VeriFast Rust frontend; -the refinement checker has not run. See the hosted results below. +Hosted verification previously stopped in the VeriFast Rust frontend. +The current revision includes a frontend fix awaiting hosted validation; +the refinement checker has not passed. See the hosted results below. The files under `verified/` are proof inputs, not evidence of a successful proof. This port does not yet close Felipe's review requests on PR #602. @@ -102,7 +103,19 @@ has no `AddUnchecked` case. Static inspection of the [26.01 exporter](https://github.com/verifast/verifast/blob/26.01/src/rust_frontend/vf_mir_exporter/src/lib.rs#L2445-L2464) found the same omission, so that release alone does not address this failure. Neither the generic contracts nor source refinement have a passing verdict. -The runner continues to fail on this error; no target or check is suppressed. +The current runner uses VeriFast 26.09 plus the narrow frontend patch in +`backend/add-unchecked.patch`. It maps `AddUnchecked` to the existing integer +addition operation, whose symbolic execution checks overflow. On inputs +where no overflow occurs, unchecked addition has the same result. Outside +that domain, unchecked addition is undefined, and the verifier must reject it. +Before checking the adapters, the runner requires a valid successor proof +and an explicit overflow diagnostic for the same operation without its +range precondition. A frontend crash does not count as the negative result. + +`backend/prepare.sh` pins both the source commit and archive hash. It builds +only the MIR exporter on the hosted Linux worker and replaces that worker's +exporter. It does not alter the verifier, its arithmetic rules, or library +contracts. Source projections and mandatory refinement remain unchanged. ## Local static checks and optional manual verification @@ -118,8 +131,10 @@ On a separate Linux machine with at least 8 GiB currently available RAM: bash verifast-proofs/core/iter/adapters/verify.sh --remote ``` -The runner uses the repository's pinned VeriFast 25.11 wrappers and Rust -nightly 2025-10-09. The wrappers can download their toolchains. Each process +The runner uses the repository's VeriFast 26.09 wrappers and Rust +nightly 2026-02-05, with the frontend patch described above. The wrappers +can download their toolchains. The remote build needs `capnp`, `rustc-dev`, +and `llvm-tools`; the workflow installs them. Each proof process has a 2 GiB address-space limit, each verification stage has a ten-minute wall limit, and the stages run sequentially. Address-space limits are per process, not a combined memory cap. Use an otherwise idle remote worker with @@ -136,8 +151,7 @@ this candidate without altering the existing LinkedList and RawVec checks. ## Remaining work -- Resolve the frontend's confirmed `AddUnchecked` limitation without changing - the original source projection or introducing an assumed contract. Then +- Validate the frontend's `AddUnchecked` mapping and both regression checks. Then obtain verifier and refinement verdicts, including checks of symbolic const sizes, `NonZero::new_unchecked`, reference creation, and generic slice drop glue. The explicit slice assertions in `drop` must be proved by that memory diff --git a/verifast-proofs/core/iter/adapters/backend/add-overflow.rs b/verifast-proofs/core/iter/adapters/backend/add-overflow.rs new file mode 100644 index 0000000000000..6ea7d45d2305e --- /dev/null +++ b/verifast-proofs/core/iter/adapters/backend/add-overflow.rs @@ -0,0 +1,11 @@ +#![crate_type = "lib"] +#![crate_name = "add_overflow"] +#![feature(core_intrinsics)] + +unsafe fn successor(value: usize) -> usize +//@ req true; +//@ ens true; +//@ on_unwind_ens false; +{ + unsafe { std::intrinsics::unchecked_add(value, 1) } +} diff --git a/verifast-proofs/core/iter/adapters/backend/add-unchecked.patch b/verifast-proofs/core/iter/adapters/backend/add-unchecked.patch new file mode 100644 index 0000000000000..bb2abe4b1ddde --- /dev/null +++ b/verifast-proofs/core/iter/adapters/backend/add-unchecked.patch @@ -0,0 +1,12 @@ +--- a/src/rust_frontend/vf_mir_exporter/src/lib.rs ++++ b/src/rust_frontend/vf_mir_exporter/src/lib.rs +@@ -2422,6 +2422,9 @@ + fn encode_bin_op(bin_op: mir::BinOp, mut bin_op_cpn: bin_op_cpn::Builder<'_>) { + match bin_op { + mir::BinOp::Add => bin_op_cpn.set_add(()), ++ // VeriFast checks integer addition for overflow. Unchecked addition ++ // has the same result on that domain and is undefined outside it. ++ mir::BinOp::AddUnchecked => bin_op_cpn.set_add(()), + mir::BinOp::Sub => bin_op_cpn.set_sub(()), + mir::BinOp::Mul => bin_op_cpn.set_mul(()), + mir::BinOp::Div => bin_op_cpn.set_div(()), diff --git a/verifast-proofs/core/iter/adapters/backend/add-valid.rs b/verifast-proofs/core/iter/adapters/backend/add-valid.rs new file mode 100644 index 0000000000000..1c9b0a7737b46 --- /dev/null +++ b/verifast-proofs/core/iter/adapters/backend/add-valid.rs @@ -0,0 +1,11 @@ +#![crate_type = "lib"] +#![crate_name = "add_valid"] +#![feature(core_intrinsics)] + +unsafe fn successor(value: usize) -> usize +//@ req value < usize::MAX; +//@ ens result == value + 1; +//@ on_unwind_ens false; +{ + unsafe { std::intrinsics::unchecked_add(value, 1) } +} diff --git a/verifast-proofs/core/iter/adapters/backend/prepare.sh b/verifast-proofs/core/iter/adapters/backend/prepare.sh new file mode 100644 index 0000000000000..d07522bd6a9d4 --- /dev/null +++ b/verifast-proofs/core/iter/adapters/backend/prepare.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ "$(uname -s)" != Linux || "${1:-}" != --remote ]]; then + echo 'The frontend build is restricted to the remote Linux verification job.' >&2 + exit 2 +fi + +backend_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +source_commit=809de4596839b739dd999f9e462242c835e3af46 +source_hash=0238aec44351c877b3c859c7252340540923c1b620f9981be176d7cea4e67a1e +build_dir="$(mktemp -d "${TMPDIR:-/tmp}/verifast-iter-backend.XXXXXXXX")" +trap 'rm -rf -- "$build_dir"' EXIT + +command -v capnp >/dev/null +curl --fail --location --retry 2 --max-time 120 --max-filesize 67108864 \ + --output "$build_dir/source.tar.gz" \ + "https://codeload.github.com/verifast/verifast/tar.gz/$source_commit" +printf '%s %s\n' "$source_hash" "$build_dir/source.tar.gz" | sha256sum --check +tar -xzf "$build_dir/source.tar.gz" --strip-components=1 -C "$build_dir" \ + "verifast-$source_commit/src/rust_frontend" +patch --batch --fuzz=0 --directory="$build_dir" -p1 < "$backend_dir/add-unchecked.patch" + +rustup component add --toolchain nightly-2026-02-05 rustc-dev llvm-tools +CARGO_BUILD_JOBS=1 CARGO_PROFILE_DEV_DEBUG=0 RUSTFLAGS='-C rpath=yes' \ + cargo +nightly-2026-02-05 build --locked --jobs 1 \ + --manifest-path "$build_dir/src/rust_frontend/vf_mir_exporter/Cargo.toml" +install -m 755 "$build_dir/src/rust_frontend/vf_mir_exporter/target/debug/vf_mir_exporter" \ + "${VERIFAST_HOME:?}/bin/vf-rust-mir-exporter" +echo 'Prepared VeriFast 26.09 with the checked AddUnchecked frontend mapping' diff --git a/verifast-proofs/core/iter/adapters/verify.sh b/verifast-proofs/core/iter/adapters/verify.sh index 8b83a56eec77d..ea31cce5e8e7b 100644 --- a/verifast-proofs/core/iter/adapters/verify.sh +++ b/verifast-proofs/core/iter/adapters/verify.sh @@ -41,13 +41,44 @@ PY command -v timeout >/dev/null ulimit -c 0 -ulimit -v 2097152 ulimit -t 600 -export VFVERSION=25.11 +export VFVERSION=26.09 export CARGO_BUILD_JOBS=1 export RAYON_NUM_THREADS=1 export PATH="$proof_dir/../../..:$PATH" +# Install the pinned release and build only its patched MIR exporter remotely. +# The workflow's cgroup also covers this build. Proof processes get the tighter +# per-process address-space limit after the compiler has finished. +export VFPLATFORM=linux +# shellcheck source=/dev/null +source "$proof_dir/../../../setup-verifast-home" +export VERIFAST_HOME +timeout --signal=TERM --kill-after=10s 900s bash backend/prepare.sh --remote +ulimit -v 2097152 + +# Check both the defined-input case and rejection of a potentially overflowing +# input before trusting the frontend mapping for the adapter proof. +timeout --signal=TERM --kill-after=10s 60s \ + verifast -rustc_args '--edition 2024' backend/add-valid.rs +negative_log="$(mktemp)" +trap 'rm -f -- "$negative_log"' EXIT +if timeout --signal=TERM --kill-after=10s 60s \ + verifast -rustc_args '--edition 2024' backend/add-overflow.rs >"$negative_log" 2>&1; then + cat "$negative_log" + echo 'The frontend accepted unchecked addition without an overflow precondition.' >&2 + exit 1 +fi +cat "$negative_log" +python3 -I - "$negative_log" <<'PY' +from pathlib import Path +import sys + +diagnostics = Path(sys.argv[1]).read_text() +if "Potential arithmetic overflow." not in diagnostics or "Rust frontend failed" in diagnostics: + sys.exit("The negative check did not produce the required overflow diagnostic.") +PY + # Keep every stage sequential and fail on any unsuccessful proof or refinement. # No assumption, unwind, reference-creation, or overflow suppression flags. timeout --signal=TERM --kill-after=10s 600s \ From d78acdad238e7240b1ebef94ccbb3abb54660b41 Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Sun, 13 Sep 2026 00:34:20 -0700 Subject: [PATCH 09/62] Translate symbolic usize const parameters in the hosted Rust frontend Signed-off-by: Onyeka Obi --- verifast-proofs/core/iter/adapters/README.md | 18 ++- .../adapters/backend/const-generics.patch | 110 ++++++++++++++++++ .../iter/adapters/backend/const-invalid.rs | 14 +++ .../core/iter/adapters/backend/const-valid.rs | 14 +++ .../core/iter/adapters/backend/prepare.sh | 31 ++++- verifast-proofs/core/iter/adapters/verify.sh | 19 ++- 6 files changed, 199 insertions(+), 7 deletions(-) create mode 100644 verifast-proofs/core/iter/adapters/backend/const-generics.patch create mode 100644 verifast-proofs/core/iter/adapters/backend/const-invalid.rs create mode 100644 verifast-proofs/core/iter/adapters/backend/const-valid.rs diff --git a/verifast-proofs/core/iter/adapters/README.md b/verifast-proofs/core/iter/adapters/README.md index 35101050549a7..0cf96fc2043b4 100644 --- a/verifast-proofs/core/iter/adapters/README.md +++ b/verifast-proofs/core/iter/adapters/README.md @@ -112,10 +112,20 @@ Before checking the adapters, the runner requires a valid successor proof and an explicit overflow diagnostic for the same operation without its range precondition. A frontend crash does not count as the negative result. -`backend/prepare.sh` pins both the source commit and archive hash. It builds -only the MIR exporter on the hosted Linux worker and replaces that worker's -exporter. It does not alter the verifier, its arithmetic rules, or library -contracts. Source projections and mandatory refinement remain unchanged. +The unchecked-add regression passed in hosted run +[34744234144](https://github.com/MavenRain/verify-rust-std/actions/runs/34744234144). +That run then reached the translator's unsupported const-parameter check. +`backend/const-generics.patch` adds symbolic `usize` const arguments and +array lengths using the existing `typeid`/`usize_of_const` representation. +It keeps const parameters distinct from Rust types and does not add `Sized` +bounds to them. The exporter rejects other const parameter types. A positive +symbolic-length proof and an incorrect-length negative test must pass before +the adapter contracts are checked. This extension awaits hosted validation. + +`backend/prepare.sh` pins the source commit, source archive hash, and upstream +dependency bundle hash. It builds the MIR exporter and the verifier's Rust +translator on the hosted Linux worker. Arithmetic rules and library contracts +are unchanged. Source projections and mandatory refinement remain unchanged. ## Local static checks and optional manual verification diff --git a/verifast-proofs/core/iter/adapters/backend/const-generics.patch b/verifast-proofs/core/iter/adapters/backend/const-generics.patch new file mode 100644 index 0000000000000..753cc8bbb63d8 --- /dev/null +++ b/verifast-proofs/core/iter/adapters/backend/const-generics.patch @@ -0,0 +1,110 @@ +--- a/src/rust_frontend/vf_mir_exporter/src/lib.rs ++++ b/src/rust_frontend/vf_mir_exporter/src/lib.rs +@@ -1539,6 +1539,11 @@ + p: &hir::GenericParam, + mut p_cpn: hir_generic_param_cpn::Builder<'_>, + ) { ++ if matches!(p.kind, hir::GenericParamKind::Const { .. }) ++ && enc_ctx.tcx.type_of(p.def_id).instantiate_identity() != enc_ctx.tcx.types.usize ++ { ++ enc_ctx.tcx.dcx().span_fatal(p.span, "Only usize const parameters are supported"); ++ } + let name_cpn = p_cpn.reborrow().init_name(); + Self::encode_hir_generic_param_name(enc_ctx, p.def_id, &p.name, name_cpn); + let span_cpn = p_cpn.reborrow().init_span(); +--- a/src/rust_frontend/vf_mir_translator/vf_mir_translator.ml ++++ b/src/rust_frontend/vf_mir_translator/vf_mir_translator.ml +@@ -801,8 +801,9 @@ + let targs = + gen_args + |> Util.flatmap @@ function +- | Mir.GenArgType arg_ty -> [ arg_ty.vf_ty ] +- | _ -> [] ++ | Mir.GenArgType arg_ty -> [ arg_ty.vf_ty ] ++ | Mir.GenArgConst const_ty -> [ const_ty ] ++ | _ -> [] + in + let lft_args = + gen_args +@@ -1247,6 +1248,7 @@ + | Const ty_const_cpn -> + let ty_expr = + match ty_const_cpn.kind with ++ | Param {name; index} -> Ast.IdentTypeExpr (loc, None, name) + | Value {ty; val_tree} -> + begin match val_tree with + | Leaf {data; size} -> +@@ -1272,7 +1274,7 @@ + match get (kind_get gen_param_cpn) with + | Type -> `Type name + | Lifetime -> `Lifetime name +- | Const -> failwith "Const generic parameters are not yet supported" ++ | Const -> `Const name + + and decode_generic_arg (gen_arg_cpn : D.generic_arg) = + let kind_cpn = gen_arg_cpn.kind in +@@ -1641,7 +1643,10 @@ + and translate_ty_const_kind (ck_cpn : D.const_kind) (loc : Ast.loc) = + let open VfMirRd.ConstKind in + match ck_cpn with +- | Param _ -> Ast.static_error loc "Todo: ConstKind::Param" None ++ | Param {name; index} -> ++ Ok (Ast.CallExpr (loc, "usize_of_const", [], [], ++ [Ast.LitPat (Ast.Typeid (loc, Ast.TypeExpr (Ast.IdentTypeExpr (loc, None, name))))], ++ Ast.Static)) + | Infer -> Ast.static_error loc "Todo: ConstKind::Infer" None + | Bound -> Ast.static_error loc "Todo: ConstKind::Bound" None + | Placeholder -> Ast.static_error loc "Todo: ConstKind::Placeholder" None +@@ -1666,12 +1671,9 @@ + let* elem_ty_info = translate_ty elem_ty_cpn loc in + let elem_ty = elem_ty_info.vf_ty in + let len_cpn = array_ty_cpn.size in +- let* (CastExpr (_, _, IntLit (_, len, _, _, _))) = +- translate_ty_const len_cpn loc +- in ++ let* Mir.GenArgConst len_ty = translate_generic_arg {kind = Const len_cpn} loc in + let vf_ty = +- Ast.StaticArrayTypeExpr +- (loc, elem_ty, LiteralConstTypeExpr (loc, Z.of_big_int len)) ++ Ast.StaticArrayTypeExpr (loc, elem_ty, len_ty) + in + let size = Ast.SizeofExpr (loc, TypeExpr vf_ty) in + let own tid vs = +@@ -5993,17 +5995,18 @@ + with + | Lifetime -> Ok (`Lifetime name) + | Type -> Ok (`Type name) +- | Const -> +- raise +- (Ast.StaticError +- ( l, +- "Structs with const parameters are not yet supported", +- None )) ++ | Const -> ++ Ok (`Const name) + in + let tparams = +- Util.flatmap (function `Type x -> [ x ] | _ -> []) generics ++ Util.flatmap (function `Type x | `Const x -> [ x ] | `Lifetime _ -> []) generics + in +- let tparams_with_bounds = Verifast0.tparams_with_default_bounds_exprs tparams in ++ let const_params = ++ Util.flatmap (function `Const x -> [ x ] | `Type _ | `Lifetime _ -> []) generics ++ in ++ let tparams_with_bounds = ++ List.map (fun x -> (x, {Ast.sized = not (List.mem x const_params)})) tparams ++ in + let lft_params = + Util.flatmap (function `Lifetime x -> [ x ] | _ -> []) generics + in +@@ -6029,7 +6032,9 @@ + |> List.map decode_predicate + in + let sized_tparams = compute_sized_tparams preds in +- let unsized_tparams = List.filter (fun x -> not (List.mem x sized_tparams)) tparams in ++ let unsized_tparams = ++ List.filter (fun x -> not (List.mem x sized_tparams || List.mem x const_params)) tparams ++ in + let send_tparams = compute_send_tparams preds in + let tparams_targs = + List.map (fun x -> Ast.IdentTypeExpr (def_loc, None, x)) vf_tparams diff --git a/verifast-proofs/core/iter/adapters/backend/const-invalid.rs b/verifast-proofs/core/iter/adapters/backend/const-invalid.rs new file mode 100644 index 0000000000000..c95d465b6b173 --- /dev/null +++ b/verifast-proofs/core/iter/adapters/backend/const-invalid.rs @@ -0,0 +1,14 @@ +#![crate_type = "lib"] +#![crate_name = "const_invalid"] + +struct Window { + values: [T; N], +} + +fn width(_: &Window) -> usize +//@ req true; +//@ ens result == usize_of_const(typeid(N)) + 1; +//@ on_unwind_ens false; +{ + N +} diff --git a/verifast-proofs/core/iter/adapters/backend/const-valid.rs b/verifast-proofs/core/iter/adapters/backend/const-valid.rs new file mode 100644 index 0000000000000..21ccb291ee377 --- /dev/null +++ b/verifast-proofs/core/iter/adapters/backend/const-valid.rs @@ -0,0 +1,14 @@ +#![crate_type = "lib"] +#![crate_name = "const_valid"] + +struct Window { + values: [T; N], +} + +fn width(_: &Window) -> usize +//@ req true; +//@ ens result == usize_of_const(typeid(N)); +//@ on_unwind_ens false; +{ + N +} diff --git a/verifast-proofs/core/iter/adapters/backend/prepare.sh b/verifast-proofs/core/iter/adapters/backend/prepare.sh index d07522bd6a9d4..7854c42677a77 100644 --- a/verifast-proofs/core/iter/adapters/backend/prepare.sh +++ b/verifast-proofs/core/iter/adapters/backend/prepare.sh @@ -18,13 +18,40 @@ curl --fail --location --retry 2 --max-time 120 --max-filesize 67108864 \ "https://codeload.github.com/verifast/verifast/tar.gz/$source_commit" printf '%s %s\n' "$source_hash" "$build_dir/source.tar.gz" | sha256sum --check tar -xzf "$build_dir/source.tar.gz" --strip-components=1 -C "$build_dir" \ - "verifast-$source_commit/src/rust_frontend" + "verifast-$source_commit/src" "verifast-$source_commit/bin" patch --batch --fuzz=0 --directory="$build_dir" -p1 < "$backend_dir/add-unchecked.patch" +patch --batch --fuzz=0 --directory="$build_dir" -p1 < "$backend_dir/const-generics.patch" + +# Use the dependency bundle pinned by upstream's setup-build.sh. Its compiler +# and package paths are built for /tmp/vfdeps-adf88dc on Linux. +curl --fail --location --retry 2 --max-time 180 --max-filesize 536870912 \ + --output "$build_dir/deps.txz" \ + https://github.com/verifast/vfdeps/releases/download/25.01/vfdeps-adf88dc-linux.txz +printf '%s %s\n' \ + 8d022c93d51a1d13ec1e782d767c60462405f6865d5ee416f82d6234e93ee580 \ + "$build_dir/deps.txz" | sha256sum --check +tar -xjf "$build_dir/deps.txz" --directory=/tmp +export PATH="/tmp/vfdeps-adf88dc/bin:$PATH" +export CAPNP_INCLUDE=/tmp/vfdeps-adf88dc/include +export CAPNP_INC_DIR="$CAPNP_INCLUDE" +# The dynamic linker expands this token after the executable is installed. +# shellcheck disable=SC2016 +export OCAMLOPT_CCLIB_FLAGS='-Wl,-rpath=$ORIGIN' +export Z3_DLL_DIR=/tmp/vfdeps-adf88dc/lib rustup component add --toolchain nightly-2026-02-05 rustc-dev llvm-tools +CARGO_BUILD_JOBS=1 cargo +nightly-2026-02-05 install --locked --jobs 1 \ + --git https://github.com/btj/capnpc-ocaml-decoder \ + --rev 2d6606d9b59cd0c88a66729f3f076c10c0c8e0b2 --root "$build_dir/decoder" +export PATH="$build_dir/decoder/bin:$PATH" CARGO_BUILD_JOBS=1 CARGO_PROFILE_DEV_DEBUG=0 RUSTFLAGS='-C rpath=yes' \ cargo +nightly-2026-02-05 build --locked --jobs 1 \ --manifest-path "$build_dir/src/rust_frontend/vf_mir_exporter/Cargo.toml" install -m 755 "$build_dir/src/rust_frontend/vf_mir_exporter/target/debug/vf_mir_exporter" \ "${VERIFAST_HOME:?}/bin/vf-rust-mir-exporter" -echo 'Prepared VeriFast 26.09 with the checked AddUnchecked frontend mapping' +( + cd "$build_dir/src" + dune build --jobs 1 vfconsole/vfconsole.exe +) +install -m 755 "$build_dir/src/_build/default/vfconsole/vfconsole.exe" "$VERIFAST_HOME/bin/verifast" +echo 'Prepared VeriFast 26.09 with checked addition and symbolic usize const parameters' diff --git a/verifast-proofs/core/iter/adapters/verify.sh b/verifast-proofs/core/iter/adapters/verify.sh index ea31cce5e8e7b..5083551983b67 100644 --- a/verifast-proofs/core/iter/adapters/verify.sh +++ b/verifast-proofs/core/iter/adapters/verify.sh @@ -47,7 +47,7 @@ export CARGO_BUILD_JOBS=1 export RAYON_NUM_THREADS=1 export PATH="$proof_dir/../../..:$PATH" -# Install the pinned release and build only its patched MIR exporter remotely. +# Install the pinned release and build its patched Rust frontend remotely. # The workflow's cgroup also covers this build. Proof processes get the tighter # per-process address-space limit after the compiler has finished. export VFPLATFORM=linux @@ -61,6 +61,8 @@ ulimit -v 2097152 # input before trusting the frontend mapping for the adapter proof. timeout --signal=TERM --kill-after=10s 60s \ verifast -rustc_args '--edition 2024' backend/add-valid.rs +timeout --signal=TERM --kill-after=10s 60s \ + verifast -rustc_args '--edition 2024' backend/const-valid.rs negative_log="$(mktemp)" trap 'rm -f -- "$negative_log"' EXIT if timeout --signal=TERM --kill-after=10s 60s \ @@ -78,6 +80,21 @@ diagnostics = Path(sys.argv[1]).read_text() if "Potential arithmetic overflow." not in diagnostics or "Rust frontend failed" in diagnostics: sys.exit("The negative check did not produce the required overflow diagnostic.") PY +if timeout --signal=TERM --kill-after=10s 60s \ + verifast -rustc_args '--edition 2024' backend/const-invalid.rs >"$negative_log" 2>&1; then + cat "$negative_log" + echo 'The frontend accepted an incorrect symbolic array length.' >&2 + exit 1 +fi +cat "$negative_log" +python3 -I - "$negative_log" <<'PY' +from pathlib import Path +import sys + +diagnostics = Path(sys.argv[1]).read_text() +if "Cannot prove" not in diagnostics or "Rust frontend failed" in diagnostics: + sys.exit("The const-parameter negative check did not reach a proof failure.") +PY # Keep every stage sequential and fail on any unsuccessful proof or refinement. # No assumption, unwind, reference-creation, or overflow suppression flags. From 4f9e3fccb007f442c47e10f11b6c6c3cd0a1edf0 Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Sun, 13 Sep 2026 00:39:37 -0700 Subject: [PATCH 10/62] Use the bundled Dune job-count option Signed-off-by: Onyeka Obi --- verifast-proofs/core/iter/adapters/backend/prepare.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/verifast-proofs/core/iter/adapters/backend/prepare.sh b/verifast-proofs/core/iter/adapters/backend/prepare.sh index 7854c42677a77..06686821bdaec 100644 --- a/verifast-proofs/core/iter/adapters/backend/prepare.sh +++ b/verifast-proofs/core/iter/adapters/backend/prepare.sh @@ -51,7 +51,7 @@ install -m 755 "$build_dir/src/rust_frontend/vf_mir_exporter/target/debug/vf_mir "${VERIFAST_HOME:?}/bin/vf-rust-mir-exporter" ( cd "$build_dir/src" - dune build --jobs 1 vfconsole/vfconsole.exe + dune build -j 1 vfconsole/vfconsole.exe ) install -m 755 "$build_dir/src/_build/default/vfconsole/vfconsole.exe" "$VERIFAST_HOME/bin/verifast" echo 'Prepared VeriFast 26.09 with checked addition and symbolic usize const parameters' From 16fbcd88924f19200f28ee803de46c8540da8d0e Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Sun, 13 Sep 2026 00:56:53 -0700 Subject: [PATCH 11/62] Fix VeriFast contract clauses and cache the compiled frontend Signed-off-by: Onyeka Obi --- .github/workflows/verifast-iter-adapters.yml | 14 ++++++++++++ .../core/iter/adapters/backend/prepare.sh | 17 +++++++++++++- .../iter/adapters/verified/map_windows.rs | 22 ++++++++++++++----- 3 files changed, 46 insertions(+), 7 deletions(-) diff --git a/.github/workflows/verifast-iter-adapters.yml b/.github/workflows/verifast-iter-adapters.yml index a71ff87d5dd93..a723985302579 100644 --- a/.github/workflows/verifast-iter-adapters.yml +++ b/.github/workflows/verifast-iter-adapters.yml @@ -56,6 +56,13 @@ jobs: sudo --non-interactive apt-get update -qq sudo --non-interactive apt-get install -y --no-install-recommends capnproto + - name: Restore compiled frontend + id: backend-cache + uses: actions/cache/restore@v4 + with: + path: ~/.cache/verifast-iter-adapters + key: ${{ runner.os }}-verifast-iter-v1-${{ hashFiles('verifast-proofs/core/iter/adapters/backend/prepare.sh', 'verifast-proofs/core/iter/adapters/backend/*.patch') }} + - name: Verify contracts and source refinement run: | # A service cgroup limits the entire compiler/verifier process tree. @@ -77,3 +84,10 @@ jobs: --property=RuntimeMaxSec=1500 \ --property=TimeoutStopSec=15 \ /usr/bin/bash verifast-proofs/core/iter/adapters/verify.sh --remote + + - name: Save compiled frontend + if: ${{ always() && !cancelled() && steps.backend-cache.outputs.cache-hit != 'true' }} + uses: actions/cache/save@v4 + with: + path: ~/.cache/verifast-iter-adapters + key: ${{ steps.backend-cache.outputs.cache-primary-key }} diff --git a/verifast-proofs/core/iter/adapters/backend/prepare.sh b/verifast-proofs/core/iter/adapters/backend/prepare.sh index 06686821bdaec..693a506512560 100644 --- a/verifast-proofs/core/iter/adapters/backend/prepare.sh +++ b/verifast-proofs/core/iter/adapters/backend/prepare.sh @@ -7,6 +7,18 @@ if [[ "$(uname -s)" != Linux || "${1:-}" != --remote ]]; then fi backend_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +# The cached exporter still needs the pinned toolchain's shared libraries. +rustup component add --toolchain nightly-2026-02-05 rustc-dev llvm-tools +cache_key="$(sha256sum "$backend_dir/prepare.sh" "$backend_dir/add-unchecked.patch" \ + "$backend_dir/const-generics.patch" | sha256sum | cut -d ' ' -f 1)" +cache_dir="$HOME/.cache/verifast-iter-adapters/$cache_key" +if [[ -f "$cache_dir/checksums" ]]; then + (cd "$cache_dir" && sha256sum --check checksums) + install -m 755 "$cache_dir/verifast" "${VERIFAST_HOME:?}/bin/verifast" + install -m 755 "$cache_dir/vf-rust-mir-exporter" "$VERIFAST_HOME/bin/vf-rust-mir-exporter" + echo 'Restored the patched frontend; all proof and regression checks will run' + exit 0 +fi source_commit=809de4596839b739dd999f9e462242c835e3af46 source_hash=0238aec44351c877b3c859c7252340540923c1b620f9981be176d7cea4e67a1e build_dir="$(mktemp -d "${TMPDIR:-/tmp}/verifast-iter-backend.XXXXXXXX")" @@ -39,7 +51,6 @@ export CAPNP_INC_DIR="$CAPNP_INCLUDE" export OCAMLOPT_CCLIB_FLAGS='-Wl,-rpath=$ORIGIN' export Z3_DLL_DIR=/tmp/vfdeps-adf88dc/lib -rustup component add --toolchain nightly-2026-02-05 rustc-dev llvm-tools CARGO_BUILD_JOBS=1 cargo +nightly-2026-02-05 install --locked --jobs 1 \ --git https://github.com/btj/capnpc-ocaml-decoder \ --rev 2d6606d9b59cd0c88a66729f3f076c10c0c8e0b2 --root "$build_dir/decoder" @@ -54,4 +65,8 @@ install -m 755 "$build_dir/src/rust_frontend/vf_mir_exporter/target/debug/vf_mir dune build -j 1 vfconsole/vfconsole.exe ) install -m 755 "$build_dir/src/_build/default/vfconsole/vfconsole.exe" "$VERIFAST_HOME/bin/verifast" +mkdir -p "$cache_dir" +install -m 755 "$VERIFAST_HOME/bin/verifast" "$cache_dir/verifast" +install -m 755 "$VERIFAST_HOME/bin/vf-rust-mir-exporter" "$cache_dir/vf-rust-mir-exporter" +(cd "$cache_dir" && sha256sum verifast vf-rust-mir-exporter > checksums) echo 'Prepared VeriFast 26.09 with checked addition and symbolic usize const parameters' diff --git a/verifast-proofs/core/iter/adapters/verified/map_windows.rs b/verifast-proofs/core/iter/adapters/verified/map_windows.rs index c9461b0634e44..47914967b280e 100644 --- a/verifast-proofs/core/iter/adapters/verified/map_windows.rs +++ b/verifast-proofs/core/iter/adapters/verified/map_windows.rs @@ -104,11 +104,14 @@ impl Buffer { /*@ req [?f]bounds(self, ?start) &*& [?q]lifetime_token('a) &*& [_](<[T; N]>.share)('a, ?t, (base(self) + start) as *[T; N]); + @*/ + /*@ ens [f]bounds(self, start) &*& [q]lifetime_token('a) &*& result == (base(self) + start) as *[T; N] &*& [_](<[T; N]>.share)('a, t, result); - on_unwind_ens false; - @*/ { + @*/ + //@ on_unwind_ens false; + { //@ open [f]bounds(self, start); debug_assert!(self.start + N <= 2 * N); @@ -124,11 +127,14 @@ impl Buffer { req bounds(self, ?start) &*& [?q]lifetime_token('a) &*& full_borrow('a, >.full_borrow_content(?t, (base(self) + start) as *MaybeUninit<[T; N]>)); + @*/ + /*@ ens bounds(self, start) &*& [q]lifetime_token('a) &*& result == (base(self) + start) as *MaybeUninit<[T; N]> &*& full_borrow('a, >.full_borrow_content(t, result)); - on_unwind_ens false; - @*/ { + @*/ + //@ on_unwind_ens false; + { //@ open bounds(self, start); debug_assert!(self.start + N <= 2 * N); @@ -145,10 +151,12 @@ impl Buffer { fn push(&mut self, next: T) /*@ req thread_token(?t) &*& live(t, self, ?start, ?values) &*& .own(t, next); + @*/ + /*@ ens thread_token(t) &*& live(t, self, start == width::() ? 0 : start + 1, append(tail(values), cons(next, nil))); - on_unwind_ens thread_token(t); @*/ + //@ on_unwind_ens thread_token(t); { //@ open live(t, self, start, values); //@ open bounds(self, start); @@ -241,9 +249,11 @@ impl Drop for Buffer { fn drop(&mut self) /*@ req thread_token(?t) &*& live(t, self, ?start, ?values); + @*/ + /*@ ens thread_token(t) &*& storage(self, start); - on_unwind_ens thread_token(t); @*/ + //@ on_unwind_ens thread_token(t); { //@ open live(t, self, start, values); //@ open bounds(self, start); From d192fb8d9a3f0f0b9e33fa4c8d29417331751d58 Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Sun, 13 Sep 2026 01:09:18 -0700 Subject: [PATCH 12/62] Translate array borrow predicates with negative regression coverage Signed-off-by: Onyeka Obi --- .../iter/adapters/backend/array-invalid.rs | 10 ++++++ .../core/iter/adapters/backend/array-valid.rs | 18 ++++++++++ .../adapters/backend/const-generics.patch | 36 ++++++++++++++++--- verifast-proofs/core/iter/adapters/verify.sh | 17 +++++++++ 4 files changed, 76 insertions(+), 5 deletions(-) create mode 100644 verifast-proofs/core/iter/adapters/backend/array-invalid.rs create mode 100644 verifast-proofs/core/iter/adapters/backend/array-valid.rs diff --git a/verifast-proofs/core/iter/adapters/backend/array-invalid.rs b/verifast-proofs/core/iter/adapters/backend/array-invalid.rs new file mode 100644 index 0000000000000..292e94cf1d7e1 --- /dev/null +++ b/verifast-proofs/core/iter/adapters/backend/array-invalid.rs @@ -0,0 +1,10 @@ +#![crate_type = "lib"] +#![crate_name = "array_invalid"] + +unsafe fn shared<'a, T, const N: usize>(p: *const [T; N]) -> &'a [T; N] +//@ req thread_token(?t) &*& [?q]lifetime_token('a); +//@ ens thread_token(t) &*& [q]lifetime_token('a) &*& result == p; +//@ on_unwind_ens false; +{ + unsafe { &*p } +} diff --git a/verifast-proofs/core/iter/adapters/backend/array-valid.rs b/verifast-proofs/core/iter/adapters/backend/array-valid.rs new file mode 100644 index 0000000000000..1b5255dd7a86f --- /dev/null +++ b/verifast-proofs/core/iter/adapters/backend/array-valid.rs @@ -0,0 +1,18 @@ +#![crate_type = "lib"] +#![crate_name = "array_valid"] + +unsafe fn shared<'a, T, const N: usize>(p: *const [T; N]) -> &'a [T; N] +//@ req thread_token(?t) &*& [?q]lifetime_token('a) &*& [_](<[T; N]>.share)('a, t, p); +//@ ens thread_token(t) &*& [q]lifetime_token('a) &*& result == p &*& [_](<[T; N]>.share)('a, t, result); +//@ on_unwind_ens false; +{ + unsafe { &*p } +} + +unsafe fn exclusive<'a, T, const N: usize>(p: *mut [T; N]) -> &'a mut [T; N] +//@ req thread_token(?t) &*& [?q]lifetime_token('a) &*& full_borrow('a, <[T; N]>.full_borrow_content(t, p)); +//@ ens thread_token(t) &*& [q]lifetime_token('a) &*& result == p &*& full_borrow('a, <[T; N]>.full_borrow_content(t, result)); +//@ on_unwind_ens false; +{ + unsafe { &mut *p } +} diff --git a/verifast-proofs/core/iter/adapters/backend/const-generics.patch b/verifast-proofs/core/iter/adapters/backend/const-generics.patch index 753cc8bbb63d8..cc6f1931878cb 100644 --- a/verifast-proofs/core/iter/adapters/backend/const-generics.patch +++ b/verifast-proofs/core/iter/adapters/backend/const-generics.patch @@ -55,7 +55,7 @@ | Infer -> Ast.static_error loc "Todo: ConstKind::Infer" None | Bound -> Ast.static_error loc "Todo: ConstKind::Bound" None | Placeholder -> Ast.static_error loc "Todo: ConstKind::Placeholder" None -@@ -1666,12 +1671,9 @@ +@@ -1666,20 +1671,18 @@ let* elem_ty_info = translate_ty elem_ty_cpn loc in let elem_ty = elem_ty_info.vf_ty in let len_cpn = array_ty_cpn.size in @@ -70,7 +70,32 @@ in let size = Ast.SizeofExpr (loc, TypeExpr vf_ty) in let own tid vs = -@@ -5993,17 +5995,18 @@ +- Error "Expressing ownership of an array is not yet supported" ++ Ok (Ast.ExprCallExpr (loc, Ast.TypePredExpr (loc, vf_ty, "own"), ++ [Ast.LitPat tid; Ast.LitPat vs])) + in + let full_bor_content t l = +- Error +- "Expressing the full borrow content of an array is not yet supported" ++ Ok (Ast.ExprCallExpr (loc, Ast.TypePredExpr (loc, vf_ty, "full_borrow_content"), ++ [Ast.LitPat t; Ast.LitPat l])) + in + let points_to tid l vid_op = + let* pat = RustBelt.Aux.vid_op_to_var_pat vid_op loc in +@@ -1692,9 +1695,9 @@ + own; + shr = + (fun k t l -> +- Error +- "Expressing the shared ownership of an array is not yet \ +- supported"); ++ Ok (Ast.CoefAsn (loc, Ast.DummyPat, ++ Ast.ExprCallExpr (loc, Ast.TypePredExpr (loc, vf_ty, "share"), ++ [Ast.LitPat k; Ast.LitPat t; Ast.LitPat l])))); + full_bor_content; + points_to; + pointee_fbc = None; +@@ -5993,17 +5996,18 @@ with | Lifetime -> Ok (`Lifetime name) | Type -> Ok (`Type name) @@ -85,9 +110,10 @@ in let tparams = - Util.flatmap (function `Type x -> [ x ] | _ -> []) generics -+ Util.flatmap (function `Type x | `Const x -> [ x ] | `Lifetime _ -> []) generics - in +- in - let tparams_with_bounds = Verifast0.tparams_with_default_bounds_exprs tparams in ++ Util.flatmap (function `Type x | `Const x -> [ x ] | `Lifetime _ -> []) generics ++ in + let const_params = + Util.flatmap (function `Const x -> [ x ] | `Type _ | `Lifetime _ -> []) generics + in @@ -97,7 +123,7 @@ let lft_params = Util.flatmap (function `Lifetime x -> [ x ] | _ -> []) generics in -@@ -6029,7 +6032,9 @@ +@@ -6029,7 +6033,9 @@ |> List.map decode_predicate in let sized_tparams = compute_sized_tparams preds in diff --git a/verifast-proofs/core/iter/adapters/verify.sh b/verifast-proofs/core/iter/adapters/verify.sh index 5083551983b67..f79a0fbeaf22c 100644 --- a/verifast-proofs/core/iter/adapters/verify.sh +++ b/verifast-proofs/core/iter/adapters/verify.sh @@ -63,6 +63,8 @@ timeout --signal=TERM --kill-after=10s 60s \ verifast -rustc_args '--edition 2024' backend/add-valid.rs timeout --signal=TERM --kill-after=10s 60s \ verifast -rustc_args '--edition 2024' backend/const-valid.rs +timeout --signal=TERM --kill-after=10s 60s \ + verifast -rustc_args '--edition 2024' backend/array-valid.rs negative_log="$(mktemp)" trap 'rm -f -- "$negative_log"' EXIT if timeout --signal=TERM --kill-after=10s 60s \ @@ -95,6 +97,21 @@ diagnostics = Path(sys.argv[1]).read_text() if "Cannot prove" not in diagnostics or "Rust frontend failed" in diagnostics: sys.exit("The const-parameter negative check did not reach a proof failure.") PY +if timeout --signal=TERM --kill-after=10s 60s \ + verifast -rustc_args '--edition 2024' backend/array-invalid.rs >"$negative_log" 2>&1; then + cat "$negative_log" + echo 'The frontend accepted an array reference without a shared borrow.' >&2 + exit 1 +fi +cat "$negative_log" +python3 -I - "$negative_log" <<'PY' +from pathlib import Path +import sys + +diagnostics = Path(sys.argv[1]).read_text() +if "No matching heap chunks" not in diagnostics or "Rust frontend failed" in diagnostics: + sys.exit("The array-reference negative check did not reach a borrow proof failure.") +PY # Keep every stage sequential and fail on any unsuccessful proof or refinement. # No assumption, unwind, reference-creation, or overflow suppression flags. From 3641c2b93f7769f924791848d1afcae22f185816 Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Sun, 13 Sep 2026 01:15:16 -0700 Subject: [PATCH 13/62] Account for reference provenance in array regression contracts Signed-off-by: Onyeka Obi --- .../core/iter/adapters/backend/array-invalid.rs | 5 +++-- .../core/iter/adapters/backend/array-valid.rs | 9 +++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/verifast-proofs/core/iter/adapters/backend/array-invalid.rs b/verifast-proofs/core/iter/adapters/backend/array-invalid.rs index 292e94cf1d7e1..38cfdd121aa0f 100644 --- a/verifast-proofs/core/iter/adapters/backend/array-invalid.rs +++ b/verifast-proofs/core/iter/adapters/backend/array-invalid.rs @@ -2,9 +2,10 @@ #![crate_name = "array_invalid"] unsafe fn shared<'a, T, const N: usize>(p: *const [T; N]) -> &'a [T; N] -//@ req thread_token(?t) &*& [?q]lifetime_token('a); -//@ ens thread_token(t) &*& [q]lifetime_token('a) &*& result == p; +//@ req thread_token(?t) &*& [?q]lifetime_token('a) &*& [?r]ref_initialized(p); +//@ ens thread_token(t) &*& [q]lifetime_token('a) &*& result == p &*& [_](<[T; N]>.share)('a, t, result) &*& [r]ref_initialized(p); //@ on_unwind_ens false; { + //@ reborrow_ref_(p); unsafe { &*p } } diff --git a/verifast-proofs/core/iter/adapters/backend/array-valid.rs b/verifast-proofs/core/iter/adapters/backend/array-valid.rs index 1b5255dd7a86f..89c4eb037ef06 100644 --- a/verifast-proofs/core/iter/adapters/backend/array-valid.rs +++ b/verifast-proofs/core/iter/adapters/backend/array-valid.rs @@ -2,16 +2,17 @@ #![crate_name = "array_valid"] unsafe fn shared<'a, T, const N: usize>(p: *const [T; N]) -> &'a [T; N] -//@ req thread_token(?t) &*& [?q]lifetime_token('a) &*& [_](<[T; N]>.share)('a, t, p); -//@ ens thread_token(t) &*& [q]lifetime_token('a) &*& result == p &*& [_](<[T; N]>.share)('a, t, result); +//@ req thread_token(?t) &*& [?q]lifetime_token('a) &*& [_](<[T; N]>.share)('a, t, p) &*& [?r]ref_initialized(p); +//@ ens thread_token(t) &*& [q]lifetime_token('a) &*& result == p &*& [_](<[T; N]>.share)('a, t, result) &*& [r]ref_initialized(p); //@ on_unwind_ens false; { + //@ reborrow_ref_(p); unsafe { &*p } } unsafe fn exclusive<'a, T, const N: usize>(p: *mut [T; N]) -> &'a mut [T; N] -//@ req thread_token(?t) &*& [?q]lifetime_token('a) &*& full_borrow('a, <[T; N]>.full_borrow_content(t, p)); -//@ ens thread_token(t) &*& [q]lifetime_token('a) &*& result == p &*& full_borrow('a, <[T; N]>.full_borrow_content(t, result)); +//@ req *p |-> ?values; +//@ ens *result |-> values &*& ref_mut_end_token(result, p); //@ on_unwind_ens false; { unsafe { &mut *p } From 104e6548ea443ee1c0062495c0a6f05ef029bc10 Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Sun, 13 Sep 2026 01:21:36 -0700 Subject: [PATCH 14/62] Enforce mutable reference creation checks in the adapter frontend Signed-off-by: Onyeka Obi --- .../iter/adapters/backend/array-mut-invalid.rs | 10 ++++++++++ .../iter/adapters/backend/const-generics.patch | 16 ++++++++++++++-- verifast-proofs/core/iter/adapters/verify.sh | 15 +++++++++++++++ 3 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 verifast-proofs/core/iter/adapters/backend/array-mut-invalid.rs diff --git a/verifast-proofs/core/iter/adapters/backend/array-mut-invalid.rs b/verifast-proofs/core/iter/adapters/backend/array-mut-invalid.rs new file mode 100644 index 0000000000000..21e04b4f6a19f --- /dev/null +++ b/verifast-proofs/core/iter/adapters/backend/array-mut-invalid.rs @@ -0,0 +1,10 @@ +#![crate_type = "lib"] +#![crate_name = "array_mut_invalid"] + +unsafe fn exclusive<'a, T, const N: usize>(p: *mut [T; N]) -> &'a mut [T; N] +//@ req true; +//@ ens true; +//@ on_unwind_ens false; +{ + unsafe { &mut *p } +} diff --git a/verifast-proofs/core/iter/adapters/backend/const-generics.patch b/verifast-proofs/core/iter/adapters/backend/const-generics.patch index cc6f1931878cb..9524783c56327 100644 --- a/verifast-proofs/core/iter/adapters/backend/const-generics.patch +++ b/verifast-proofs/core/iter/adapters/backend/const-generics.patch @@ -95,7 +95,19 @@ full_bor_content; points_to; pointee_fbc = None; -@@ -5993,17 +5996,18 @@ +@@ -3020,11 +3023,8 @@ + let* place_expr, place_is_mutable = translate_place place_cpn loc in + let (path, line, _), _ = Ast.lexed_loc loc in + let ignore_ref_creation = +- bor_kind = Mut +- || + not place_is_mutable && place_does_not_need_drop + || +- (* ignore &mut E for now *) + if TranslatorArgs.ignore_ref_creation then true + else + let directives = +@@ -5993,17 +5993,18 @@ with | Lifetime -> Ok (`Lifetime name) | Type -> Ok (`Type name) @@ -123,7 +135,7 @@ let lft_params = Util.flatmap (function `Lifetime x -> [ x ] | _ -> []) generics in -@@ -6029,7 +6033,9 @@ +@@ -6029,7 +6030,9 @@ |> List.map decode_predicate in let sized_tparams = compute_sized_tparams preds in diff --git a/verifast-proofs/core/iter/adapters/verify.sh b/verifast-proofs/core/iter/adapters/verify.sh index f79a0fbeaf22c..499d189743a31 100644 --- a/verifast-proofs/core/iter/adapters/verify.sh +++ b/verifast-proofs/core/iter/adapters/verify.sh @@ -112,6 +112,21 @@ diagnostics = Path(sys.argv[1]).read_text() if "No matching heap chunks" not in diagnostics or "Rust frontend failed" in diagnostics: sys.exit("The array-reference negative check did not reach a borrow proof failure.") PY +if timeout --signal=TERM --kill-after=10s 60s \ + verifast -rustc_args '--edition 2024' backend/array-mut-invalid.rs >"$negative_log" 2>&1; then + cat "$negative_log" + echo 'The frontend accepted a mutable array reference without owning its storage.' >&2 + exit 1 +fi +cat "$negative_log" +python3 -I - "$negative_log" <<'PY' +from pathlib import Path +import sys + +diagnostics = Path(sys.argv[1]).read_text() +if "No matching heap chunks" not in diagnostics or "Rust frontend failed" in diagnostics: + sys.exit("The mutable-array negative check did not reach a storage proof failure.") +PY # Keep every stage sequential and fail on any unsuccessful proof or refinement. # No assumption, unwind, reference-creation, or overflow suppression flags. From 0efac51f8908adfb5b1e544fcb810610ca4fa6cf Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Sun, 13 Sep 2026 01:28:48 -0700 Subject: [PATCH 15/62] Align adapter proofs with reference borrowing rules Signed-off-by: Onyeka Obi --- .../adapters/backend/array-mut-invalid.rs | 4 ++-- .../core/iter/adapters/backend/array-valid.rs | 4 ++-- .../iter/adapters/verified/map_windows.rs | 20 +++++++++++++------ verifast-proofs/core/iter/adapters/verify.sh | 13 +++++++++--- 4 files changed, 28 insertions(+), 13 deletions(-) diff --git a/verifast-proofs/core/iter/adapters/backend/array-mut-invalid.rs b/verifast-proofs/core/iter/adapters/backend/array-mut-invalid.rs index 21e04b4f6a19f..6567e310f09d2 100644 --- a/verifast-proofs/core/iter/adapters/backend/array-mut-invalid.rs +++ b/verifast-proofs/core/iter/adapters/backend/array-mut-invalid.rs @@ -2,8 +2,8 @@ #![crate_name = "array_mut_invalid"] unsafe fn exclusive<'a, T, const N: usize>(p: *mut [T; N]) -> &'a mut [T; N] -//@ req true; -//@ ens true; +//@ req thread_token(?t) &*& [?q]lifetime_token('a); +//@ ens thread_token(t) &*& [q]lifetime_token('a); //@ on_unwind_ens false; { unsafe { &mut *p } diff --git a/verifast-proofs/core/iter/adapters/backend/array-valid.rs b/verifast-proofs/core/iter/adapters/backend/array-valid.rs index 89c4eb037ef06..60c69a2f32929 100644 --- a/verifast-proofs/core/iter/adapters/backend/array-valid.rs +++ b/verifast-proofs/core/iter/adapters/backend/array-valid.rs @@ -11,8 +11,8 @@ unsafe fn shared<'a, T, const N: usize>(p: *const [T; N]) -> &'a [T; N] } unsafe fn exclusive<'a, T, const N: usize>(p: *mut [T; N]) -> &'a mut [T; N] -//@ req *p |-> ?values; -//@ ens *result |-> values &*& ref_mut_end_token(result, p); +//@ req thread_token(?t) &*& [?q]lifetime_token('a) &*& full_borrow('a, <[T; N]>.full_borrow_content(t, p)); +//@ ens thread_token(t) &*& [q]lifetime_token('a) &*& full_borrow('a, <[T; N]>.full_borrow_content(t, result)); //@ on_unwind_ens false; { unsafe { &mut *p } diff --git a/verifast-proofs/core/iter/adapters/verified/map_windows.rs b/verifast-proofs/core/iter/adapters/verified/map_windows.rs index 47914967b280e..df46a3eb017b5 100644 --- a/verifast-proofs/core/iter/adapters/verified/map_windows.rs +++ b/verifast-proofs/core/iter/adapters/verified/map_windows.rs @@ -103,11 +103,13 @@ impl Buffer { fn as_array_ref<'a>(&'a self) -> &'a [T; N] /*@ req [?f]bounds(self, ?start) &*& [?q]lifetime_token('a) &*& + type_interp::<[T; N]>() &*& [_](<[T; N]>.share)('a, ?t, (base(self) + start) as *[T; N]); @*/ /*@ ens [f]bounds(self, start) &*& [q]lifetime_token('a) &*& - result == (base(self) + start) as *[T; N] &*& + type_interp::<[T; N]>() &*& + ref_origin(result) == ref_origin((base(self) + start) as *[T; N]) &*& [_](<[T; N]>.share)('a, t, result); @*/ //@ on_unwind_ens false; @@ -116,7 +118,14 @@ impl Buffer { debug_assert!(self.start + N <= 2 * N); // SAFETY: our invariant guarantees these elements are initialized. + //@ let window = (base(self) + start) as *[T; N]; + //@ let reference = precreate_ref(window); + //@ init_ref_share('a, t, reference); + //@ let r = open_frac_borrow('a, ref_initialized_(reference), q); + //@ open [r]ref_initialized_(reference); let result = unsafe { &*self.buffer_ptr().add(self.start).cast() }; + //@ close [r]ref_initialized_(reference); + //@ close_frac_borrow(r, ref_initialized_(reference)); //@ close [f]bounds(self, start); result } @@ -124,13 +133,12 @@ impl Buffer { #[inline] fn as_uninit_array_mut<'a>(&'a mut self) -> &'a mut MaybeUninit<[T; N]> /*@ - req bounds(self, ?start) &*& [?q]lifetime_token('a) &*& - full_borrow('a, >.full_borrow_content(?t, + req thread_token(?t) &*& bounds(self, ?start) &*& [?q]lifetime_token('a) &*& + full_borrow('a, >.full_borrow_content(t, (base(self) + start) as *MaybeUninit<[T; N]>)); @*/ /*@ - ens bounds(self, start) &*& [q]lifetime_token('a) &*& - result == (base(self) + start) as *MaybeUninit<[T; N]> &*& + ens thread_token(t) &*& bounds(self, start) &*& [q]lifetime_token('a) &*& full_borrow('a, >.full_borrow_content(t, result)); @*/ //@ on_unwind_ens false; @@ -228,7 +236,7 @@ impl Buffer { //@ std::mem::open_MaybeUninit(to_drop); //@ close points_to(to_drop as *T, head(values)); unsafe { ptr::drop_in_place(to_drop.cast_init()) }; - //@ std::mem::close_MaybeUninit(to_drop); + //@ std::mem::close_MaybeUninit_(to_drop); /*@ if start == width::() { close array(to_drop, width::(), _); diff --git a/verifast-proofs/core/iter/adapters/verify.sh b/verifast-proofs/core/iter/adapters/verify.sh index 499d189743a31..1be48d57df349 100644 --- a/verifast-proofs/core/iter/adapters/verify.sh +++ b/verifast-proofs/core/iter/adapters/verify.sh @@ -128,11 +128,18 @@ if "No matching heap chunks" not in diagnostics or "Rust frontend failed" in dia sys.exit("The mutable-array negative check did not reach a storage proof failure.") PY -# Keep every stage sequential and fail on any unsuccessful proof or refinement. +# Keep every stage sequential and require both proof and refinement to succeed. +# Collect their independent diagnostics even when the first stage fails. # No assumption, unwind, reference-creation, or overflow suppression flags. +proof_status=0 timeout --signal=TERM --kill-after=10s 600s \ - verifast -rustc_args '--edition 2024' -skip_specless_fns verified/lib.rs + verifast -rustc_args '--edition 2024' -skip_specless_fns verified/lib.rs || proof_status=$? +refinement_status=0 timeout --signal=TERM --kill-after=10s 600s \ - refinement-checker --rustc-args '--edition 2024' original/lib.rs verified/lib.rs + refinement-checker --rustc-args '--edition 2024' original/lib.rs verified/lib.rs || refinement_status=$? python3 -I check_sources.py +if (( proof_status != 0 || refinement_status != 0 )); then + printf 'FAIL: proof status %s; refinement status %s\n' "$proof_status" "$refinement_status" >&2 + exit 1 +fi echo 'PASS: generic adapter contracts, source refinement, and source identity' From 5de9200ebdd565d36ef53d19289fc9853d4accb0 Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Sun, 13 Sep 2026 01:34:24 -0700 Subject: [PATCH 16/62] Isolate direct mutable array reference creation in regression tests Signed-off-by: Onyeka Obi --- .../core/iter/adapters/backend/array-mut-invalid.rs | 3 ++- verifast-proofs/core/iter/adapters/backend/array-valid.rs | 8 +++++--- verifast-proofs/core/iter/adapters/verify.sh | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/verifast-proofs/core/iter/adapters/backend/array-mut-invalid.rs b/verifast-proofs/core/iter/adapters/backend/array-mut-invalid.rs index 6567e310f09d2..f6ea2f30307d0 100644 --- a/verifast-proofs/core/iter/adapters/backend/array-mut-invalid.rs +++ b/verifast-proofs/core/iter/adapters/backend/array-mut-invalid.rs @@ -1,10 +1,11 @@ #![crate_type = "lib"] #![crate_name = "array_mut_invalid"] +#[allow(unsafe_op_in_unsafe_fn)] unsafe fn exclusive<'a, T, const N: usize>(p: *mut [T; N]) -> &'a mut [T; N] //@ req thread_token(?t) &*& [?q]lifetime_token('a); //@ ens thread_token(t) &*& [q]lifetime_token('a); //@ on_unwind_ens false; { - unsafe { &mut *p } + &mut *p } diff --git a/verifast-proofs/core/iter/adapters/backend/array-valid.rs b/verifast-proofs/core/iter/adapters/backend/array-valid.rs index 60c69a2f32929..5947ab50ced40 100644 --- a/verifast-proofs/core/iter/adapters/backend/array-valid.rs +++ b/verifast-proofs/core/iter/adapters/backend/array-valid.rs @@ -10,10 +10,12 @@ unsafe fn shared<'a, T, const N: usize>(p: *const [T; N]) -> &'a [T; N] unsafe { &*p } } +// Keep one reference-creation expression without an unsafe-block coercion. +#[allow(unsafe_op_in_unsafe_fn)] unsafe fn exclusive<'a, T, const N: usize>(p: *mut [T; N]) -> &'a mut [T; N] -//@ req thread_token(?t) &*& [?q]lifetime_token('a) &*& full_borrow('a, <[T; N]>.full_borrow_content(t, p)); -//@ ens thread_token(t) &*& [q]lifetime_token('a) &*& full_borrow('a, <[T; N]>.full_borrow_content(t, result)); +//@ req *p |-> ?values; +//@ ens *result |-> values &*& ref_mut_end_token(result, p); //@ on_unwind_ens false; { - unsafe { &mut *p } + &mut *p } diff --git a/verifast-proofs/core/iter/adapters/verify.sh b/verifast-proofs/core/iter/adapters/verify.sh index 1be48d57df349..2a807d8ea410f 100644 --- a/verifast-proofs/core/iter/adapters/verify.sh +++ b/verifast-proofs/core/iter/adapters/verify.sh @@ -64,7 +64,7 @@ timeout --signal=TERM --kill-after=10s 60s \ timeout --signal=TERM --kill-after=10s 60s \ verifast -rustc_args '--edition 2024' backend/const-valid.rs timeout --signal=TERM --kill-after=10s 60s \ - verifast -rustc_args '--edition 2024' backend/array-valid.rs + verifast -verbose 1 -rustc_args '--edition 2024' backend/array-valid.rs negative_log="$(mktemp)" trap 'rm -f -- "$negative_log"' EXIT if timeout --signal=TERM --kill-after=10s 60s \ From c6a460e90c7fac8ee52853740a915778e45beabf Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Sun, 13 Sep 2026 01:38:37 -0700 Subject: [PATCH 17/62] Test array reference creation independently of return reborrows Signed-off-by: Onyeka Obi --- .../core/iter/adapters/backend/array-mut-invalid.rs | 9 ++++----- .../core/iter/adapters/backend/array-valid.rs | 8 ++++---- verifast-proofs/core/iter/adapters/verify.sh | 2 +- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/verifast-proofs/core/iter/adapters/backend/array-mut-invalid.rs b/verifast-proofs/core/iter/adapters/backend/array-mut-invalid.rs index f6ea2f30307d0..ff22a6f85f32d 100644 --- a/verifast-proofs/core/iter/adapters/backend/array-mut-invalid.rs +++ b/verifast-proofs/core/iter/adapters/backend/array-mut-invalid.rs @@ -1,11 +1,10 @@ #![crate_type = "lib"] #![crate_name = "array_mut_invalid"] -#[allow(unsafe_op_in_unsafe_fn)] -unsafe fn exclusive<'a, T, const N: usize>(p: *mut [T; N]) -> &'a mut [T; N] -//@ req thread_token(?t) &*& [?q]lifetime_token('a); -//@ ens thread_token(t) &*& [q]lifetime_token('a); +unsafe fn exclusive(p: *mut [T; N]) -> *mut [T; N] +//@ req true; +//@ ens true; //@ on_unwind_ens false; { - &mut *p + unsafe { &mut *p as *mut [T; N] } } diff --git a/verifast-proofs/core/iter/adapters/backend/array-valid.rs b/verifast-proofs/core/iter/adapters/backend/array-valid.rs index 5947ab50ced40..4379f7d3508b9 100644 --- a/verifast-proofs/core/iter/adapters/backend/array-valid.rs +++ b/verifast-proofs/core/iter/adapters/backend/array-valid.rs @@ -10,12 +10,12 @@ unsafe fn shared<'a, T, const N: usize>(p: *const [T; N]) -> &'a [T; N] unsafe { &*p } } -// Keep one reference-creation expression without an unsafe-block coercion. -#[allow(unsafe_op_in_unsafe_fn)] -unsafe fn exclusive<'a, T, const N: usize>(p: *mut [T; N]) -> &'a mut [T; N] +// Return the new reference as a raw pointer to test creation independently +// of the extra lifetime reborrow inserted when returning a mutable reference. +unsafe fn exclusive(p: *mut [T; N]) -> *mut [T; N] //@ req *p |-> ?values; //@ ens *result |-> values &*& ref_mut_end_token(result, p); //@ on_unwind_ens false; { - &mut *p + unsafe { &mut *p as *mut [T; N] } } diff --git a/verifast-proofs/core/iter/adapters/verify.sh b/verifast-proofs/core/iter/adapters/verify.sh index 2a807d8ea410f..1be48d57df349 100644 --- a/verifast-proofs/core/iter/adapters/verify.sh +++ b/verifast-proofs/core/iter/adapters/verify.sh @@ -64,7 +64,7 @@ timeout --signal=TERM --kill-after=10s 60s \ timeout --signal=TERM --kill-after=10s 60s \ verifast -rustc_args '--edition 2024' backend/const-valid.rs timeout --signal=TERM --kill-after=10s 60s \ - verifast -verbose 1 -rustc_args '--edition 2024' backend/array-valid.rs + verifast -rustc_args '--edition 2024' backend/array-valid.rs negative_log="$(mktemp)" trap 'rm -f -- "$negative_log"' EXIT if timeout --signal=TERM --kill-after=10s 60s \ From 41fbb1ea5c01d0039969fb8f593012ae8d913373 Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Sun, 13 Sep 2026 01:43:19 -0700 Subject: [PATCH 18/62] Preserve original pointer helper bodies for source refinement Signed-off-by: Onyeka Obi --- verifast-proofs/core/iter/adapters/verified/map_windows.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/verifast-proofs/core/iter/adapters/verified/map_windows.rs b/verifast-proofs/core/iter/adapters/verified/map_windows.rs index df46a3eb017b5..7d6b13469619b 100644 --- a/verifast-proofs/core/iter/adapters/verified/map_windows.rs +++ b/verifast-proofs/core/iter/adapters/verified/map_windows.rs @@ -87,7 +87,7 @@ impl Buffer { //@ ens result == base(self); //@ on_unwind_ens false; { - (&raw const self.buffer).cast() + self.buffer.as_flattened().as_ptr() } #[inline] @@ -96,7 +96,7 @@ impl Buffer { //@ ens result == base(self); //@ on_unwind_ens false; { - (&raw mut self.buffer).cast() + self.buffer.as_flattened_mut().as_mut_ptr() } #[inline] @@ -238,6 +238,7 @@ impl Buffer { unsafe { ptr::drop_in_place(to_drop.cast_init()) }; //@ std::mem::close_MaybeUninit_(to_drop); /*@ + { if start == width::() { close array(to_drop, width::(), _); close array(buffer_mut_ptr, 0, nil); @@ -249,6 +250,7 @@ impl Buffer { close bounds(self, start == width::() ? 0 : start + 1); close live(t, self, start == width::() ? 0 : start + 1, append(tail(values), cons(next, nil))); + } @*/ } } From b90c981fa6d372627c26e4df9bf33acf871a1314 Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Sun, 13 Sep 2026 01:47:49 -0700 Subject: [PATCH 19/62] Match source pointer casts and disambiguate predicate arguments Signed-off-by: Onyeka Obi --- .../core/iter/adapters/verified/map_windows.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/verifast-proofs/core/iter/adapters/verified/map_windows.rs b/verifast-proofs/core/iter/adapters/verified/map_windows.rs index 7d6b13469619b..021971e78faba 100644 --- a/verifast-proofs/core/iter/adapters/verified/map_windows.rs +++ b/verifast-proofs/core/iter/adapters/verified/map_windows.rs @@ -87,7 +87,7 @@ impl Buffer { //@ ens result == base(self); //@ on_unwind_ens false; { - self.buffer.as_flattened().as_ptr() + self.buffer.as_ptr().cast() } #[inline] @@ -96,7 +96,7 @@ impl Buffer { //@ ens result == base(self); //@ on_unwind_ens false; { - self.buffer.as_flattened_mut().as_mut_ptr() + self.buffer.as_mut_ptr().cast() } #[inline] @@ -247,8 +247,8 @@ impl Buffer { close array(to_drop, 1, _); array_join(buffer_mut_ptr); } - close bounds(self, start == width::() ? 0 : start + 1); - close live(t, self, start == width::() ? 0 : start + 1, + close bounds(self, (start == width::() ? 0 : start + 1)); + close live(t, self, (start == width::() ? 0 : start + 1), append(tail(values), cons(next, nil))); } @*/ From 978390e6fe66b9f9f0f77d5e9a8fb9f5a0d1c1bb Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Sun, 13 Sep 2026 01:49:18 -0700 Subject: [PATCH 20/62] Use Rust conditional expression syntax in ghost predicates Signed-off-by: Onyeka Obi --- verifast-proofs/core/iter/adapters/verified/map_windows.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/verifast-proofs/core/iter/adapters/verified/map_windows.rs b/verifast-proofs/core/iter/adapters/verified/map_windows.rs index 021971e78faba..e0e1afc7cd8d1 100644 --- a/verifast-proofs/core/iter/adapters/verified/map_windows.rs +++ b/verifast-proofs/core/iter/adapters/verified/map_windows.rs @@ -161,7 +161,7 @@ impl Buffer { req thread_token(?t) &*& live(t, self, ?start, ?values) &*& .own(t, next); @*/ /*@ - ens thread_token(t) &*& live(t, self, start == width::() ? 0 : start + 1, + ens thread_token(t) &*& live(t, self, if start == width::() { 0 } else { start + 1 }, append(tail(values), cons(next, nil))); @*/ //@ on_unwind_ens thread_token(t); @@ -247,8 +247,8 @@ impl Buffer { close array(to_drop, 1, _); array_join(buffer_mut_ptr); } - close bounds(self, (start == width::() ? 0 : start + 1)); - close live(t, self, (start == width::() ? 0 : start + 1), + close bounds(self, if start == width::() { 0 } else { start + 1 }); + close live(t, self, if start == width::() { 0 } else { start + 1 }, append(tail(values), cons(next, nil))); } @*/ From 0c05bc70548619e57e36b89b7476d3c395599006 Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Sun, 13 Sep 2026 01:57:41 -0700 Subject: [PATCH 21/62] Preserve symbolic const identity during source refinement Signed-off-by: Onyeka Obi --- verifast-proofs/core/iter/adapters/README.md | 32 ++++++++++++----- .../adapters/backend/const-generics.patch | 33 ++++++++++++++++- .../core/iter/adapters/backend/prepare.sh | 8 +++-- .../adapters/backend/refinement-invalid.rs | 6 ++++ .../adapters/backend/refinement-original.rs | 6 ++++ .../iter/adapters/backend/refinement-valid.rs | 6 ++++ .../iter/adapters/verified/map_windows.rs | 36 +++++++++---------- verifast-proofs/core/iter/adapters/verify.sh | 19 ++++++++++ 8 files changed, 116 insertions(+), 30 deletions(-) create mode 100644 verifast-proofs/core/iter/adapters/backend/refinement-invalid.rs create mode 100644 verifast-proofs/core/iter/adapters/backend/refinement-original.rs create mode 100644 verifast-proofs/core/iter/adapters/backend/refinement-valid.rs diff --git a/verifast-proofs/core/iter/adapters/README.md b/verifast-proofs/core/iter/adapters/README.md index 0cf96fc2043b4..f85b5fc79f2f5 100644 --- a/verifast-proofs/core/iter/adapters/README.md +++ b/verifast-proofs/core/iter/adapters/README.md @@ -1,9 +1,9 @@ # Generic iterator adapter proof port This is an **unvalidated VeriFast port candidate** for part of Challenge 16. -Hosted verification previously stopped in the VeriFast Rust frontend. -The current revision includes a frontend fix awaiting hosted validation; -the refinement checker has not passed. See the hosted results below. +Hosted verification passes the frontend regression checks and now reaches +the adapter proof and source refinement stages. Those stages still need +passing verdicts. See the hosted results below. The files under `verified/` are proof inputs, not evidence of a successful proof. This port does not yet close Felipe's review requests on PR #602. @@ -60,9 +60,9 @@ contract on every selected method. It also rejects common proof suppression directives. This lexical guard does not parse or validate VeriFast proofs. Only a successful verifier run can establish the contracts. -The annotated version changes the two pointer helpers to raw field-address -expressions and names the returned references so ghost assertions can follow -their construction. `refinement-checker` must establish that these changes +The annotated version keeps the original pointer helper implementations and +names the returned references so ghost assertions can follow their +construction. `refinement-checker` must establish that these changes preserve the projected Rust behavior. It is mandatory, even if VeriFast passes. No compiler directives are ignored during refinement. @@ -120,12 +120,27 @@ array lengths using the existing `typeid`/`usize_of_const` representation. It keeps const parameters distinct from Rust types and does not add `Sized` bounds to them. The exporter rejects other const parameter types. A positive symbolic-length proof and an incorrect-length negative test must pass before -the adapter contracts are checked. This extension awaits hosted validation. +the adapter contracts are checked. Both passed in hosted run +[34745790943](https://github.com/model-checking/verify-rust-std/actions/runs/34745790943). + +The same patch routes array ownership and borrowing through VeriFast's +existing type predicates. It also removes the upstream frontend's blanket +shortcut for mutable-reference creation. Hosted run +[34748227762](https://github.com/model-checking/verify-rust-std/actions/runs/34748227762) +passed every backend regression gate: valid arithmetic, symbolic width, +shared array reborrowing, and mutable array reference creation; rejection +of overflow, a wrong width, missing shared ownership, and missing mutable +storage. The mutable test converts the new reference to a raw pointer to +test creation independently of a further return reborrow. These fixtures +validate the frontend extension, not the adapter contracts. `backend/prepare.sh` pins the source commit, source archive hash, and upstream dependency bundle hash. It builds the MIR exporter and the verifier's Rust translator on the hosted Linux worker. Arithmetic rules and library contracts are unchanged. Source projections and mandatory refinement remain unchanged. +The workflow caches only the compiled frontend, keyed by its preparation +script and patches, with binary checksums checked on restoration. Every +regression, proof, and refinement gate reruns after a cache hit. ## Local static checks and optional manual verification @@ -161,8 +176,7 @@ this candidate without altering the existing LinkedList and RawVec checks. ## Remaining work -- Validate the frontend's `AddUnchecked` mapping and both regression checks. Then - obtain verifier and refinement verdicts, including checks of symbolic const +- Obtain verifier and refinement verdicts for the adapters, including symbolic const sizes, `NonZero::new_unchecked`, reference creation, and generic slice drop glue. The explicit slice assertions in `drop` must be proved by that memory model; no local axiom has been added to make them pass. diff --git a/verifast-proofs/core/iter/adapters/backend/const-generics.patch b/verifast-proofs/core/iter/adapters/backend/const-generics.patch index 9524783c56327..027db4c0bdecf 100644 --- a/verifast-proofs/core/iter/adapters/backend/const-generics.patch +++ b/verifast-proofs/core/iter/adapters/backend/const-generics.patch @@ -40,7 +40,7 @@ | Lifetime -> `Lifetime name - | Const -> failwith "Const generic parameters are not yet supported" + | Const -> `Const name - + and decode_generic_arg (gen_arg_cpn : D.generic_arg) = let kind_cpn = gen_arg_cpn.kind in @@ -1641,7 +1643,10 @@ @@ -146,3 +146,34 @@ let send_tparams = compute_send_tparams preds in let tparams_targs = List.map (fun x -> Ast.IdentTypeExpr (def_loc, None, x)) vf_tparams +--- a/src/refinement_checker/refinement_checker.ml ++++ b/src/refinement_checker/refinement_checker.ml +@@ -320,6 +320,7 @@ + | Closure of term list + | FnDef of string * gen_arg list + | ScalarInt of literal_const_expr ++| ConstParamTerm of string + | StaticPtr of string + | ConstPtr of string (* The bytes the constant pointer points to *) + | SliceConstant of ty * string +@@ -335,6 +336,7 @@ + | EnumValue (variant, ts) -> Printf.sprintf "EnumValue %s %s" variant (string_of_terms ts) + | FnDef (fn, genArgs) -> Printf.sprintf "FnDef %s [%s]" fn (String.concat "; " (List.map string_of_gen_arg genArgs)) + | ScalarInt literal -> Printf.sprintf "ScalarInt %s" (string_of_literal_const_expr literal) ++| ConstParamTerm name -> Printf.sprintf "ConstParamTerm %s" name + | ConstPtr bytes -> Printf.sprintf "ConstPtr %s" bytes + | SliceConstant (ty, bytes) -> Printf.sprintf "SliceConstant %s %s" (string_of_ty ty) bytes + | Closure ts -> Printf.sprintf "Closure %s" (string_of_terms ts) +@@ -362,7 +364,11 @@ + + let eval_mir_const genv mir_const_cpn = + match mir_const_cpn with +- Ty mir_ty_const_cpn -> failwith "Using typesystem constant expressions as MIR constant operands is not yet supported" ++ Ty mir_ty_const_cpn -> ++ begin match decode_const_expr genv mir_ty_const_cpn with ++ ParamConstExpr name -> ConstParamTerm name ++ | LiteralConstExpr literal -> ScalarInt literal ++ end + | Val mir_val_const_cpn -> + let ty = decode_ty genv mir_val_const_cpn.ty in + let mir_const_value_cpn = mir_val_const_cpn.const_value in diff --git a/verifast-proofs/core/iter/adapters/backend/prepare.sh b/verifast-proofs/core/iter/adapters/backend/prepare.sh index 693a506512560..35315b7cf6850 100644 --- a/verifast-proofs/core/iter/adapters/backend/prepare.sh +++ b/verifast-proofs/core/iter/adapters/backend/prepare.sh @@ -16,6 +16,7 @@ if [[ -f "$cache_dir/checksums" ]]; then (cd "$cache_dir" && sha256sum --check checksums) install -m 755 "$cache_dir/verifast" "${VERIFAST_HOME:?}/bin/verifast" install -m 755 "$cache_dir/vf-rust-mir-exporter" "$VERIFAST_HOME/bin/vf-rust-mir-exporter" + install -m 755 "$cache_dir/refinement-checker" "$VERIFAST_HOME/bin/refinement-checker" echo 'Restored the patched frontend; all proof and regression checks will run' exit 0 fi @@ -62,11 +63,14 @@ install -m 755 "$build_dir/src/rust_frontend/vf_mir_exporter/target/debug/vf_mir "${VERIFAST_HOME:?}/bin/vf-rust-mir-exporter" ( cd "$build_dir/src" - dune build -j 1 vfconsole/vfconsole.exe + dune build -j 1 vfconsole/vfconsole.exe refinement_checker/refinement_checker.exe ) install -m 755 "$build_dir/src/_build/default/vfconsole/vfconsole.exe" "$VERIFAST_HOME/bin/verifast" +install -m 755 "$build_dir/src/_build/default/refinement_checker/refinement_checker.exe" \ + "$VERIFAST_HOME/bin/refinement-checker" mkdir -p "$cache_dir" install -m 755 "$VERIFAST_HOME/bin/verifast" "$cache_dir/verifast" install -m 755 "$VERIFAST_HOME/bin/vf-rust-mir-exporter" "$cache_dir/vf-rust-mir-exporter" -(cd "$cache_dir" && sha256sum verifast vf-rust-mir-exporter > checksums) +install -m 755 "$VERIFAST_HOME/bin/refinement-checker" "$cache_dir/refinement-checker" +(cd "$cache_dir" && sha256sum verifast vf-rust-mir-exporter refinement-checker > checksums) echo 'Prepared VeriFast 26.09 with checked addition and symbolic usize const parameters' diff --git a/verifast-proofs/core/iter/adapters/backend/refinement-invalid.rs b/verifast-proofs/core/iter/adapters/backend/refinement-invalid.rs new file mode 100644 index 0000000000000..f9ba5e07db0da --- /dev/null +++ b/verifast-proofs/core/iter/adapters/backend/refinement-invalid.rs @@ -0,0 +1,6 @@ +#![crate_type = "lib"] +#![crate_name = "const_refinement"] + +pub fn width() -> usize { + M +} diff --git a/verifast-proofs/core/iter/adapters/backend/refinement-original.rs b/verifast-proofs/core/iter/adapters/backend/refinement-original.rs new file mode 100644 index 0000000000000..560363376c188 --- /dev/null +++ b/verifast-proofs/core/iter/adapters/backend/refinement-original.rs @@ -0,0 +1,6 @@ +#![crate_type = "lib"] +#![crate_name = "const_refinement"] + +pub fn width() -> usize { + N +} diff --git a/verifast-proofs/core/iter/adapters/backend/refinement-valid.rs b/verifast-proofs/core/iter/adapters/backend/refinement-valid.rs new file mode 100644 index 0000000000000..45896f2db6565 --- /dev/null +++ b/verifast-proofs/core/iter/adapters/backend/refinement-valid.rs @@ -0,0 +1,6 @@ +#![crate_type = "lib"] +#![crate_name = "const_refinement"] + +pub fn width() -> usize { + A +} diff --git a/verifast-proofs/core/iter/adapters/verified/map_windows.rs b/verifast-proofs/core/iter/adapters/verified/map_windows.rs index e0e1afc7cd8d1..57b0fe70294a8 100644 --- a/verifast-proofs/core/iter/adapters/verified/map_windows.rs +++ b/verifast-proofs/core/iter/adapters/verified/map_windows.rs @@ -7,7 +7,7 @@ struct Buffer { // Invariant: `self.buffer[self.start..self.start + N]` is initialized, // with all other elements being uninitialized. This also // implies that `self.start <= N`. - buffer: [[MaybeUninit; N]; 2], + buffer: [[std::mem::MaybeUninit; N]; 2], start: usize, } @@ -15,8 +15,8 @@ struct Buffer { fix width() -> usize { usize_of_const(typeid(N)) } -fix base(b: *Buffer) -> *MaybeUninit { - &(*b).buffer as *MaybeUninit +fix base(b: *Buffer) -> *std::mem::MaybeUninit { + &(*b).buffer as *std::mem::MaybeUninit } // These are constructor/layout restrictions, not a finite proof bound. @@ -31,7 +31,7 @@ pred bounds(b: *Buffer; start: usize) = pred live(t: thread_id_t, b: *Buffer; start: usize, values: list) = bounds(b, start) &*& length(values) == width::() &*& base(b)[..start] |-> ?prefix &*& - (base(b) + start)[..width::()] |-> map(MaybeUninit::new, values) &*& + (base(b) + start)[..width::()] |-> map(std::mem::MaybeUninit::new, values) &*& (base(b) + start + width::())[..width::() - start] |-> ?suffix &*& foreach(values, (.own)(t)); @@ -39,8 +39,8 @@ pred storage(b: *Buffer; start: usize) = bounds(b, start) &*& base(b)[..2 * width::()] |-> ?slots; // Unwrap initialized slots without requiring T: Copy or duplicating T.own. -lem initialized_slots(p: *MaybeUninit, values: list) - req p[..length(values)] |-> map(MaybeUninit::new, values); +lem initialized_slots(p: *std::mem::MaybeUninit, values: list) + req p[..length(values)] |-> map(std::mem::MaybeUninit::new, values); ens (p as *T)[..length(values)] |-> values; { open array(p, length(values), _); @@ -59,17 +59,17 @@ lem initialized_slots(p: *MaybeUninit, values: list) lem wrap_slots(p: *T, values: list) req p[..length(values)] |-> values; - ens (p as *MaybeUninit)[..length(values)] |-> map(MaybeUninit::new, values); + ens (p as *std::mem::MaybeUninit)[..length(values)] |-> map(std::mem::MaybeUninit::new, values); { open array(p, length(values), values); match values { nil => { - close array(p as *MaybeUninit, 0, nil); + close array(p as *std::mem::MaybeUninit, 0, nil); } cons(value, tail) => { - std::mem::close_MaybeUninit(p as *MaybeUninit); + std::mem::close_MaybeUninit(p as *std::mem::MaybeUninit); wrap_slots(p + 1, tail); - close array(p as *MaybeUninit, length(values), _); + close array(p as *std::mem::MaybeUninit, length(values), _); } } } @@ -82,7 +82,7 @@ lem wrap_slots(p: *T, values: list) impl Buffer { #[inline] - fn buffer_ptr(&self) -> *const MaybeUninit + fn buffer_ptr(&self) -> *const std::mem::MaybeUninit //@ req true; //@ ens result == base(self); //@ on_unwind_ens false; @@ -91,7 +91,7 @@ impl Buffer { } #[inline] - fn buffer_mut_ptr(&mut self) -> *mut MaybeUninit + fn buffer_mut_ptr(&mut self) -> *mut std::mem::MaybeUninit //@ req true; //@ ens result == base(self); //@ on_unwind_ens false; @@ -131,15 +131,15 @@ impl Buffer { } #[inline] - fn as_uninit_array_mut<'a>(&'a mut self) -> &'a mut MaybeUninit<[T; N]> + fn as_uninit_array_mut<'a>(&'a mut self) -> &'a mut std::mem::MaybeUninit<[T; N]> /*@ req thread_token(?t) &*& bounds(self, ?start) &*& [?q]lifetime_token('a) &*& - full_borrow('a, >.full_borrow_content(t, - (base(self) + start) as *MaybeUninit<[T; N]>)); + full_borrow('a, >.full_borrow_content(t, + (base(self) + start) as *std::mem::MaybeUninit<[T; N]>)); @*/ /*@ ens thread_token(t) &*& bounds(self, start) &*& [q]lifetime_token('a) &*& - full_borrow('a, >.full_borrow_content(t, result)); + full_borrow('a, >.full_borrow_content(t, result)); @*/ //@ on_unwind_ens false; { @@ -196,7 +196,7 @@ impl Buffer { //@ open array(buffer_mut_ptr + width::() - 1, 1, _); ptr::copy_nonoverlapping(buffer_mut_ptr.add(self.start + 1), buffer_mut_ptr, N - 1); (*buffer_mut_ptr.add(N - 1)).write(next); - //@ close array(buffer_mut_ptr + width::() - 1, 1, cons(MaybeUninit::new(next), nil)); + //@ close array(buffer_mut_ptr + width::() - 1, 1, cons(std::mem::MaybeUninit::new(next), nil)); //@ array_join(buffer_mut_ptr); buffer_mut_ptr.add(self.start) }; @@ -220,7 +220,7 @@ impl Buffer { //@ open array(buffer_mut_ptr + start + width::(), width::() - start, _); (*buffer_mut_ptr.add(self.start + N)).write(next); //@ close array(buffer_mut_ptr + start + width::() + 1, 0, nil); - //@ close array(buffer_mut_ptr + start + width::(), 1, cons(MaybeUninit::new(next), nil)); + //@ close array(buffer_mut_ptr + start + width::(), 1, cons(std::mem::MaybeUninit::new(next), nil)); //@ array_join(buffer_mut_ptr + start + 1); buffer_mut_ptr.add(self.start) }; diff --git a/verifast-proofs/core/iter/adapters/verify.sh b/verifast-proofs/core/iter/adapters/verify.sh index 1be48d57df349..6ead736c75d5d 100644 --- a/verifast-proofs/core/iter/adapters/verify.sh +++ b/verifast-proofs/core/iter/adapters/verify.sh @@ -127,6 +127,25 @@ diagnostics = Path(sys.argv[1]).read_text() if "No matching heap chunks" not in diagnostics or "Rust frontend failed" in diagnostics: sys.exit("The mutable-array negative check did not reach a storage proof failure.") PY +timeout --signal=TERM --kill-after=10s 60s \ + refinement-checker --rustc-args '--edition 2024' \ + backend/refinement-original.rs backend/refinement-valid.rs +if timeout --signal=TERM --kill-after=10s 60s \ + refinement-checker --rustc-args '--edition 2024' \ + backend/refinement-original.rs backend/refinement-invalid.rs >"$negative_log" 2>&1; then + cat "$negative_log" + echo 'The refinement checker equated distinct symbolic const parameters.' >&2 + exit 1 +fi +cat "$negative_log" +python3 -I - "$negative_log" <<'PY' +from pathlib import Path +import sys + +diagnostics = Path(sys.argv[1]).read_text() +if not all(s in diagnostics for s in ("ConstParamTerm N", "ConstParamTerm M", "not equal")): + sys.exit("The refinement negative check did not distinguish the two const parameters.") +PY # Keep every stage sequential and require both proof and refinement to succeed. # Collect their independent diagnostics even when the first stage fails. From 504ccdc43126053af4204b9a72443037235ba351 Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Sun, 13 Sep 2026 02:02:37 -0700 Subject: [PATCH 22/62] Build the declared refinement checker executable target Signed-off-by: Onyeka Obi --- verifast-proofs/core/iter/adapters/backend/prepare.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/verifast-proofs/core/iter/adapters/backend/prepare.sh b/verifast-proofs/core/iter/adapters/backend/prepare.sh index 35315b7cf6850..0d91d4b17821f 100644 --- a/verifast-proofs/core/iter/adapters/backend/prepare.sh +++ b/verifast-proofs/core/iter/adapters/backend/prepare.sh @@ -63,10 +63,10 @@ install -m 755 "$build_dir/src/rust_frontend/vf_mir_exporter/target/debug/vf_mir "${VERIFAST_HOME:?}/bin/vf-rust-mir-exporter" ( cd "$build_dir/src" - dune build -j 1 vfconsole/vfconsole.exe refinement_checker/refinement_checker.exe + dune build -j 1 vfconsole/vfconsole.exe refinement_checker/main.exe ) install -m 755 "$build_dir/src/_build/default/vfconsole/vfconsole.exe" "$VERIFAST_HOME/bin/verifast" -install -m 755 "$build_dir/src/_build/default/refinement_checker/refinement_checker.exe" \ +install -m 755 "$build_dir/src/_build/default/refinement_checker/main.exe" \ "$VERIFAST_HOME/bin/refinement-checker" mkdir -p "$cache_dir" install -m 755 "$VERIFAST_HOME/bin/verifast" "$cache_dir/verifast" From 3d71d2361bac69e2c9dc1b5af504479cf7d52808 Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Sun, 13 Sep 2026 02:07:48 -0700 Subject: [PATCH 23/62] Decode the constant payload from typed MIR operands Signed-off-by: Onyeka Obi --- verifast-proofs/core/iter/adapters/backend/const-generics.patch | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/verifast-proofs/core/iter/adapters/backend/const-generics.patch b/verifast-proofs/core/iter/adapters/backend/const-generics.patch index 027db4c0bdecf..e12aa1961839f 100644 --- a/verifast-proofs/core/iter/adapters/backend/const-generics.patch +++ b/verifast-proofs/core/iter/adapters/backend/const-generics.patch @@ -170,7 +170,7 @@ match mir_const_cpn with - Ty mir_ty_const_cpn -> failwith "Using typesystem constant expressions as MIR constant operands is not yet supported" + Ty mir_ty_const_cpn -> -+ begin match decode_const_expr genv mir_ty_const_cpn with ++ begin match decode_const_expr genv mir_ty_const_cpn.const with + ParamConstExpr name -> ConstParamTerm name + | LiteralConstExpr literal -> ScalarInt literal + end From e7df29fd0494bd11ff29e181e94cb616b1998425 Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Sun, 13 Sep 2026 02:28:39 -0700 Subject: [PATCH 24/62] Preserve const parameter constraints in VeriFast refinement Signed-off-by: Onyeka Obi --- .../adapters/backend/const-generics.patch | 51 ++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/verifast-proofs/core/iter/adapters/backend/const-generics.patch b/verifast-proofs/core/iter/adapters/backend/const-generics.patch index e12aa1961839f..6b8583c5d76bd 100644 --- a/verifast-proofs/core/iter/adapters/backend/const-generics.patch +++ b/verifast-proofs/core/iter/adapters/backend/const-generics.patch @@ -1,6 +1,33 @@ +--- a/src/rust_frontend/vf_mir/vf_mir.capnp ++++ b/src/rust_frontend/vf_mir/vf_mir.capnp +@@ -422,6 +422,10 @@ + outlives @0: Outlives; + trait @2: Trait; + projection @3: Projection; ++ constArgHasType :group { ++ constant @4: TyConst; ++ ty @5: Ty; ++ } + ignored @1: Void; # A predicate that we are ignoring for now + } + } --- a/src/rust_frontend/vf_mir_exporter/src/lib.rs +++ b/src/rust_frontend/vf_mir_exporter/src/lib.rs -@@ -1539,6 +1539,11 @@ +@@ -1224,6 +1224,13 @@ + Self::encode_typesystem_constant(enc_ctx.tcx, enc_ctx, &const_, term_cpn.init_const()), + } + } ++ ty::ClauseKind::ConstArgHasType(constant, ty) => { ++ let mut constraint_cpn = pred_cpn.init_const_arg_has_type(); ++ Self::encode_typesystem_constant( ++ enc_ctx.tcx, enc_ctx, &constant, constraint_cpn.reborrow().init_constant(), ++ ); ++ Self::encode_ty(enc_ctx.tcx, enc_ctx, ty, constraint_cpn.init_ty()); ++ } + _ => pred_cpn.set_ignored(()), + } + } +@@ -1539,6 +1546,11 @@ p: &hir::GenericParam, mut p_cpn: hir_generic_param_cpn::Builder<'_>, ) { @@ -177,3 +204,25 @@ | Val mir_val_const_cpn -> let ty = decode_ty genv mir_val_const_cpn.ty in let mir_const_value_cpn = mir_val_const_cpn.const_value in +@@ -2336,7 +2342,20 @@ + let projection_preds1, preds1 = List.partition (function Projection _ -> true | _ -> false) preds1 in + if List.length projection_preds0 <> List.length projection_preds1 then failwith "The two functions have a different number of projection predicates"; + List.iter2 (fun pred0 pred1 -> check_predicate_refines_predicate root_genv0 pred0 root_genv1 pred1) projection_preds0 projection_preds1; +- if preds0 <> [] || preds1 <> [] then failwith "Predicate kind not supported"; ++ let is_const_predicate = function ++ | ConstArgHasType _ -> true ++ | Outlives _ | Trait _ | Projection _ | Ignored -> false ++ in ++ let decode_const_predicate genv = function ++ | ConstArgHasType pred -> Some (decode_const_expr genv pred.constant, decode_ty genv pred.ty) ++ | Outlives _ | Trait _ | Projection _ | Ignored -> None ++ in ++ let const_preds0, preds0 = List.partition is_const_predicate preds0 in ++ let const_preds1, preds1 = List.partition is_const_predicate preds1 in ++ if preds0 <> [] || preds1 <> [] || ++ List.map (decode_const_predicate root_genv0) const_preds0 <> ++ List.map (decode_const_predicate root_genv1) const_preds1 then ++ failwith "Predicate kind not supported or const parameter predicates differ"; + let inputs0 = body0.inputs in + let inputs1 = body1.inputs in + let inputs0 = List.map (decode_ty root_genv0) inputs0 in From 7f30c582950de59d1e1af95b6dcacc2f39483bd9 Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Sun, 13 Sep 2026 02:35:03 -0700 Subject: [PATCH 25/62] Use VeriFast ownership constructors and preserve Rust declarations Signed-off-by: Onyeka Obi --- .../core/iter/adapters/verified/map_windows.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/verifast-proofs/core/iter/adapters/verified/map_windows.rs b/verifast-proofs/core/iter/adapters/verified/map_windows.rs index 57b0fe70294a8..fad3d0cca18d6 100644 --- a/verifast-proofs/core/iter/adapters/verified/map_windows.rs +++ b/verifast-proofs/core/iter/adapters/verified/map_windows.rs @@ -7,7 +7,7 @@ struct Buffer { // Invariant: `self.buffer[self.start..self.start + N]` is initialized, // with all other elements being uninitialized. This also // implies that `self.start <= N`. - buffer: [[std::mem::MaybeUninit; N]; 2], + buffer: [[MaybeUninit; N]; 2], start: usize, } @@ -33,7 +33,7 @@ pred live(t: thread_id_t, b: *Buffer; start: usize, values: list) base(b)[..start] |-> ?prefix &*& (base(b) + start)[..width::()] |-> map(std::mem::MaybeUninit::new, values) &*& (base(b) + start + width::())[..width::() - start] |-> ?suffix &*& - foreach(values, (.own)(t)); + foreach(values, own::(t)); pred storage(b: *Buffer; start: usize) = bounds(b, start) &*& base(b)[..2 * width::()] |-> ?slots; @@ -82,7 +82,7 @@ lem wrap_slots(p: *T, values: list) impl Buffer { #[inline] - fn buffer_ptr(&self) -> *const std::mem::MaybeUninit + fn buffer_ptr(&self) -> *const MaybeUninit //@ req true; //@ ens result == base(self); //@ on_unwind_ens false; @@ -91,7 +91,7 @@ impl Buffer { } #[inline] - fn buffer_mut_ptr(&mut self) -> *mut std::mem::MaybeUninit + fn buffer_mut_ptr(&mut self) -> *mut MaybeUninit //@ req true; //@ ens result == base(self); //@ on_unwind_ens false; @@ -131,7 +131,7 @@ impl Buffer { } #[inline] - fn as_uninit_array_mut<'a>(&'a mut self) -> &'a mut std::mem::MaybeUninit<[T; N]> + fn as_uninit_array_mut<'a>(&'a mut self) -> &'a mut MaybeUninit<[T; N]> /*@ req thread_token(?t) &*& bounds(self, ?start) &*& [?q]lifetime_token('a) &*& full_borrow('a, >.full_borrow_content(t, @@ -168,7 +168,7 @@ impl Buffer { { //@ open live(t, self, start, values); //@ open bounds(self, start); - //@ open foreach(values, (.own)(t)); + //@ open foreach(values, own::(t)); //@ open array(base(self) + start, width::(), _); let buffer_mut_ptr = self.buffer_mut_ptr(); debug_assert!(self.start + N <= 2 * N); @@ -230,8 +230,8 @@ impl Buffer { // SAFETY: the index is valid and this is element `a` in the // diagram above and has not been dropped yet. - //@ close foreach(nil, (.own)(t)); - //@ close foreach(cons(next, nil), (.own)(t)); + //@ close foreach(nil, own::(t)); + //@ close foreach(cons(next, nil), own::(t)); //@ foreach_append(tail(values), cons(next, nil)); //@ std::mem::open_MaybeUninit(to_drop); //@ close points_to(to_drop as *T, head(values)); From 9d327085824b00de34c638f6cce70ea1c16fb14d Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Sun, 13 Sep 2026 02:38:08 -0700 Subject: [PATCH 26/62] Allow arbitrary generic ownership in the live buffer predicate Signed-off-by: Onyeka Obi --- verifast-proofs/core/iter/adapters/verified/map_windows.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/verifast-proofs/core/iter/adapters/verified/map_windows.rs b/verifast-proofs/core/iter/adapters/verified/map_windows.rs index fad3d0cca18d6..eb0558ca2ad12 100644 --- a/verifast-proofs/core/iter/adapters/verified/map_windows.rs +++ b/verifast-proofs/core/iter/adapters/verified/map_windows.rs @@ -28,7 +28,7 @@ pred bounds(b: *Buffer; start: usize) = pointer_within_limits(base(b) + 2 * width::()) == true; // Inactive slots may retain copied bytes. They carry no ownership of T. -pred live(t: thread_id_t, b: *Buffer; start: usize, values: list) = +pred live(t: thread_id_t, b: *Buffer, start: usize, values: list) = bounds(b, start) &*& length(values) == width::() &*& base(b)[..start] |-> ?prefix &*& (base(b) + start)[..width::()] |-> map(std::mem::MaybeUninit::new, values) &*& From 37077346f6d4b7174f014519a830798c6ec0cb79 Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Sun, 13 Sep 2026 02:41:33 -0700 Subject: [PATCH 27/62] Dereference the step receiver in ghost field assertions Signed-off-by: Onyeka Obi --- verifast-proofs/core/iter/adapters/verified/step_by.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/verifast-proofs/core/iter/adapters/verified/step_by.rs b/verifast-proofs/core/iter/adapters/verified/step_by.rs index 90a1f9eaea9f8..7a30517f29bd9 100644 --- a/verifast-proofs/core/iter/adapters/verified/step_by.rs +++ b/verifast-proofs/core/iter/adapters/verified/step_by.rs @@ -27,8 +27,8 @@ impl StepBy { /// aka `self.step_minus_one + 1`. #[inline] fn original_step(&self) -> NonZero -//@ req [?f]self.step_minus_one |-> ?step &*& step < usize::MAX; - //@ ens [f]self.step_minus_one |-> step &*& result.get() == step + 1 &*& 0 < result.get(); +//@ req [?f](*self).step_minus_one |-> ?step &*& step < usize::MAX; + //@ ens [f](*self).step_minus_one |-> step &*& result.get() == step + 1 &*& 0 < result.get(); //@ on_unwind_ens false; { // SAFETY: By type invariant, `step_minus_one` cannot be `MAX`, which From 462c7b4673ff412ce5b0db7feb6a838111cbfe9d Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Sun, 13 Sep 2026 02:44:41 -0700 Subject: [PATCH 28/62] Require buffer pointer bounds and record passing source refinement Signed-off-by: Onyeka Obi --- verifast-proofs/core/iter/adapters/README.md | 27 ++++++++++++------- .../iter/adapters/verified/map_windows.rs | 4 +-- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/verifast-proofs/core/iter/adapters/README.md b/verifast-proofs/core/iter/adapters/README.md index f85b5fc79f2f5..aa6d52a39c949 100644 --- a/verifast-proofs/core/iter/adapters/README.md +++ b/verifast-proofs/core/iter/adapters/README.md @@ -1,9 +1,9 @@ # Generic iterator adapter proof port This is an **unvalidated VeriFast port candidate** for part of Challenge 16. -Hosted verification passes the frontend regression checks and now reaches -the adapter proof and source refinement stages. Those stages still need -passing verdicts. See the hosted results below. +Hosted verification passes the frontend regression checks and full adapter +source refinement. The adapter contracts still need a passing proof verdict. +See the hosted results below. The files under `verified/` are proof inputs, not evidence of a successful proof. This port does not yet close Felipe's review requests on PR #602. @@ -102,7 +102,7 @@ The [25.11 exporter](https://github.com/verifast/verifast/blob/25.11/src/rust_fr has no `AddUnchecked` case. Static inspection of the [26.01 exporter](https://github.com/verifast/verifast/blob/26.01/src/rust_frontend/vf_mir_exporter/src/lib.rs#L2445-L2464) found the same omission, so that release alone does not address this failure. -Neither the generic contracts nor source refinement have a passing verdict. +That original run established neither the contracts nor source refinement. The current runner uses VeriFast 26.09 plus the narrow frontend patch in `backend/add-unchecked.patch`. It maps `AddUnchecked` to the existing integer addition operation, whose symbolic execution checks overflow. On inputs @@ -134,13 +134,22 @@ storage. The mutable test converts the new reference to a raw pointer to test creation independently of a further return reborrow. These fixtures validate the frontend extension, not the adapter contracts. +The patch also preserves const operands and `ConstArgHasType` constraints +through the MIR schema and refinement checker. Constraints are compared after +generic parameter renaming; unsupported predicates still fail refinement. +Hosted run [34749576406](https://github.com/model-checking/verify-rust-std/actions/runs/34749576406) +accepted a renamed const parameter and rejected changing the return from +`N` to `M`. Run [34749771017](https://github.com/model-checking/verify-rust-std/actions/runs/34749771017) +then passed refinement of the full adapter projection. Its contract proof +failed, so the workflow correctly remained unsuccessful. + `backend/prepare.sh` pins the source commit, source archive hash, and upstream -dependency bundle hash. It builds the MIR exporter and the verifier's Rust -translator on the hosted Linux worker. Arithmetic rules and library contracts +dependency bundle hash. It builds the MIR exporter, verifier, and refinement +checker on the hosted Linux worker. Arithmetic rules and library contracts are unchanged. Source projections and mandatory refinement remain unchanged. -The workflow caches only the compiled frontend, keyed by its preparation -script and patches, with binary checksums checked on restoration. Every -regression, proof, and refinement gate reruns after a cache hit. +The workflow caches these three binaries, keyed by the preparation script +and patches, with binary checksums checked on restoration. Every regression, +proof, and refinement gate reruns after a cache hit. ## Local static checks and optional manual verification diff --git a/verifast-proofs/core/iter/adapters/verified/map_windows.rs b/verifast-proofs/core/iter/adapters/verified/map_windows.rs index eb0558ca2ad12..2ff9e3eebc171 100644 --- a/verifast-proofs/core/iter/adapters/verified/map_windows.rs +++ b/verifast-proofs/core/iter/adapters/verified/map_windows.rs @@ -83,7 +83,7 @@ lem wrap_slots(p: *T, values: list) impl Buffer { #[inline] fn buffer_ptr(&self) -> *const MaybeUninit -//@ req true; +//@ req pointer_within_limits(base(self)) == true; //@ ens result == base(self); //@ on_unwind_ens false; { @@ -92,7 +92,7 @@ impl Buffer { #[inline] fn buffer_mut_ptr(&mut self) -> *mut MaybeUninit -//@ req true; +//@ req pointer_within_limits(base(self)) == true; //@ ens result == base(self); //@ on_unwind_ens false; { From 6fecaa18320eec8f155d3a977a537a5ab8871e37 Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Sun, 13 Sep 2026 02:47:58 -0700 Subject: [PATCH 29/62] Collect bounded per-method diagnostics after a hosted proof failure Signed-off-by: Onyeka Obi --- verifast-proofs/core/iter/adapters/verify.sh | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/verifast-proofs/core/iter/adapters/verify.sh b/verifast-proofs/core/iter/adapters/verify.sh index 6ead736c75d5d..0e3703361f739 100644 --- a/verifast-proofs/core/iter/adapters/verify.sh +++ b/verifast-proofs/core/iter/adapters/verify.sh @@ -157,6 +157,25 @@ refinement_status=0 timeout --signal=TERM --kill-after=10s 600s \ refinement-checker --rustc-args '--edition 2024' original/lib.rs verified/lib.rs || refinement_status=$? python3 -I check_sources.py +if (( proof_status != 0 )); then + # These bounded, sequential diagnostics cannot replace the full proof verdict. + while IFS= read -r proof_location; do + diagnostic_status=0 + timeout --signal=TERM --kill-after=5s 30s \ + verifast -rustc_args '--edition 2024' -skip_specless_fns \ + -focus "$proof_location" verified/lib.rs || diagnostic_status=$? + printf 'Diagnostic %s: status %s\n' "$proof_location" "$diagnostic_status" + done < <(python3 -I - <<'PY' +from pathlib import Path +import re + +for path in (Path("verified/map_windows.rs"), Path("verified/step_by.rs")): + for line_number, line in enumerate(path.read_text().splitlines(), 1): + if re.match(r"\s*(fn|lem)\s+\w+", line): + print(f"{path}:{line_number}") +PY + ) +fi if (( proof_status != 0 || refinement_status != 0 )); then printf 'FAIL: proof status %s; refinement status %s\n' "$proof_status" "$refinement_status" >&2 exit 1 From 30057f0943b5dca5d3a64060ae005ae9ac998289 Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Sun, 13 Sep 2026 02:53:49 -0700 Subject: [PATCH 30/62] Express private proof method obligations with unsafe signatures Signed-off-by: Onyeka Obi --- verifast-proofs/core/iter/adapters/README.md | 11 ++++++++--- .../core/iter/adapters/verified/map_windows.rs | 12 ++++++------ .../core/iter/adapters/verified/step_by.rs | 2 +- verifast-proofs/core/iter/adapters/verify.sh | 2 +- 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/verifast-proofs/core/iter/adapters/README.md b/verifast-proofs/core/iter/adapters/README.md index aa6d52a39c949..f1e9ffe2621a0 100644 --- a/verifast-proofs/core/iter/adapters/README.md +++ b/verifast-proofs/core/iter/adapters/README.md @@ -60,9 +60,14 @@ contract on every selected method. It also rejects common proof suppression directives. This lexical guard does not parse or validate VeriFast proofs. Only a successful verifier run can establish the contracts. -The annotated version keeps the original pointer helper implementations and -names the returned references so ghost assertions can follow their -construction. `refinement-checker` must establish that these changes +The annotated version keeps the original pointer helper bodies and names +returned references so ghost assertions can follow their construction. +Private proof methods are marked `unsafe` to express their explicit caller +obligations. The upstream refinement checker permits this change for private +functions; the original projection and standard library keep their original +signatures. The `Drop` trait implementation retains its safe signature and +must establish its contract from the type ownership invariant. +`refinement-checker` must establish that these changes preserve the projected Rust behavior. It is mandatory, even if VeriFast passes. No compiler directives are ignored during refinement. diff --git a/verifast-proofs/core/iter/adapters/verified/map_windows.rs b/verifast-proofs/core/iter/adapters/verified/map_windows.rs index 2ff9e3eebc171..fea9241289f2e 100644 --- a/verifast-proofs/core/iter/adapters/verified/map_windows.rs +++ b/verifast-proofs/core/iter/adapters/verified/map_windows.rs @@ -82,7 +82,7 @@ lem wrap_slots(p: *T, values: list) impl Buffer { #[inline] - fn buffer_ptr(&self) -> *const MaybeUninit + unsafe fn buffer_ptr(&self) -> *const MaybeUninit //@ req pointer_within_limits(base(self)) == true; //@ ens result == base(self); //@ on_unwind_ens false; @@ -91,7 +91,7 @@ impl Buffer { } #[inline] - fn buffer_mut_ptr(&mut self) -> *mut MaybeUninit + unsafe fn buffer_mut_ptr(&mut self) -> *mut MaybeUninit //@ req pointer_within_limits(base(self)) == true; //@ ens result == base(self); //@ on_unwind_ens false; @@ -100,7 +100,7 @@ impl Buffer { } #[inline] - fn as_array_ref<'a>(&'a self) -> &'a [T; N] + unsafe fn as_array_ref<'a>(&'a self) -> &'a [T; N] /*@ req [?f]bounds(self, ?start) &*& [?q]lifetime_token('a) &*& type_interp::<[T; N]>() &*& @@ -131,7 +131,7 @@ impl Buffer { } #[inline] - fn as_uninit_array_mut<'a>(&'a mut self) -> &'a mut MaybeUninit<[T; N]> + unsafe fn as_uninit_array_mut<'a>(&'a mut self) -> &'a mut MaybeUninit<[T; N]> /*@ req thread_token(?t) &*& bounds(self, ?start) &*& [?q]lifetime_token('a) &*& full_borrow('a, >.full_borrow_content(t, @@ -156,7 +156,7 @@ impl Buffer { /// /// All the elements will be shifted to the front end when pushing reaches /// the back end. - fn push(&mut self, next: T) + unsafe fn push(&mut self, next: T) /*@ req thread_token(?t) &*& live(t, self, ?start, ?values) &*& .own(t, next); @*/ @@ -170,7 +170,7 @@ impl Buffer { //@ open bounds(self, start); //@ open foreach(values, own::(t)); //@ open array(base(self) + start, width::(), _); - let buffer_mut_ptr = self.buffer_mut_ptr(); + let buffer_mut_ptr = unsafe { self.buffer_mut_ptr() }; debug_assert!(self.start + N <= 2 * N); let to_drop = if self.start == N { diff --git a/verifast-proofs/core/iter/adapters/verified/step_by.rs b/verifast-proofs/core/iter/adapters/verified/step_by.rs index 7a30517f29bd9..d93343ad14b2d 100644 --- a/verifast-proofs/core/iter/adapters/verified/step_by.rs +++ b/verifast-proofs/core/iter/adapters/verified/step_by.rs @@ -26,7 +26,7 @@ impl StepBy { /// The `step` that was originally passed to `Iterator::step_by(step)`, /// aka `self.step_minus_one + 1`. #[inline] - fn original_step(&self) -> NonZero + unsafe fn original_step(&self) -> NonZero //@ req [?f](*self).step_minus_one |-> ?step &*& step < usize::MAX; //@ ens [f](*self).step_minus_one |-> step &*& result.get() == step + 1 &*& 0 < result.get(); //@ on_unwind_ens false; diff --git a/verifast-proofs/core/iter/adapters/verify.sh b/verifast-proofs/core/iter/adapters/verify.sh index 0e3703361f739..7ad2cb6ca6d96 100644 --- a/verifast-proofs/core/iter/adapters/verify.sh +++ b/verifast-proofs/core/iter/adapters/verify.sh @@ -171,7 +171,7 @@ import re for path in (Path("verified/map_windows.rs"), Path("verified/step_by.rs")): for line_number, line in enumerate(path.read_text().splitlines(), 1): - if re.match(r"\s*(fn|lem)\s+\w+", line): + if re.match(r"\s*(?:unsafe\s+)?fn\s+\w+", line): print(f"{path}:{line_number}") PY ) From 04094ce0e76f70aa136dbfa3de177c9866d1d8ec Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Sun, 13 Sep 2026 02:58:55 -0700 Subject: [PATCH 31/62] Use raw buffer addresses and explicit borrowing predicates in proofs Signed-off-by: Onyeka Obi --- .../core/iter/adapters/verified/map_windows.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/verifast-proofs/core/iter/adapters/verified/map_windows.rs b/verifast-proofs/core/iter/adapters/verified/map_windows.rs index fea9241289f2e..a528afedea55e 100644 --- a/verifast-proofs/core/iter/adapters/verified/map_windows.rs +++ b/verifast-proofs/core/iter/adapters/verified/map_windows.rs @@ -25,6 +25,7 @@ pred bounds(b: *Buffer; start: usize) = width::() <= usize::MAX / 2 &*& start <= width::() &*& 2 * width::() * std::mem::size_of::() <= isize::MAX &*& pointer_within_limits(base(b)) == true &*& + pointer_within_limits(base(b) + start) == true &*& pointer_within_limits(base(b) + 2 * width::()) == true; // Inactive slots may retain copied bytes. They carry no ownership of T. @@ -87,7 +88,7 @@ impl Buffer { //@ ens result == base(self); //@ on_unwind_ens false; { - self.buffer.as_ptr().cast() + &raw const self.buffer as *const MaybeUninit } #[inline] @@ -96,7 +97,7 @@ impl Buffer { //@ ens result == base(self); //@ on_unwind_ens false; { - self.buffer.as_mut_ptr().cast() + &raw mut self.buffer as *mut MaybeUninit } #[inline] @@ -122,9 +123,9 @@ impl Buffer { //@ let reference = precreate_ref(window); //@ init_ref_share('a, t, reference); //@ let r = open_frac_borrow('a, ref_initialized_(reference), q); - //@ open [r]ref_initialized_(reference); + //@ open [r]ref_initialized_::<[T; N]>(reference)(); let result = unsafe { &*self.buffer_ptr().add(self.start).cast() }; - //@ close [r]ref_initialized_(reference); + //@ close [r]ref_initialized_::<[T; N]>(reference)(); //@ close_frac_borrow(r, ref_initialized_(reference)); //@ close [f]bounds(self, start); result @@ -194,6 +195,7 @@ impl Buffer { let to_drop = unsafe { //@ array_split(buffer_mut_ptr, width::() - 1); //@ open array(buffer_mut_ptr + width::() - 1, 1, _); + //@ array_to_array_(buffer_mut_ptr); ptr::copy_nonoverlapping(buffer_mut_ptr.add(self.start + 1), buffer_mut_ptr, N - 1); (*buffer_mut_ptr.add(N - 1)).write(next); //@ close array(buffer_mut_ptr + width::() - 1, 1, cons(std::mem::MaybeUninit::new(next), nil)); From bfc7e53343a383619ad2326d6807e676a83ec7ef Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Sun, 13 Sep 2026 03:05:53 -0700 Subject: [PATCH 32/62] Restore helper source refinement and require shared reference permissions Signed-off-by: Onyeka Obi --- .../core/iter/adapters/verified/map_windows.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/verifast-proofs/core/iter/adapters/verified/map_windows.rs b/verifast-proofs/core/iter/adapters/verified/map_windows.rs index a528afedea55e..d5b711714d624 100644 --- a/verifast-proofs/core/iter/adapters/verified/map_windows.rs +++ b/verifast-proofs/core/iter/adapters/verified/map_windows.rs @@ -84,11 +84,12 @@ lem wrap_slots(p: *T, values: list) impl Buffer { #[inline] unsafe fn buffer_ptr(&self) -> *const MaybeUninit -//@ req pointer_within_limits(base(self)) == true; - //@ ens result == base(self); +//@ req pointer_within_limits(base(self)) == true &*& [?f]ref_initialized(&(*self).buffer); + //@ ens [f]ref_initialized(&(*self).buffer) &*& result == base(self); //@ on_unwind_ens false; { - &raw const self.buffer as *const MaybeUninit + //@ reborrow_ref_(&(*self).buffer); + self.buffer.as_ptr().cast() } #[inline] @@ -97,18 +98,21 @@ impl Buffer { //@ ens result == base(self); //@ on_unwind_ens false; { - &raw mut self.buffer as *mut MaybeUninit + self.buffer.as_mut_ptr().cast() } #[inline] unsafe fn as_array_ref<'a>(&'a self) -> &'a [T; N] /*@ req [?f]bounds(self, ?start) &*& [?q]lifetime_token('a) &*& + [_]frac_borrow('a, ref_initialized_(self)) &*& + [?bf]ref_initialized(&(*self).buffer) &*& type_interp::<[T; N]>() &*& [_](<[T; N]>.share)('a, ?t, (base(self) + start) as *[T; N]); @*/ /*@ ens [f]bounds(self, start) &*& [q]lifetime_token('a) &*& + [bf]ref_initialized(&(*self).buffer) &*& type_interp::<[T; N]>() &*& ref_origin(result) == ref_origin((base(self) + start) as *[T; N]) &*& [_](<[T; N]>.share)('a, t, result); From b23dbe1a2d5c2569052e5dd695c30fe8d224756d Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Sun, 13 Sep 2026 03:16:29 -0700 Subject: [PATCH 33/62] Translate erased slice coercions and test nested array pointers Signed-off-by: Onyeka Obi --- .../core/iter/adapters/backend/array-valid.rs | 11 ++++++++++ .../adapters/backend/const-generics.patch | 20 +++++++++++++++++-- .../iter/adapters/backend/matrix-invalid.rs | 12 +++++++++++ .../iter/adapters/verified/map_windows.rs | 3 ++- verifast-proofs/core/iter/adapters/verify.sh | 15 ++++++++++++++ 5 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 verifast-proofs/core/iter/adapters/backend/matrix-invalid.rs diff --git a/verifast-proofs/core/iter/adapters/backend/array-valid.rs b/verifast-proofs/core/iter/adapters/backend/array-valid.rs index 4379f7d3508b9..a6e5895957e04 100644 --- a/verifast-proofs/core/iter/adapters/backend/array-valid.rs +++ b/verifast-proofs/core/iter/adapters/backend/array-valid.rs @@ -19,3 +19,14 @@ unsafe fn exclusive(p: *mut [T; N]) -> *mut [T; N] { unsafe { &mut *p as *mut [T; N] } } + +unsafe fn matrix_ptr( + p: *const [[std::mem::MaybeUninit; N]; 2], +) -> *const std::mem::MaybeUninit +//@ req pointer_within_limits(p) == true &*& [?f]ref_initialized(p); +//@ ens [f]ref_initialized(p) &*& result == p as *std::mem::MaybeUninit; +//@ on_unwind_ens false; +{ + //@ reborrow_ref_(p); + unsafe { (*p).as_ptr().cast() } +} diff --git a/verifast-proofs/core/iter/adapters/backend/const-generics.patch b/verifast-proofs/core/iter/adapters/backend/const-generics.patch index 6b8583c5d76bd..83845e8051335 100644 --- a/verifast-proofs/core/iter/adapters/backend/const-generics.patch +++ b/verifast-proofs/core/iter/adapters/backend/const-generics.patch @@ -134,7 +134,23 @@ if TranslatorArgs.ignore_ref_creation then true else let directives = -@@ -5993,17 +5993,18 @@ +@@ -3157,7 +3157,14 @@ + Ok (`TrRvalueExpr e) + | _ -> + let* ty_info = translate_decoded_ty cast_data.ty loc in +- let ty = ty_info.vf_ty in ++ let ty = ++ match ty_info.vf_ty with ++ | Ast.RustRefTypeExpr (_, Ast.InferredTypeExpr _, _, (Ast.SliceTypeExpr _ as slice_ty)) ++ when cast_data.kind = D.PointerCoercion -> ++ (* The borrow is checked by Rvalue::Ref; this coercion only adds slice metadata. *) ++ Ast.PtrTypeExpr (loc, slice_ty) ++ | ty -> ty ++ in + match operand with + | `TrOperandCopy expr + | `TrOperandMove (expr, _ (*place_is_mutable*)) +@@ -5993,17 +6000,18 @@ with | Lifetime -> Ok (`Lifetime name) | Type -> Ok (`Type name) @@ -162,7 +178,7 @@ let lft_params = Util.flatmap (function `Lifetime x -> [ x ] | _ -> []) generics in -@@ -6029,7 +6030,9 @@ +@@ -6029,7 +6037,9 @@ |> List.map decode_predicate in let sized_tparams = compute_sized_tparams preds in diff --git a/verifast-proofs/core/iter/adapters/backend/matrix-invalid.rs b/verifast-proofs/core/iter/adapters/backend/matrix-invalid.rs new file mode 100644 index 0000000000000..559672e5dc2b9 --- /dev/null +++ b/verifast-proofs/core/iter/adapters/backend/matrix-invalid.rs @@ -0,0 +1,12 @@ +#![crate_type = "lib"] +#![crate_name = "matrix_invalid"] + +unsafe fn matrix_ptr( + p: *const [[std::mem::MaybeUninit; N]; 2], +) -> *const std::mem::MaybeUninit +//@ req pointer_within_limits(p) == true; +//@ ens result == p as *std::mem::MaybeUninit; +//@ on_unwind_ens false; +{ + unsafe { (*p).as_ptr().cast() } +} diff --git a/verifast-proofs/core/iter/adapters/verified/map_windows.rs b/verifast-proofs/core/iter/adapters/verified/map_windows.rs index d5b711714d624..9936cb9c95af6 100644 --- a/verifast-proofs/core/iter/adapters/verified/map_windows.rs +++ b/verifast-proofs/core/iter/adapters/verified/map_windows.rs @@ -123,12 +123,13 @@ impl Buffer { debug_assert!(self.start + N <= 2 * N); // SAFETY: our invariant guarantees these elements are initialized. + let buffer_ptr = unsafe { self.buffer_ptr() }; //@ let window = (base(self) + start) as *[T; N]; //@ let reference = precreate_ref(window); //@ init_ref_share('a, t, reference); //@ let r = open_frac_borrow('a, ref_initialized_(reference), q); //@ open [r]ref_initialized_::<[T; N]>(reference)(); - let result = unsafe { &*self.buffer_ptr().add(self.start).cast() }; + let result = unsafe { &*buffer_ptr.add(self.start).cast() }; //@ close [r]ref_initialized_::<[T; N]>(reference)(); //@ close_frac_borrow(r, ref_initialized_(reference)); //@ close [f]bounds(self, start); diff --git a/verifast-proofs/core/iter/adapters/verify.sh b/verifast-proofs/core/iter/adapters/verify.sh index 7ad2cb6ca6d96..00fa3725df6ce 100644 --- a/verifast-proofs/core/iter/adapters/verify.sh +++ b/verifast-proofs/core/iter/adapters/verify.sh @@ -147,6 +147,21 @@ if not all(s in diagnostics for s in ("ConstParamTerm N", "ConstParamTerm M", "n sys.exit("The refinement negative check did not distinguish the two const parameters.") PY +if timeout --signal=TERM --kill-after=10s 60s \ + verifast -rustc_args '--edition 2024' backend/matrix-invalid.rs >"$negative_log" 2>&1; then + echo 'The array-to-slice negative check unexpectedly passed.' >&2 + exit 1 +fi +cat "$negative_log" +python3 -I - "$negative_log" <<'PY' +from pathlib import Path +import sys + +diagnostics = Path(sys.argv[1]).read_text() +if "No matching heap chunks" not in diagnostics or "Rust frontend failed" in diagnostics: + sys.exit("The array-to-slice negative check did not reject missing reference permissions.") +PY + # Keep every stage sequential and require both proof and refinement to succeed. # Collect their independent diagnostics even when the first stage fails. # No assumption, unwind, reference-creation, or overflow suppression flags. From 80878b6c7ef625ff9bc5d206206f2c4be36d0b15 Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Sun, 13 Sep 2026 03:25:32 -0700 Subject: [PATCH 34/62] Model pointer casts and the NonZero usize constructor for hosted proofs Signed-off-by: Onyeka Obi --- verifast-proofs/core/iter/adapters/README.md | 19 ++++++++-- .../core/iter/adapters/backend/array-valid.rs | 10 +++++ .../adapters/backend/const-generics.patch | 25 +++++++++++-- .../iter/adapters/backend/nonzero-invalid.rs | 10 +++++ .../iter/adapters/backend/nonzero-usize.patch | 37 +++++++++++++++++++ .../iter/adapters/backend/nonzero-valid.rs | 10 +++++ .../core/iter/adapters/backend/prepare.sh | 8 +++- verifast-proofs/core/iter/adapters/verify.sh | 17 +++++++++ 8 files changed, 126 insertions(+), 10 deletions(-) create mode 100644 verifast-proofs/core/iter/adapters/backend/nonzero-invalid.rs create mode 100644 verifast-proofs/core/iter/adapters/backend/nonzero-usize.patch create mode 100644 verifast-proofs/core/iter/adapters/backend/nonzero-valid.rs diff --git a/verifast-proofs/core/iter/adapters/README.md b/verifast-proofs/core/iter/adapters/README.md index f1e9ffe2621a0..9324570f82be3 100644 --- a/verifast-proofs/core/iter/adapters/README.md +++ b/verifast-proofs/core/iter/adapters/README.md @@ -151,10 +151,21 @@ failed, so the workflow correctly remained unsuccessful. `backend/prepare.sh` pins the source commit, source archive hash, and upstream dependency bundle hash. It builds the MIR exporter, verifier, and refinement checker on the hosted Linux worker. Arithmetic rules and library contracts -are unchanged. Source projections and mandatory refinement remain unchanged. -The workflow caches these three binaries, keyed by the preparation script -and patches, with binary checksums checked on restoration. Every regression, -proof, and refinement gate reruns after a cache hit. +for ownership, borrowing, and destruction are unchanged. Source projections +and mandatory refinement remain unchanged. The workflow caches these three +binaries and the library specification, keyed by the preparation script and +patches, with checksums checked on restoration. Every regression, proof, and +refinement gate reruns after a cache hit. + +`backend/nonzero-usize.patch` adds one trusted library contract for the +`usize` instantiation of `NonZero::new_unchecked`. It requires a positive +input, preserves its value through `get()`, and cannot unwind. This matches +the [standard library implementation and its safety requirement](../../../../library/core/src/num/nonzero.rs). +The frontend routes only the `usize` instantiation to this specification. +Other instantiations remain unsupported. The constructor implementation is +not proved by this port; the new specification is part of its trusted library +boundary. A positive fixture and rejection of a missing nonzero precondition +are mandatory. No additional ownership, borrowing, or drop axiom is supplied. ## Local static checks and optional manual verification diff --git a/verifast-proofs/core/iter/adapters/backend/array-valid.rs b/verifast-proofs/core/iter/adapters/backend/array-valid.rs index a6e5895957e04..c37c674fb25df 100644 --- a/verifast-proofs/core/iter/adapters/backend/array-valid.rs +++ b/verifast-proofs/core/iter/adapters/backend/array-valid.rs @@ -30,3 +30,13 @@ unsafe fn matrix_ptr( //@ reborrow_ref_(p); unsafe { (*p).as_ptr().cast() } } + +unsafe fn matrix_mut_ptr( + p: *mut [[std::mem::MaybeUninit; N]; 2], +) -> *mut std::mem::MaybeUninit +//@ req *p |-> ?matrix; +//@ ens *(result as *[[std::mem::MaybeUninit; N]; 2]) |-> matrix &*& ref_mut_end_token(result as *[[std::mem::MaybeUninit; N]; 2], p); +//@ on_unwind_ens false; +{ + unsafe { (*p).as_mut_ptr().cast() } +} diff --git a/verifast-proofs/core/iter/adapters/backend/const-generics.patch b/verifast-proofs/core/iter/adapters/backend/const-generics.patch index 83845e8051335..34383b3745d22 100644 --- a/verifast-proofs/core/iter/adapters/backend/const-generics.patch +++ b/verifast-proofs/core/iter/adapters/backend/const-generics.patch @@ -122,7 +122,24 @@ full_bor_content; points_to; pointee_fbc = None; -@@ -3020,11 +3023,8 @@ +@@ -2433,7 +2436,15 @@ + Ok + ( tmp_rvalue_binders, + FnCallResult (CastExpr (fn_loc, PtrTypeExpr (fn_loc, ManifestTypeExpr (fn_loc, Int (Unsigned, FixedWidthRank 0))), arg)) ) +- | "std::slice::::as_ptr" -> ++ | "std::ptr::const_ptr::::cast" ++ | "std::ptr::mut_ptr::::cast" -> ( ++ match substs, args with ++ | [ Mir.GenArgType _; Mir.GenArgType target ], [ arg ] -> ++ Ok (tmp_rvalue_binders, FnCallResult ++ (Ast.CastExpr (fn_loc, PtrTypeExpr (fn_loc, target.vf_ty), arg))) ++ | inputs -> Error (`TrFnCallRExpr "Invalid generic arguments for pointer cast")) ++ | "std::slice::::as_ptr" ++ | "std::slice::::as_mut_ptr" -> + let [ Mir.GenArgType gen_arg_ty_info; _ ] = substs in + let [ arg ] = args in + Ok +@@ -3020,11 +3031,8 @@ let* place_expr, place_is_mutable = translate_place place_cpn loc in let (path, line, _), _ = Ast.lexed_loc loc in let ignore_ref_creation = @@ -134,7 +151,7 @@ if TranslatorArgs.ignore_ref_creation then true else let directives = -@@ -3157,7 +3157,14 @@ +@@ -3157,7 +3165,14 @@ Ok (`TrRvalueExpr e) | _ -> let* ty_info = translate_decoded_ty cast_data.ty loc in @@ -150,7 +167,7 @@ match operand with | `TrOperandCopy expr | `TrOperandMove (expr, _ (*place_is_mutable*)) -@@ -5993,17 +6000,18 @@ +@@ -5993,17 +6008,18 @@ with | Lifetime -> Ok (`Lifetime name) | Type -> Ok (`Type name) @@ -178,7 +195,7 @@ let lft_params = Util.flatmap (function `Lifetime x -> [ x ] | _ -> []) generics in -@@ -6029,7 +6037,9 @@ +@@ -6029,7 +6045,9 @@ |> List.map decode_predicate in let sized_tparams = compute_sized_tparams preds in diff --git a/verifast-proofs/core/iter/adapters/backend/nonzero-invalid.rs b/verifast-proofs/core/iter/adapters/backend/nonzero-invalid.rs new file mode 100644 index 0000000000000..3dbb3f6da4e28 --- /dev/null +++ b/verifast-proofs/core/iter/adapters/backend/nonzero-invalid.rs @@ -0,0 +1,10 @@ +#![crate_type = "lib"] +#![crate_name = "nonzero_invalid"] + +unsafe fn construct(value: usize) -> std::num::NonZero +//@ req true; +//@ ens result.get() == value; +//@ on_unwind_ens false; +{ + unsafe { std::num::NonZero::new_unchecked(value) } +} diff --git a/verifast-proofs/core/iter/adapters/backend/nonzero-usize.patch b/verifast-proofs/core/iter/adapters/backend/nonzero-usize.patch new file mode 100644 index 0000000000000..10ec473a7a4d5 --- /dev/null +++ b/verifast-proofs/core/iter/adapters/backend/nonzero-usize.patch @@ -0,0 +1,37 @@ +--- a/src/rust_frontend/vf_mir_translator/vf_mir_translator.ml ++++ b/src/rust_frontend/vf_mir_translator/vf_mir_translator.ml +@@ -2386,6 +2386,18 @@ + (`TrFnCallRExpr + (Printf.sprintf "Invalid (generic) arg(s) for %s" + fn_name))) ++ | "std::num::NonZero::::new_unchecked" ++ when List.length substs = 1 && ++ List.for_all ++ (function ++ | Mir.GenArgType info -> ++ info.vf_ty = Ast.ManifestTypeExpr ++ (Ast.type_expr_loc info.vf_ty, Ast.Int (Ast.Unsigned, Ast.PtrRank)) ++ | Mir.GenArgLifetime _ | Mir.GenArgConst _ -> false) ++ substs -> ++ Ok (tmp_rvalue_binders, FnCallOutcome ++ (Ast.CallExpr (fn_loc, "std::num::new_nonzero_usize", [], [], ++ List.map (fun arg -> Ast.LitPat arg) args, Ast.Static))) + | "std::ptr::null_mut" -> + Ok + ( [], +--- a/bin/rust/std/lib.rsspec ++++ b/bin/rust/std/lib.rsspec +@@ -265,6 +265,13 @@ + struct NonZero; + + //@ fix NonZero::get(nz: NonZero) -> T; ++ ++ // Trusted library boundary for NonZero::::new_unchecked. ++ // The frontend routes only the usize instantiation to this specification. ++ unsafe fn new_nonzero_usize(value: usize) -> NonZero; ++ //@ req 0 < value; ++ //@ ens result.get() == value; ++ //@ on_unwind_ens false; + + /*@ + diff --git a/verifast-proofs/core/iter/adapters/backend/nonzero-valid.rs b/verifast-proofs/core/iter/adapters/backend/nonzero-valid.rs new file mode 100644 index 0000000000000..a0cb3c76e2668 --- /dev/null +++ b/verifast-proofs/core/iter/adapters/backend/nonzero-valid.rs @@ -0,0 +1,10 @@ +#![crate_type = "lib"] +#![crate_name = "nonzero_valid"] + +unsafe fn construct(value: usize) -> std::num::NonZero +//@ req 0 < value; +//@ ens result.get() == value; +//@ on_unwind_ens false; +{ + unsafe { std::num::NonZero::new_unchecked(value) } +} diff --git a/verifast-proofs/core/iter/adapters/backend/prepare.sh b/verifast-proofs/core/iter/adapters/backend/prepare.sh index 0d91d4b17821f..d6acb2155a509 100644 --- a/verifast-proofs/core/iter/adapters/backend/prepare.sh +++ b/verifast-proofs/core/iter/adapters/backend/prepare.sh @@ -10,13 +10,14 @@ backend_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" # The cached exporter still needs the pinned toolchain's shared libraries. rustup component add --toolchain nightly-2026-02-05 rustc-dev llvm-tools cache_key="$(sha256sum "$backend_dir/prepare.sh" "$backend_dir/add-unchecked.patch" \ - "$backend_dir/const-generics.patch" | sha256sum | cut -d ' ' -f 1)" + "$backend_dir/const-generics.patch" "$backend_dir/nonzero-usize.patch" | sha256sum | cut -d ' ' -f 1)" cache_dir="$HOME/.cache/verifast-iter-adapters/$cache_key" if [[ -f "$cache_dir/checksums" ]]; then (cd "$cache_dir" && sha256sum --check checksums) install -m 755 "$cache_dir/verifast" "${VERIFAST_HOME:?}/bin/verifast" install -m 755 "$cache_dir/vf-rust-mir-exporter" "$VERIFAST_HOME/bin/vf-rust-mir-exporter" install -m 755 "$cache_dir/refinement-checker" "$VERIFAST_HOME/bin/refinement-checker" + install -m 644 "$cache_dir/std-lib.rsspec" "$VERIFAST_HOME/bin/rust/std/lib.rsspec" echo 'Restored the patched frontend; all proof and regression checks will run' exit 0 fi @@ -34,6 +35,7 @@ tar -xzf "$build_dir/source.tar.gz" --strip-components=1 -C "$build_dir" \ "verifast-$source_commit/src" "verifast-$source_commit/bin" patch --batch --fuzz=0 --directory="$build_dir" -p1 < "$backend_dir/add-unchecked.patch" patch --batch --fuzz=0 --directory="$build_dir" -p1 < "$backend_dir/const-generics.patch" +patch --batch --fuzz=0 --directory="$build_dir" -p1 < "$backend_dir/nonzero-usize.patch" # Use the dependency bundle pinned by upstream's setup-build.sh. Its compiler # and package paths are built for /tmp/vfdeps-adf88dc on Linux. @@ -68,9 +70,11 @@ install -m 755 "$build_dir/src/rust_frontend/vf_mir_exporter/target/debug/vf_mir install -m 755 "$build_dir/src/_build/default/vfconsole/vfconsole.exe" "$VERIFAST_HOME/bin/verifast" install -m 755 "$build_dir/src/_build/default/refinement_checker/main.exe" \ "$VERIFAST_HOME/bin/refinement-checker" +install -m 644 "$build_dir/bin/rust/std/lib.rsspec" "$VERIFAST_HOME/bin/rust/std/lib.rsspec" mkdir -p "$cache_dir" install -m 755 "$VERIFAST_HOME/bin/verifast" "$cache_dir/verifast" install -m 755 "$VERIFAST_HOME/bin/vf-rust-mir-exporter" "$cache_dir/vf-rust-mir-exporter" install -m 755 "$VERIFAST_HOME/bin/refinement-checker" "$cache_dir/refinement-checker" -(cd "$cache_dir" && sha256sum verifast vf-rust-mir-exporter refinement-checker > checksums) +install -m 644 "$VERIFAST_HOME/bin/rust/std/lib.rsspec" "$cache_dir/std-lib.rsspec" +(cd "$cache_dir" && sha256sum verifast vf-rust-mir-exporter refinement-checker std-lib.rsspec > checksums) echo 'Prepared VeriFast 26.09 with checked addition and symbolic usize const parameters' diff --git a/verifast-proofs/core/iter/adapters/verify.sh b/verifast-proofs/core/iter/adapters/verify.sh index 00fa3725df6ce..2e8039e05998d 100644 --- a/verifast-proofs/core/iter/adapters/verify.sh +++ b/verifast-proofs/core/iter/adapters/verify.sh @@ -162,6 +162,23 @@ if "No matching heap chunks" not in diagnostics or "Rust frontend failed" in dia sys.exit("The array-to-slice negative check did not reject missing reference permissions.") PY +timeout --signal=TERM --kill-after=10s 60s \ + verifast -rustc_args '--edition 2024' backend/nonzero-valid.rs +if timeout --signal=TERM --kill-after=10s 60s \ + verifast -rustc_args '--edition 2024' backend/nonzero-invalid.rs >"$negative_log" 2>&1; then + echo 'The NonZero negative check unexpectedly passed.' >&2 + exit 1 +fi +cat "$negative_log" +python3 -I - "$negative_log" <<'PY' +from pathlib import Path +import sys + +diagnostics = Path(sys.argv[1]).read_text() +if "Cannot prove" not in diagnostics or "Rust frontend failed" in diagnostics: + sys.exit("The NonZero negative check did not reject the missing nonzero precondition.") +PY + # Keep every stage sequential and require both proof and refinement to succeed. # Collect their independent diagnostics even when the first stage fails. # No assumption, unwind, reference-creation, or overflow suppression flags. From f42ac07e86a677ef15e6ce73a38f2f76e53160cb Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Sun, 13 Sep 2026 03:34:46 -0700 Subject: [PATCH 35/62] Include heap context for a failing hosted array regression Signed-off-by: Onyeka Obi --- verifast-proofs/core/iter/adapters/verify.sh | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/verifast-proofs/core/iter/adapters/verify.sh b/verifast-proofs/core/iter/adapters/verify.sh index 2e8039e05998d..14e5d1167cc7e 100644 --- a/verifast-proofs/core/iter/adapters/verify.sh +++ b/verifast-proofs/core/iter/adapters/verify.sh @@ -63,8 +63,15 @@ timeout --signal=TERM --kill-after=10s 60s \ verifast -rustc_args '--edition 2024' backend/add-valid.rs timeout --signal=TERM --kill-after=10s 60s \ verifast -rustc_args '--edition 2024' backend/const-valid.rs -timeout --signal=TERM --kill-after=10s 60s \ - verifast -rustc_args '--edition 2024' backend/array-valid.rs +if ! timeout --signal=TERM --kill-after=10s 60s \ + verifast -rustc_args '--edition 2024' backend/array-valid.rs; then + # The regression already failed. Emit heap context without changing that verdict. + diagnostic_status=0 + timeout --signal=TERM --kill-after=5s 30s \ + verifast -json -rustc_args '--edition 2024' backend/array-valid.rs || diagnostic_status=$? + printf 'Array regression diagnostic status: %s\n' "$diagnostic_status" + exit 1 +fi negative_log="$(mktemp)" trap 'rm -f -- "$negative_log"' EXIT if timeout --signal=TERM --kill-after=10s 60s \ From f579051dcb3bcd393597ee49836747bc82a31ad4 Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Sun, 13 Sep 2026 03:52:50 -0700 Subject: [PATCH 36/62] Preserve pointer and length when unsizing generic arrays Signed-off-by: Onyeka Obi --- .../adapters/backend/array-length-invalid.rs | 12 +++++ .../core/iter/adapters/backend/array-valid.rs | 10 +++++ .../adapters/backend/const-generics.patch | 45 +++++++++++++++++-- verifast-proofs/core/iter/adapters/verify.sh | 15 +++++++ 4 files changed, 79 insertions(+), 3 deletions(-) create mode 100644 verifast-proofs/core/iter/adapters/backend/array-length-invalid.rs diff --git a/verifast-proofs/core/iter/adapters/backend/array-length-invalid.rs b/verifast-proofs/core/iter/adapters/backend/array-length-invalid.rs new file mode 100644 index 0000000000000..abba535bbf91a --- /dev/null +++ b/verifast-proofs/core/iter/adapters/backend/array-length-invalid.rs @@ -0,0 +1,12 @@ +#![crate_type = "lib"] +#![crate_name = "array_length_invalid"] + +unsafe fn array_len(p: *const [T; N]) -> usize +//@ req [?f]ref_initialized(p); +//@ ens [f]ref_initialized(p) &*& result == usize_of_const(typeid(N)) + 1; +//@ on_unwind_ens false; +{ + //@ reborrow_ref_(p); + let slice: &[T] = unsafe { &*p }; + slice.len() +} diff --git a/verifast-proofs/core/iter/adapters/backend/array-valid.rs b/verifast-proofs/core/iter/adapters/backend/array-valid.rs index c37c674fb25df..d65ac982d8755 100644 --- a/verifast-proofs/core/iter/adapters/backend/array-valid.rs +++ b/verifast-proofs/core/iter/adapters/backend/array-valid.rs @@ -40,3 +40,13 @@ unsafe fn matrix_mut_ptr( { unsafe { (*p).as_mut_ptr().cast() } } + +unsafe fn array_len(p: *const [T; N]) -> usize +//@ req [?f]ref_initialized(p); +//@ ens [f]ref_initialized(p) &*& result == usize_of_const(typeid(N)); +//@ on_unwind_ens false; +{ + //@ reborrow_ref_(p); + let slice: &[T] = unsafe { &*p }; + slice.len() +} diff --git a/verifast-proofs/core/iter/adapters/backend/const-generics.patch b/verifast-proofs/core/iter/adapters/backend/const-generics.patch index 34383b3745d22..793a38f223209 100644 --- a/verifast-proofs/core/iter/adapters/backend/const-generics.patch +++ b/verifast-proofs/core/iter/adapters/backend/const-generics.patch @@ -11,6 +11,14 @@ ignored @1: Void; # A predicate that we are ignoring for now } } +@@ -681,6 +685,7 @@ + kind @2: CastKind; + operand @0: Operand; + ty @1: Ty; ++ sourceTy @3: Ty; + } + + struct AggregateData { --- a/src/rust_frontend/vf_mir_exporter/src/lib.rs +++ b/src/rust_frontend/vf_mir_exporter/src/lib.rs @@ -1224,6 +1224,13 @@ @@ -39,6 +47,16 @@ let name_cpn = p_cpn.reborrow().init_name(); Self::encode_hir_generic_param_name(enc_ctx, p.def_id, &p.name, name_cpn); let span_cpn = p_cpn.reborrow().init_span(); +@@ -2287,6 +2299,9 @@ + } + let operand_cpn = cast_data_cpn.reborrow().init_operand(); + Self::encode_operand(tcx, enc_ctx, operand, operand_cpn); ++ let source_ty = operand.ty(&enc_ctx.body().local_decls, tcx); ++ let source_ty_cpn = cast_data_cpn.reborrow().init_source_ty(); ++ Self::encode_ty(tcx, enc_ctx, source_ty, source_ty_cpn); + let ty_cpn = cast_data_cpn.init_ty(); + Self::encode_ty(tcx, enc_ctx, *ty, ty_cpn); + } --- a/src/rust_frontend/vf_mir_translator/vf_mir_translator.ml +++ b/src/rust_frontend/vf_mir_translator/vf_mir_translator.ml @@ -801,8 +801,9 @@ @@ -151,7 +169,7 @@ if TranslatorArgs.ignore_ref_creation then true else let directives = -@@ -3157,7 +3165,14 @@ +@@ -3157,12 +3165,34 @@ Ok (`TrRvalueExpr e) | _ -> let* ty_info = translate_decoded_ty cast_data.ty loc in @@ -163,11 +181,32 @@ + (* The borrow is checked by Rvalue::Ref; this coercion only adds slice metadata. *) + Ast.PtrTypeExpr (loc, slice_ty) + | ty -> ty ++ in ++ let cast_expr expr = ++ match cast_data.kind, cast_data.source_ty.kind, cast_data.ty.kind with ++ | D.PointerCoercion, ++ (D.Ref {ty = {kind = D.Array array_ty}} | D.RawPtr {ty = {kind = D.Array array_ty}}), ++ (D.Ref {ty = {kind = D.Slice elem_ty}} | D.RawPtr {ty = {kind = D.Slice elem_ty}}) -> ++ let* elem_ty_info = translate_decoded_ty elem_ty loc in ++ let* len = translate_ty_const array_ty.size loc in ++ (* Unsize preserves the borrowed data pointer and adds the array length. *) ++ let call = Ast.CallExpr (loc, "std::ptr::slice_from_raw_parts_mut", ++ [elem_ty_info.vf_ty], [], ++ [LitPat (Ast.CastExpr (loc, PtrTypeExpr (loc, elem_ty_info.vf_ty), expr)); ++ LitPat len], Static) in ++ Ok (`TrRvalueExpr (result_of_outcome loc call)) ++ | cast -> Ok (`TrRvalueExpr (Ast.CastExpr (loc, ty, expr))) + in match operand with | `TrOperandCopy expr | `TrOperandMove (expr, _ (*place_is_mutable*)) -@@ -5993,17 +6008,18 @@ + | `TrTypedConstantScalar expr -> +- Ok (`TrRvalueExpr (Ast.CastExpr (loc, ty, expr))) ++ cast_expr expr + | `TrTypedConstantRvalueBinderBuilder rvalue_binder_builder -> + failwith "Todo: Rvalue::Cast" + (*Todo @Nima: We need a better design (refactor) for passing different results of operand translation*) +@@ -5993,17 +6023,18 @@ with | Lifetime -> Ok (`Lifetime name) | Type -> Ok (`Type name) @@ -195,7 +234,7 @@ let lft_params = Util.flatmap (function `Lifetime x -> [ x ] | _ -> []) generics in -@@ -6029,7 +6045,9 @@ +@@ -6029,7 +6060,9 @@ |> List.map decode_predicate in let sized_tparams = compute_sized_tparams preds in diff --git a/verifast-proofs/core/iter/adapters/verify.sh b/verifast-proofs/core/iter/adapters/verify.sh index 14e5d1167cc7e..cd6b80e8f0d6a 100644 --- a/verifast-proofs/core/iter/adapters/verify.sh +++ b/verifast-proofs/core/iter/adapters/verify.sh @@ -74,6 +74,21 @@ if ! timeout --signal=TERM --kill-after=10s 60s \ fi negative_log="$(mktemp)" trap 'rm -f -- "$negative_log"' EXIT +if timeout --signal=TERM --kill-after=10s 60s \ + verifast -rustc_args '--edition 2024' backend/array-length-invalid.rs >"$negative_log" 2>&1; then + cat "$negative_log" + echo 'The array-to-slice coercion accepted an incorrect length.' >&2 + exit 1 +fi +cat "$negative_log" +python3 -I - "$negative_log" <<'PY' +from pathlib import Path +import sys + +diagnostics = Path(sys.argv[1]).read_text() +if "Cannot prove" not in diagnostics or "Rust frontend failed" in diagnostics: + raise SystemExit("The array length check did not report the expected proof failure") +PY if timeout --signal=TERM --kill-after=10s 60s \ verifast -rustc_args '--edition 2024' backend/add-overflow.rs >"$negative_log" 2>&1; then cat "$negative_log" From 8d7c1049fe224ec7fc912c6ca0ee053cb6a1200f Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Sun, 13 Sep 2026 04:00:52 -0700 Subject: [PATCH 37/62] Update refinement cast records and retain bounded proof context Signed-off-by: Onyeka Obi --- .../adapters/backend/const-generics.patch | 26 +++++++++++++++- verifast-proofs/core/iter/adapters/verify.sh | 31 +++++++++++++++++-- 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/verifast-proofs/core/iter/adapters/backend/const-generics.patch b/verifast-proofs/core/iter/adapters/backend/const-generics.patch index 793a38f223209..4083f79fafc2f 100644 --- a/verifast-proofs/core/iter/adapters/backend/const-generics.patch +++ b/verifast-proofs/core/iter/adapters/backend/const-generics.patch @@ -276,7 +276,31 @@ | Val mir_val_const_cpn -> let ty = decode_ty genv mir_val_const_cpn.ty in let mir_const_value_cpn = mir_val_const_cpn.const_value in -@@ -2336,7 +2342,20 @@ +@@ -442,6 +448,7 @@ + lhs_place=local "contents_ptr"; + rhs_rvalue=Cast { + kind=PtrToPtr; ++ source_ty={kind=RawPtr {mutability=Not; ty={kind=Param "T"}}}; + operand=Copy { + local={name="self"}; + projection=[ +@@ -472,6 +479,7 @@ + lhs_place=local "contents_ptr2"; + rhs_rvalue=Cast { + kind=PtrToPtr; ++ source_ty={kind=RawPtr {mutability=Not; ty={kind=Param "T"}}}; + operand=Copy { + local={name="self"}; + projection=[ +@@ -658,6 +666,7 @@ + lhs_place=local "result"; + rhs_rvalue=Cast { + kind=PtrToPtr; ++ source_ty={kind=RawPtr {mutability=Not; ty={kind=Param "T"}}}; + operand=Copy { + local={name="b"}; + projection=[ +@@ -2336,7 +2345,20 @@ let projection_preds1, preds1 = List.partition (function Projection _ -> true | _ -> false) preds1 in if List.length projection_preds0 <> List.length projection_preds1 then failwith "The two functions have a different number of projection predicates"; List.iter2 (fun pred0 pred1 -> check_predicate_refines_predicate root_genv0 pred0 root_genv1 pred1) projection_preds0 projection_preds1; diff --git a/verifast-proofs/core/iter/adapters/verify.sh b/verifast-proofs/core/iter/adapters/verify.sh index cd6b80e8f0d6a..5752452df51b8 100644 --- a/verifast-proofs/core/iter/adapters/verify.sh +++ b/verifast-proofs/core/iter/adapters/verify.sh @@ -216,9 +216,34 @@ if (( proof_status != 0 )); then while IFS= read -r proof_location; do diagnostic_status=0 timeout --signal=TERM --kill-after=5s 30s \ - verifast -rustc_args '--edition 2024' -skip_specless_fns \ - -focus "$proof_location" verified/lib.rs || diagnostic_status=$? - printf 'Diagnostic %s: status %s\n' "$proof_location" "$diagnostic_status" + verifast -json -rustc_args '--edition 2024' -skip_specless_fns \ + -focus "$proof_location" verified/lib.rs >"$negative_log" 2>&1 || diagnostic_status=$? + printf 'Diagnostic %s: process status %s\n' "$proof_location" "$diagnostic_status" + python3 -I - "$negative_log" <<'PY' +from pathlib import Path +import json +import sys + +path = Path(sys.argv[1]) +if path.stat().st_size > 8 * 1024 * 1024: + raise SystemExit("Diagnostic exceeded the 8 MiB parsing limit") +lines = path.read_text().splitlines() +for line in reversed(lines): + if line.startswith('["VeriFast-Json",'): + result = json.loads(line)[3]["result"] + if result[0] == "SymbolicExecutionError": + print(json.dumps(result[2:4])) + for frame in result[1]: + if frame[0] == "Executing": + print(json.dumps(frame)[:7000]) + break + else: + print(json.dumps(result)[:7000]) + break +else: + print("No JSON verdict; last diagnostic lines:") + print("\n".join(lines[-15:])[:7000]) +PY done < <(python3 -I - <<'PY' from pathlib import Path import re From 4a9dbb3e044fadfea35c06e075d9a58d08c04801 Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Sun, 13 Sep 2026 04:08:32 -0700 Subject: [PATCH 38/62] Track nested array storage through mutable buffer borrows Signed-off-by: Onyeka Obi --- .../core/iter/adapters/array_layout.rsspec | 48 +++++++++++++++++++ .../adapters/backend/matrix-layout-valid.rs | 13 +++++ .../iter/adapters/verified/map_windows.rs | 25 ++++++++-- verifast-proofs/core/iter/adapters/verify.sh | 8 +++- 4 files changed, 88 insertions(+), 6 deletions(-) create mode 100644 verifast-proofs/core/iter/adapters/array_layout.rsspec create mode 100644 verifast-proofs/core/iter/adapters/backend/matrix-layout-valid.rs diff --git a/verifast-proofs/core/iter/adapters/array_layout.rsspec b/verifast-proofs/core/iter/adapters/array_layout.rsspec new file mode 100644 index 0000000000000..aeda150478a90 --- /dev/null +++ b/verifast-proofs/core/iter/adapters/array_layout.rsspec @@ -0,0 +1,48 @@ +/*@ + +fix matrix_elems(matrix: [[T; N]; 2]) -> list { + append(Array_elems(head(Array_elems(matrix))), + Array_elems(head(tail(Array_elems(matrix))))) +} + +// Keep a fraction while changing representations so precision relates the values. +lem pack_array(p: *[T; N]) + req (p as *T)[..usize_of_const(typeid(N))] |-> ?elems; + ens *p |-> ?array &*& Array_elems(array) == elems; +{ + split_fraction array(p as *T, usize_of_const(typeid(N)), elems) by 1/2; + array_to_Array(p); + Array_to_array(p); + merge_fractions array(p as *T, usize_of_const(typeid(N)), _); + array_to_Array(p); +} + +lem unpack_matrix(p: *[[T; N]; 2]) + req *p |-> ?matrix; + ens (p as *T)[..2 * usize_of_const(typeid(N))] |-> matrix_elems(matrix); +{ + Array_to_array(p); + open array(p as *[T; N], 2, _); + open array((p as *[T; N]) + 1, 1, _); + open array((p as *[T; N]) + 2, 0, _); + Array_to_array(p as *[T; N]); + Array_to_array((p as *[T; N]) + 1); + array_join(p as *T); +} + +lem pack_matrix(p: *[[T; N]; 2]) + req (p as *T)[..2 * usize_of_const(typeid(N))] |-> ?elems; + ens *p |-> ?matrix &*& matrix_elems(matrix) == elems; +{ + array_split(p as *T, usize_of_const(typeid(N))); + pack_array(p as *[T; N]); + pack_array((p as *[T; N]) + 1); + assert *(p as *[T; N]) |-> ?first; + assert *((p as *[T; N]) + 1) |-> ?second; + close array((p as *[T; N]) + 2, 0, nil); + close array((p as *[T; N]) + 1, 1, cons(second, nil)); + close array(p as *[T; N], 2, cons(first, cons(second, nil))); + pack_array(p); +} + +@*/ diff --git a/verifast-proofs/core/iter/adapters/backend/matrix-layout-valid.rs b/verifast-proofs/core/iter/adapters/backend/matrix-layout-valid.rs new file mode 100644 index 0000000000000..57d0fc9e208d4 --- /dev/null +++ b/verifast-proofs/core/iter/adapters/backend/matrix-layout-valid.rs @@ -0,0 +1,13 @@ +#![crate_type = "lib"] +#![crate_name = "matrix_layout_valid"] + +//@ include!{"../array_layout.rsspec"} + +unsafe fn round_trip(p: *mut [[T; N]; 2]) +//@ req *p |-> ?matrix; +//@ ens *p |-> ?after &*& matrix_elems(after) == matrix_elems(matrix); +//@ on_unwind_ens false; +{ + //@ unpack_matrix(p); + //@ pack_matrix(p); +} diff --git a/verifast-proofs/core/iter/adapters/verified/map_windows.rs b/verifast-proofs/core/iter/adapters/verified/map_windows.rs index 9936cb9c95af6..91d4395192d45 100644 --- a/verifast-proofs/core/iter/adapters/verified/map_windows.rs +++ b/verifast-proofs/core/iter/adapters/verified/map_windows.rs @@ -3,6 +3,8 @@ use crate::mem::MaybeUninit; use crate::{fmt, ptr}; +//@ include!{"../array_layout.rsspec"} + struct Buffer { // Invariant: `self.buffer[self.start..self.start + N]` is initialized, // with all other elements being uninitialized. This also @@ -94,8 +96,8 @@ impl Buffer { #[inline] unsafe fn buffer_mut_ptr(&mut self) -> *mut MaybeUninit -//@ req pointer_within_limits(base(self)) == true; - //@ ens result == base(self); +//@ req (*self).buffer |-> ?matrix; + //@ ens *(result as *[[std::mem::MaybeUninit; N]; 2]) |-> matrix &*& ref_mut_end_token(result as *[[std::mem::MaybeUninit; N]; 2], &(*self).buffer); //@ on_unwind_ens false; { self.buffer.as_mut_ptr().cast() @@ -174,9 +176,15 @@ impl Buffer { { //@ open live(t, self, start, values); //@ open bounds(self, start); - //@ open foreach(values, own::(t)); - //@ open array(base(self) + start, width::(), _); + //@ array_join(base(self)); + //@ array_join(base(self)); + //@ pack_matrix(&(*self).buffer); let buffer_mut_ptr = unsafe { self.buffer_mut_ptr() }; + //@ unpack_matrix(buffer_mut_ptr as *[[std::mem::MaybeUninit; N]; 2]); + //@ array_split(buffer_mut_ptr, start); + //@ array_split(buffer_mut_ptr + start, width::()); + //@ open foreach(values, own::(t)); + //@ open array(buffer_mut_ptr + start, width::(), _); debug_assert!(self.start + N <= 2 * N); let to_drop = if self.start == N { @@ -203,6 +211,7 @@ impl Buffer { //@ array_to_array_(buffer_mut_ptr); ptr::copy_nonoverlapping(buffer_mut_ptr.add(self.start + 1), buffer_mut_ptr, N - 1); (*buffer_mut_ptr.add(N - 1)).write(next); + //@ end_ref_mut_::>(); //@ close array(buffer_mut_ptr + width::() - 1, 1, cons(std::mem::MaybeUninit::new(next), nil)); //@ array_join(buffer_mut_ptr); buffer_mut_ptr.add(self.start) @@ -226,6 +235,7 @@ impl Buffer { let to_drop = unsafe { //@ open array(buffer_mut_ptr + start + width::(), width::() - start, _); (*buffer_mut_ptr.add(self.start + N)).write(next); + //@ end_ref_mut_::>(); //@ close array(buffer_mut_ptr + start + width::() + 1, 0, nil); //@ close array(buffer_mut_ptr + start + width::(), 1, cons(std::mem::MaybeUninit::new(next), nil)); //@ array_join(buffer_mut_ptr + start + 1); @@ -254,6 +264,13 @@ impl Buffer { close array(to_drop, 1, _); array_join(buffer_mut_ptr); } + array_join(buffer_mut_ptr); + array_join(buffer_mut_ptr); + pack_matrix(buffer_mut_ptr as *[[std::mem::MaybeUninit; N]; 2]); + end_ref_mut_::<[[std::mem::MaybeUninit; N]; 2]>(); + unpack_matrix(&(*self).buffer); + array_split(base(self), if start == width::() { 0 } else { start + 1 }); + array_split(base(self) + (if start == width::() { 0 } else { start + 1 }), width::()); close bounds(self, if start == width::() { 0 } else { start + 1 }); close live(t, self, if start == width::() { 0 } else { start + 1 }, append(tail(values), cons(next, nil))); diff --git a/verifast-proofs/core/iter/adapters/verify.sh b/verifast-proofs/core/iter/adapters/verify.sh index 5752452df51b8..f008be46925f4 100644 --- a/verifast-proofs/core/iter/adapters/verify.sh +++ b/verifast-proofs/core/iter/adapters/verify.sh @@ -204,6 +204,9 @@ PY # Keep every stage sequential and require both proof and refinement to succeed. # Collect their independent diagnostics even when the first stage fails. # No assumption, unwind, reference-creation, or overflow suppression flags. +layout_status=0 +timeout --signal=TERM --kill-after=10s 60s \ + verifast -rustc_args '--edition 2024' backend/matrix-layout-valid.rs || layout_status=$? proof_status=0 timeout --signal=TERM --kill-after=10s 600s \ verifast -rustc_args '--edition 2024' -skip_specless_fns verified/lib.rs || proof_status=$? @@ -255,8 +258,9 @@ for path in (Path("verified/map_windows.rs"), Path("verified/step_by.rs")): PY ) fi -if (( proof_status != 0 || refinement_status != 0 )); then - printf 'FAIL: proof status %s; refinement status %s\n' "$proof_status" "$refinement_status" >&2 +if (( proof_status != 0 || refinement_status != 0 || layout_status != 0 )); then + printf 'FAIL: proof status %s; refinement status %s; layout status %s\n' \ + "$proof_status" "$refinement_status" "$layout_status" >&2 exit 1 fi echo 'PASS: generic adapter contracts, source refinement, and source identity' From 12202e003e6181782af85a8da40b4545d41d536e Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Sun, 13 Sep 2026 04:14:11 -0700 Subject: [PATCH 39/62] Load shared layout lemmas through a Rust module Signed-off-by: Onyeka Obi --- .../iter/adapters/{array_layout.rsspec => array_layout.rs} | 0 .../core/iter/adapters/backend/matrix-layout-valid.rs | 4 +++- verifast-proofs/core/iter/adapters/verified/map_windows.rs | 4 +++- 3 files changed, 6 insertions(+), 2 deletions(-) rename verifast-proofs/core/iter/adapters/{array_layout.rsspec => array_layout.rs} (100%) diff --git a/verifast-proofs/core/iter/adapters/array_layout.rsspec b/verifast-proofs/core/iter/adapters/array_layout.rs similarity index 100% rename from verifast-proofs/core/iter/adapters/array_layout.rsspec rename to verifast-proofs/core/iter/adapters/array_layout.rs diff --git a/verifast-proofs/core/iter/adapters/backend/matrix-layout-valid.rs b/verifast-proofs/core/iter/adapters/backend/matrix-layout-valid.rs index 57d0fc9e208d4..582612575c138 100644 --- a/verifast-proofs/core/iter/adapters/backend/matrix-layout-valid.rs +++ b/verifast-proofs/core/iter/adapters/backend/matrix-layout-valid.rs @@ -1,7 +1,9 @@ #![crate_type = "lib"] #![crate_name = "matrix_layout_valid"] -//@ include!{"../array_layout.rsspec"} +#[path = "../array_layout.rs"] +mod array_layout; +//@ use array_layout::{matrix_elems, pack_matrix, unpack_matrix}; unsafe fn round_trip(p: *mut [[T; N]; 2]) //@ req *p |-> ?matrix; diff --git a/verifast-proofs/core/iter/adapters/verified/map_windows.rs b/verifast-proofs/core/iter/adapters/verified/map_windows.rs index 91d4395192d45..98b9ac40104f4 100644 --- a/verifast-proofs/core/iter/adapters/verified/map_windows.rs +++ b/verifast-proofs/core/iter/adapters/verified/map_windows.rs @@ -3,7 +3,9 @@ use crate::mem::MaybeUninit; use crate::{fmt, ptr}; -//@ include!{"../array_layout.rsspec"} +#[path = "../array_layout.rs"] +mod array_layout; +//@ use array_layout::{matrix_elems, pack_matrix, unpack_matrix}; struct Buffer { // Invariant: `self.buffer[self.start..self.start + N]` is initialized, From e0f60b1ae432fcc1bf14def03d080c2c9050b849 Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Sun, 13 Sep 2026 04:19:53 -0700 Subject: [PATCH 40/62] Keep proof imports in matching crate roots Signed-off-by: Onyeka Obi --- verifast-proofs/core/iter/adapters/array_layout.rs | 7 ++++++- verifast-proofs/core/iter/adapters/original/lib.rs | 2 ++ verifast-proofs/core/iter/adapters/verified/lib.rs | 2 ++ verifast-proofs/core/iter/adapters/verified/map_windows.rs | 4 +--- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/verifast-proofs/core/iter/adapters/array_layout.rs b/verifast-proofs/core/iter/adapters/array_layout.rs index aeda150478a90..9c4158f9744e9 100644 --- a/verifast-proofs/core/iter/adapters/array_layout.rs +++ b/verifast-proofs/core/iter/adapters/array_layout.rs @@ -6,11 +6,16 @@ fix matrix_elems(matrix: [[T; N]; 2]) -> list { } // Keep a fraction while changing representations so precision relates the values. +lem split_array_fraction(p: *T, count: usize) + req [?f]array(p, count, ?elems); + ens [f/2]array(p, count, elems) &*& [f/2]array(p, count, elems); +{} + lem pack_array(p: *[T; N]) req (p as *T)[..usize_of_const(typeid(N))] |-> ?elems; ens *p |-> ?array &*& Array_elems(array) == elems; { - split_fraction array(p as *T, usize_of_const(typeid(N)), elems) by 1/2; + split_array_fraction(p as *T, usize_of_const(typeid(N))); array_to_Array(p); Array_to_array(p); merge_fractions array(p as *T, usize_of_const(typeid(N)), _); diff --git a/verifast-proofs/core/iter/adapters/original/lib.rs b/verifast-proofs/core/iter/adapters/original/lib.rs index 73a46822695da..5bfd29ed2f8fa 100644 --- a/verifast-proofs/core/iter/adapters/original/lib.rs +++ b/verifast-proofs/core/iter/adapters/original/lib.rs @@ -9,5 +9,7 @@ extern crate core as std; #[stable(feature = "rust1", since = "1.0.0")] pub use std::{fmt, intrinsics, mem, num, ptr}; +#[path = "../array_layout.rs"] +mod array_layout; mod map_windows; mod step_by; diff --git a/verifast-proofs/core/iter/adapters/verified/lib.rs b/verifast-proofs/core/iter/adapters/verified/lib.rs index 73a46822695da..5bfd29ed2f8fa 100644 --- a/verifast-proofs/core/iter/adapters/verified/lib.rs +++ b/verifast-proofs/core/iter/adapters/verified/lib.rs @@ -9,5 +9,7 @@ extern crate core as std; #[stable(feature = "rust1", since = "1.0.0")] pub use std::{fmt, intrinsics, mem, num, ptr}; +#[path = "../array_layout.rs"] +mod array_layout; mod map_windows; mod step_by; diff --git a/verifast-proofs/core/iter/adapters/verified/map_windows.rs b/verifast-proofs/core/iter/adapters/verified/map_windows.rs index 98b9ac40104f4..f583f9413901e 100644 --- a/verifast-proofs/core/iter/adapters/verified/map_windows.rs +++ b/verifast-proofs/core/iter/adapters/verified/map_windows.rs @@ -3,9 +3,7 @@ use crate::mem::MaybeUninit; use crate::{fmt, ptr}; -#[path = "../array_layout.rs"] -mod array_layout; -//@ use array_layout::{matrix_elems, pack_matrix, unpack_matrix}; +//@ use crate::array_layout::{matrix_elems, pack_matrix, unpack_matrix}; struct Buffer { // Invariant: `self.buffer[self.start..self.start + N]` is initialized, From 767c95293b015f487a8a24ee0b69465f80c9df36 Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Sun, 13 Sep 2026 04:25:18 -0700 Subject: [PATCH 41/62] Retain the original array view while proving value preservation Signed-off-by: Onyeka Obi --- verifast-proofs/core/iter/adapters/array_layout.rs | 9 ++++----- .../core/iter/adapters/verified/map_windows.rs | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/verifast-proofs/core/iter/adapters/array_layout.rs b/verifast-proofs/core/iter/adapters/array_layout.rs index 9c4158f9744e9..c18eed5b1eb9f 100644 --- a/verifast-proofs/core/iter/adapters/array_layout.rs +++ b/verifast-proofs/core/iter/adapters/array_layout.rs @@ -6,18 +6,17 @@ fix matrix_elems(matrix: [[T; N]; 2]) -> list { } // Keep a fraction while changing representations so precision relates the values. -lem split_array_fraction(p: *T, count: usize) - req [?f]array(p, count, ?elems); - ens [f/2]array(p, count, elems) &*& [f/2]array(p, count, elems); -{} +pred_ctor saved_array(p: *T, count: usize, elems: list)(;) = + [1/2]array(p, count, elems); lem pack_array(p: *[T; N]) req (p as *T)[..usize_of_const(typeid(N))] |-> ?elems; ens *p |-> ?array &*& Array_elems(array) == elems; { - split_array_fraction(p as *T, usize_of_const(typeid(N))); + close saved_array(p as *T, usize_of_const(typeid(N)), elems)(); array_to_Array(p); Array_to_array(p); + open saved_array(p as *T, usize_of_const(typeid(N)), elems)(); merge_fractions array(p as *T, usize_of_const(typeid(N)), _); array_to_Array(p); } diff --git a/verifast-proofs/core/iter/adapters/verified/map_windows.rs b/verifast-proofs/core/iter/adapters/verified/map_windows.rs index f583f9413901e..b5206634cd167 100644 --- a/verifast-proofs/core/iter/adapters/verified/map_windows.rs +++ b/verifast-proofs/core/iter/adapters/verified/map_windows.rs @@ -3,7 +3,7 @@ use crate::mem::MaybeUninit; use crate::{fmt, ptr}; -//@ use crate::array_layout::{matrix_elems, pack_matrix, unpack_matrix}; +//@ use array_layout::{matrix_elems, pack_matrix, unpack_matrix}; struct Buffer { // Invariant: `self.buffer[self.start..self.start + N]` is initialized, From 5d44d8fc567ca912878c37bc13e06e3b82686d48 Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Sun, 13 Sep 2026 04:31:00 -0700 Subject: [PATCH 42/62] Preserve backing storage across the writable window borrow Signed-off-by: Onyeka Obi --- verifast-proofs/core/iter/adapters/README.md | 9 ++-- .../core/iter/adapters/array_layout.rs | 28 ++++++++++- .../adapters/backend/matrix-layout-valid.rs | 13 +++++- .../iter/adapters/verified/map_windows.rs | 46 +++++++++++++++++-- 4 files changed, 85 insertions(+), 11 deletions(-) diff --git a/verifast-proofs/core/iter/adapters/README.md b/verifast-proofs/core/iter/adapters/README.md index 9324570f82be3..36935fd5e7ca6 100644 --- a/verifast-proofs/core/iter/adapters/README.md +++ b/verifast-proofs/core/iter/adapters/README.md @@ -15,7 +15,7 @@ or unwind bounds. The target contracts are: | --- | --- | | `StepBy::original_step` | Preserve the step field and return its nonzero successor, for any `I`. | | `Buffer::as_array_ref` | Derive the active window's reference from its shared borrow and valid buffer bounds. | -| `Buffer::as_uninit_array_mut` | Derive a writable window from its exclusive storage borrow, without requiring initialized `T` values. | +| `Buffer::as_uninit_array_mut` | Derive a writable window from an exclusive borrow of the backing array, without requiring initialized `T` values. | | `Buffer::push` | Consume one new `T`, drop the old front, and preserve ownership of the shifted window on normal return. | | `Buffer::drop` | Drop exactly the initialized window and recover its storage on normal return. | @@ -41,9 +41,10 @@ intended `MapWindows` constructor and Rust layout restrictions. They are explicit caller obligations here. Constructor preservation and the surrounding `MapWindows` iterator implementation are outside this projection. -The reference accessors require the corresponding lifetime borrow of the -window. Establishing those borrows from the full safe abstraction is also -outside this port. The mutable accessor deliberately accepts storage that +The shared accessor requires a lifetime borrow of the window. The mutable +accessor requires a borrow of the whole backing array because its pointer +helper borrows that array. Establishing those borrows from the full safe +abstraction is outside this port. The mutable accessor accepts storage that does not yet own initialized `T` values, as required by the clone path. ## Source correspondence diff --git a/verifast-proofs/core/iter/adapters/array_layout.rs b/verifast-proofs/core/iter/adapters/array_layout.rs index c18eed5b1eb9f..791797137ecba 100644 --- a/verifast-proofs/core/iter/adapters/array_layout.rs +++ b/verifast-proofs/core/iter/adapters/array_layout.rs @@ -1,5 +1,9 @@ /*@ +// MaybeUninit owns storage, without requiring ownership of a contained T. +pred_ctor maybe_uninit_own()(t: thread_id_t, value: std::mem::MaybeUninit) = true; +type_pred_def for >.own = maybe_uninit_own::; + fix matrix_elems(matrix: [[T; N]; 2]) -> list { append(Array_elems(head(Array_elems(matrix))), Array_elems(head(tail(Array_elems(matrix))))) @@ -13,10 +17,10 @@ lem pack_array(p: *[T; N]) req (p as *T)[..usize_of_const(typeid(N))] |-> ?elems; ens *p |-> ?array &*& Array_elems(array) == elems; { - close saved_array(p as *T, usize_of_const(typeid(N)), elems)(); + close saved_array::(p as *T, usize_of_const(typeid(N)), elems)(); array_to_Array(p); Array_to_array(p); - open saved_array(p as *T, usize_of_const(typeid(N)), elems)(); + open saved_array::(p as *T, usize_of_const(typeid(N)), elems)(); merge_fractions array(p as *T, usize_of_const(typeid(N)), _); array_to_Array(p); } @@ -49,4 +53,24 @@ lem pack_matrix(p: *[[T; N]; 2]) pack_array(p); } +// These conversions preserve writable storage. They do not claim initialized T values. +lem collapse_window(p: *std::mem::MaybeUninit) + req p[..usize_of_const(typeid(N))] |-> _; + ens *(p as *std::mem::MaybeUninit<[T; N]>) |-> _; +{ + array__to_u8s_(p, usize_of_const(typeid(N))); + array_to_array_(p as *u8); + from_u8s_(p as *[T; N]); + std::mem::close_MaybeUninit_(p as *std::mem::MaybeUninit<[T; N]>); +} + +lem expand_window(p: *std::mem::MaybeUninit<[T; N]>) + req *p |-> _; + ens (p as *std::mem::MaybeUninit)[..usize_of_const(typeid(N))] |-> _; +{ + std::mem::open_MaybeUninit(p); + Array__to_array_(p as *[T; N]); + std::mem::array__to_array_MaybeUninit(p as *T); +} + @*/ diff --git a/verifast-proofs/core/iter/adapters/backend/matrix-layout-valid.rs b/verifast-proofs/core/iter/adapters/backend/matrix-layout-valid.rs index 582612575c138..5e63605b34fda 100644 --- a/verifast-proofs/core/iter/adapters/backend/matrix-layout-valid.rs +++ b/verifast-proofs/core/iter/adapters/backend/matrix-layout-valid.rs @@ -3,7 +3,7 @@ #[path = "../array_layout.rs"] mod array_layout; -//@ use array_layout::{matrix_elems, pack_matrix, unpack_matrix}; +//@ use array_layout::{collapse_window, expand_window, matrix_elems, pack_array, pack_matrix, unpack_matrix}; unsafe fn round_trip(p: *mut [[T; N]; 2]) //@ req *p |-> ?matrix; @@ -13,3 +13,14 @@ unsafe fn round_trip(p: *mut [[T; N]; 2]) //@ unpack_matrix(p); //@ pack_matrix(p); } + +unsafe fn writable_window(p: *mut [std::mem::MaybeUninit; N]) +//@ req *p |-> _; +//@ ens *p |-> _; +//@ on_unwind_ens false; +{ + //@ Array_to_array(p); + //@ collapse_window::(p as *std::mem::MaybeUninit); + //@ expand_window(p as *std::mem::MaybeUninit<[T; N]>); + //@ pack_array(p); +} diff --git a/verifast-proofs/core/iter/adapters/verified/map_windows.rs b/verifast-proofs/core/iter/adapters/verified/map_windows.rs index b5206634cd167..f4808017cd915 100644 --- a/verifast-proofs/core/iter/adapters/verified/map_windows.rs +++ b/verifast-proofs/core/iter/adapters/verified/map_windows.rs @@ -3,7 +3,7 @@ use crate::mem::MaybeUninit; use crate::{fmt, ptr}; -//@ use array_layout::{matrix_elems, pack_matrix, unpack_matrix}; +//@ use array_layout::{collapse_window, expand_window, matrix_elems, pack_matrix, unpack_matrix}; struct Buffer { // Invariant: `self.buffer[self.start..self.start + N]` is initialized, @@ -41,6 +41,8 @@ pred live(t: thread_id_t, b: *Buffer, start: usize, values: list) pred storage(b: *Buffer; start: usize) = bounds(b, start) &*& base(b)[..2 * width::()] |-> ?slots; +pred_ctor writable_matrix(b: *Buffer)(;) = (*b).buffer |-> _; + // Unwrap initialized slots without requiring T: Copy or duplicating T.own. lem initialized_slots(p: *std::mem::MaybeUninit, values: list) req p[..length(values)] |-> map(std::mem::MaybeUninit::new, values); @@ -142,8 +144,7 @@ impl Buffer { unsafe fn as_uninit_array_mut<'a>(&'a mut self) -> &'a mut MaybeUninit<[T; N]> /*@ req thread_token(?t) &*& bounds(self, ?start) &*& [?q]lifetime_token('a) &*& - full_borrow('a, >.full_borrow_content(t, - (base(self) + start) as *std::mem::MaybeUninit<[T; N]>)); + full_borrow('a, writable_matrix(self)); @*/ /*@ ens thread_token(t) &*& bounds(self, start) &*& [q]lifetime_token('a) &*& @@ -155,7 +156,44 @@ impl Buffer { debug_assert!(self.start + N <= 2 * N); // SAFETY: our invariant guarantees these elements are in bounds. - let result = unsafe { &mut *self.buffer_mut_ptr().add(self.start).cast() }; + //@ open_full_borrow_strong_('a, writable_matrix(self)); + //@ open writable_matrix::(self)(); + let buffer_mut_ptr = unsafe { self.buffer_mut_ptr() }; + //@ unpack_matrix(buffer_mut_ptr as *[[std::mem::MaybeUninit; N]; 2]); + //@ array_split(buffer_mut_ptr, start); + //@ array_split(buffer_mut_ptr + start, width::()); + //@ collapse_window::(buffer_mut_ptr + start); + let result = unsafe { &mut *buffer_mut_ptr.add(self.start).cast() }; + /*@ + { + let window = (buffer_mut_ptr + start) as *std::mem::MaybeUninit<[T; N]>; + pred ctx(;) = + ref_mut_end_token(result, window) &*& + ref_mut_end_token(buffer_mut_ptr as *[[std::mem::MaybeUninit; N]; 2], &(*self).buffer) &*& + buffer_mut_ptr[..start] |-> _ &*& + (buffer_mut_ptr + start + width::())[..width::() - start] |-> _; + produce_lem_ptr_chunk restore_full_borrow_(ctx, + >.full_borrow_content(t, result), + writable_matrix(self))() { + open ctx(); + open_full_borrow_content::>(t, result); + open >.own(t, _); + end_ref_mut_::>(); + expand_window(window); + array_join(buffer_mut_ptr); + array_join(buffer_mut_ptr); + pack_matrix(buffer_mut_ptr as *[[std::mem::MaybeUninit; N]; 2]); + end_ref_mut_::<[[std::mem::MaybeUninit; N]; 2]>(); + close writable_matrix::(self)(); + } { + assert *result |-> ?contents; + close >.own(t, contents); + close_full_borrow_content::>(t, result); + close ctx(); + close_full_borrow_strong_(); + } + } + @*/ //@ close bounds(self, start); result } From 06f64c982f67510ded3cf0bd81d6a61fe2388888 Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Sun, 13 Sep 2026 04:45:58 -0700 Subject: [PATCH 43/62] Specify MaybeUninit ownership without granting ownership of T Signed-off-by: Onyeka Obi --- verifast-proofs/core/iter/adapters/README.md | 16 ++++++++++++---- .../core/iter/adapters/array_layout.rs | 4 ---- .../adapters/backend/maybeuninit-own-valid.rs | 10 ++++++++++ .../backend/maybeuninit-ownership.patch | 18 ++++++++++++++++++ .../core/iter/adapters/backend/prepare.sh | 4 +++- .../iter/adapters/backend/value-own-invalid.rs | 9 +++++++++ .../core/iter/adapters/verified/map_windows.rs | 7 ++++--- verifast-proofs/core/iter/adapters/verify.sh | 17 +++++++++++++++++ 8 files changed, 73 insertions(+), 12 deletions(-) create mode 100644 verifast-proofs/core/iter/adapters/backend/maybeuninit-own-valid.rs create mode 100644 verifast-proofs/core/iter/adapters/backend/maybeuninit-ownership.patch create mode 100644 verifast-proofs/core/iter/adapters/backend/value-own-invalid.rs diff --git a/verifast-proofs/core/iter/adapters/README.md b/verifast-proofs/core/iter/adapters/README.md index 36935fd5e7ca6..1f23e6ef63b51 100644 --- a/verifast-proofs/core/iter/adapters/README.md +++ b/verifast-proofs/core/iter/adapters/README.md @@ -151,9 +151,9 @@ failed, so the workflow correctly remained unsuccessful. `backend/prepare.sh` pins the source commit, source archive hash, and upstream dependency bundle hash. It builds the MIR exporter, verifier, and refinement -checker on the hosted Linux worker. Arithmetic rules and library contracts -for ownership, borrowing, and destruction are unchanged. Source projections -and mandatory refinement remain unchanged. The workflow caches these three +checker on the hosted Linux worker. Arithmetic rules and the existing borrowing +and destruction contracts are unchanged. Mandatory source refinement still +checks the selected implementations. The workflow caches these three binaries and the library specification, keyed by the preparation script and patches, with checksums checked on restoration. Every regression, proof, and refinement gate reruns after a cache hit. @@ -166,7 +166,15 @@ The frontend routes only the `usize` instantiation to this specification. Other instantiations remain unsupported. The constructor implementation is not proved by this port; the new specification is part of its trusted library boundary. A positive fixture and rejection of a missing nonzero precondition -are mandatory. No additional ownership, borrowing, or drop axiom is supplied. +are mandatory. + +`backend/maybeuninit-ownership.patch` extends the trusted library specification +with introduction and disposal rules for `MaybeUninit` ownership. Owning +that wrapper does not require ownership of a contained `T`; its memory remains +tracked by separate storage predicates. These rules model the wrapper's +ownership semantics and are not proved from its implementation by this port. +A positive wrapper fixture and rejection of missing ownership of an ordinary +`T` are mandatory. No new borrowing or generic drop contract is introduced. ## Local static checks and optional manual verification diff --git a/verifast-proofs/core/iter/adapters/array_layout.rs b/verifast-proofs/core/iter/adapters/array_layout.rs index 791797137ecba..bc88b5fa4b79f 100644 --- a/verifast-proofs/core/iter/adapters/array_layout.rs +++ b/verifast-proofs/core/iter/adapters/array_layout.rs @@ -1,9 +1,5 @@ /*@ -// MaybeUninit owns storage, without requiring ownership of a contained T. -pred_ctor maybe_uninit_own()(t: thread_id_t, value: std::mem::MaybeUninit) = true; -type_pred_def for >.own = maybe_uninit_own::; - fix matrix_elems(matrix: [[T; N]; 2]) -> list { append(Array_elems(head(Array_elems(matrix))), Array_elems(head(tail(Array_elems(matrix))))) diff --git a/verifast-proofs/core/iter/adapters/backend/maybeuninit-own-valid.rs b/verifast-proofs/core/iter/adapters/backend/maybeuninit-own-valid.rs new file mode 100644 index 0000000000000..aa1600c2f6ae3 --- /dev/null +++ b/verifast-proofs/core/iter/adapters/backend/maybeuninit-own-valid.rs @@ -0,0 +1,10 @@ +#![crate_type = "lib"] +#![crate_name = "maybeuninit_own_valid"] + +unsafe fn wrapper_ownership(p: *mut std::mem::MaybeUninit) +//@ req thread_token(?t) &*& *p |-> ?value; +//@ ens thread_token(t) &*& *p |-> value &*& >.own(t, value); +//@ on_unwind_ens false; +{ + //@ std::mem::MaybeUninit_own_init(t, value); +} diff --git a/verifast-proofs/core/iter/adapters/backend/maybeuninit-ownership.patch b/verifast-proofs/core/iter/adapters/backend/maybeuninit-ownership.patch new file mode 100644 index 0000000000000..31391218dd9f3 --- /dev/null +++ b/verifast-proofs/core/iter/adapters/backend/maybeuninit-ownership.patch @@ -0,0 +1,18 @@ +--- a/bin/rust/std/lib.rsspec ++++ b/bin/rust/std/lib.rsspec +@@ -436,6 +436,15 @@ + struct MaybeUninit; + + /*@ ++ ++ // MaybeUninit does not own or drop a contained T. Storage is tracked separately. ++ lem MaybeUninit_own_init(t: thread_id_t, value: MaybeUninit); ++ req true; ++ ens >.own(t, value); ++ ++ lem MaybeUninit_own_dispose(t: thread_id_t, value: MaybeUninit); ++ req >.own(t, value); ++ ens true; + + fix MaybeUninit::inner(v: MaybeUninit) -> option; + fix MaybeUninit::uninit() -> MaybeUninit; diff --git a/verifast-proofs/core/iter/adapters/backend/prepare.sh b/verifast-proofs/core/iter/adapters/backend/prepare.sh index d6acb2155a509..09b36bc3f2d48 100644 --- a/verifast-proofs/core/iter/adapters/backend/prepare.sh +++ b/verifast-proofs/core/iter/adapters/backend/prepare.sh @@ -10,7 +10,8 @@ backend_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" # The cached exporter still needs the pinned toolchain's shared libraries. rustup component add --toolchain nightly-2026-02-05 rustc-dev llvm-tools cache_key="$(sha256sum "$backend_dir/prepare.sh" "$backend_dir/add-unchecked.patch" \ - "$backend_dir/const-generics.patch" "$backend_dir/nonzero-usize.patch" | sha256sum | cut -d ' ' -f 1)" + "$backend_dir/const-generics.patch" "$backend_dir/nonzero-usize.patch" \ + "$backend_dir/maybeuninit-ownership.patch" | sha256sum | cut -d ' ' -f 1)" cache_dir="$HOME/.cache/verifast-iter-adapters/$cache_key" if [[ -f "$cache_dir/checksums" ]]; then (cd "$cache_dir" && sha256sum --check checksums) @@ -36,6 +37,7 @@ tar -xzf "$build_dir/source.tar.gz" --strip-components=1 -C "$build_dir" \ patch --batch --fuzz=0 --directory="$build_dir" -p1 < "$backend_dir/add-unchecked.patch" patch --batch --fuzz=0 --directory="$build_dir" -p1 < "$backend_dir/const-generics.patch" patch --batch --fuzz=0 --directory="$build_dir" -p1 < "$backend_dir/nonzero-usize.patch" +patch --batch --fuzz=0 --directory="$build_dir" -p1 < "$backend_dir/maybeuninit-ownership.patch" # Use the dependency bundle pinned by upstream's setup-build.sh. Its compiler # and package paths are built for /tmp/vfdeps-adf88dc on Linux. diff --git a/verifast-proofs/core/iter/adapters/backend/value-own-invalid.rs b/verifast-proofs/core/iter/adapters/backend/value-own-invalid.rs new file mode 100644 index 0000000000000..b42825bc8938d --- /dev/null +++ b/verifast-proofs/core/iter/adapters/backend/value-own-invalid.rs @@ -0,0 +1,9 @@ +#![crate_type = "lib"] +#![crate_name = "value_own_invalid"] + +unsafe fn missing_value_ownership(p: *mut T) +//@ req thread_token(?t) &*& *p |-> ?value; +//@ ens thread_token(t) &*& *p |-> value &*& .own(t, value); +//@ on_unwind_ens false; +{ +} diff --git a/verifast-proofs/core/iter/adapters/verified/map_windows.rs b/verifast-proofs/core/iter/adapters/verified/map_windows.rs index f4808017cd915..b916c095c14fd 100644 --- a/verifast-proofs/core/iter/adapters/verified/map_windows.rs +++ b/verifast-proofs/core/iter/adapters/verified/map_windows.rs @@ -165,8 +165,8 @@ impl Buffer { //@ collapse_window::(buffer_mut_ptr + start); let result = unsafe { &mut *buffer_mut_ptr.add(self.start).cast() }; /*@ + let window = (buffer_mut_ptr + start) as *std::mem::MaybeUninit<[T; N]>; { - let window = (buffer_mut_ptr + start) as *std::mem::MaybeUninit<[T; N]>; pred ctx(;) = ref_mut_end_token(result, window) &*& ref_mut_end_token(buffer_mut_ptr as *[[std::mem::MaybeUninit; N]; 2], &(*self).buffer) &*& @@ -177,7 +177,8 @@ impl Buffer { writable_matrix(self))() { open ctx(); open_full_borrow_content::>(t, result); - open >.own(t, _); + assert *result |-> ?borrowed_contents; + std::mem::MaybeUninit_own_dispose::<[T; N]>(t, borrowed_contents); end_ref_mut_::>(); expand_window(window); array_join(buffer_mut_ptr); @@ -187,7 +188,7 @@ impl Buffer { close writable_matrix::(self)(); } { assert *result |-> ?contents; - close >.own(t, contents); + std::mem::MaybeUninit_own_init::<[T; N]>(t, contents); close_full_borrow_content::>(t, result); close ctx(); close_full_borrow_strong_(); diff --git a/verifast-proofs/core/iter/adapters/verify.sh b/verifast-proofs/core/iter/adapters/verify.sh index f008be46925f4..3c4d938392d0d 100644 --- a/verifast-proofs/core/iter/adapters/verify.sh +++ b/verifast-proofs/core/iter/adapters/verify.sh @@ -201,6 +201,23 @@ if "Cannot prove" not in diagnostics or "Rust frontend failed" in diagnostics: sys.exit("The NonZero negative check did not reject the missing nonzero precondition.") PY +timeout --signal=TERM --kill-after=10s 60s \ + verifast -rustc_args '--edition 2024' backend/maybeuninit-own-valid.rs +if timeout --signal=TERM --kill-after=10s 60s \ + verifast -rustc_args '--edition 2024' backend/value-own-invalid.rs >"$negative_log" 2>&1; then + echo 'The ownership model created ownership of an arbitrary T.' >&2 + exit 1 +fi +cat "$negative_log" +python3 -I - "$negative_log" <<'PY' +from pathlib import Path +import sys + +diagnostics = Path(sys.argv[1]).read_text() +if "No matching heap chunks" not in diagnostics or "Rust frontend failed" in diagnostics: + raise SystemExit("The negative ownership check did not reject missing T ownership") +PY + # Keep every stage sequential and require both proof and refinement to succeed. # Collect their independent diagnostics even when the first stage fails. # No assumption, unwind, reference-creation, or overflow suppression flags. From f750710e55e1d643f5ef53aaa683b3b276998325 Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Sun, 13 Sep 2026 05:04:27 -0700 Subject: [PATCH 44/62] Connect generic drop glue to array storage ownership Signed-off-by: Onyeka Obi --- verifast-proofs/core/iter/adapters/README.md | 23 ++-- .../core/iter/adapters/array_layout.rs | 83 ++++++++++++++ .../iter/adapters/backend/array-layout.patch | 20 ++++ .../iter/adapters/backend/layout-invalid.rs | 8 ++ .../core/iter/adapters/backend/prepare.sh | 3 +- .../iter/adapters/verified/map_windows.rs | 104 +++++++++++++++--- verifast-proofs/core/iter/adapters/verify.sh | 21 +++- 7 files changed, 233 insertions(+), 29 deletions(-) create mode 100644 verifast-proofs/core/iter/adapters/backend/array-layout.patch create mode 100644 verifast-proofs/core/iter/adapters/backend/layout-invalid.rs diff --git a/verifast-proofs/core/iter/adapters/README.md b/verifast-proofs/core/iter/adapters/README.md index 1f23e6ef63b51..0475f20354a0f 100644 --- a/verifast-proofs/core/iter/adapters/README.md +++ b/verifast-proofs/core/iter/adapters/README.md @@ -17,13 +17,14 @@ or unwind bounds. The target contracts are: | `Buffer::as_array_ref` | Derive the active window's reference from its shared borrow and valid buffer bounds. | | `Buffer::as_uninit_array_mut` | Derive a writable window from an exclusive borrow of the backing array, without requiring initialized `T` values. | | `Buffer::push` | Consume one new `T`, drop the old front, and preserve ownership of the shifted window on normal return. | -| `Buffer::drop` | Drop exactly the initialized window and recover its storage on normal return. | +| `Buffer::drop` | Drop exactly the initialized window and recover its storage on normal return and unwind. | The two raw buffer pointer helpers also have contracts. `push` and `drop` -use the standard library specification of generic drop glue. Their unwind -postconditions return the thread token only. Unwind paths remain enabled, -but these contracts do not establish buffer-state recovery after a panicking -destructor. This is a remaining obligation for a full safe-abstraction proof. +use the standard library specification of generic drop glue. The candidate +`drop` safety proof recovers field storage on both outcomes through one lifetime +loan frame. This proof has not passed validation. The `push` unwind postcondition +returns the thread token only; preserving its buffer state after a panicking +element destructor remains a safe-abstraction obligation. ## Ownership and bounds @@ -176,6 +177,13 @@ ownership semantics and are not proved from its implementation by this port. A positive wrapper fixture and rejection of missing ownership of an ordinary `T` are mandatory. No new borrowing or generic drop contract is introduced. +`backend/array-layout.patch` supplies trusted size and alignment relations for +array type IDs and `MaybeUninit`. The facts follow the +[Rust array layout guarantee](https://doc.rust-lang.org/reference/type-layout.html#array-layout) +and the wrapper's documented layout. They include zero-sized element types. +The matrix conversion and writable-window proofs remain mandatory; the layout +facts do not grant storage or ownership permissions. + ## Local static checks and optional manual verification The static check reads small files and starts no compiler or solver: @@ -212,8 +220,9 @@ this candidate without altering the existing LinkedList and RawVec checks. - Obtain verifier and refinement verdicts for the adapters, including symbolic const sizes, `NonZero::new_unchecked`, reference creation, and generic slice drop glue. - The explicit slice assertions in `drop` must be proved by that memory - model; no local axiom has been added to make them pass. + The lifetime loans in `drop` must connect element storage to the slice + contract and recover storage on both outcomes. There is no adapter-specific + borrowing or drop axiom. - Complete the safe-abstraction and unwind-state obligations described above. - Port the remaining Challenge 16 targets, including arbitrary-length filter, filter-map, and zip iteration. This package has no proof of those loops. diff --git a/verifast-proofs/core/iter/adapters/array_layout.rs b/verifast-proofs/core/iter/adapters/array_layout.rs index bc88b5fa4b79f..9f6023a565591 100644 --- a/verifast-proofs/core/iter/adapters/array_layout.rs +++ b/verifast-proofs/core/iter/adapters/array_layout.rs @@ -1,5 +1,9 @@ /*@ +pred_ctor array_owned()(t: thread_id_t, value: [T; N]) = + foreach(Array_elems(value), own::(t)); +type_pred_def for <[T; N]>.own = array_owned::; + fix matrix_elems(matrix: [[T; N]; 2]) -> list { append(Array_elems(head(Array_elems(matrix))), Array_elems(head(tail(Array_elems(matrix))))) @@ -25,6 +29,7 @@ lem unpack_matrix(p: *[[T; N]; 2]) req *p |-> ?matrix; ens (p as *T)[..2 * usize_of_const(typeid(N))] |-> matrix_elems(matrix); { + std::mem::array_layout::(); Array_to_array(p); open array(p as *[T; N], 2, _); open array((p as *[T; N]) + 1, 1, _); @@ -38,6 +43,7 @@ lem pack_matrix(p: *[[T; N]; 2]) req (p as *T)[..2 * usize_of_const(typeid(N))] |-> ?elems; ens *p |-> ?matrix &*& matrix_elems(matrix) == elems; { + std::mem::array_layout::(); array_split(p as *T, usize_of_const(typeid(N))); pack_array(p as *[T; N]); pack_array((p as *[T; N]) + 1); @@ -54,6 +60,8 @@ lem collapse_window(p: *std::mem::MaybeUninit) req p[..usize_of_const(typeid(N))] |-> _; ens *(p as *std::mem::MaybeUninit<[T; N]>) |-> _; { + std::mem::array_layout::(); + std::mem::MaybeUninit_layout::(); array__to_u8s_(p, usize_of_const(typeid(N))); array_to_array_(p as *u8); from_u8s_(p as *[T; N]); @@ -64,9 +72,84 @@ lem expand_window(p: *std::mem::MaybeUninit<[T; N]>) req *p |-> _; ens (p as *std::mem::MaybeUninit)[..usize_of_const(typeid(N))] |-> _; { + std::mem::MaybeUninit_layout::(); std::mem::open_MaybeUninit(p); Array__to_array_(p as *[T; N]); std::mem::array__to_array_MaybeUninit(p as *T); } +lem own_uninit_values(t: thread_id_t, values: list>) + req true; + ens foreach(values, own::>(t)); +{ + match values { + nil => {} + cons(value, rest) => { + std::mem::MaybeUninit_own_init(t, value); + close own::>(t)(value); + own_uninit_values(t, rest); + } + } + close foreach(values, own::>(t)); +} + +lem own_uninit_rows(t: thread_id_t, rows: list<[std::mem::MaybeUninit; N]>) + req true; + ens foreach(rows, own::<[std::mem::MaybeUninit; N]>(t)); +{ + match rows { + nil => {} + cons(row, rest) => { + own_uninit_values(t, Array_elems(row)); + close <[std::mem::MaybeUninit; N]>.own(t, row); + close own::<[std::mem::MaybeUninit; N]>(t)(row); + own_uninit_rows(t, rest); + } + } + close foreach(rows, own::<[std::mem::MaybeUninit; N]>(t)); +} + +lem own_matrix_storage(t: thread_id_t, matrix: [[std::mem::MaybeUninit; N]; 2]) + req true; + ens <[[std::mem::MaybeUninit; N]; 2]>.own(t, matrix); +{ + own_uninit_rows(t, Array_elems(matrix)); + close <[[std::mem::MaybeUninit; N]; 2]>.own(t, matrix); +} + +pred array_borrow_tokens(k: lifetime_t, p: *T, count: usize;) = + pointer_within_limits(p) == true &*& + if count == 0 { true } else { + points_to_at_lft_end_token(k, p) &*& array_borrow_tokens(k, p + 1, count - 1) + }; + +lem lend_array(k: lifetime_t, p: *T, count: usize) + req 0 <= count &*& p[..count] |-> ?values; + ens array_at_lft(k, p, count, values) &*& array_borrow_tokens(k, p, count); +{ + open array(p, count, values); + if count > 0 { + borrow_points_to_at_lft(k, p); + lend_array(k, p + 1, count - 1); + } + close array_at_lft(k, p, count, values); + close array_borrow_tokens(k, p, count); +} + +lem reclaim_array(k: lifetime_t, p: *T, count: usize) + req 0 <= count &*& [_]lifetime_dead_token(k) &*& + array_borrow_tokens(k, p, count) &*& array_at_lft_(k, p, count, _); + ens p[..count] |-?-> _; +{ + open array_borrow_tokens(k, p, count); + open array_at_lft_(k, p, count, _); + if count > 0 { + borrow_points_to_at_lft_end(p); + // Discard expired lifetime bookkeeping after recovering the storage. + leak points_to_at_lft_(k, p, _); + reclaim_array(k, p + 1, count - 1); + } + close array_(p, count, _); +} + @*/ diff --git a/verifast-proofs/core/iter/adapters/backend/array-layout.patch b/verifast-proofs/core/iter/adapters/backend/array-layout.patch new file mode 100644 index 0000000000000..e2e9edeb2c91d --- /dev/null +++ b/verifast-proofs/core/iter/adapters/backend/array-layout.patch @@ -0,0 +1,20 @@ +--- a/bin/rust/std/lib.rsspec ++++ b/bin/rust/std/lib.rsspec +@@ -371,6 +371,17 @@ + + /*@ + ++ // Rust array layout and MaybeUninit transparency, including zero-sized T. ++ lem array_layout(); ++ req true; ++ ens std::mem::size_of(typeid([T; N])) == std::mem::size_of::() * usize_of_const(typeid(N)) &*& ++ std::mem::align_of(typeid([T; N])) == std::mem::align_of::(); ++ ++ lem MaybeUninit_layout(); ++ req true; ++ ens std::mem::size_of::>() == std::mem::size_of::() &*& ++ std::mem::align_of::>() == std::mem::align_of::(); ++ + fix size_of_val(x: *T) -> usize; + fix align_of_val(x: *T) -> usize; + diff --git a/verifast-proofs/core/iter/adapters/backend/layout-invalid.rs b/verifast-proofs/core/iter/adapters/backend/layout-invalid.rs new file mode 100644 index 0000000000000..e105380e138fd --- /dev/null +++ b/verifast-proofs/core/iter/adapters/backend/layout-invalid.rs @@ -0,0 +1,8 @@ +#![allow(dead_code)] + +unsafe fn incorrect_stride() +//@ req true; +//@ ens std::mem::size_of(typeid([T; N])) == std::mem::size_of::() * usize_of_const(typeid(N)) + 1; +{ + //@ std::mem::array_layout::(); +} diff --git a/verifast-proofs/core/iter/adapters/backend/prepare.sh b/verifast-proofs/core/iter/adapters/backend/prepare.sh index 09b36bc3f2d48..2254836533d7a 100644 --- a/verifast-proofs/core/iter/adapters/backend/prepare.sh +++ b/verifast-proofs/core/iter/adapters/backend/prepare.sh @@ -11,7 +11,7 @@ backend_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" rustup component add --toolchain nightly-2026-02-05 rustc-dev llvm-tools cache_key="$(sha256sum "$backend_dir/prepare.sh" "$backend_dir/add-unchecked.patch" \ "$backend_dir/const-generics.patch" "$backend_dir/nonzero-usize.patch" \ - "$backend_dir/maybeuninit-ownership.patch" | sha256sum | cut -d ' ' -f 1)" + "$backend_dir/maybeuninit-ownership.patch" "$backend_dir/array-layout.patch" | sha256sum | cut -d ' ' -f 1)" cache_dir="$HOME/.cache/verifast-iter-adapters/$cache_key" if [[ -f "$cache_dir/checksums" ]]; then (cd "$cache_dir" && sha256sum --check checksums) @@ -38,6 +38,7 @@ patch --batch --fuzz=0 --directory="$build_dir" -p1 < "$backend_dir/add-unchecke patch --batch --fuzz=0 --directory="$build_dir" -p1 < "$backend_dir/const-generics.patch" patch --batch --fuzz=0 --directory="$build_dir" -p1 < "$backend_dir/nonzero-usize.patch" patch --batch --fuzz=0 --directory="$build_dir" -p1 < "$backend_dir/maybeuninit-ownership.patch" +patch --batch --fuzz=0 --directory="$build_dir" -p1 < "$backend_dir/array-layout.patch" # Use the dependency bundle pinned by upstream's setup-build.sh. Its compiler # and package paths are built for /tmp/vfdeps-adf88dc on Linux. diff --git a/verifast-proofs/core/iter/adapters/verified/map_windows.rs b/verifast-proofs/core/iter/adapters/verified/map_windows.rs index b916c095c14fd..3028eacfd4c2f 100644 --- a/verifast-proofs/core/iter/adapters/verified/map_windows.rs +++ b/verifast-proofs/core/iter/adapters/verified/map_windows.rs @@ -3,7 +3,7 @@ use crate::mem::MaybeUninit; use crate::{fmt, ptr}; -//@ use array_layout::{collapse_window, expand_window, matrix_elems, pack_matrix, unpack_matrix}; +//@ use array_layout::{array_borrow_tokens, collapse_window, expand_window, lend_array, matrix_elems, own_matrix_storage, pack_matrix, reclaim_array, unpack_matrix}; struct Buffer { // Invariant: `self.buffer[self.start..self.start + N]` is initialized, @@ -43,6 +43,50 @@ pred storage(b: *Buffer; start: usize) = pred_ctor writable_matrix(b: *Buffer)(;) = (*b).buffer |-> _; +pred >.own(t, buffer) = + exists::>(?values) &*& + 0 < width::() &*& width::() <= usize::MAX / 2 &*& + buffer.start <= width::() &*& + 2 * width::() * std::mem::size_of::() <= isize::MAX &*& + length(values) == width::() &*& + take(width::(), drop(buffer.start, matrix_elems(buffer.buffer))) == + map(std::mem::MaybeUninit::new, values) &*& + foreach(values, own::(t)); + +// The same frame survives normal and unwinding generic drop glue. +pred drop_frame(b: *Buffer, start: usize, + matrix: *[[std::mem::MaybeUninit; N]; 2], k: lifetime_t, slice: *[T]) = + bounds(b, start) &*& ref_mut_end_token(matrix, &(*b).buffer) &*& + (matrix as *std::mem::MaybeUninit)[..start] |-> _ &*& + ((matrix as *std::mem::MaybeUninit) + start + width::())[..width::() - start] |-> _ &*& + array_borrow_tokens(k, ((matrix as *std::mem::MaybeUninit) + start) as *T, width::()) &*& + close_points_to_at_lft_token(1, k, slice, 1) &*& + slice as *T == ((matrix as *std::mem::MaybeUninit) + start) as *T &*& + ptr_len(slice) == width::(); + +lem finish_drop_storage(b: *Buffer, start: usize, + matrix: *[[std::mem::MaybeUninit; N]; 2], k: lifetime_t, slice: *[T]) + nonghost_callers_only + req drop_frame(b, start, matrix, k, slice) &*& *slice |-> _; + ens storage(b, start); +{ + open drop_frame(b, start, matrix, k, slice); + open bounds(b, start); + close_points_to_at_lft_(slice); + open_points_to_slice_at_lft_(slice); + end_lifetime(k); + let p = matrix as *std::mem::MaybeUninit; + reclaim_array(k, (p + start) as *T, width::()); + std::mem::array__to_array_MaybeUninit((p + start) as *T); + array_join(p); + array_join(p); + pack_matrix(matrix); + end_ref_mut(matrix); + unpack_matrix(&(*b).buffer); + close bounds(b, start); + close storage(b, start); +} + // Unwrap initialized slots without requiring T: Copy or duplicating T.own. lem initialized_slots(p: *std::mem::MaybeUninit, values: list) req p[..length(values)] |-> map(std::mem::MaybeUninit::new, values); @@ -164,8 +208,8 @@ impl Buffer { //@ array_split(buffer_mut_ptr + start, width::()); //@ collapse_window::(buffer_mut_ptr + start); let result = unsafe { &mut *buffer_mut_ptr.add(self.start).cast() }; + //@ let window = (buffer_mut_ptr + start) as *std::mem::MaybeUninit<[T; N]>; /*@ - let window = (buffer_mut_ptr + start) as *std::mem::MaybeUninit<[T; N]>; { pred ctx(;) = ref_mut_end_token(result, window) &*& @@ -324,31 +368,55 @@ impl Drop for Buffer { req thread_token(?t) &*& live(t, self, ?start, ?values); @*/ /*@ - ens thread_token(t) &*& storage(self, start); + ens thread_token(t) &*& drop_frame(self, start, ?matrix, ?k, ?slice) &*& *slice |-> _; + @*/ + //@ on_unwind_ens thread_token(t) &*& drop_frame(self, start, ?matrix, ?k, ?slice) &*& *slice |-> _; + /*@ + safety_proof { + open Buffer_full_borrow_content::(_t, self)(); + open >.own(_t, ?buffer); + open exists::>(?values); + let start = buffer.start; + unpack_matrix(&(*self).buffer); + array_split(base(self), start); + array_split(base(self) + start, width::()); + close bounds(self, start); + close live(_t, self, start, values); + call(); + assert drop_frame(self, start, ?matrix, ?k, ?slice); + finish_drop_storage(self, start, matrix, k, slice); + open storage(self, start); + open bounds(self, start); + pack_matrix(&(*self).buffer); + assert (*self).buffer |-> ?after; + own_matrix_storage(_t, after); + } @*/ - //@ on_unwind_ens thread_token(t); { //@ open live(t, self, start, values); //@ open bounds(self, start); + //@ array_join(base(self)); + //@ array_join(base(self)); + //@ pack_matrix(&(*self).buffer); // SAFETY: our invariant guarantees that N elements starting from // `self.start` are initialized. We drop them here. unsafe { - let initialized_part: *mut [T] = crate::ptr::slice_from_raw_parts_mut( - self.buffer_mut_ptr().add(self.start).cast(), - N, - ); - //@ initialized_slots(base(self) + start, values); - // This assertion must be discharged by the Rust slice memory model. - // No additional axiom or assumed drop contract is supplied here. - //@ assert *initialized_part |-> slice_of_elems(values); + let buffer_mut_ptr = self.buffer_mut_ptr(); + let initialized_part: *mut [T] = + crate::ptr::slice_from_raw_parts_mut(buffer_mut_ptr.add(self.start).cast(), N); + //@ let matrix = buffer_mut_ptr as *[[std::mem::MaybeUninit; N]; 2]; + //@ unpack_matrix(matrix); + //@ array_split(buffer_mut_ptr, start); + //@ array_split(buffer_mut_ptr + start, width::()); + //@ initialized_slots(buffer_mut_ptr + start, values); + //@ let k = begin_lifetime(); + //@ lend_array(k, (buffer_mut_ptr + start) as *T, width::()); + //@ close_points_to_slice_at_lft(initialized_part); + //@ open_points_to_at_lft(initialized_part, 1); //@ close <[T]>.own(t, slice_of_elems(values)); + //@ close bounds(self, start); + //@ close drop_frame(self, start, matrix, k, initialized_part); ptr::drop_in_place(initialized_part); - //@ assert (initialized_part as *T)[..width::()] |-?-> ?remaining; - //@ std::mem::array__to_array_MaybeUninit(initialized_part as *T); } - //@ array_join(base(self)); - //@ array_join(base(self)); - //@ close bounds(self, start); - //@ close storage(self, start); } } diff --git a/verifast-proofs/core/iter/adapters/verify.sh b/verifast-proofs/core/iter/adapters/verify.sh index 3c4d938392d0d..35e7d3cd63d70 100644 --- a/verifast-proofs/core/iter/adapters/verify.sh +++ b/verifast-proofs/core/iter/adapters/verify.sh @@ -221,6 +221,21 @@ PY # Keep every stage sequential and require both proof and refinement to succeed. # Collect their independent diagnostics even when the first stage fails. # No assumption, unwind, reference-creation, or overflow suppression flags. +if timeout --signal=TERM --kill-after=5s 30s \ + verifast -rustc_args '--edition 2024' backend/layout-invalid.rs >"$negative_log" 2>&1; then + echo 'The incorrect array stride unexpectedly verified' >&2 + exit 1 +fi +cat "$negative_log" +python3 -I - "$negative_log" <<'PY' +from pathlib import Path +import sys + +diagnostics = Path(sys.argv[1]).read_text() +if "Cannot prove condition" not in diagnostics or "Rust frontend failed" in diagnostics: + raise SystemExit("The negative layout check did not reject the incorrect stride") +PY + layout_status=0 timeout --signal=TERM --kill-after=10s 60s \ verifast -rustc_args '--edition 2024' backend/matrix-layout-valid.rs || layout_status=$? @@ -231,7 +246,7 @@ refinement_status=0 timeout --signal=TERM --kill-after=10s 600s \ refinement-checker --rustc-args '--edition 2024' original/lib.rs verified/lib.rs || refinement_status=$? python3 -I check_sources.py -if (( proof_status != 0 )); then +if (( proof_status != 0 || layout_status != 0 )); then # These bounded, sequential diagnostics cannot replace the full proof verdict. while IFS= read -r proof_location; do diagnostic_status=0 @@ -268,9 +283,9 @@ PY from pathlib import Path import re -for path in (Path("verified/map_windows.rs"), Path("verified/step_by.rs")): +for path in (Path("array_layout.rs"), Path("verified/map_windows.rs"), Path("verified/step_by.rs")): for line_number, line in enumerate(path.read_text().splitlines(), 1): - if re.match(r"\s*(?:unsafe\s+)?fn\s+\w+", line): + if re.match(r"\s*(?:unsafe\s+)?(?:fn|lem)\s+\w+", line): print(f"{path}:{line_number}") PY ) From cb41ee41060b1140c88b491501b25c6c3274219d Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Sun, 13 Sep 2026 05:09:08 -0700 Subject: [PATCH 45/62] Use the array alignment expression supported by VeriFast Signed-off-by: Onyeka Obi --- verifast-proofs/core/iter/adapters/backend/array-layout.patch | 2 +- verifast-proofs/core/iter/adapters/verified/map_windows.rs | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/verifast-proofs/core/iter/adapters/backend/array-layout.patch b/verifast-proofs/core/iter/adapters/backend/array-layout.patch index e2e9edeb2c91d..5bbabbaa39a22 100644 --- a/verifast-proofs/core/iter/adapters/backend/array-layout.patch +++ b/verifast-proofs/core/iter/adapters/backend/array-layout.patch @@ -8,7 +8,7 @@ + lem array_layout(); + req true; + ens std::mem::size_of(typeid([T; N])) == std::mem::size_of::() * usize_of_const(typeid(N)) &*& -+ std::mem::align_of(typeid([T; N])) == std::mem::align_of::(); ++ std::mem::align_of::<[T; N]>() == std::mem::align_of::(); + + lem MaybeUninit_layout(); + req true; diff --git a/verifast-proofs/core/iter/adapters/verified/map_windows.rs b/verifast-proofs/core/iter/adapters/verified/map_windows.rs index 3028eacfd4c2f..0f31fc7e55e29 100644 --- a/verifast-proofs/core/iter/adapters/verified/map_windows.rs +++ b/verifast-proofs/core/iter/adapters/verified/map_windows.rs @@ -92,6 +92,7 @@ lem initialized_slots(p: *std::mem::MaybeUninit, values: list) req p[..length(values)] |-> map(std::mem::MaybeUninit::new, values); ens (p as *T)[..length(values)] |-> values; { + std::mem::MaybeUninit_layout::(); open array(p, length(values), _); match values { nil => { @@ -110,6 +111,7 @@ lem wrap_slots(p: *T, values: list) req p[..length(values)] |-> values; ens (p as *std::mem::MaybeUninit)[..length(values)] |-> map(std::mem::MaybeUninit::new, values); { + std::mem::MaybeUninit_layout::(); open array(p, length(values), values); match values { nil => { From f225042129a9ccca2041bad7743f4e674df50d17 Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Sun, 13 Sep 2026 05:17:11 -0700 Subject: [PATCH 46/62] Accept symbolic lengths in ghost array conversion helpers Signed-off-by: Onyeka Obi --- verifast-proofs/core/iter/adapters/README.md | 4 ++++ .../core/iter/adapters/array_layout.rs | 14 +++++------ .../iter/adapters/backend/array-layout.patch | 23 ++++++++++++++++++- .../core/iter/adapters/backend/prepare.sh | 5 +++- .../iter/adapters/verified/map_windows.rs | 2 +- 5 files changed, 38 insertions(+), 10 deletions(-) diff --git a/verifast-proofs/core/iter/adapters/README.md b/verifast-proofs/core/iter/adapters/README.md index 0475f20354a0f..cb6326ab76a6a 100644 --- a/verifast-proofs/core/iter/adapters/README.md +++ b/verifast-proofs/core/iter/adapters/README.md @@ -183,6 +183,10 @@ array type IDs and `MaybeUninit`. The facts follow the and the wrapper's documented layout. They include zero-sized element types. The matrix conversion and writable-window proofs remain mandatory; the layout facts do not grant storage or ownership permissions. +The patch also removes implicit `Sized` bounds from the array length parameters +of the three existing array conversion lemmas. Symbolic const parameters are +not Rust value types. The element type bounds and every storage precondition +remain in force, and the patched prelude is cached with checksum validation. ## Local static checks and optional manual verification diff --git a/verifast-proofs/core/iter/adapters/array_layout.rs b/verifast-proofs/core/iter/adapters/array_layout.rs index 9f6023a565591..c75e66e75d12d 100644 --- a/verifast-proofs/core/iter/adapters/array_layout.rs +++ b/verifast-proofs/core/iter/adapters/array_layout.rs @@ -13,7 +13,7 @@ fix matrix_elems(matrix: [[T; N]; 2]) -> list { pred_ctor saved_array(p: *T, count: usize, elems: list)(;) = [1/2]array(p, count, elems); -lem pack_array(p: *[T; N]) +lem pack_array(p: *[T; N]) req (p as *T)[..usize_of_const(typeid(N))] |-> ?elems; ens *p |-> ?array &*& Array_elems(array) == elems; { @@ -25,7 +25,7 @@ lem pack_array(p: *[T; N]) array_to_Array(p); } -lem unpack_matrix(p: *[[T; N]; 2]) +lem unpack_matrix(p: *[[T; N]; 2]) req *p |-> ?matrix; ens (p as *T)[..2 * usize_of_const(typeid(N))] |-> matrix_elems(matrix); { @@ -39,7 +39,7 @@ lem unpack_matrix(p: *[[T; N]; 2]) array_join(p as *T); } -lem pack_matrix(p: *[[T; N]; 2]) +lem pack_matrix(p: *[[T; N]; 2]) req (p as *T)[..2 * usize_of_const(typeid(N))] |-> ?elems; ens *p |-> ?matrix &*& matrix_elems(matrix) == elems; { @@ -56,7 +56,7 @@ lem pack_matrix(p: *[[T; N]; 2]) } // These conversions preserve writable storage. They do not claim initialized T values. -lem collapse_window(p: *std::mem::MaybeUninit) +lem collapse_window(p: *std::mem::MaybeUninit) req p[..usize_of_const(typeid(N))] |-> _; ens *(p as *std::mem::MaybeUninit<[T; N]>) |-> _; { @@ -68,7 +68,7 @@ lem collapse_window(p: *std::mem::MaybeUninit) std::mem::close_MaybeUninit_(p as *std::mem::MaybeUninit<[T; N]>); } -lem expand_window(p: *std::mem::MaybeUninit<[T; N]>) +lem expand_window(p: *std::mem::MaybeUninit<[T; N]>) req *p |-> _; ens (p as *std::mem::MaybeUninit)[..usize_of_const(typeid(N))] |-> _; { @@ -93,7 +93,7 @@ lem own_uninit_values(t: thread_id_t, values: list>) close foreach(values, own::>(t)); } -lem own_uninit_rows(t: thread_id_t, rows: list<[std::mem::MaybeUninit; N]>) +lem own_uninit_rows(t: thread_id_t, rows: list<[std::mem::MaybeUninit; N]>) req true; ens foreach(rows, own::<[std::mem::MaybeUninit; N]>(t)); { @@ -109,7 +109,7 @@ lem own_uninit_rows(t: thread_id_t, rows: list<[std::mem::MaybeUninit; close foreach(rows, own::<[std::mem::MaybeUninit; N]>(t)); } -lem own_matrix_storage(t: thread_id_t, matrix: [[std::mem::MaybeUninit; N]; 2]) +lem own_matrix_storage(t: thread_id_t, matrix: [[std::mem::MaybeUninit; N]; 2]) req true; ens <[[std::mem::MaybeUninit; N]; 2]>.own(t, matrix); { diff --git a/verifast-proofs/core/iter/adapters/backend/array-layout.patch b/verifast-proofs/core/iter/adapters/backend/array-layout.patch index 5bbabbaa39a22..66bafd45c0a7b 100644 --- a/verifast-proofs/core/iter/adapters/backend/array-layout.patch +++ b/verifast-proofs/core/iter/adapters/backend/array-layout.patch @@ -5,7 +5,7 @@ /*@ + // Rust array layout and MaybeUninit transparency, including zero-sized T. -+ lem array_layout(); ++ lem array_layout(); + req true; + ens std::mem::size_of(typeid([T; N])) == std::mem::size_of::() * usize_of_const(typeid(N)) &*& + std::mem::align_of::<[T; N]>() == std::mem::align_of::(); @@ -18,3 +18,24 @@ fix size_of_val(x: *T) -> usize; fix align_of_val(x: *T) -> usize; +--- a/bin/rust/prelude_core.rsspec ++++ b/bin/rust/prelude_core.rsspec +@@ -312,15 +312,15 @@ + req true; + ens slice_of_elems(slice_elems(slice)) == slice; + +-lem Array__to_array_(p: *[T; N]); ++lem Array__to_array_(p: *[T; N]); + req [?f](*p |-?-> ?array_); + ens [f](p as *T)[..usize_of_const(typeid(N))] |-?-> match array_ { none => repeat(nat_of_int(usize_of_const(typeid(N))), none), some(array) => map(some, Array_elems(array)) }; + +-lem Array_to_array(p: *[T; N]); ++lem Array_to_array(p: *[T; N]); + req [?f](*p |-> ?array); + ens [f](p as *T)[..usize_of_const(typeid(N))] |-> Array_elems(array); + +-lem array_to_Array(p: *[T; N]); ++lem array_to_Array(p: *[T; N]); + req [?f](p as *T)[..usize_of_const(typeid(N))] |-> ?elems; + ens [f](*p |-> Array_of_elems(elems)); + diff --git a/verifast-proofs/core/iter/adapters/backend/prepare.sh b/verifast-proofs/core/iter/adapters/backend/prepare.sh index 2254836533d7a..6dec61916d1f0 100644 --- a/verifast-proofs/core/iter/adapters/backend/prepare.sh +++ b/verifast-proofs/core/iter/adapters/backend/prepare.sh @@ -19,6 +19,7 @@ if [[ -f "$cache_dir/checksums" ]]; then install -m 755 "$cache_dir/vf-rust-mir-exporter" "$VERIFAST_HOME/bin/vf-rust-mir-exporter" install -m 755 "$cache_dir/refinement-checker" "$VERIFAST_HOME/bin/refinement-checker" install -m 644 "$cache_dir/std-lib.rsspec" "$VERIFAST_HOME/bin/rust/std/lib.rsspec" + install -m 644 "$cache_dir/prelude_core.rsspec" "$VERIFAST_HOME/bin/rust/prelude_core.rsspec" echo 'Restored the patched frontend; all proof and regression checks will run' exit 0 fi @@ -74,10 +75,12 @@ install -m 755 "$build_dir/src/_build/default/vfconsole/vfconsole.exe" "$VERIFAS install -m 755 "$build_dir/src/_build/default/refinement_checker/main.exe" \ "$VERIFAST_HOME/bin/refinement-checker" install -m 644 "$build_dir/bin/rust/std/lib.rsspec" "$VERIFAST_HOME/bin/rust/std/lib.rsspec" +install -m 644 "$build_dir/bin/rust/prelude_core.rsspec" "$VERIFAST_HOME/bin/rust/prelude_core.rsspec" mkdir -p "$cache_dir" install -m 755 "$VERIFAST_HOME/bin/verifast" "$cache_dir/verifast" install -m 755 "$VERIFAST_HOME/bin/vf-rust-mir-exporter" "$cache_dir/vf-rust-mir-exporter" install -m 755 "$VERIFAST_HOME/bin/refinement-checker" "$cache_dir/refinement-checker" install -m 644 "$VERIFAST_HOME/bin/rust/std/lib.rsspec" "$cache_dir/std-lib.rsspec" -(cd "$cache_dir" && sha256sum verifast vf-rust-mir-exporter refinement-checker std-lib.rsspec > checksums) +install -m 644 "$VERIFAST_HOME/bin/rust/prelude_core.rsspec" "$cache_dir/prelude_core.rsspec" +(cd "$cache_dir" && sha256sum verifast vf-rust-mir-exporter refinement-checker std-lib.rsspec prelude_core.rsspec > checksums) echo 'Prepared VeriFast 26.09 with checked addition and symbolic usize const parameters' diff --git a/verifast-proofs/core/iter/adapters/verified/map_windows.rs b/verifast-proofs/core/iter/adapters/verified/map_windows.rs index 0f31fc7e55e29..8828f9bcf807d 100644 --- a/verifast-proofs/core/iter/adapters/verified/map_windows.rs +++ b/verifast-proofs/core/iter/adapters/verified/map_windows.rs @@ -64,7 +64,7 @@ pred drop_frame(b: *Buffer, start: usize, slice as *T == ((matrix as *std::mem::MaybeUninit) + start) as *T &*& ptr_len(slice) == width::(); -lem finish_drop_storage(b: *Buffer, start: usize, +lem finish_drop_storage(b: *Buffer, start: usize, matrix: *[[std::mem::MaybeUninit; N]; 2], k: lifetime_t, slice: *[T]) nonghost_callers_only req drop_frame(b, start, matrix, k, slice) &*& *slice |-> _; From 07fd0441d369e4487e9b87df5a50c3042f53d8aa Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Sun, 13 Sep 2026 05:22:20 -0700 Subject: [PATCH 47/62] Close array ownership through its predicate constructor Signed-off-by: Onyeka Obi --- verifast-proofs/core/iter/adapters/array_layout.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/verifast-proofs/core/iter/adapters/array_layout.rs b/verifast-proofs/core/iter/adapters/array_layout.rs index c75e66e75d12d..3913346fffbe2 100644 --- a/verifast-proofs/core/iter/adapters/array_layout.rs +++ b/verifast-proofs/core/iter/adapters/array_layout.rs @@ -101,7 +101,7 @@ lem own_uninit_rows(t: thread_id_t, rows: list<[std::mem::MaybeUni nil => {} cons(row, rest) => { own_uninit_values(t, Array_elems(row)); - close <[std::mem::MaybeUninit; N]>.own(t, row); + close array_owned::, N>()(t, row); close own::<[std::mem::MaybeUninit; N]>(t)(row); own_uninit_rows(t, rest); } @@ -114,7 +114,7 @@ lem own_matrix_storage(t: thread_id_t, matrix: [[std::mem::MaybeUn ens <[[std::mem::MaybeUninit; N]; 2]>.own(t, matrix); { own_uninit_rows(t, Array_elems(matrix)); - close <[[std::mem::MaybeUninit; N]; 2]>.own(t, matrix); + close array_owned::<[std::mem::MaybeUninit; N], 2>()(t, matrix); } pred array_borrow_tokens(k: lifetime_t, p: *T, count: usize;) = From 6d69913dbf15707b0398c89ec579968ac7865279 Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Sun, 13 Sep 2026 05:37:15 -0700 Subject: [PATCH 48/62] Prove buffer ownership transfer and covariance obligations Signed-off-by: Onyeka Obi --- verifast-proofs/core/iter/adapters/README.md | 10 ++ .../core/iter/adapters/array_layout.rs | 112 +++++++++++++++++- .../adapters/backend/array-subtyping.patch | 49 ++++++++ .../core/iter/adapters/backend/prepare.sh | 4 +- .../iter/adapters/backend/subtype-invalid.rs | 8 ++ .../adapters/backend/value-send-invalid.rs | 9 ++ .../iter/adapters/verified/map_windows.rs | 41 ++++++- verifast-proofs/core/iter/adapters/verify.sh | 17 +++ 8 files changed, 247 insertions(+), 3 deletions(-) create mode 100644 verifast-proofs/core/iter/adapters/backend/array-subtyping.patch create mode 100644 verifast-proofs/core/iter/adapters/backend/subtype-invalid.rs create mode 100644 verifast-proofs/core/iter/adapters/backend/value-send-invalid.rs diff --git a/verifast-proofs/core/iter/adapters/README.md b/verifast-proofs/core/iter/adapters/README.md index cb6326ab76a6a..c9a584b3995c3 100644 --- a/verifast-proofs/core/iter/adapters/README.md +++ b/verifast-proofs/core/iter/adapters/README.md @@ -188,6 +188,16 @@ of the three existing array conversion lemmas. Symbolic const parameters are not Rust value types. The element type bounds and every storage precondition remain in force, and the patched prelude is cached with checksum validation. +`backend/array-subtyping.patch` models the standard covariance, fixed element +count, and `Send` conditions of arrays and `MaybeUninit`, plus representation +preservation under [Rust subtyping](https://doc.rust-lang.org/reference/subtyping.html). +The `Send` conditions follow the [array](https://doc.rust-lang.org/std/primitive.array.html#impl-Send-for-%5BT;+N%5D) +and [wrapper](https://doc.rust-lang.org/std/mem/union.MaybeUninit.html#impl-Send-for-MaybeUninit%3CT%3E) +implementations. These are trusted type-model facts. The +`Buffer` ownership proofs must still transfer every active element's ownership; +they cannot produce generic `T` ownership from those facts. Negative fixtures +require rejection when the subtype relation or `T: Send` is missing. + ## Local static checks and optional manual verification The static check reads small files and starts no compiler or solver: diff --git a/verifast-proofs/core/iter/adapters/array_layout.rs b/verifast-proofs/core/iter/adapters/array_layout.rs index 3913346fffbe2..f4a9cf4e2950a 100644 --- a/verifast-proofs/core/iter/adapters/array_layout.rs +++ b/verifast-proofs/core/iter/adapters/array_layout.rs @@ -4,11 +4,121 @@ pred_ctor array_owned()(t: thread_id_t, value: [T; N]) = foreach(Array_elems(value), own::(t)); type_pred_def for <[T; N]>.own = array_owned::; +lem mapped_append(f: fix(a, b), xs: list, ys: list) + req true; + ens map(f, append(xs, ys)) == append(map(f, xs), map(f, ys)); +{ + match xs { + nil => {} + cons(x, rest) => { mapped_append(f, rest, ys); } + } +} + +lem mapped_length(f: fix(a, b), xs: list) + req true; + ens length(map(f, xs)) == length(xs); +{ + match xs { + nil => {} + cons(x, rest) => { mapped_length(f, rest); } + } +} + +lem mapped_range(f: fix(a, b), xs: list, start: usize, count: usize) + req 0 <= start &*& 0 <= count; + ens take(count, drop(start, map(f, xs))) == map(f, take(count, drop(start, xs))); +{ + match xs { + nil => {} + cons(x, rest) => { + if start > 0 { + mapped_range(f, rest, start - 1, count); + } else { + if count > 0 { mapped_range(f, rest, 0, count - 1); } + } + } + } +} + +lem owned_values_mono(t: thread_id_t, values: list) + req type_interp::() &*& type_interp::() &*& + is_subtype_of::() == true &*& foreach(values, own::(t)); + ens type_interp::() &*& type_interp::() &*& + foreach(map(upcast::, values), own::(t)); +{ + open foreach(values, own::(t)); + match values { + nil => {} + cons(value, rest) => { + open own::(t)(value); + own_mono::(t, value); + close own::(t)(upcast::(value)); + owned_values_mono::(t, rest); + } + } + close foreach(map(upcast::, values), own::(t)); +} + +lem owned_values_send(t0: thread_id_t, t1: thread_id_t, values: list) + req type_interp::() &*& is_Send(typeid(T)) == true &*& foreach(values, own::(t0)); + ens type_interp::() &*& foreach(values, own::(t1)); +{ + open foreach(values, own::(t0)); + match values { + nil => {} + cons(value, rest) => { + open own::(t0)(value); + Send::send::(t0, t1, value); + close own::(t1)(value); + owned_values_send(t0, t1, rest); + } + } + close foreach(values, own::(t1)); +} + +lem mapped_uninit_upcast(values: list) + req is_subtype_of::() == true; + ens map(upcast::, std::mem::MaybeUninit>, + map(std::mem::MaybeUninit::new, values)) == + map(std::mem::MaybeUninit::new, map(upcast::, values)); +{ + match values { + nil => {} + cons(value, rest) => { + std::mem::MaybeUninit_upcast_new::(value); + mapped_uninit_upcast::(rest); + } + } +} + fix matrix_elems(matrix: [[T; N]; 2]) -> list { append(Array_elems(head(Array_elems(matrix))), Array_elems(head(tail(Array_elems(matrix))))) } +lem matrix_upcast(matrix: [[T0; N]; 2]) + req is_subtype_of::() == true; + ens matrix_elems::(upcast::<[[T0; N]; 2], [[T1; N]; 2]>(matrix)) == + map(upcast::, matrix_elems(matrix)); +{ + std::mem::array_subtype::(); + std::mem::array_upcast::<[T0; N], [T1; N], 2>(matrix); + std::mem::array_elems_length::<[T0; N], 2>(matrix); + match Array_elems(matrix) { + nil => {} + cons(first, rest) => { + match rest { + nil => {} + cons(second, suffix) => { + std::mem::array_upcast::(first); + std::mem::array_upcast::(second); + mapped_append(upcast::, Array_elems(first), Array_elems(second)); + } + } + } + } +} + // Keep a fraction while changing representations so precision relates the values. pred_ctor saved_array(p: *T, count: usize, elems: list)(;) = [1/2]array(p, count, elems); @@ -117,7 +227,7 @@ lem own_matrix_storage(t: thread_id_t, matrix: [[std::mem::MaybeUn close array_owned::<[std::mem::MaybeUninit; N], 2>()(t, matrix); } -pred array_borrow_tokens(k: lifetime_t, p: *T, count: usize;) = +pred array_borrow_tokens(k: lifetime_t, p: *T, count: usize) = pointer_within_limits(p) == true &*& if count == 0 { true } else { points_to_at_lft_end_token(k, p) &*& array_borrow_tokens(k, p + 1, count - 1) diff --git a/verifast-proofs/core/iter/adapters/backend/array-subtyping.patch b/verifast-proofs/core/iter/adapters/backend/array-subtyping.patch new file mode 100644 index 0000000000000..99f2a8581caa7 --- /dev/null +++ b/verifast-proofs/core/iter/adapters/backend/array-subtyping.patch @@ -0,0 +1,49 @@ +--- a/bin/rust/std/lib.rsspec ++++ b/bin/rust/std/lib.rsspec +@@ -381,6 +381,46 @@ + req true; + ens std::mem::size_of::>() == std::mem::size_of::() &*& + std::mem::align_of::>() == std::mem::align_of::(); ++ ++ // Rust subtyping changes lifetime information, preserving layout and values. ++ lem upcast_identity(value: T); ++ req true; ++ ens upcast::(value) == value; ++ ++ lem subtype_layout(); ++ req is_subtype_of::() == true; ++ ens std::mem::size_of::() == std::mem::size_of::() &*& ++ std::mem::align_of::() == std::mem::align_of::(); ++ ++ lem array_subtype(); ++ req is_subtype_of::() == true; ++ ens is_subtype_of::<[T0; N], [T1; N]>() == true; ++ ++ lem array_upcast(value: [T0; N]); ++ req is_subtype_of::() == true; ++ ens Array_elems(upcast::<[T0; N], [T1; N]>(value)) == ++ map(upcast::, Array_elems(value)); ++ ++ lem array_elems_length(value: [T; N]); ++ req true; ++ ens length(Array_elems(value)) == usize_of_const(typeid(N)); ++ ++ lem MaybeUninit_subtype(); ++ req is_subtype_of::() == true; ++ ens is_subtype_of::, MaybeUninit>() == true; ++ ++ lem MaybeUninit_upcast_new(value: T0); ++ req is_subtype_of::() == true; ++ ens upcast::, MaybeUninit>(MaybeUninit::new(value)) == ++ MaybeUninit::new(upcast::(value)); ++ ++ lem array_Send(); ++ req true; ++ ens is_Send(typeid([T; N])) == is_Send(typeid(T)); ++ ++ lem MaybeUninit_Send(); ++ req true; ++ ens is_Send(typeid(MaybeUninit)) == is_Send(typeid(T)); + + fix size_of_val(x: *T) -> usize; + fix align_of_val(x: *T) -> usize; diff --git a/verifast-proofs/core/iter/adapters/backend/prepare.sh b/verifast-proofs/core/iter/adapters/backend/prepare.sh index 6dec61916d1f0..a14d91e7979d7 100644 --- a/verifast-proofs/core/iter/adapters/backend/prepare.sh +++ b/verifast-proofs/core/iter/adapters/backend/prepare.sh @@ -11,7 +11,8 @@ backend_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" rustup component add --toolchain nightly-2026-02-05 rustc-dev llvm-tools cache_key="$(sha256sum "$backend_dir/prepare.sh" "$backend_dir/add-unchecked.patch" \ "$backend_dir/const-generics.patch" "$backend_dir/nonzero-usize.patch" \ - "$backend_dir/maybeuninit-ownership.patch" "$backend_dir/array-layout.patch" | sha256sum | cut -d ' ' -f 1)" + "$backend_dir/maybeuninit-ownership.patch" "$backend_dir/array-layout.patch" \ + "$backend_dir/array-subtyping.patch" | sha256sum | cut -d ' ' -f 1)" cache_dir="$HOME/.cache/verifast-iter-adapters/$cache_key" if [[ -f "$cache_dir/checksums" ]]; then (cd "$cache_dir" && sha256sum --check checksums) @@ -40,6 +41,7 @@ patch --batch --fuzz=0 --directory="$build_dir" -p1 < "$backend_dir/const-generi patch --batch --fuzz=0 --directory="$build_dir" -p1 < "$backend_dir/nonzero-usize.patch" patch --batch --fuzz=0 --directory="$build_dir" -p1 < "$backend_dir/maybeuninit-ownership.patch" patch --batch --fuzz=0 --directory="$build_dir" -p1 < "$backend_dir/array-layout.patch" +patch --batch --fuzz=0 --directory="$build_dir" -p1 < "$backend_dir/array-subtyping.patch" # Use the dependency bundle pinned by upstream's setup-build.sh. Its compiler # and package paths are built for /tmp/vfdeps-adf88dc on Linux. diff --git a/verifast-proofs/core/iter/adapters/backend/subtype-invalid.rs b/verifast-proofs/core/iter/adapters/backend/subtype-invalid.rs new file mode 100644 index 0000000000000..99ebb62ec7898 --- /dev/null +++ b/verifast-proofs/core/iter/adapters/backend/subtype-invalid.rs @@ -0,0 +1,8 @@ +#![allow(dead_code)] + +unsafe fn missing_subtype() +//@ req true; +//@ ens true; +{ + //@ std::mem::array_subtype::(); +} diff --git a/verifast-proofs/core/iter/adapters/backend/value-send-invalid.rs b/verifast-proofs/core/iter/adapters/backend/value-send-invalid.rs new file mode 100644 index 0000000000000..e235cf8a884d3 --- /dev/null +++ b/verifast-proofs/core/iter/adapters/backend/value-send-invalid.rs @@ -0,0 +1,9 @@ +#![allow(dead_code)] + +unsafe fn missing_send() +//@ req type_interp::() &*& .own(?t0, ?value) &*& exists::(?t1); +//@ ens type_interp::() &*& .own(t1, value); +{ + //@ open exists::(t1); + //@ Send::send::(t0, t1, value); +} diff --git a/verifast-proofs/core/iter/adapters/verified/map_windows.rs b/verifast-proofs/core/iter/adapters/verified/map_windows.rs index 8828f9bcf807d..afecd88509b35 100644 --- a/verifast-proofs/core/iter/adapters/verified/map_windows.rs +++ b/verifast-proofs/core/iter/adapters/verified/map_windows.rs @@ -4,6 +4,7 @@ use crate::mem::MaybeUninit; use crate::{fmt, ptr}; //@ use array_layout::{array_borrow_tokens, collapse_window, expand_window, lend_array, matrix_elems, own_matrix_storage, pack_matrix, reclaim_array, unpack_matrix}; +//@ use array_layout::{mapped_length, mapped_range, mapped_uninit_upcast, matrix_upcast, owned_values_mono, owned_values_send}; struct Buffer { // Invariant: `self.buffer[self.start..self.start + N]` is initialized, @@ -46,13 +47,51 @@ pred_ctor writable_matrix(b: *Buffer)(;) = (*b).buffer |-> _; pred >.own(t, buffer) = exists::>(?values) &*& 0 < width::() &*& width::() <= usize::MAX / 2 &*& - buffer.start <= width::() &*& + 0 <= buffer.start &*& buffer.start <= width::() &*& 2 * width::() * std::mem::size_of::() <= isize::MAX &*& length(values) == width::() &*& take(width::(), drop(buffer.start, matrix_elems(buffer.buffer))) == map(std::mem::MaybeUninit::new, values) &*& foreach(values, own::(t)); +lem Buffer_own_mono() + req type_interp::() &*& type_interp::() &*& type_interp::() &*& + Buffer_own::(?t, ?buffer) &*& is_subtype_of::() == true; + ens type_interp::() &*& type_interp::() &*& type_interp::() &*& + Buffer_own::(t, Buffer:: { + buffer: upcast(buffer.buffer), start: upcast(buffer.start) }); +{ + open Buffer_own::(t, buffer); + open exists::>(?values); + std::mem::subtype_layout::(); + std::mem::upcast_identity(buffer.start); + std::mem::MaybeUninit_subtype::(); + matrix_upcast::, std::mem::MaybeUninit, N>(buffer.buffer); + mapped_range(upcast::, std::mem::MaybeUninit>, + matrix_elems(buffer.buffer), buffer.start, width::()); + mapped_uninit_upcast::(values); + mapped_length(upcast::, values); + owned_values_mono::(t, values); + close exists(map(upcast::, values)); + close Buffer_own::(t, Buffer:: { + buffer: upcast(buffer.buffer), start: upcast(buffer.start) }); +} + +lem Buffer_send(t1: thread_id_t) + req type_interp::() &*& type_interp::() &*& Buffer_own::(?t0, ?buffer) &*& + is_Send(typeid(Buffer)) == true; + ens type_interp::() &*& type_interp::() &*& Buffer_own::(t1, buffer); +{ + open Buffer_own::(t0, buffer); + open exists::>(?values); + std::mem::array_Send::<[std::mem::MaybeUninit; N], 2>(); + std::mem::array_Send::, N>(); + std::mem::MaybeUninit_Send::(); + owned_values_send(t0, t1, values); + close exists(values); + close Buffer_own::(t1, buffer); +} + // The same frame survives normal and unwinding generic drop glue. pred drop_frame(b: *Buffer, start: usize, matrix: *[[std::mem::MaybeUninit; N]; 2], k: lifetime_t, slice: *[T]) = diff --git a/verifast-proofs/core/iter/adapters/verify.sh b/verifast-proofs/core/iter/adapters/verify.sh index 35e7d3cd63d70..fb9dd34962f28 100644 --- a/verifast-proofs/core/iter/adapters/verify.sh +++ b/verifast-proofs/core/iter/adapters/verify.sh @@ -236,6 +236,23 @@ if "Cannot prove condition" not in diagnostics or "Rust frontend failed" in diag raise SystemExit("The negative layout check did not reject the incorrect stride") PY +for negative_fixture in backend/subtype-invalid.rs backend/value-send-invalid.rs; do + if timeout --signal=TERM --kill-after=5s 30s \ + verifast -rustc_args '--edition 2024' "$negative_fixture" >"$negative_log" 2>&1; then + printf 'Invalid generic conversion unexpectedly verified: %s\n' "$negative_fixture" >&2 + exit 1 + fi + cat "$negative_log" + python3 -I - "$negative_log" <<'PY' +from pathlib import Path +import sys + +diagnostics = Path(sys.argv[1]).read_text() +if "Cannot prove condition" not in diagnostics or "Rust frontend failed" in diagnostics: + raise SystemExit("The negative conversion check did not reject its missing precondition") +PY +done + layout_status=0 timeout --signal=TERM --kill-after=10s 60s \ verifast -rustc_args '--edition 2024' backend/matrix-layout-valid.rs || layout_status=$? From af08c4154f36b9a830a672c32c509449c12fe9b5 Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Sun, 13 Sep 2026 05:42:22 -0700 Subject: [PATCH 49/62] Type generic conversion maps at their call sites Signed-off-by: Onyeka Obi --- verifast-proofs/core/iter/adapters/array_layout.rs | 12 ++++++------ .../core/iter/adapters/backend/array-subtyping.patch | 2 +- .../core/iter/adapters/verified/map_windows.rs | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/verifast-proofs/core/iter/adapters/array_layout.rs b/verifast-proofs/core/iter/adapters/array_layout.rs index f4a9cf4e2950a..55284c0bc8349 100644 --- a/verifast-proofs/core/iter/adapters/array_layout.rs +++ b/verifast-proofs/core/iter/adapters/array_layout.rs @@ -44,7 +44,7 @@ lem owned_values_mono(t: thread_id_t, values: list) req type_interp::() &*& type_interp::() &*& is_subtype_of::() == true &*& foreach(values, own::(t)); ens type_interp::() &*& type_interp::() &*& - foreach(map(upcast::, values), own::(t)); + foreach(map::(upcast, values), own::(t)); { open foreach(values, own::(t)); match values { @@ -56,7 +56,7 @@ lem owned_values_mono(t: thread_id_t, values: list) owned_values_mono::(t, rest); } } - close foreach(map(upcast::, values), own::(t)); + close foreach(map::(upcast, values), own::(t)); } lem owned_values_send(t0: thread_id_t, t1: thread_id_t, values: list) @@ -78,9 +78,9 @@ lem owned_values_send(t0: thread_id_t, t1: thread_id_t, values: list) lem mapped_uninit_upcast(values: list) req is_subtype_of::() == true; - ens map(upcast::, std::mem::MaybeUninit>, + ens map::, std::mem::MaybeUninit>(upcast, map(std::mem::MaybeUninit::new, values)) == - map(std::mem::MaybeUninit::new, map(upcast::, values)); + map(std::mem::MaybeUninit::new, map::(upcast, values)); { match values { nil => {} @@ -99,7 +99,7 @@ fix matrix_elems(matrix: [[T; N]; 2]) -> list { lem matrix_upcast(matrix: [[T0; N]; 2]) req is_subtype_of::() == true; ens matrix_elems::(upcast::<[[T0; N]; 2], [[T1; N]; 2]>(matrix)) == - map(upcast::, matrix_elems(matrix)); + map::(upcast, matrix_elems(matrix)); { std::mem::array_subtype::(); std::mem::array_upcast::<[T0; N], [T1; N], 2>(matrix); @@ -112,7 +112,7 @@ lem matrix_upcast(matrix: [[T0; N]; 2]) cons(second, suffix) => { std::mem::array_upcast::(first); std::mem::array_upcast::(second); - mapped_append(upcast::, Array_elems(first), Array_elems(second)); + mapped_append::(upcast, Array_elems(first), Array_elems(second)); } } } diff --git a/verifast-proofs/core/iter/adapters/backend/array-subtyping.patch b/verifast-proofs/core/iter/adapters/backend/array-subtyping.patch index 99f2a8581caa7..58e1464023adf 100644 --- a/verifast-proofs/core/iter/adapters/backend/array-subtyping.patch +++ b/verifast-proofs/core/iter/adapters/backend/array-subtyping.patch @@ -22,7 +22,7 @@ + lem array_upcast(value: [T0; N]); + req is_subtype_of::() == true; + ens Array_elems(upcast::<[T0; N], [T1; N]>(value)) == -+ map(upcast::, Array_elems(value)); ++ map::(upcast, Array_elems(value)); + + lem array_elems_length(value: [T; N]); + req true; diff --git a/verifast-proofs/core/iter/adapters/verified/map_windows.rs b/verifast-proofs/core/iter/adapters/verified/map_windows.rs index afecd88509b35..acf0710d3562b 100644 --- a/verifast-proofs/core/iter/adapters/verified/map_windows.rs +++ b/verifast-proofs/core/iter/adapters/verified/map_windows.rs @@ -67,12 +67,12 @@ lem Buffer_own_mono() std::mem::upcast_identity(buffer.start); std::mem::MaybeUninit_subtype::(); matrix_upcast::, std::mem::MaybeUninit, N>(buffer.buffer); - mapped_range(upcast::, std::mem::MaybeUninit>, + mapped_range::, std::mem::MaybeUninit>(upcast, matrix_elems(buffer.buffer), buffer.start, width::()); mapped_uninit_upcast::(values); - mapped_length(upcast::, values); + mapped_length::(upcast, values); owned_values_mono::(t, values); - close exists(map(upcast::, values)); + close exists(map::(upcast, values)); close Buffer_own::(t, Buffer:: { buffer: upcast(buffer.buffer), start: upcast(buffer.start) }); } From b58a18832a3e97261272e3fcf5f8bb83f46cd410 Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Sun, 13 Sep 2026 05:56:52 -0700 Subject: [PATCH 50/62] Retain wrapper values and establish recursive proof progress Signed-off-by: Onyeka Obi --- verifast-proofs/core/iter/adapters/README.md | 8 +++--- .../core/iter/adapters/array_layout.rs | 20 +++++++------- .../iter/adapters/backend/array-layout.patch | 27 +++++-------------- .../adapters/backend/matrix-layout-valid.rs | 1 + .../iter/adapters/verified/map_windows.rs | 16 ++++++----- verifast-proofs/core/iter/adapters/verify.sh | 3 ++- 6 files changed, 34 insertions(+), 41 deletions(-) diff --git a/verifast-proofs/core/iter/adapters/README.md b/verifast-proofs/core/iter/adapters/README.md index c9a584b3995c3..c77aef9b0e82f 100644 --- a/verifast-proofs/core/iter/adapters/README.md +++ b/verifast-proofs/core/iter/adapters/README.md @@ -43,8 +43,10 @@ explicit caller obligations here. Constructor preservation and the surrounding `MapWindows` iterator implementation are outside this projection. The shared accessor requires a lifetime borrow of the window. The mutable -accessor requires a borrow of the whole backing array because its pointer -helper borrows that array. Establishing those borrows from the full safe +accessor requires a borrow of the whole backing array of initialized +`MaybeUninit` wrappers because its pointer helper borrows that array. +These wrappers can contain uninitialized bytes and need no `T` ownership. +Establishing those borrows from the full safe abstraction is outside this port. The mutable accessor accepts storage that does not yet own initialized `T` values, as required by the clone path. @@ -184,7 +186,7 @@ and the wrapper's documented layout. They include zero-sized element types. The matrix conversion and writable-window proofs remain mandatory; the layout facts do not grant storage or ownership permissions. The patch also removes implicit `Sized` bounds from the array length parameters -of the three existing array conversion lemmas. Symbolic const parameters are +of the existing array conversion lemmas. Symbolic const parameters are not Rust value types. The element type bounds and every storage precondition remain in force, and the patched prelude is cached with checksum validation. diff --git a/verifast-proofs/core/iter/adapters/array_layout.rs b/verifast-proofs/core/iter/adapters/array_layout.rs index 55284c0bc8349..e826f4b1e832b 100644 --- a/verifast-proofs/core/iter/adapters/array_layout.rs +++ b/verifast-proofs/core/iter/adapters/array_layout.rs @@ -46,10 +46,10 @@ lem owned_values_mono(t: thread_id_t, values: list) ens type_interp::() &*& type_interp::() &*& foreach(map::(upcast, values), own::(t)); { - open foreach(values, own::(t)); match values { - nil => {} + nil => { open foreach(values, own::(t)); } cons(value, rest) => { + open foreach(values, own::(t)); open own::(t)(value); own_mono::(t, value); close own::(t)(upcast::(value)); @@ -63,10 +63,10 @@ lem owned_values_send(t0: thread_id_t, t1: thread_id_t, values: list) req type_interp::() &*& is_Send(typeid(T)) == true &*& foreach(values, own::(t0)); ens type_interp::() &*& foreach(values, own::(t1)); { - open foreach(values, own::(t0)); match values { - nil => {} + nil => { open foreach(values, own::(t0)); } cons(value, rest) => { + open foreach(values, own::(t0)); open own::(t0)(value); Send::send::(t0, t1, value); close own::(t1)(value); @@ -167,8 +167,8 @@ lem pack_matrix(p: *[[T; N]; 2]) // These conversions preserve writable storage. They do not claim initialized T values. lem collapse_window(p: *std::mem::MaybeUninit) - req p[..usize_of_const(typeid(N))] |-> _; - ens *(p as *std::mem::MaybeUninit<[T; N]>) |-> _; + req p[..usize_of_const(typeid(N))] |-> ?slots; + ens *(p as *std::mem::MaybeUninit<[T; N]>) |-> ?window; { std::mem::array_layout::(); std::mem::MaybeUninit_layout::(); @@ -179,8 +179,8 @@ lem collapse_window(p: *std::mem::MaybeUninit) } lem expand_window(p: *std::mem::MaybeUninit<[T; N]>) - req *p |-> _; - ens (p as *std::mem::MaybeUninit)[..usize_of_const(typeid(N))] |-> _; + req *p |-> ?window; + ens (p as *std::mem::MaybeUninit)[..usize_of_const(typeid(N))] |-> ?slots; { std::mem::MaybeUninit_layout::(); std::mem::open_MaybeUninit(p); @@ -247,8 +247,8 @@ lem lend_array(k: lifetime_t, p: *T, count: usize) } lem reclaim_array(k: lifetime_t, p: *T, count: usize) - req 0 <= count &*& [_]lifetime_dead_token(k) &*& - array_borrow_tokens(k, p, count) &*& array_at_lft_(k, p, count, _); + req array_borrow_tokens(k, p, count) &*& 0 <= count &*& + [_]lifetime_dead_token(k) &*& array_at_lft_(k, p, count, _); ens p[..count] |-?-> _; { open array_borrow_tokens(k, p, count); diff --git a/verifast-proofs/core/iter/adapters/backend/array-layout.patch b/verifast-proofs/core/iter/adapters/backend/array-layout.patch index 66bafd45c0a7b..7e2755d544544 100644 --- a/verifast-proofs/core/iter/adapters/backend/array-layout.patch +++ b/verifast-proofs/core/iter/adapters/backend/array-layout.patch @@ -1,9 +1,6 @@ --- a/bin/rust/std/lib.rsspec +++ b/bin/rust/std/lib.rsspec -@@ -371,6 +371,17 @@ - - /*@ - +@@ -373,0 +374,11 @@ + // Rust array layout and MaybeUninit transparency, including zero-sized T. + lem array_layout(); + req true; @@ -15,27 +12,17 @@ + ens std::mem::size_of::>() == std::mem::size_of::() &*& + std::mem::align_of::>() == std::mem::align_of::(); + - fix size_of_val(x: *T) -> usize; - fix align_of_val(x: *T) -> usize; - +@@ -478 +489 @@ +- lem Array__MaybeUninit_to_Array_MaybeUninit(self: *[MaybeUninit; N]); ++ lem Array__MaybeUninit_to_Array_MaybeUninit(self: *[MaybeUninit; N]); --- a/bin/rust/prelude_core.rsspec +++ b/bin/rust/prelude_core.rsspec -@@ -312,15 +312,15 @@ - req true; - ens slice_of_elems(slice_elems(slice)) == slice; - +@@ -315 +315 @@ -lem Array__to_array_(p: *[T; N]); +lem Array__to_array_(p: *[T; N]); - req [?f](*p |-?-> ?array_); - ens [f](p as *T)[..usize_of_const(typeid(N))] |-?-> match array_ { none => repeat(nat_of_int(usize_of_const(typeid(N))), none), some(array) => map(some, Array_elems(array)) }; - +@@ -319 +319 @@ -lem Array_to_array(p: *[T; N]); +lem Array_to_array(p: *[T; N]); - req [?f](*p |-> ?array); - ens [f](p as *T)[..usize_of_const(typeid(N))] |-> Array_elems(array); - +@@ -323 +323 @@ -lem array_to_Array(p: *[T; N]); +lem array_to_Array(p: *[T; N]); - req [?f](p as *T)[..usize_of_const(typeid(N))] |-> ?elems; - ens [f](*p |-> Array_of_elems(elems)); - diff --git a/verifast-proofs/core/iter/adapters/backend/matrix-layout-valid.rs b/verifast-proofs/core/iter/adapters/backend/matrix-layout-valid.rs index 5e63605b34fda..a9cd9861d6dea 100644 --- a/verifast-proofs/core/iter/adapters/backend/matrix-layout-valid.rs +++ b/verifast-proofs/core/iter/adapters/backend/matrix-layout-valid.rs @@ -19,6 +19,7 @@ unsafe fn writable_window(p: *mut [std::mem::MaybeUninit; //@ ens *p |-> _; //@ on_unwind_ens false; { + //@ std::mem::Array__MaybeUninit_to_Array_MaybeUninit(p); //@ Array_to_array(p); //@ collapse_window::(p as *std::mem::MaybeUninit); //@ expand_window(p as *std::mem::MaybeUninit<[T; N]>); diff --git a/verifast-proofs/core/iter/adapters/verified/map_windows.rs b/verifast-proofs/core/iter/adapters/verified/map_windows.rs index acf0710d3562b..0450632b15c7c 100644 --- a/verifast-proofs/core/iter/adapters/verified/map_windows.rs +++ b/verifast-proofs/core/iter/adapters/verified/map_windows.rs @@ -42,7 +42,7 @@ pred live(t: thread_id_t, b: *Buffer, start: usize, values: list) pred storage(b: *Buffer; start: usize) = bounds(b, start) &*& base(b)[..2 * width::()] |-> ?slots; -pred_ctor writable_matrix(b: *Buffer)(;) = (*b).buffer |-> _; +pred_ctor writable_matrix(b: *Buffer)(;) = (*b).buffer |-> ?matrix; pred >.own(t, buffer) = exists::>(?values) &*& @@ -96,8 +96,8 @@ lem Buffer_send(t1: thread_id_t) pred drop_frame(b: *Buffer, start: usize, matrix: *[[std::mem::MaybeUninit; N]; 2], k: lifetime_t, slice: *[T]) = bounds(b, start) &*& ref_mut_end_token(matrix, &(*b).buffer) &*& - (matrix as *std::mem::MaybeUninit)[..start] |-> _ &*& - ((matrix as *std::mem::MaybeUninit) + start + width::())[..width::() - start] |-> _ &*& + (matrix as *std::mem::MaybeUninit)[..start] |-> ?prefix &*& + ((matrix as *std::mem::MaybeUninit) + start + width::())[..width::() - start] |-> ?suffix &*& array_borrow_tokens(k, ((matrix as *std::mem::MaybeUninit) + start) as *T, width::()) &*& close_points_to_at_lft_token(1, k, slice, 1) &*& slice as *T == ((matrix as *std::mem::MaybeUninit) + start) as *T &*& @@ -255,8 +255,8 @@ impl Buffer { pred ctx(;) = ref_mut_end_token(result, window) &*& ref_mut_end_token(buffer_mut_ptr as *[[std::mem::MaybeUninit; N]; 2], &(*self).buffer) &*& - buffer_mut_ptr[..start] |-> _ &*& - (buffer_mut_ptr + start + width::())[..width::() - start] |-> _; + buffer_mut_ptr[..start] |-> ?prefix &*& + (buffer_mut_ptr + start + width::())[..width::() - start] |-> ?suffix; produce_lem_ptr_chunk restore_full_borrow_(ctx, >.full_borrow_content(t, result), writable_matrix(self))() { @@ -336,6 +336,8 @@ impl Buffer { ptr::copy_nonoverlapping(buffer_mut_ptr.add(self.start + 1), buffer_mut_ptr, N - 1); (*buffer_mut_ptr.add(N - 1)).write(next); //@ end_ref_mut_::>(); + //@ open array(buffer_mut_ptr + width::(), 0, _); + //@ close array(buffer_mut_ptr + width::(), 0, nil); //@ close array(buffer_mut_ptr + width::() - 1, 1, cons(std::mem::MaybeUninit::new(next), nil)); //@ array_join(buffer_mut_ptr); buffer_mut_ptr.add(self.start) @@ -443,12 +445,12 @@ impl Drop for Buffer { // `self.start` are initialized. We drop them here. unsafe { let buffer_mut_ptr = self.buffer_mut_ptr(); - let initialized_part: *mut [T] = - crate::ptr::slice_from_raw_parts_mut(buffer_mut_ptr.add(self.start).cast(), N); //@ let matrix = buffer_mut_ptr as *[[std::mem::MaybeUninit; N]; 2]; //@ unpack_matrix(matrix); //@ array_split(buffer_mut_ptr, start); //@ array_split(buffer_mut_ptr + start, width::()); + let initialized_part: *mut [T] = + crate::ptr::slice_from_raw_parts_mut(buffer_mut_ptr.add(self.start).cast(), N); //@ initialized_slots(buffer_mut_ptr + start, values); //@ let k = begin_lifetime(); //@ lend_array(k, (buffer_mut_ptr + start) as *T, width::()); diff --git a/verifast-proofs/core/iter/adapters/verify.sh b/verifast-proofs/core/iter/adapters/verify.sh index fb9dd34962f28..f5b4e55d06b97 100644 --- a/verifast-proofs/core/iter/adapters/verify.sh +++ b/verifast-proofs/core/iter/adapters/verify.sh @@ -303,7 +303,8 @@ import re for path in (Path("array_layout.rs"), Path("verified/map_windows.rs"), Path("verified/step_by.rs")): for line_number, line in enumerate(path.read_text().splitlines(), 1): if re.match(r"\s*(?:unsafe\s+)?(?:fn|lem)\s+\w+", line): - print(f"{path}:{line_number}") + location_path = "verified/../array_layout.rs" if path.name == "array_layout.rs" else str(path) + print(f"{location_path}:{line_number}") PY ) fi From 96d098356edd34b61c690ebb4adea028ba503de8 Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Sun, 13 Sep 2026 06:14:20 -0700 Subject: [PATCH 51/62] Repair induction and window borrow proof obligations Signed-off-by: Onyeka Obi --- .../core/iter/adapters/array_layout.rs | 35 ++++++--- .../iter/adapters/verified/map_windows.rs | 77 +++++++++++-------- 2 files changed, 69 insertions(+), 43 deletions(-) diff --git a/verifast-proofs/core/iter/adapters/array_layout.rs b/verifast-proofs/core/iter/adapters/array_layout.rs index e826f4b1e832b..c7a7ceb9b0f94 100644 --- a/verifast-proofs/core/iter/adapters/array_layout.rs +++ b/verifast-proofs/core/iter/adapters/array_layout.rs @@ -24,6 +24,15 @@ lem mapped_length(f: fix(a, b), xs: list) } } +lem joined_window(prefix: list, window: list, suffix: list) + req true; + ens take(length(window), drop(length(prefix), append(append(prefix, window), suffix))) == window; +{ + append_assoc(prefix, window, suffix); + drop_append(length(prefix), prefix, append(window, suffix)); + take_append_l(length(window), window, suffix); +} + lem mapped_range(f: fix(a, b), xs: list, start: usize, count: usize) req 0 <= start &*& 0 <= count; ens take(count, drop(start, map(f, xs))) == map(f, take(count, drop(start, xs))); @@ -47,16 +56,19 @@ lem owned_values_mono(t: thread_id_t, values: list) foreach(map::(upcast, values), own::(t)); { match values { - nil => { open foreach(values, own::(t)); } + nil => { + open foreach(values, own::(t)); + close foreach(map::(upcast, values), own::(t)); + } cons(value, rest) => { open foreach(values, own::(t)); open own::(t)(value); own_mono::(t, value); close own::(t)(upcast::(value)); owned_values_mono::(t, rest); + close foreach(map::(upcast, values), own::(t)); } } - close foreach(map::(upcast, values), own::(t)); } lem owned_values_send(t0: thread_id_t, t1: thread_id_t, values: list) @@ -64,16 +76,19 @@ lem owned_values_send(t0: thread_id_t, t1: thread_id_t, values: list) ens type_interp::() &*& foreach(values, own::(t1)); { match values { - nil => { open foreach(values, own::(t0)); } + nil => { + open foreach(values, own::(t0)); + close foreach(values, own::(t1)); + } cons(value, rest) => { open foreach(values, own::(t0)); open own::(t0)(value); Send::send::(t0, t1, value); close own::(t1)(value); owned_values_send(t0, t1, rest); + close foreach(values, own::(t1)); } } - close foreach(values, own::(t1)); } lem mapped_uninit_upcast(values: list) @@ -172,8 +187,8 @@ lem collapse_window(p: *std::mem::MaybeUninit) { std::mem::array_layout::(); std::mem::MaybeUninit_layout::(); + array_to_array_(p); array__to_u8s_(p, usize_of_const(typeid(N))); - array_to_array_(p as *u8); from_u8s_(p as *[T; N]); std::mem::close_MaybeUninit_(p as *std::mem::MaybeUninit<[T; N]>); } @@ -193,14 +208,14 @@ lem own_uninit_values(t: thread_id_t, values: list>) ens foreach(values, own::>(t)); { match values { - nil => {} + nil => { close foreach(values, own::>(t)); } cons(value, rest) => { std::mem::MaybeUninit_own_init(t, value); close own::>(t)(value); own_uninit_values(t, rest); + close foreach(values, own::>(t)); } } - close foreach(values, own::>(t)); } lem own_uninit_rows(t: thread_id_t, rows: list<[std::mem::MaybeUninit; N]>) @@ -208,15 +223,15 @@ lem own_uninit_rows(t: thread_id_t, rows: list<[std::mem::MaybeUni ens foreach(rows, own::<[std::mem::MaybeUninit; N]>(t)); { match rows { - nil => {} + nil => { close foreach(rows, own::<[std::mem::MaybeUninit; N]>(t)); } cons(row, rest) => { own_uninit_values(t, Array_elems(row)); close array_owned::, N>()(t, row); close own::<[std::mem::MaybeUninit; N]>(t)(row); own_uninit_rows(t, rest); + close foreach(rows, own::<[std::mem::MaybeUninit; N]>(t)); } } - close foreach(rows, own::<[std::mem::MaybeUninit; N]>(t)); } lem own_matrix_storage(t: thread_id_t, matrix: [[std::mem::MaybeUninit; N]; 2]) @@ -234,7 +249,7 @@ pred array_borrow_tokens(k: lifetime_t, p: *T, count: usize) = }; lem lend_array(k: lifetime_t, p: *T, count: usize) - req 0 <= count &*& p[..count] |-> ?values; + req p[..count] |-> ?values &*& 0 <= count; ens array_at_lft(k, p, count, values) &*& array_borrow_tokens(k, p, count); { open array(p, count, values); diff --git a/verifast-proofs/core/iter/adapters/verified/map_windows.rs b/verifast-proofs/core/iter/adapters/verified/map_windows.rs index 0450632b15c7c..e5da742b3f8eb 100644 --- a/verifast-proofs/core/iter/adapters/verified/map_windows.rs +++ b/verifast-proofs/core/iter/adapters/verified/map_windows.rs @@ -4,7 +4,7 @@ use crate::mem::MaybeUninit; use crate::{fmt, ptr}; //@ use array_layout::{array_borrow_tokens, collapse_window, expand_window, lend_array, matrix_elems, own_matrix_storage, pack_matrix, reclaim_array, unpack_matrix}; -//@ use array_layout::{mapped_length, mapped_range, mapped_uninit_upcast, matrix_upcast, owned_values_mono, owned_values_send}; +//@ use array_layout::{joined_window, mapped_append, mapped_length, mapped_range, mapped_uninit_upcast, matrix_upcast, owned_values_mono, owned_values_send}; struct Buffer { // Invariant: `self.buffer[self.start..self.start + N]` is initialized, @@ -248,40 +248,42 @@ impl Buffer { //@ array_split(buffer_mut_ptr, start); //@ array_split(buffer_mut_ptr + start, width::()); //@ collapse_window::(buffer_mut_ptr + start); - let result = unsafe { &mut *buffer_mut_ptr.add(self.start).cast() }; - //@ let window = (buffer_mut_ptr + start) as *std::mem::MaybeUninit<[T; N]>; - /*@ - { - pred ctx(;) = - ref_mut_end_token(result, window) &*& - ref_mut_end_token(buffer_mut_ptr as *[[std::mem::MaybeUninit; N]; 2], &(*self).buffer) &*& - buffer_mut_ptr[..start] |-> ?prefix &*& - (buffer_mut_ptr + start + width::())[..width::() - start] |-> ?suffix; - produce_lem_ptr_chunk restore_full_borrow_(ctx, - >.full_borrow_content(t, result), - writable_matrix(self))() { - open ctx(); - open_full_borrow_content::>(t, result); - assert *result |-> ?borrowed_contents; - std::mem::MaybeUninit_own_dispose::<[T; N]>(t, borrowed_contents); - end_ref_mut_::>(); - expand_window(window); - array_join(buffer_mut_ptr); - array_join(buffer_mut_ptr); - pack_matrix(buffer_mut_ptr as *[[std::mem::MaybeUninit; N]; 2]); - end_ref_mut_::<[[std::mem::MaybeUninit; N]; 2]>(); - close writable_matrix::(self)(); - } { - assert *result |-> ?contents; - std::mem::MaybeUninit_own_init::<[T; N]>(t, contents); - close_full_borrow_content::>(t, result); - close ctx(); - close_full_borrow_strong_(); + unsafe { + let result = &mut *buffer_mut_ptr.add(self.start).cast(); + //@ let window = (buffer_mut_ptr + start) as *std::mem::MaybeUninit<[T; N]>; + /*@ + { + pred ctx() = + ref_mut_end_token(result, window) &*& + ref_mut_end_token(buffer_mut_ptr as *[[std::mem::MaybeUninit; N]; 2], &(*self).buffer) &*& + buffer_mut_ptr[..start] |-> ?prefix &*& + (buffer_mut_ptr + start + width::())[..width::() - start] |-> ?suffix; + produce_lem_ptr_chunk restore_full_borrow_(ctx, + >.full_borrow_content(t, result), + writable_matrix(self))() { + open ctx(); + open_full_borrow_content::>(t, result); + assert *result |-> ?borrowed_contents; + std::mem::MaybeUninit_own_dispose::<[T; N]>(t, borrowed_contents); + end_ref_mut_::>(); + expand_window(window); + array_join(buffer_mut_ptr); + array_join(buffer_mut_ptr); + pack_matrix(buffer_mut_ptr as *[[std::mem::MaybeUninit; N]; 2]); + end_ref_mut_::<[[std::mem::MaybeUninit; N]; 2]>(); + close writable_matrix::(self)(); + } { + assert *result |-> ?contents; + std::mem::MaybeUninit_own_init::<[T; N]>(t, contents); + close_full_borrow_content::>(t, result); + close ctx(); + close_full_borrow_strong_(); + } } + @*/ + //@ close bounds(self, start); + result } - @*/ - //@ close bounds(self, start); - result } /// Pushes a new item `next` to the back, and pops the front-most one. @@ -300,6 +302,9 @@ impl Buffer { { //@ open live(t, self, start, values); //@ open bounds(self, start); + //@ assert base(self)[..start] |-> ?prefix_slots; + //@ assert (base(self) + start + width::())[..width::() - start] |-> ?suffix_slots; + //@ joined_window(prefix_slots, map(std::mem::MaybeUninit::new, values), suffix_slots); //@ array_join(base(self)); //@ array_join(base(self)); //@ pack_matrix(&(*self).buffer); @@ -374,10 +379,13 @@ impl Buffer { // SAFETY: the index is valid and this is element `a` in the // diagram above and has not been dropped yet. //@ close foreach(nil, own::(t)); + //@ close own::(t)(next); //@ close foreach(cons(next, nil), own::(t)); //@ foreach_append(tail(values), cons(next, nil)); + //@ mapped_append::>(std::mem::MaybeUninit::new, tail(values), cons(next, nil)); //@ std::mem::open_MaybeUninit(to_drop); //@ close points_to(to_drop as *T, head(values)); + //@ open own::(t)(head(values)); unsafe { ptr::drop_in_place(to_drop.cast_init()) }; //@ std::mem::close_MaybeUninit_(to_drop); /*@ @@ -438,6 +446,9 @@ impl Drop for Buffer { { //@ open live(t, self, start, values); //@ open bounds(self, start); + //@ assert base(self)[..start] |-> ?prefix_slots; + //@ assert (base(self) + start + width::())[..width::() - start] |-> ?suffix_slots; + //@ joined_window(prefix_slots, map(std::mem::MaybeUninit::new, values), suffix_slots); //@ array_join(base(self)); //@ array_join(base(self)); //@ pack_matrix(&(*self).buffer); From 9bb5507e393f0df99ca21d3bd6c19b93691a5062 Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Sun, 13 Sep 2026 06:24:40 -0700 Subject: [PATCH 52/62] Translate MaybeUninit pointer casts without granting storage Signed-off-by: Onyeka Obi --- verifast-proofs/core/iter/adapters/README.md | 6 ++++++ .../iter/adapters/backend/cast-init-invalid.rs | 11 +++++++++++ .../iter/adapters/backend/cast-init-valid.rs | 11 +++++++++++ .../iter/adapters/backend/const-generics.patch | 16 +++++++++++----- verifast-proofs/core/iter/adapters/verify.sh | 16 ++++++++++++++++ 5 files changed, 55 insertions(+), 5 deletions(-) create mode 100644 verifast-proofs/core/iter/adapters/backend/cast-init-invalid.rs create mode 100644 verifast-proofs/core/iter/adapters/backend/cast-init-valid.rs diff --git a/verifast-proofs/core/iter/adapters/README.md b/verifast-proofs/core/iter/adapters/README.md index c77aef9b0e82f..f1278ca8efd2a 100644 --- a/verifast-proofs/core/iter/adapters/README.md +++ b/verifast-proofs/core/iter/adapters/README.md @@ -143,6 +143,12 @@ storage. The mutable test converts the new reference to a raw pointer to test creation independently of a further return reborrow. These fixtures validate the frontend extension, not the adapter contracts. +The `MaybeUninit` pointer `cast_init` translation is an ordinary raw-pointer +cast, matching its [Rust implementation](https://github.com/rust-lang/rust/blob/master/library/core/src/ptr/mut_ptr.rs). +It preserves the address and grants no permission to read or drop `T`. +The runner checks both address preservation and rejection of a read without +initialized storage before checking the adapters. + The patch also preserves const operands and `ConstArgHasType` constraints through the MIR schema and refinement checker. Constraints are compared after generic parameter renaming; unsupported predicates still fail refinement. diff --git a/verifast-proofs/core/iter/adapters/backend/cast-init-invalid.rs b/verifast-proofs/core/iter/adapters/backend/cast-init-invalid.rs new file mode 100644 index 0000000000000..97d5704eb0733 --- /dev/null +++ b/verifast-proofs/core/iter/adapters/backend/cast-init-invalid.rs @@ -0,0 +1,11 @@ +#![crate_type = "lib"] +#![crate_name = "cast_init_invalid"] +#![feature(cast_maybe_uninit)] + +unsafe fn read_without_storage(p: *mut std::mem::MaybeUninit) -> T +//@ req true; +//@ ens true; +//@ on_unwind_ens false; +{ + unsafe { p.cast_init().read() } +} diff --git a/verifast-proofs/core/iter/adapters/backend/cast-init-valid.rs b/verifast-proofs/core/iter/adapters/backend/cast-init-valid.rs new file mode 100644 index 0000000000000..ece9d3da93d38 --- /dev/null +++ b/verifast-proofs/core/iter/adapters/backend/cast-init-valid.rs @@ -0,0 +1,11 @@ +#![crate_type = "lib"] +#![crate_name = "cast_init_valid"] +#![feature(cast_maybe_uninit)] + +unsafe fn cast_pointer(p: *mut std::mem::MaybeUninit) -> *mut T +//@ req true; +//@ ens result == p as *T; +//@ on_unwind_ens false; +{ + p.cast_init() +} diff --git a/verifast-proofs/core/iter/adapters/backend/const-generics.patch b/verifast-proofs/core/iter/adapters/backend/const-generics.patch index 4083f79fafc2f..d33b2bc3895da 100644 --- a/verifast-proofs/core/iter/adapters/backend/const-generics.patch +++ b/verifast-proofs/core/iter/adapters/backend/const-generics.patch @@ -140,7 +140,7 @@ full_bor_content; points_to; pointee_fbc = None; -@@ -2433,7 +2436,15 @@ +@@ -2433,7 +2436,21 @@ Ok ( tmp_rvalue_binders, FnCallResult (CastExpr (fn_loc, PtrTypeExpr (fn_loc, ManifestTypeExpr (fn_loc, Int (Unsigned, FixedWidthRank 0))), arg)) ) @@ -152,12 +152,18 @@ + Ok (tmp_rvalue_binders, FnCallResult + (Ast.CastExpr (fn_loc, PtrTypeExpr (fn_loc, target.vf_ty), arg))) + | inputs -> Error (`TrFnCallRExpr "Invalid generic arguments for pointer cast")) ++ | "std::ptr::mut_ptr::>::cast_init" -> ( ++ match substs, args with ++ | [ Mir.GenArgType target ], [ arg ] -> ++ Ok (tmp_rvalue_binders, FnCallResult ++ (Ast.CastExpr (fn_loc, PtrTypeExpr (fn_loc, target.vf_ty), arg))) ++ | inputs -> Error (`TrFnCallRExpr "Invalid generic arguments for MaybeUninit pointer cast")) + | "std::slice::::as_ptr" + | "std::slice::::as_mut_ptr" -> let [ Mir.GenArgType gen_arg_ty_info; _ ] = substs in let [ arg ] = args in Ok -@@ -3020,11 +3031,8 @@ +@@ -3020,11 +3037,8 @@ let* place_expr, place_is_mutable = translate_place place_cpn loc in let (path, line, _), _ = Ast.lexed_loc loc in let ignore_ref_creation = @@ -169,7 +175,7 @@ if TranslatorArgs.ignore_ref_creation then true else let directives = -@@ -3157,12 +3165,34 @@ +@@ -3157,12 +3171,34 @@ Ok (`TrRvalueExpr e) | _ -> let* ty_info = translate_decoded_ty cast_data.ty loc in @@ -206,7 +212,7 @@ | `TrTypedConstantRvalueBinderBuilder rvalue_binder_builder -> failwith "Todo: Rvalue::Cast" (*Todo @Nima: We need a better design (refactor) for passing different results of operand translation*) -@@ -5993,17 +6023,18 @@ +@@ -5993,17 +6029,18 @@ with | Lifetime -> Ok (`Lifetime name) | Type -> Ok (`Type name) @@ -234,7 +240,7 @@ let lft_params = Util.flatmap (function `Lifetime x -> [ x ] | _ -> []) generics in -@@ -6029,7 +6060,9 @@ +@@ -6029,7 +6066,9 @@ |> List.map decode_predicate in let sized_tparams = compute_sized_tparams preds in diff --git a/verifast-proofs/core/iter/adapters/verify.sh b/verifast-proofs/core/iter/adapters/verify.sh index f5b4e55d06b97..f1da5119ca3d2 100644 --- a/verifast-proofs/core/iter/adapters/verify.sh +++ b/verifast-proofs/core/iter/adapters/verify.sh @@ -203,6 +203,22 @@ PY timeout --signal=TERM --kill-after=10s 60s \ verifast -rustc_args '--edition 2024' backend/maybeuninit-own-valid.rs +timeout --signal=TERM --kill-after=10s 60s \ + verifast -rustc_args '--edition 2024' backend/cast-init-valid.rs +if timeout --signal=TERM --kill-after=10s 60s \ + verifast -rustc_args '--edition 2024' backend/cast-init-invalid.rs >"$negative_log" 2>&1; then + echo 'The pointer cast granted access to missing initialized storage.' >&2 + exit 1 +fi +cat "$negative_log" +python3 -I - "$negative_log" <<'PY' +from pathlib import Path +import sys + +diagnostics = Path(sys.argv[1]).read_text() +if "No matching heap chunks" not in diagnostics or "Rust frontend failed" in diagnostics: + raise SystemExit("The cast negative check did not reject a read without storage") +PY if timeout --signal=TERM --kill-after=10s 60s \ verifast -rustc_args '--edition 2024' backend/value-own-invalid.rs >"$negative_log" 2>&1; then echo 'The ownership model created ownership of an arbitrary T.' >&2 From 1d60d9c6645124db441a07ca801c966e88f1dc27 Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Sun, 13 Sep 2026 06:30:34 -0700 Subject: [PATCH 53/62] Recognize the std path for MaybeUninit pointer casts Signed-off-by: Onyeka Obi --- .../core/iter/adapters/backend/const-generics.patch | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/verifast-proofs/core/iter/adapters/backend/const-generics.patch b/verifast-proofs/core/iter/adapters/backend/const-generics.patch index d33b2bc3895da..9c824b8521cfa 100644 --- a/verifast-proofs/core/iter/adapters/backend/const-generics.patch +++ b/verifast-proofs/core/iter/adapters/backend/const-generics.patch @@ -140,7 +140,7 @@ full_bor_content; points_to; pointee_fbc = None; -@@ -2433,7 +2436,21 @@ +@@ -2433,7 +2436,22 @@ Ok ( tmp_rvalue_binders, FnCallResult (CastExpr (fn_loc, PtrTypeExpr (fn_loc, ManifestTypeExpr (fn_loc, Int (Unsigned, FixedWidthRank 0))), arg)) ) @@ -152,7 +152,8 @@ + Ok (tmp_rvalue_binders, FnCallResult + (Ast.CastExpr (fn_loc, PtrTypeExpr (fn_loc, target.vf_ty), arg))) + | inputs -> Error (`TrFnCallRExpr "Invalid generic arguments for pointer cast")) -+ | "std::ptr::mut_ptr::>::cast_init" -> ( ++ | "std::ptr::mut_ptr::>::cast_init" ++ | "std::ptr::mut_ptr::>::cast_init" -> ( + match substs, args with + | [ Mir.GenArgType target ], [ arg ] -> + Ok (tmp_rvalue_binders, FnCallResult @@ -163,7 +164,7 @@ let [ Mir.GenArgType gen_arg_ty_info; _ ] = substs in let [ arg ] = args in Ok -@@ -3020,11 +3037,8 @@ +@@ -3020,11 +3038,8 @@ let* place_expr, place_is_mutable = translate_place place_cpn loc in let (path, line, _), _ = Ast.lexed_loc loc in let ignore_ref_creation = @@ -175,7 +176,7 @@ if TranslatorArgs.ignore_ref_creation then true else let directives = -@@ -3157,12 +3171,34 @@ +@@ -3157,12 +3172,34 @@ Ok (`TrRvalueExpr e) | _ -> let* ty_info = translate_decoded_ty cast_data.ty loc in @@ -212,7 +213,7 @@ | `TrTypedConstantRvalueBinderBuilder rvalue_binder_builder -> failwith "Todo: Rvalue::Cast" (*Todo @Nima: We need a better design (refactor) for passing different results of operand translation*) -@@ -5993,17 +6029,18 @@ +@@ -5993,17 +6030,18 @@ with | Lifetime -> Ok (`Lifetime name) | Type -> Ok (`Type name) @@ -240,7 +241,7 @@ let lft_params = Util.flatmap (function `Lifetime x -> [ x ] | _ -> []) generics in -@@ -6029,7 +6066,9 @@ +@@ -6029,7 +6067,9 @@ |> List.map decode_predicate in let sized_tparams = compute_sized_tparams preds in From d9fc86796974f0d013683bddac050c1cc4d006ff Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Sun, 13 Sep 2026 06:41:14 -0700 Subject: [PATCH 54/62] Preserve and restore the window across panicking element drop Signed-off-by: Onyeka Obi --- verifast-proofs/core/iter/adapters/README.md | 6 +- .../iter/adapters/verified/map_windows.rs | 82 +++++++++++++------ 2 files changed, 62 insertions(+), 26 deletions(-) diff --git a/verifast-proofs/core/iter/adapters/README.md b/verifast-proofs/core/iter/adapters/README.md index f1278ca8efd2a..9b4e4fc92f9e0 100644 --- a/verifast-proofs/core/iter/adapters/README.md +++ b/verifast-proofs/core/iter/adapters/README.md @@ -23,8 +23,10 @@ The two raw buffer pointer helpers also have contracts. `push` and `drop` use the standard library specification of generic drop glue. The candidate `drop` safety proof recovers field storage on both outcomes through one lifetime loan frame. This proof has not passed validation. The `push` unwind postcondition -returns the thread token only; preserving its buffer state after a panicking -element destructor remains a safe-abstraction obligation. +retains the surviving values, backing storage, and borrow token in +`push_drop_frame`, together with the old front's storage after destruction. +`finish_push_storage` must prove that those resources restore `live`, including +after a panicking element destructor. Its proof remains subject to hosted validation. ## Ownership and bounds diff --git a/verifast-proofs/core/iter/adapters/verified/map_windows.rs b/verifast-proofs/core/iter/adapters/verified/map_windows.rs index e5da742b3f8eb..eb6d8de4f9385 100644 --- a/verifast-proofs/core/iter/adapters/verified/map_windows.rs +++ b/verifast-proofs/core/iter/adapters/verified/map_windows.rs @@ -92,6 +92,56 @@ lem Buffer_send(t1: thread_id_t) close Buffer_own::(t1, buffer); } +// A destructor cannot consume ownership of the surviving window. +pred push_drop_frame(t: thread_id_t, b: *Buffer, start: usize, + values: list, matrix: *[[std::mem::MaybeUninit; N]; 2]) = + bounds(b, if start == width::() { 0 } else { start + 1 }) &*& + 0 <= start &*& start <= width::() &*& length(values) == width::() &*& + ref_mut_end_token(matrix, &(*b).buffer) &*& foreach(values, own::(t)) &*& + if start == width::() { + (matrix as *std::mem::MaybeUninit)[..width::()] |-> map(std::mem::MaybeUninit::new, values) &*& + ((matrix as *std::mem::MaybeUninit) + start + 1)[..width::() - 1] |-> ?stale + } else { + (matrix as *std::mem::MaybeUninit)[..start] |-> ?prefix &*& + ((matrix as *std::mem::MaybeUninit) + start + 1)[..width::()] |-> map(std::mem::MaybeUninit::new, values) &*& + ((matrix as *std::mem::MaybeUninit) + start + width::() + 1)[..width::() - start - 1] |-> ?suffix + }; + +// This ghost restoration is valid after either outcome of dropping the old front. +lem finish_push_storage(t: thread_id_t, b: *Buffer, start: usize, + values: list, matrix: *[[std::mem::MaybeUninit; N]; 2]) + req push_drop_frame(t, b, start, values, matrix) &*& + *(((matrix as *std::mem::MaybeUninit) + start) as *T) |-> _; + ens live(t, b, if start == width::() { 0 } else { start + 1 }, values); +{ + open push_drop_frame(t, b, start, values, matrix); + open bounds(b, if start == width::() { 0 } else { start + 1 }); + let p = matrix as *std::mem::MaybeUninit; + std::mem::close_MaybeUninit_(p + start); + if start == width::() { + close array(p + start, width::(), _); + assert (p + start)[..width::()] |-> ?suffix; + joined_window(nil, map(std::mem::MaybeUninit::new, values), suffix); + array_join(p); + } else { + close array(p + start + 1, 0, nil); + close array(p + start, 1, _); + array_join(p); + assert p[..start + 1] |-> ?prefix; + assert (p + start + width::() + 1)[..width::() - start - 1] |-> ?suffix; + joined_window(prefix, map(std::mem::MaybeUninit::new, values), suffix); + array_join(p); + array_join(p); + } + pack_matrix(matrix); + end_ref_mut(matrix); + unpack_matrix(&(*b).buffer); + array_split(base(b), if start == width::() { 0 } else { start + 1 }); + array_split(base(b) + (if start == width::() { 0 } else { start + 1 }), width::()); + close bounds(b, if start == width::() { 0 } else { start + 1 }); + close live(t, b, if start == width::() { 0 } else { start + 1 }, values); +} + // The same frame survives normal and unwinding generic drop glue. pred drop_frame(b: *Buffer, start: usize, matrix: *[[std::mem::MaybeUninit; N]; 2], k: lifetime_t, slice: *[T]) = @@ -298,7 +348,11 @@ impl Buffer { ens thread_token(t) &*& live(t, self, if start == width::() { 0 } else { start + 1 }, append(tail(values), cons(next, nil))); @*/ - //@ on_unwind_ens thread_token(t); + /*@ + on_unwind_ens thread_token(t) &*& + push_drop_frame(t, self, start, append(tail(values), cons(next, nil)), ?matrix) &*& + *(((matrix as *std::mem::MaybeUninit) + start) as *T) |-> _; + @*/ { //@ open live(t, self, start, values); //@ open bounds(self, start); @@ -383,33 +437,13 @@ impl Buffer { //@ close foreach(cons(next, nil), own::(t)); //@ foreach_append(tail(values), cons(next, nil)); //@ mapped_append::>(std::mem::MaybeUninit::new, tail(values), cons(next, nil)); + //@ close bounds(self, if start == width::() { 0 } else { start + 1 }); + //@ close push_drop_frame(t, self, start, append(tail(values), cons(next, nil)), buffer_mut_ptr as *[[std::mem::MaybeUninit; N]; 2]); //@ std::mem::open_MaybeUninit(to_drop); //@ close points_to(to_drop as *T, head(values)); //@ open own::(t)(head(values)); unsafe { ptr::drop_in_place(to_drop.cast_init()) }; - //@ std::mem::close_MaybeUninit_(to_drop); - /*@ - { - if start == width::() { - close array(to_drop, width::(), _); - close array(buffer_mut_ptr, 0, nil); - } else { - close array(to_drop + 1, 0, nil); - close array(to_drop, 1, _); - array_join(buffer_mut_ptr); - } - array_join(buffer_mut_ptr); - array_join(buffer_mut_ptr); - pack_matrix(buffer_mut_ptr as *[[std::mem::MaybeUninit; N]; 2]); - end_ref_mut_::<[[std::mem::MaybeUninit; N]; 2]>(); - unpack_matrix(&(*self).buffer); - array_split(base(self), if start == width::() { 0 } else { start + 1 }); - array_split(base(self) + (if start == width::() { 0 } else { start + 1 }), width::()); - close bounds(self, if start == width::() { 0 } else { start + 1 }); - close live(t, self, if start == width::() { 0 } else { start + 1 }, - append(tail(values), cons(next, nil))); - } - @*/ + //@ finish_push_storage(t, self, start, append(tail(values), cons(next, nil)), buffer_mut_ptr as *[[std::mem::MaybeUninit; N]; 2]); } } From a4980f78c73582b24efc3edba363fbfe6bb3b0c5 Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Sun, 13 Sep 2026 06:45:59 -0700 Subject: [PATCH 55/62] Derive shifted window bounds before borrowing the matrix Signed-off-by: Onyeka Obi --- verifast-proofs/core/iter/adapters/verified/map_windows.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/verifast-proofs/core/iter/adapters/verified/map_windows.rs b/verifast-proofs/core/iter/adapters/verified/map_windows.rs index eb6d8de4f9385..900be6014547b 100644 --- a/verifast-proofs/core/iter/adapters/verified/map_windows.rs +++ b/verifast-proofs/core/iter/adapters/verified/map_windows.rs @@ -356,6 +356,9 @@ impl Buffer { { //@ open live(t, self, start, values); //@ open bounds(self, start); + //@ open array(base(self) + start, width::(), map(std::mem::MaybeUninit::new, values)); + //@ assert pointer_within_limits(base(self) + start + 1) == true; + //@ close array(base(self) + start, width::(), map(std::mem::MaybeUninit::new, values)); //@ assert base(self)[..start] |-> ?prefix_slots; //@ assert (base(self) + start + width::())[..width::() - start] |-> ?suffix_slots; //@ joined_window(prefix_slots, map(std::mem::MaybeUninit::new, values), suffix_slots); From 3ff4cf08b0133fee94a6abbbd50a63c38f94ed83 Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Sun, 13 Sep 2026 06:53:45 -0700 Subject: [PATCH 56/62] Diagnose compiler cleanup and assertion reachability remotely Signed-off-by: Onyeka Obi --- verifast-proofs/core/iter/adapters/verify.sh | 29 ++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/verifast-proofs/core/iter/adapters/verify.sh b/verifast-proofs/core/iter/adapters/verify.sh index f1da5119ca3d2..90d24fba83e61 100644 --- a/verifast-proofs/core/iter/adapters/verify.sh +++ b/verifast-proofs/core/iter/adapters/verify.sh @@ -280,6 +280,35 @@ timeout --signal=TERM --kill-after=10s 600s \ refinement-checker --rustc-args '--edition 2024' original/lib.rs verified/lib.rs || refinement_status=$? python3 -I check_sources.py if (( proof_status != 0 || layout_status != 0 )); then + # Inspect compiler-generated cleanup when reachability diagnostics remain. + diagnostic_status=0 + timeout --signal=TERM --kill-after=5s 60s \ + rustup run nightly-2026-02-05 rustc --edition 2024 --crate-type lib \ + --emit=mir -Zmir-include-spans=yes -o "$negative_log" verified/lib.rs || diagnostic_status=$? + printf 'MIR diagnostic process status: %s\n' "$diagnostic_status" + if (( diagnostic_status == 0 )); then + python3 -I - "$negative_log" <<'PY' +from pathlib import Path +import sys + +path = Path(sys.argv[1]) +if path.stat().st_size > 8 * 1024 * 1024: + raise SystemExit("MIR diagnostic exceeded the 8 MiB parsing limit") +printing = False +budget = 24000 +for line in path.read_text().splitlines(): + if line.startswith("fn "): + printing = "::push(" in line + if printing and budget > 0: + print(line[:budget]) + budget -= len(line) + 1 +PY + fi + diagnostic_status=0 + timeout --signal=TERM --kill-after=5s 60s \ + verifast -rustc_args '--edition 2024 -C debug-assertions=no' \ + -skip_specless_fns verified/lib.rs || diagnostic_status=$? + printf 'Debug-assertions-disabled diagnostic status: %s\n' "$diagnostic_status" # These bounded, sequential diagnostics cannot replace the full proof verdict. while IFS= read -r proof_location; do diagnostic_status=0 From 10157452f5fe226d9bb4f7f45d20ade1bab4d7be Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Sun, 13 Sep 2026 07:05:44 -0700 Subject: [PATCH 57/62] Account for assertion expansion and moved-argument cleanup Signed-off-by: Onyeka Obi --- verifast-proofs/core/iter/adapters/README.md | 12 ++++++++++++ .../core/iter/adapters/verified/map_windows.rs | 9 +++++---- verifast-proofs/core/iter/adapters/verify.sh | 4 ++-- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/verifast-proofs/core/iter/adapters/README.md b/verifast-proofs/core/iter/adapters/README.md index 9b4e4fc92f9e0..0711e726562f5 100644 --- a/verifast-proofs/core/iter/adapters/README.md +++ b/verifast-proofs/core/iter/adapters/README.md @@ -54,6 +54,18 @@ does not yet own initialized `T` values, as required by the clone path. ## Source correspondence +The annotated projection spells the three window-bound `debug_assert!` calls +as `assert!`. Both refinement inputs and the full proof explicitly enable debug +assertions, so refinement must establish the equivalence of these bodies. +The conditions remain proof obligations. This avoids unreachable branches from +the `cfg!(debug_assertions)` macro expansion. + +One line-local `allow_dead_code` directive covers Rust's generated cleanup for +the `next` argument at the end of `push`. Hosted MIR inspection shows its drop +flag is cleared on both branches before the only potentially unwinding call. +An explicit `live` assertion after restoration keeps the normal completion +path subject to reachability checking. No global dead-code option is used. + `source/` contains complete, hashed copies of the two std source files. `source-map.json` records the exact lines projected into `original/`: the actual struct declarations and selected method implementations. diff --git a/verifast-proofs/core/iter/adapters/verified/map_windows.rs b/verifast-proofs/core/iter/adapters/verified/map_windows.rs index 900be6014547b..cd7b3feab2486 100644 --- a/verifast-proofs/core/iter/adapters/verified/map_windows.rs +++ b/verifast-proofs/core/iter/adapters/verified/map_windows.rs @@ -259,7 +259,7 @@ impl Buffer { //@ on_unwind_ens false; { //@ open [f]bounds(self, start); - debug_assert!(self.start + N <= 2 * N); + assert!(self.start + N <= 2 * N); // SAFETY: our invariant guarantees these elements are initialized. let buffer_ptr = unsafe { self.buffer_ptr() }; @@ -288,7 +288,7 @@ impl Buffer { //@ on_unwind_ens false; { //@ open bounds(self, start); - debug_assert!(self.start + N <= 2 * N); + assert!(self.start + N <= 2 * N); // SAFETY: our invariant guarantees these elements are in bounds. //@ open_full_borrow_strong_('a, writable_matrix(self)); @@ -371,7 +371,7 @@ impl Buffer { //@ array_split(buffer_mut_ptr + start, width::()); //@ open foreach(values, own::(t)); //@ open array(buffer_mut_ptr + start, width::(), _); - debug_assert!(self.start + N <= 2 * N); + assert!(self.start + N <= 2 * N); let to_drop = if self.start == N { // We have reached the end of our buffer and have to copy @@ -447,7 +447,8 @@ impl Buffer { //@ open own::(t)(head(values)); unsafe { ptr::drop_in_place(to_drop.cast_init()) }; //@ finish_push_storage(t, self, start, append(tail(values), cons(next, nil)), buffer_mut_ptr as *[[std::mem::MaybeUninit; N]; 2]); - } + //@ assert live(t, self, if start == width::() { 0 } else { start + 1 }, append(tail(values), cons(next, nil))); + } //~allow_dead_code // Rust emits cleanup for the already moved next argument. } impl Drop for Buffer { diff --git a/verifast-proofs/core/iter/adapters/verify.sh b/verifast-proofs/core/iter/adapters/verify.sh index 90d24fba83e61..eaf87c93636d0 100644 --- a/verifast-proofs/core/iter/adapters/verify.sh +++ b/verifast-proofs/core/iter/adapters/verify.sh @@ -274,10 +274,10 @@ timeout --signal=TERM --kill-after=10s 60s \ verifast -rustc_args '--edition 2024' backend/matrix-layout-valid.rs || layout_status=$? proof_status=0 timeout --signal=TERM --kill-after=10s 600s \ - verifast -rustc_args '--edition 2024' -skip_specless_fns verified/lib.rs || proof_status=$? + verifast -rustc_args '--edition 2024 -C debug-assertions=yes' -skip_specless_fns verified/lib.rs || proof_status=$? refinement_status=0 timeout --signal=TERM --kill-after=10s 600s \ - refinement-checker --rustc-args '--edition 2024' original/lib.rs verified/lib.rs || refinement_status=$? + refinement-checker --rustc-args '--edition 2024 -C debug-assertions=yes' original/lib.rs verified/lib.rs || refinement_status=$? python3 -I check_sources.py if (( proof_status != 0 || layout_status != 0 )); then # Inspect compiler-generated cleanup when reachability diagnostics remain. From 2adc93479a7d4513b8bb29fc217900fe1454a883 Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Sun, 13 Sep 2026 07:10:30 -0700 Subject: [PATCH 58/62] Preserve conditional assertion control flow in the proof projection Signed-off-by: Onyeka Obi --- verifast-proofs/core/iter/adapters/README.md | 11 +++++----- .../iter/adapters/verified/map_windows.rs | 21 +++++++++++++++---- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/verifast-proofs/core/iter/adapters/README.md b/verifast-proofs/core/iter/adapters/README.md index 0711e726562f5..c40b7cd41c9d3 100644 --- a/verifast-proofs/core/iter/adapters/README.md +++ b/verifast-proofs/core/iter/adapters/README.md @@ -54,11 +54,12 @@ does not yet own initialized `T` values, as required by the clone path. ## Source correspondence -The annotated projection spells the three window-bound `debug_assert!` calls -as `assert!`. Both refinement inputs and the full proof explicitly enable debug -assertions, so refinement must establish the equivalence of these bodies. -The conditions remain proof obligations. This avoids unreachable branches from -the `cfg!(debug_assertions)` macro expansion. +The annotated projection expands the three window-bound `debug_assert!` calls +into their `if cfg!(debug_assertions) { assert!(...) }` bodies. Both refinement +inputs and the full proof explicitly enable debug assertions; refinement must +establish the equivalence of these bodies. A ghost assertion proves each bounds +condition before the expansion. Line-local reachability directives account for +the disabled configuration branch and the excluded assertion failure path. One line-local `allow_dead_code` directive covers Rust's generated cleanup for the `next` argument at the end of `push`. Hosted MIR inspection shows its drop diff --git a/verifast-proofs/core/iter/adapters/verified/map_windows.rs b/verifast-proofs/core/iter/adapters/verified/map_windows.rs index cd7b3feab2486..635af827850c4 100644 --- a/verifast-proofs/core/iter/adapters/verified/map_windows.rs +++ b/verifast-proofs/core/iter/adapters/verified/map_windows.rs @@ -259,7 +259,11 @@ impl Buffer { //@ on_unwind_ens false; { //@ open [f]bounds(self, start); - assert!(self.start + N <= 2 * N); + //@ assert start + width::() <= 2 * width::(); + if cfg!(debug_assertions) { + //~allow_dead_code // The disabled configuration branch is unreachable. + assert!(self.start + N <= 2 * N); //~allow_dead_code // The proven bounds exclude assertion failure. + } // SAFETY: our invariant guarantees these elements are initialized. let buffer_ptr = unsafe { self.buffer_ptr() }; @@ -288,7 +292,11 @@ impl Buffer { //@ on_unwind_ens false; { //@ open bounds(self, start); - assert!(self.start + N <= 2 * N); + //@ assert start + width::() <= 2 * width::(); + if cfg!(debug_assertions) { + //~allow_dead_code // The disabled configuration branch is unreachable. + assert!(self.start + N <= 2 * N); //~allow_dead_code // The proven bounds exclude assertion failure. + } // SAFETY: our invariant guarantees these elements are in bounds. //@ open_full_borrow_strong_('a, writable_matrix(self)); @@ -356,6 +364,7 @@ impl Buffer { { //@ open live(t, self, start, values); //@ open bounds(self, start); + //@ let next_value = next; //@ open array(base(self) + start, width::(), map(std::mem::MaybeUninit::new, values)); //@ assert pointer_within_limits(base(self) + start + 1) == true; //@ close array(base(self) + start, width::(), map(std::mem::MaybeUninit::new, values)); @@ -371,7 +380,11 @@ impl Buffer { //@ array_split(buffer_mut_ptr + start, width::()); //@ open foreach(values, own::(t)); //@ open array(buffer_mut_ptr + start, width::(), _); - assert!(self.start + N <= 2 * N); + //@ assert start + width::() <= 2 * width::(); + if cfg!(debug_assertions) { + //~allow_dead_code // The disabled configuration branch is unreachable. + assert!(self.start + N <= 2 * N); //~allow_dead_code // The proven bounds exclude assertion failure. + } let to_drop = if self.start == N { // We have reached the end of our buffer and have to copy @@ -447,7 +460,7 @@ impl Buffer { //@ open own::(t)(head(values)); unsafe { ptr::drop_in_place(to_drop.cast_init()) }; //@ finish_push_storage(t, self, start, append(tail(values), cons(next, nil)), buffer_mut_ptr as *[[std::mem::MaybeUninit; N]; 2]); - //@ assert live(t, self, if start == width::() { 0 } else { start + 1 }, append(tail(values), cons(next, nil))); + //@ assert live(t, self, if start == width::() { 0 } else { start + 1 }, append(tail(values), cons(next_value, nil))); } //~allow_dead_code // Rust emits cleanup for the already moved next argument. } From fcec2d188bece4a0a0e97d7b563d416467e48132 Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Sun, 13 Sep 2026 07:14:57 -0700 Subject: [PATCH 59/62] Preserve reachability directives and test the normal-path guard Signed-off-by: Onyeka Obi --- verifast-proofs/core/iter/adapters/README.md | 3 +++ .../backend/ghost-reachability-invalid.rs | 10 ++++++++++ .../core/iter/adapters/verified/map_windows.rs | 12 ++++++------ verifast-proofs/core/iter/adapters/verify.sh | 16 ++++++++++++++++ 4 files changed, 35 insertions(+), 6 deletions(-) create mode 100644 verifast-proofs/core/iter/adapters/backend/ghost-reachability-invalid.rs diff --git a/verifast-proofs/core/iter/adapters/README.md b/verifast-proofs/core/iter/adapters/README.md index c40b7cd41c9d3..ee6846013ff5f 100644 --- a/verifast-proofs/core/iter/adapters/README.md +++ b/verifast-proofs/core/iter/adapters/README.md @@ -66,6 +66,9 @@ the `next` argument at the end of `push`. Hosted MIR inspection shows its drop flag is cleared on both branches before the only potentially unwinding call. An explicit `live` assertion after restoration keeps the normal completion path subject to reachability checking. No global dead-code option is used. +A mandatory negative fixture gives a function contradictory preconditions and +permits only its generated return to be unreachable. The ghost assertion must +still be rejected, guarding the normal-path check used in `push`. `source/` contains complete, hashed copies of the two std source files. `source-map.json` records the exact lines projected into `original/`: diff --git a/verifast-proofs/core/iter/adapters/backend/ghost-reachability-invalid.rs b/verifast-proofs/core/iter/adapters/backend/ghost-reachability-invalid.rs new file mode 100644 index 0000000000000..f450e13f22142 --- /dev/null +++ b/verifast-proofs/core/iter/adapters/backend/ghost-reachability-invalid.rs @@ -0,0 +1,10 @@ +#![crate_type = "lib"] +#![crate_name = "ghost_reachability_invalid"] + +unsafe fn inconsistent_input(value: bool) +//@ req value == true &*& value == false; +//@ ens true; +//@ on_unwind_ens false; +{ + //@ assert true; +} //~allow_dead_code // Allow the return only; the ghost assertion must still fail reachability. diff --git a/verifast-proofs/core/iter/adapters/verified/map_windows.rs b/verifast-proofs/core/iter/adapters/verified/map_windows.rs index 635af827850c4..ef442de48fea1 100644 --- a/verifast-proofs/core/iter/adapters/verified/map_windows.rs +++ b/verifast-proofs/core/iter/adapters/verified/map_windows.rs @@ -260,8 +260,8 @@ impl Buffer { { //@ open [f]bounds(self, start); //@ assert start + width::() <= 2 * width::(); - if cfg!(debug_assertions) { - //~allow_dead_code // The disabled configuration branch is unreachable. + #[rustfmt::skip] + if cfg!(debug_assertions) { //~allow_dead_code // The disabled configuration branch is unreachable. assert!(self.start + N <= 2 * N); //~allow_dead_code // The proven bounds exclude assertion failure. } @@ -293,8 +293,8 @@ impl Buffer { { //@ open bounds(self, start); //@ assert start + width::() <= 2 * width::(); - if cfg!(debug_assertions) { - //~allow_dead_code // The disabled configuration branch is unreachable. + #[rustfmt::skip] + if cfg!(debug_assertions) { //~allow_dead_code // The disabled configuration branch is unreachable. assert!(self.start + N <= 2 * N); //~allow_dead_code // The proven bounds exclude assertion failure. } @@ -381,8 +381,8 @@ impl Buffer { //@ open foreach(values, own::(t)); //@ open array(buffer_mut_ptr + start, width::(), _); //@ assert start + width::() <= 2 * width::(); - if cfg!(debug_assertions) { - //~allow_dead_code // The disabled configuration branch is unreachable. + #[rustfmt::skip] + if cfg!(debug_assertions) { //~allow_dead_code // The disabled configuration branch is unreachable. assert!(self.start + N <= 2 * N); //~allow_dead_code // The proven bounds exclude assertion failure. } diff --git a/verifast-proofs/core/iter/adapters/verify.sh b/verifast-proofs/core/iter/adapters/verify.sh index eaf87c93636d0..b4ff0f1e58fba 100644 --- a/verifast-proofs/core/iter/adapters/verify.sh +++ b/verifast-proofs/core/iter/adapters/verify.sh @@ -269,6 +269,22 @@ if "Cannot prove condition" not in diagnostics or "Rust frontend failed" in diag PY done +if timeout --signal=TERM --kill-after=5s 30s \ + verifast -rustc_args '--edition 2024' backend/ghost-reachability-invalid.rs >"$negative_log" 2>&1; then + echo 'An unreachable normal-path ghost assertion unexpectedly verified.' >&2 + exit 1 +fi +cat "$negative_log" +python3 -I - "$negative_log" <<'PY' +from pathlib import Path +import sys + +diagnostics = Path(sys.argv[1]).read_text() +if not any("ghost-reachability-invalid.rs(9," in line and line.endswith(": dead code") + for line in diagnostics.splitlines()): + raise SystemExit("The reachability negative check did not reject the ghost assertion") +PY + layout_status=0 timeout --signal=TERM --kill-after=10s 60s \ verifast -rustc_args '--edition 2024' backend/matrix-layout-valid.rs || layout_status=$? From 4727219c5909822027683155cb2b439da9e50830 Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Sun, 13 Sep 2026 07:18:04 -0700 Subject: [PATCH 60/62] Keep proof directives stable with method-level formatting attributes Signed-off-by: Onyeka Obi --- verifast-proofs/core/iter/adapters/verified/map_windows.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/verifast-proofs/core/iter/adapters/verified/map_windows.rs b/verifast-proofs/core/iter/adapters/verified/map_windows.rs index ef442de48fea1..7a6e484609715 100644 --- a/verifast-proofs/core/iter/adapters/verified/map_windows.rs +++ b/verifast-proofs/core/iter/adapters/verified/map_windows.rs @@ -241,6 +241,7 @@ impl Buffer { } #[inline] + #[rustfmt::skip] // Keep the line-specific VeriFast directives in place. unsafe fn as_array_ref<'a>(&'a self) -> &'a [T; N] /*@ req [?f]bounds(self, ?start) &*& [?q]lifetime_token('a) &*& @@ -260,7 +261,6 @@ impl Buffer { { //@ open [f]bounds(self, start); //@ assert start + width::() <= 2 * width::(); - #[rustfmt::skip] if cfg!(debug_assertions) { //~allow_dead_code // The disabled configuration branch is unreachable. assert!(self.start + N <= 2 * N); //~allow_dead_code // The proven bounds exclude assertion failure. } @@ -280,6 +280,7 @@ impl Buffer { } #[inline] + #[rustfmt::skip] // Keep the line-specific VeriFast directives in place. unsafe fn as_uninit_array_mut<'a>(&'a mut self) -> &'a mut MaybeUninit<[T; N]> /*@ req thread_token(?t) &*& bounds(self, ?start) &*& [?q]lifetime_token('a) &*& @@ -293,7 +294,6 @@ impl Buffer { { //@ open bounds(self, start); //@ assert start + width::() <= 2 * width::(); - #[rustfmt::skip] if cfg!(debug_assertions) { //~allow_dead_code // The disabled configuration branch is unreachable. assert!(self.start + N <= 2 * N); //~allow_dead_code // The proven bounds exclude assertion failure. } @@ -348,6 +348,7 @@ impl Buffer { /// /// All the elements will be shifted to the front end when pushing reaches /// the back end. + #[rustfmt::skip] // Keep the line-specific VeriFast directives in place. unsafe fn push(&mut self, next: T) /*@ req thread_token(?t) &*& live(t, self, ?start, ?values) &*& .own(t, next); @@ -381,7 +382,6 @@ impl Buffer { //@ open foreach(values, own::(t)); //@ open array(buffer_mut_ptr + start, width::(), _); //@ assert start + width::() <= 2 * width::(); - #[rustfmt::skip] if cfg!(debug_assertions) { //~allow_dead_code // The disabled configuration branch is unreachable. assert!(self.start + N <= 2 * N); //~allow_dead_code // The proven bounds exclude assertion failure. } From 6cc9cce69aac424a7fe6604165357921f26ac34d Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Sun, 13 Sep 2026 07:24:27 -0700 Subject: [PATCH 61/62] Keep formatting metadata inside equivalent proof bodies Signed-off-by: Onyeka Obi --- verifast-proofs/core/iter/adapters/original/lib.rs | 2 +- verifast-proofs/core/iter/adapters/verified/lib.rs | 2 +- verifast-proofs/core/iter/adapters/verified/map_windows.rs | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/verifast-proofs/core/iter/adapters/original/lib.rs b/verifast-proofs/core/iter/adapters/original/lib.rs index 5bfd29ed2f8fa..506672c09b373 100644 --- a/verifast-proofs/core/iter/adapters/original/lib.rs +++ b/verifast-proofs/core/iter/adapters/original/lib.rs @@ -1,7 +1,7 @@ #![no_std] #![crate_type = "lib"] #![allow(dead_code, unused_imports, internal_features)] -#![feature(cast_maybe_uninit, core_intrinsics, staged_api)] +#![feature(cast_maybe_uninit, core_intrinsics, staged_api, stmt_expr_attributes)] #![stable(feature = "rust1", since = "1.0.0")] extern crate core as std; diff --git a/verifast-proofs/core/iter/adapters/verified/lib.rs b/verifast-proofs/core/iter/adapters/verified/lib.rs index 5bfd29ed2f8fa..506672c09b373 100644 --- a/verifast-proofs/core/iter/adapters/verified/lib.rs +++ b/verifast-proofs/core/iter/adapters/verified/lib.rs @@ -1,7 +1,7 @@ #![no_std] #![crate_type = "lib"] #![allow(dead_code, unused_imports, internal_features)] -#![feature(cast_maybe_uninit, core_intrinsics, staged_api)] +#![feature(cast_maybe_uninit, core_intrinsics, staged_api, stmt_expr_attributes)] #![stable(feature = "rust1", since = "1.0.0")] extern crate core as std; diff --git a/verifast-proofs/core/iter/adapters/verified/map_windows.rs b/verifast-proofs/core/iter/adapters/verified/map_windows.rs index 7a6e484609715..ef442de48fea1 100644 --- a/verifast-proofs/core/iter/adapters/verified/map_windows.rs +++ b/verifast-proofs/core/iter/adapters/verified/map_windows.rs @@ -241,7 +241,6 @@ impl Buffer { } #[inline] - #[rustfmt::skip] // Keep the line-specific VeriFast directives in place. unsafe fn as_array_ref<'a>(&'a self) -> &'a [T; N] /*@ req [?f]bounds(self, ?start) &*& [?q]lifetime_token('a) &*& @@ -261,6 +260,7 @@ impl Buffer { { //@ open [f]bounds(self, start); //@ assert start + width::() <= 2 * width::(); + #[rustfmt::skip] if cfg!(debug_assertions) { //~allow_dead_code // The disabled configuration branch is unreachable. assert!(self.start + N <= 2 * N); //~allow_dead_code // The proven bounds exclude assertion failure. } @@ -280,7 +280,6 @@ impl Buffer { } #[inline] - #[rustfmt::skip] // Keep the line-specific VeriFast directives in place. unsafe fn as_uninit_array_mut<'a>(&'a mut self) -> &'a mut MaybeUninit<[T; N]> /*@ req thread_token(?t) &*& bounds(self, ?start) &*& [?q]lifetime_token('a) &*& @@ -294,6 +293,7 @@ impl Buffer { { //@ open bounds(self, start); //@ assert start + width::() <= 2 * width::(); + #[rustfmt::skip] if cfg!(debug_assertions) { //~allow_dead_code // The disabled configuration branch is unreachable. assert!(self.start + N <= 2 * N); //~allow_dead_code // The proven bounds exclude assertion failure. } @@ -348,7 +348,6 @@ impl Buffer { /// /// All the elements will be shifted to the front end when pushing reaches /// the back end. - #[rustfmt::skip] // Keep the line-specific VeriFast directives in place. unsafe fn push(&mut self, next: T) /*@ req thread_token(?t) &*& live(t, self, ?start, ?values) &*& .own(t, next); @@ -382,6 +381,7 @@ impl Buffer { //@ open foreach(values, own::(t)); //@ open array(buffer_mut_ptr + start, width::(), _); //@ assert start + width::() <= 2 * width::(); + #[rustfmt::skip] if cfg!(debug_assertions) { //~allow_dead_code // The disabled configuration branch is unreachable. assert!(self.start + N <= 2 * N); //~allow_dead_code // The proven bounds exclude assertion failure. } From 3897f7bbf0685b82e9dd94c15a19be0d7b1b1c59 Mon Sep 17 00:00:00 2001 From: Onyeka Obi Date: Sun, 13 Sep 2026 07:34:36 -0700 Subject: [PATCH 62/62] Document passing generic adapter proofs and refinement Signed-off-by: Onyeka Obi --- verifast-proofs/README.md | 6 +- verifast-proofs/core/iter/adapters/README.md | 75 ++++++++----------- .../iter/adapters/verified/map_windows.rs | 2 +- .../core/iter/adapters/verified/step_by.rs | 2 +- 4 files changed, 35 insertions(+), 50 deletions(-) diff --git a/verifast-proofs/README.md b/verifast-proofs/README.md index 6bf0716e51481..3609743607d1f 100644 --- a/verifast-proofs/README.md +++ b/verifast-proofs/README.md @@ -7,9 +7,9 @@ Specifically, it currently contains the following proofs: - Partial proof of [LinkedList](alloc/collections/linked_list.rs/) - Partial proof of [RawVec](alloc/raw_vec/mod.rs/) -A [generic iterator adapter port candidate](core/iter/adapters/) is also -available. It has not yet passed VeriFast or refinement validation and is -not included in the existing proof-coverage claims. +- Generic contracts for selected [iterator adapter methods](core/iter/adapters/), + with passing proof and source refinement checks. See its README for caller + obligations, trusted backend extensions, and remaining coverage. See each proof's accompanying README for a tour of the proof and applicable caveats. diff --git a/verifast-proofs/core/iter/adapters/README.md b/verifast-proofs/core/iter/adapters/README.md index ee6846013ff5f..f11b6eea6b664 100644 --- a/verifast-proofs/core/iter/adapters/README.md +++ b/verifast-proofs/core/iter/adapters/README.md @@ -1,11 +1,12 @@ # Generic iterator adapter proof port -This is an **unvalidated VeriFast port candidate** for part of Challenge 16. -Hosted verification passes the frontend regression checks and full adapter -source refinement. The adapter contracts still need a passing proof verdict. -See the hosted results below. -The files under `verified/` are proof inputs, not evidence of a successful -proof. This port does not yet close Felipe's review requests on PR #602. +This port proves generic contracts for part of Challenge 16. Hosted run +[34762580703](https://github.com/model-checking/verify-rust-std/actions/runs/34762580703) +at `6cc9cce69aac424a7fe6604165357921f26ac34d` passed the full adapter proof, +source refinement, source identity checks, and every backend regression gate. +Coverage is limited to the contracts below and depends on the documented +caller obligations and trusted backend extensions. Other Challenge 16 targets +and the complete safe abstraction remain open. The functions retain generic type parameters. `Buffer` also retains symbolic `N`; there are no representative element types, fixed array sizes, @@ -20,13 +21,13 @@ or unwind bounds. The target contracts are: | `Buffer::drop` | Drop exactly the initialized window and recover its storage on normal return and unwind. | The two raw buffer pointer helpers also have contracts. `push` and `drop` -use the standard library specification of generic drop glue. The candidate +use the standard library specification of generic drop glue. The `drop` safety proof recovers field storage on both outcomes through one lifetime -loan frame. This proof has not passed validation. The `push` unwind postcondition +loan frame. The `push` unwind postcondition retains the surviving values, backing storage, and borrow token in `push_drop_frame`, together with the old front's storage after destruction. -`finish_push_storage` must prove that those resources restore `live`, including -after a panicking element destructor. Its proof remains subject to hosted validation. +`finish_push_storage` proves that those resources restore `live`, including +after a panicking element destructor. ## Ownership and bounds @@ -118,19 +119,15 @@ The job has a 30-minute timeout and cancels superseded runs. Tool errors, proof failures, and resource-limit failures fail the job. Verification and refinement stay sequential and retain the per-process limits below. -Hosted run [34733601368](https://github.com/MavenRain/verify-rust-std/actions/runs/34733601368) -at commit `9e95bb7e83553d0e47035821eaecf4be030cab6b` passed the source checks -and their nine tests, then failed before proof checking. The Rust MIR exporter -encountered `AddUnchecked` in `StepBy::original_step` and panicked with -`not yet implemented` at `src/lib.rs:2455`. The earlier missing -`cast_maybe_uninit` feature flag was fixed in that commit. - -The [25.11 exporter](https://github.com/verifast/verifast/blob/25.11/src/rust_frontend/vf_mir_exporter/src/lib.rs#L2436-L2455) -has no `AddUnchecked` case. Static inspection of the -[26.01 exporter](https://github.com/verifast/verifast/blob/26.01/src/rust_frontend/vf_mir_exporter/src/lib.rs#L2445-L2464) -found the same omission, so that release alone does not address this failure. -That original run established neither the contracts nor source refinement. -The current runner uses VeriFast 26.09 plus the narrow frontend patch in +Run [34762580703](https://github.com/model-checking/verify-rust-std/actions/runs/34762580703) +verified 433 statements in the full adapter proof and 149 in the standalone +matrix-layout fixture on `x86_64-unknown-linux-gnu`. The full refinement checker +accepted the annotated implementations, and the source checker confirmed their +original projections against the current std snapshots. All positive and +negative backend fixtures passed, including the reachability guard. The source +checker also passed its nine tests. No compiler or verifier was run locally. + +The runner uses VeriFast 26.09 plus the narrow frontend patch in `backend/add-unchecked.patch`. It maps `AddUnchecked` to the existing integer addition operation, whose symbolic execution checks overflow. On inputs where no overflow occurs, unchecked addition has the same result. Outside @@ -139,22 +136,17 @@ Before checking the adapters, the runner requires a valid successor proof and an explicit overflow diagnostic for the same operation without its range precondition. A frontend crash does not count as the negative result. -The unchecked-add regression passed in hosted run -[34744234144](https://github.com/MavenRain/verify-rust-std/actions/runs/34744234144). -That run then reached the translator's unsupported const-parameter check. `backend/const-generics.patch` adds symbolic `usize` const arguments and array lengths using the existing `typeid`/`usize_of_const` representation. It keeps const parameters distinct from Rust types and does not add `Sized` bounds to them. The exporter rejects other const parameter types. A positive symbolic-length proof and an incorrect-length negative test must pass before -the adapter contracts are checked. Both passed in hosted run -[34745790943](https://github.com/model-checking/verify-rust-std/actions/runs/34745790943). +the adapter contracts are checked. The same patch routes array ownership and borrowing through VeriFast's existing type predicates. It also removes the upstream frontend's blanket -shortcut for mutable-reference creation. Hosted run -[34748227762](https://github.com/model-checking/verify-rust-std/actions/runs/34748227762) -passed every backend regression gate: valid arithmetic, symbolic width, +shortcut for mutable-reference creation. Mandatory regression gates cover +valid arithmetic, symbolic width, shared array reborrowing, and mutable array reference creation; rejection of overflow, a wrong width, missing shared ownership, and missing mutable storage. The mutable test converts the new reference to a raw pointer to @@ -170,11 +162,8 @@ initialized storage before checking the adapters. The patch also preserves const operands and `ConstArgHasType` constraints through the MIR schema and refinement checker. Constraints are compared after generic parameter renaming; unsupported predicates still fail refinement. -Hosted run [34749576406](https://github.com/model-checking/verify-rust-std/actions/runs/34749576406) -accepted a renamed const parameter and rejected changing the return from -`N` to `M`. Run [34749771017](https://github.com/model-checking/verify-rust-std/actions/runs/34749771017) -then passed refinement of the full adapter projection. Its contract proof -failed, so the workflow correctly remained unsuccessful. +The refinement fixtures require acceptance of a renamed const parameter and +rejection of a change in the returned value from `N` to `M`. `backend/prepare.sh` pins the source commit, source archive hash, and upstream dependency bundle hash. It builds the MIR exporter, verifier, and refinement @@ -254,21 +243,17 @@ The runner does not suppress unwind paths, overflow checks, or reference creation checks, and does not allow assumed proof obligations. The existing VeriFast workflow is unchanged. The separate adapter job checks -this candidate without altering the existing LinkedList and RawVec checks. +these contracts without altering the existing LinkedList and RawVec checks. ## Remaining work -- Obtain verifier and refinement verdicts for the adapters, including symbolic const - sizes, `NonZero::new_unchecked`, reference creation, and generic slice drop glue. - The lifetime loans in `drop` must connect element storage to the slice - contract and recover storage on both outcomes. There is no adapter-specific - borrowing or drop axiom. -- Complete the safe-abstraction and unwind-state obligations described above. +- Prove constructor preservation and establish the accessor borrows from the + full safe abstraction, including the surrounding `MapWindows` implementation. - Port the remaining Challenge 16 targets, including arbitrary-length filter, filter-map, and zip iteration. This package has no proof of those loops. - Reconcile the real nonempty-source `next_chunk::<0>()` defect in filter and filter-map. The `MapWindows` constructor's exclusion of zero-sized windows does not justify excluding that separate, valid filter input domain. -Existing Kani results remain bounded where documented. Existing PR CI at -`4459557` predates this port and does not validate it. +Existing Kani results remain bounded where documented. The successful VeriFast +run establishes only the selected contracts, not completion of Challenge 16. diff --git a/verifast-proofs/core/iter/adapters/verified/map_windows.rs b/verifast-proofs/core/iter/adapters/verified/map_windows.rs index ef442de48fea1..9b992de59336b 100644 --- a/verifast-proofs/core/iter/adapters/verified/map_windows.rs +++ b/verifast-proofs/core/iter/adapters/verified/map_windows.rs @@ -1,4 +1,4 @@ -// Generic proof candidate. See ../README.md for validation status and scope. +// Generic contract proof. See ../README.md for validation evidence and scope. use crate::mem::MaybeUninit; use crate::{fmt, ptr}; diff --git a/verifast-proofs/core/iter/adapters/verified/step_by.rs b/verifast-proofs/core/iter/adapters/verified/step_by.rs index d93343ad14b2d..bfbf327ddc227 100644 --- a/verifast-proofs/core/iter/adapters/verified/step_by.rs +++ b/verifast-proofs/core/iter/adapters/verified/step_by.rs @@ -1,4 +1,4 @@ -// Generic proof candidate. See ../README.md for validation status and scope. +// Generic contract proof. See ../README.md for validation evidence and scope. use crate::intrinsics; use crate::num::NonZero;