Allow zero-sized offsets on dangling pointers in ptr contracts - #625
Allow zero-sized offsets on dangling pointers in ptr contracts#625tautschnig wants to merge 1 commit into
Conversation
The requires/ensures clauses of the pointer arithmetic operations
(<*mut T>::{offset,add,sub}, <*const T>::{offset,add,sub},
NonNull::{add,sub}, NonNull::offset_from_unsigned) demanded
same_allocation unconditionally (modulo a ZST escape), although the
documented semantics explicitly permit zero-sized offsets on any
pointer, including dangling ones: "The computed offset, count *
size_of::<T>() bytes, must not overflow isize" and only "if the
computed offset is non-zero, then self must be derived from a pointer
to some allocated object".
The stricter-than-documented clauses are violated by legitimate std
code: slices are allowed to be backed by dangling pointers when empty
(e.g. slice::from_raw_parts(ptr, 0) for arbitrary aligned non-null
ptr), and Iter::new then computes ptr.add(0), and len() computes
end.offset_from_unsigned(begin) on two equal dangling pointers. With
dependency contracts asserted (the Kani default since
model-checking/kani#3802), the slice::iter::verify::verify_tup
harnesses fail on these clauses; evaluating same_allocation on an
allocation-less pointer is additionally a Kani unsupported construct
("Kani does not support reasoning about pointer to unallocated
memory"). The CI configuration currently masks this with
--no-assert-contracts.
Add the documented escape hatches: `count == 0 ||` ahead of the
same-allocation disjunct of offset/add/sub requires and ensures
(matching the precedent already present in NonNull::offset), and an
equal-address escape in NonNull::offset_from_unsigned (matching the
precedent in <*const T>::offset_from).
The unconditional clauses were introduced with the original contracts
in 014965a ("Contracts and Harnesses for `<*mut T>::add`, `sub` and
`offset`" model-checking#113), 688b15b ("Contracts & Harnesses for
`non_null::sub` and `non_null::sub_ptr` and `non_null::offset_from`"
model-checking#93) and siblings.
Verified (Kani 152c6a8c + CBMC 6.10.0):
* slice::iter::verify::verify_tup::{check_next_back_unchecked,
check_advance_back_by} now pass with contracts asserted;
* all 265 proof harnesses matching non_null_check_{add,sub,
offset_from_unsigned} and ptr::verify::check_{mut,const}_{add,sub,
offset} pass both with and without --no-assert-contracts.
Co-authored-by: Kiro <kiro-agent@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR updates the formal contracts on core pointer-arithmetic APIs to match the documented Rust semantics that zero-sized offsets are permitted even on dangling pointers, avoiding unnecessary same_allocation requirements that break valid std patterns (e.g., empty slices backed by dangling-but-aligned pointers) when dependency contracts are asserted.
Changes:
- Relax
#[requires]/#[ensures]for*mut T::{offset,add,sub}and*const T::{offset,add,sub}by short-circuiting allocation checks whencount == 0(and preserving the existing ZST escape). - Relax
NonNull::{add,sub}similarly with acount == 0 || size_of::<T>() == 0escape beforesame_allocation. - Relax
NonNull::offset_from_unsignedby allowing equal-address pointers to bypasssame_allocation(zero-sized span), matching the precedent in raw-pointeroffset_from.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| library/core/src/ptr/non_null.rs | Weakens NonNull pointer arithmetic contracts to allow zero offsets / equal-address spans without requiring same_allocation. |
| library/core/src/ptr/mut_ptr.rs | Weakens *mut T offset/add/sub contracts to skip allocation checks when count == 0. |
| library/core/src/ptr/const_ptr.rs | Weakens *const T offset/add/sub contracts to skip allocation checks when count == 0. |
| (self.as_ptr().addr()) >= (subtracted.as_ptr().addr()) && | ||
| (self.as_ptr().addr() - subtracted.as_ptr().addr()) % core::mem::size_of::<T>() == 0 |
feliperodri
left a comment
There was a problem hiding this comment.
Static review (no local Kani run this time). Adds a count == 0 (plus the existing size_of::<T>() == 0) short-circuit to the same-allocation pre/postconditions of offset/add/sub across const_ptr, mut_ptr and NonNull, and an equal-address short-circuit to NonNull::offset_from. This is a sound precondition relaxation: zero-sized offsets and zero-length spans are always valid per the pointer-method docs, including on dangling pointers such as empty-slice pointers, so the clauses previously rejected inputs that are actually UB-free. The change is identical across the three impls; the mut_ptr and NonNull versions are directly exercised by proof_for_contract.
Minor note: const_ptr's offset/add/sub have no direct proof_for_contract (pre-existing — only offset_from/byte_offset do), so those particular clauses are exercised only transitively / once dependency contracts are asserted; the change is nonetheless sound by inspection and matches the directly-verified mut_ptr/NonNull versions. No concerns.
The requires/ensures clauses of the pointer arithmetic operations (
<*mut T>::{offset,add,sub},<*const T>::{offset,add,sub},NonNull::{add,sub},NonNull::offset_from_unsigned) demandsame_allocationunconditionally (modulo a ZST escape), although the documented semantics explicitly permit zero-sized offsets on any pointer, including dangling ones: only "if the computed offset is non-zero, then self must be derived from a pointer to some allocated object".The stricter-than-documented clauses are violated by legitimate std code: empty slices may be backed by dangling pointers (
slice::from_raw_parts(ptr, 0)for arbitrary aligned non-nullptr— exactly whatslice::iter's ownany_slicehelper generates), whereuponIter::newcomputesptr.add(0)andlen()computesend.offset_from_unsigned(begin)on two equal dangling pointers. With dependency contracts asserted (the Kani default since model-checking/kani#3802), theslice::iter::verify::verify_tupharnesses fail on these clauses — and evaluatingsame_allocationon an allocation-less pointer is additionally a Kani unsupported construct ("Kani does not support reasoning about pointer to unallocated memory"). CI currently masks this via--no-assert-contracts.This PR adds the documented escape hatches:
count == 0 ||ahead of the same-allocation disjunct in offset/add/sub requires and ensures — matching the precedent already present inNonNull::offset— and an equal-address escape inNonNull::offset_from_unsigned, matching the precedent in<*const T>::offset_from.Blame: the unconditional clauses date back to the original contract PRs #113 (014965a) and #93 (688b15b) and siblings.
Verified with Kani 152c6a8c + CBMC 6.10.0:
slice::iter::verify::verify_tup::{check_next_back_unchecked,check_advance_back_by}now pass with contracts asserted;non_null_check_{add,sub,offset_from_unsigned}andptr::verify::check_{mut,const}_{add,sub,offset}pass both with and without--no-assert-contracts.Together with #622, #623, #624 and model-checking/kani#4709/rust-lang#4710, this resolves all verdict differences found on a 125-harness sample when running without
--no-assert-contracts, exceptnon_null_check_from_raw_part_trait(Kani's "unstable vtable comparison 'Eq'" limitation, reached byas_ptr's postcondition on adyn Traitpointee — tracked separately).By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 and MIT licenses.