Skip to content

Override std::ptr::align_offset - #2396

Open
tautschnig wants to merge 12 commits into
model-checking:mainfrom
tautschnig:align_offset
Open

Override std::ptr::align_offset#2396
tautschnig wants to merge 12 commits into
model-checking:mainfrom
tautschnig:align_offset

Conversation

@tautschnig

@tautschnig tautschnig commented Apr 20, 2023

Copy link
Copy Markdown
Member

This hook intercepts calls to std::ptr::align_offset<T> as CBMC's memory model has no concept of alignment of allocations, so we would have to non-deterministically choose an alignment of the base pointer, add the pointer's offset to it, and then do the math that is done in library/core/src/ptr/mod.rs. Instead, we choose to always return either 0 when the pointer points to the beginning of an object (and, therefore, is necessarily aligned), or usize::MAX, per align_offset's documentation, which states: "It is permissible for the implementation to always return usize::MAX. Only your algorithm’s performance can depend on getting a usable offset here, not its correctness."

Towards: #2363

@tautschnig

Copy link
Copy Markdown
Member Author

Perhaps @karkhaz:

  1. The alleged performance regression (https://github.com/model-checking/kani/actions/runs/4753944159/jobs/8446153448?pr=2396) is the same as previously reported upon a merge into main: https://github.com/model-checking/kani/actions/runs/4747428362/jobs/8432323295.
  2. Would benchcomp also tell us about any performance improvements?

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

Is there any potential unsoundness with this change?

Comment thread kani-compiler/src/codegen_cprover_gotoc/overrides/hooks.rs Outdated

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

I don't think this implementation is sound. The code should panic if the input is not a power of two.
For example, the following code panic in Rust:

use std::mem::align_of;

fn main() {
  let x = 10;
  let ptr = &x as *const i32;
  let _ = ptr.align_offset(5);
}

@tautschnig tautschnig self-assigned this Apr 27, 2023
@carolynzech
carolynzech marked this pull request as draft April 7, 2025 17:06
@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 Jun 27, 2025
@tautschnig
tautschnig dismissed celinval’s stale review June 27, 2025 12:39

Requested changes have been implemented.

@tautschnig
tautschnig marked this pull request as ready for review June 27, 2025 13:16
@tautschnig tautschnig assigned zhassan-aws and unassigned tautschnig Jun 27, 2025
This hook intercepts calls to `std::ptr::align_offset<T>` as CBMC's memory
model has no concept of alignment of allocations, so we would have to
non-deterministically choose an alignment of the base pointer, add the
pointer's offset to it, and then do the math that is done in
`library/core/src/ptr/mod.rs`. Instead, we choose to always return
`usize::MAX`, per `align_offset`'s documentation, which states: "It is
permissible for the implementation to always return usize::MAX. Only your
algorithm’s performance can depend on getting a usable offset here, not its
correctness."

Fixes: model-checking#2363
@feliperodri feliperodri assigned tautschnig and unassigned zhassan-aws Jul 2, 2026
Copilot AI lite review requested due to automatic review settings August 5, 2026 20:22

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 PR introduces a new gotoc hook to override std::ptr::align_offset during codegen, motivated by CBMC’s lack of allocation-alignment modeling and the performance impact this has on symbolic execution (Issue #2363). It also adds regression tests to exercise the override and to ensure the motivating performance scenario is covered.

Changes:

  • Add a new GotocHook (AlignOffset) that intercepts std::ptr::align_offset::<T> calls in gotoc codegen.
  • Add a regression test for Issue #2363 based on string splitting/collection.
  • Add a std-override test intended to confirm the align_offset hook is being used.

Reviewed changes

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

File Description
kani-compiler/src/codegen_cprover_gotoc/overrides/hooks.rs Adds the AlignOffset hook and registers it in fn_hooks().
tests/kani/Strings/2363.rs Adds a regression/performance-oriented harness reproducing the reported slowdown scenario.
tests/kani/StdOverrides/align_offset.rs Adds a test to validate that Kani’s align_offset override is active.

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

Comment thread kani-compiler/src/codegen_cprover_gotoc/overrides/hooks.rs Outdated
Comment thread tests/kani/StdOverrides/align_offset.rs Outdated
Mechanical repairs so CI reports the one question that actually needs
deciding rather than three that do not.

- `hooks.rs` lost its trailing newline, which fails the format check at the
  very first step of `kani-regression.sh`. That is why all five `regression`
  jobs and `llbc-regression` were red: they died before running a test.
- `ptr` became unused when the hook stopped inspecting the pointer, failing
  `clippy-check` with `-D warnings`.
- `tests/kani/StdOverrides/align_offset.rs` still asserted that a pointer to
  the start of an object yields `0`, while the hook now returns `usize::MAX`
  unconditionally, so the test contradicted the implementation it tests. It
  now expects `usize::MAX` for both pointers, which is also what
  demonstrates the hook is in use: Rust's own implementation returns `0` for
  both when the requested alignment is 1.
- `hook_applies` now matches `core::ptr::align_offset` as well as
  `std::ptr::align_offset`. Kani renders the path as `std::ptr` for an
  ordinary crate, but a `no_std` crate sees `core::ptr` and the hook would
  silently not apply. This is the robustness half of the request to cover
  `core::ptr::align_offset`.

Also merges current `main`, which the branch was behind.

Both added tests pass locally, and `tests/kani/Strings/2363.rs` completes in
1.6s, which is the performance improvement this hook exists to deliver.

What this does NOT fix is the `perf` failure, which is a semantic question
rather than a build problem. See the PR discussion.
…e::MAX

Returning `usize::MAX` unconditionally is sound but reports failures in
correct code. `debug_assert_eq!(p.align_offset(a), 0)` is a pattern real
crates use -- aws/s2n-quic does it in
`quic/s2n-quic-platform/src/message/cmsg/decode.rs` -- and Kani honours
debug assertions, so the perf suite began failing with

    Failed Checks: "contents must be aligned to cmsghdr"

on code that is provably fine. Reduced to a standalone case: a
`#[repr(align(8))]` buffer asserting `align_offset(align_of::<u64>()) == 0`
fails with the hook and passes without it, which I confirmed by toggling
only the hook registration on one build. CI agrees: `perf` passed on the
commit before the switch to unconditional `usize::MAX` and failed on it.

The hook now reports aligned when `__CPROVER_pointer_offset(ptr)` is a
multiple of `align`, and `usize::MAX` otherwise, which the documentation
explicitly permits. The offset is the only part of an address CBMC's memory
model represents, so this is what the real implementation concludes under
that model -- a shortcut rather than a change in behaviour, which is the
whole point of the hook.

The caveat is recorded on the hook: it reports "aligned" for a pointer whose
object base is less aligned than `align`. CBMC cannot represent that either
way, so the real implementation does not discover it under this model
either. That trade-off is worth making explicit rather than silently
choosing the alternative that produces false positives.

The test now distinguishes the hook from the real implementation, which the
previous version could not: with alignment 1 every pointer is aligned, so
both branches returned the same value either way. It checks offset 0 against
alignment 4 (aligned, 0) and offset 1 against alignment 4, where the hook
answers `usize::MAX` and the real implementation would answer 3.

Verified locally: the whole `kani` suite (602 passed, 0 failed), the
`Strings/2363.rs` performance test in 1.8s, and the s2n-quic pattern above.
The `expected` suite shows 182 passed with one failure,
`shadow/unsupported_num_objects`, which fails identically without this
change -- it counts CBMC object IDs and my local CBMC is 6.8.0 against the
pinned 6.10.0.
@feliperodri

Copy link
Copy Markdown
Member

I've pushed two commits here to get this unstuck (2daf127, 8fa15a3). @tautschnig, the second one changes the semantics of your hook, the reasoning is below and in the commit message.

The mechanical failures (2daf127)

Three of the four red jobs had nothing to do with the feature:

  • hooks.rs had lost its trailing newline. That fails the format check, which is the first step of kani-regression.sh — so all five regression jobs and llbc-regression died before running a single test. One missing byte was taking out six jobs.
  • ptr became unused when the hook stopped inspecting the pointer, failing clippy-check under -D warnings.
  • The test contradicted the implementation. tests/kani/StdOverrides/align_offset.rs still asserted 0 for a pointer at an object's base while the hook had been changed to return usize::MAX unconditionally.

I also merged current main (the branch was behind), and made hook_applies match core::ptr::align_offset::< as well as std::ptr::align_offset::<. Kani renders the path as std::ptr for an ordinary crate, but a no_std crate sees core::ptr and the hook would silently not apply, that's the durable half of @zhassan-aws's original request.

The real blocker (8fa15a3)

The last commit before mine switched the hook to return usize::MAX unconditionally. That's sound, but it makes Kani report failures in correct code, and it's what broke perf:

Failed Checks: "contents must be aligned to cmsghdr"
 File: "quic/s2n-quic-platform/src/message/cmsg/decode.rs", line 136

s2n-quic does this:

debug_assert_eq!(
    cursor.align_offset(align_of::<cmsghdr>()),
    0,
    "contents must be aligned to cmsghdr"
);

The documentation does say callers must not depend on a usable offset for correctness, but real crates depend on it in debug assertions, and Kani honours those. Reduced to a standalone case, a buffer that is provably aligned:

#[repr(align(8))]
struct Aligned([u8; 32]);

#[kani::proof]
fn asserts_alignment_of_an_aligned_buffer() {
    let buf = Aligned([0u8; 32]);
    let cursor = buf.0.as_ptr();
    assert_eq!(cursor.align_offset(align_of::<u64>()), 0);
}

This fails with the hook and passes without it. I confirmed causation by toggling only the hook registration on one build. CI's own history agrees: perf passed on 32e0731 (the merge commit, with the original offset-based behaviour) and failed on 50f4a37 (the switch to unconditional usize::MAX).

So the hook now reports aligned when __CPROVER_pointer_offset(ptr) is a multiple of align, and usize::MAX otherwise, which align_offset explicitly permits. The offset is the only part of an address CBMC's memory model represents, so this is what the real implementation concludes under that model. That makes it a shortcut rather than a change in behaviour, which is the point of the hook.

The caveat is documented on the hook rather than papered over: it reports "aligned" for a pointer whose object base is less aligned than align. CBMC cannot represent that either way, so the real implementation does not discover it under this model either. @tautschnig, you know that model well, so if you think that's the wrong trade, the alternatives are to model alignment properly with a non-deterministic base (which defeats the performance purpose, and would make any
alignment assertion fail anyway) or to accept the false positives and adjust the s2n-quic expectations.

Test

The previous test could not actually detect the hook: with alignment 1 every pointer is aligned, so the hook and the real implementation agree. It now checks offset 0 against alignment 4 (aligned, so 0) and offset 1 against alignment 4, where the hook answers usize::MAX and the real implementation would answer 3.

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

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

Suppressed comments (2)

kani-compiler/src/codegen_cprover_gotoc/overrides/hooks.rs:1317

  • usize::MAX is taken from the host compiler and may not match the target machine model’s usize width (e.g., 32-bit targets). Prefer deriving the max value from place_expr’s type using the MachineModel so the hook remains target-width correct.
        let rhs = is_aligned.ternary(
            Expr::int_constant(0, place_expr.typ().clone()),
            Expr::int_constant(usize::MAX, place_expr.typ().clone()),
        );

tests/kani/StdOverrides/align_offset.rs:11

  • This test assumes x.as_ptr() is 4-byte aligned, but [u8; 8] only guarantees 1-byte alignment in Rust. On targets where the stack/placement isn’t 4-aligned, base_ptr.align_offset(4) may legitimately be non-zero and the test would fail. Consider explicitly aligning the allocation to 4 so the assertion is portable and the comment is accurate.
    let x = [10u8; 8];
    let base_ptr = x.as_ptr();
    // Offset 0 within the object is a multiple of any alignment, so this is already aligned.
    assert_eq!(base_ptr.align_offset(4), 0);

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.

5 participants