Skip to content

Fix OffsetModel missing UB for offsets wrapping CBMC's pointer encoding - #4671

Merged
feliperodri merged 2 commits into
model-checking:mainfrom
tautschnig:fix-offset-model-wrap
Aug 14, 2026
Merged

Fix OffsetModel missing UB for offsets wrapping CBMC's pointer encoding#4671
feliperodri merged 2 commits into
model-checking:mainfrom
tautschnig:fix-offset-model-wrap

Conversation

@tautschnig

Copy link
Copy Markdown
Member

CBMC encodes pointers as an (object, offset) pair where the offset field has pointer_width - object_bits bits. Pointer arithmetic therefore wraps offsets at 2^(64 - object_bits) rather than 2^64 (#1150). For the OffsetModel this meant that a symbolic byte offset that is a non-zero multiple of 2^(64 - object_bits) (2^48 with Kani's default of 16 object bits) made wrapping_byte_offset alias the original pointer, so the subsequent same-allocation safety check passed spuriously and genuine out-of-bounds offset calls verified successfully - including "proving" the false fact that the result equals the base pointer.

Detect the wrap by additionally requiring that the address of the result is consistent with full-width integer arithmetic on the address of the original pointer; any truncation in the pointer addition falsifies this equation. Two alternative formulations do not work:

  • Computing the new pointer via integer arithmetic (addr().wrapping_add_signed(off) followed by a cast back, as previously suggested in a FIXME here) is sound, but makes the solver case-split the integer address over every object; harnesses using e.g. sort() then run out of memory. These are the "unexpected failures" mentioned in the removed comment (reproduced on: expected/cover/cover-pass, expected/string-repeat, expected/loop-backedge, expected/shadow/unsupported_num_objects).

  • Comparing POINTER_OFFSET(new) against full-width POINTER_OFFSET(orig) + offset is defeated by CBMC's simplifier, which rewrites POINTER_OFFSET(p + k) to POINTER_OFFSET(p) + k in full-width arithmetic, making the equation trivially true.

The addr() transmute is opaque to that simplification, keeps all operations in the pointer domain, and adds negligible solver cost: the full expected (459) and kani (593) suites pass unchanged.

The underlying encoding wrap is fixed on the CBMC side in diffblue/cbmc#9134 (out-of-range results are directed to the invalid object). The equation here also states Rust's address-arithmetic contract for offset and produces the same verdicts with and without that CBMC fix, so it is kept as a semantic guard rather than a version-specific workaround.

Partially addresses #1150: this fixes the missed-UB soundness hole in the checked offset/arith_offset intrinsic model. Not addressed is the wrapping_offset family, which Kani codegens to plain CBMC pointer addition: under CBMC versions without the encoding fix its result aliases the base pointer for wrap-multiple offsets (tests/kani/PointerOffset/fixme_wrap_nonzero_offset.rs), and even with the CBMC fix the result address is indeterminate rather than the exactly-wrapped address that Rust defines. Exact modeling would require integer arithmetic plus an integer-to-pointer cast, which makes the solver case-split over all objects (see above).

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

CBMC encodes pointers as an (object, offset) pair where the offset
field has pointer_width - object_bits bits. Pointer arithmetic
therefore wraps offsets at 2^(64 - object_bits) rather than 2^64
(model-checking#1150). For the
OffsetModel this meant that a symbolic byte offset that is a non-zero
multiple of 2^(64 - object_bits) (2^48 with Kani's default of 16
object bits) made `wrapping_byte_offset` alias the original pointer,
so the subsequent same-allocation safety check passed spuriously and
genuine out-of-bounds `offset` calls verified successfully - including
"proving" the false fact that the result equals the base pointer.

Detect the wrap by additionally requiring that the address of the
result is consistent with full-width integer arithmetic on the address
of the original pointer; any truncation in the pointer addition
falsifies this equation. Two alternative formulations do not work:

* Computing the new pointer via integer arithmetic
  (addr().wrapping_add_signed(off) followed by a cast back, as
  previously suggested in a FIXME here) is sound, but makes the solver
  case-split the integer address over every object; harnesses using
  e.g. `sort()` then run out of memory. These are the "unexpected
  failures" mentioned in the removed comment (reproduced on:
  expected/cover/cover-pass, expected/string-repeat,
  expected/loop-backedge, expected/shadow/unsupported_num_objects).

* Comparing POINTER_OFFSET(new) against full-width
  POINTER_OFFSET(orig) + offset is defeated by CBMC's simplifier,
  which rewrites POINTER_OFFSET(p + k) to POINTER_OFFSET(p) + k in
  full-width arithmetic, making the equation trivially true.

The `addr()` transmute is opaque to that simplification, keeps all
operations in the pointer domain, and adds negligible solver cost:
the full expected (459) and kani (593) suites pass unchanged.

The underlying encoding wrap is fixed on the CBMC side in
diffblue/cbmc#9134 (out-of-range results are
directed to the invalid object). The equation here also states Rust's
address-arithmetic contract for `offset` and produces the same verdicts
with and without that CBMC fix, so it is kept as a semantic guard
rather than a version-specific workaround.

Partially addresses model-checking#1150: this fixes the missed-UB soundness hole in
the checked `offset`/`arith_offset` intrinsic model. Not addressed is
the `wrapping_offset` family, which Kani codegens to plain CBMC pointer
addition: under CBMC versions without the encoding fix its result
aliases the base pointer for wrap-multiple offsets
(tests/kani/PointerOffset/fixme_wrap_nonzero_offset.rs), and even with
the CBMC fix the result address is indeterminate rather than the
exactly-wrapped address that Rust defines. Exact modeling would require
integer arithmetic plus an integer-to-pointer cast, which makes the
solver case-split over all objects (see above).

Co-authored-by: Kiro <kiro-agent@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 24, 2026 18:50
@tautschnig
tautschnig requested a review from a team as a code owner July 24, 2026 18:50
@github-actions github-actions Bot added Z-EndToEndBenchCI Tag a PR to run benchmark CI Z-CompilerBenchCI Tag a PR to run benchmark CI labels Jul 24, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This pull request strengthens Kani’s OffsetModel safety checking to detect and reject undefined behavior that was previously missed due to CBMC’s pointer encoding truncating/wrapping the offset field (issue #1150), and adds a regression test to ensure the soundness hole stays closed across CBMC versions.

Changes:

  • Add an address-consistency guard (addr()-based) to detect offset truncation/wrap in CBMC’s pointer encoding before relying on same-allocation checks.
  • Add a new expected-suite regression test covering symbolic high-offset wrap cases (positive, negative, and isize::MIN), plus an in-bounds control harness.
  • Add corresponding .expected output asserting the new failures are reported.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
tests/expected/offset-bounds-check/offset_wrap_sym.rs New regression harnesses exercising the high-offset wrap soundness hole and a valid in-bounds negative-offset case.
tests/expected/offset-bounds-check/offset_wrap_sym.expected Expected output for the new regression tests (3 failures, 1 success).
library/kani_core/src/models.rs Tightens OffsetModel UB detection by adding an address-arithmetic consistency check alongside same-allocation checking.

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

Comment thread library/kani_core/src/models.rs Outdated
Comment thread library/kani_core/src/models.rs

@feliperodri feliperodri left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'd implement the Copilot comments before merging it.

…checks

- Replace `orig_ptr.addr().wrapping_add(byte_offset as usize)` with
  `wrapping_add_signed(byte_offset)` so the signed-cast dependence is
  spelled out in the primitive rather than left implicit in a two's-
  complement cast (byte_offset is isize).
- Split the conjoined safety check into two separate `safety_check`s
  with distinct messages, so a wrap-arithmetic failure is no longer
  reported as a same-allocation escape. Update the affected
  .expected outputs: offset_wrap_sym (all three wrap harnesses),
  out_of_bounds_ub_check (both harnesses were wrap cases in disguise
  because CBMC encodes negative offsets by adding 2^offset_bits), and
  start_from_oob (the return-to-original-object case is also detected
  by the address-arithmetic mismatch, not by same-allocation).

Signed-off-by: Felipe Monteiro <felisous@amazon.com>
@feliperodri
feliperodri added this pull request to the merge queue Aug 13, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Aug 13, 2026
@feliperodri
feliperodri added this pull request to the merge queue Aug 13, 2026
Merged via the queue into model-checking:main with commit fb037f3 Aug 14, 2026
33 of 37 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Z-CompilerBenchCI Tag a PR to run benchmark CI Z-EndToEndBenchCI Tag a PR to run benchmark CI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants