Skip to content

Rust unable to compare two numbers in --release mode (miscompile?) #141021

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
ThomasHabets opened this issue May 15, 2025 · 11 comments
Closed

Rust unable to compare two numbers in --release mode (miscompile?) #141021

ThomasHabets opened this issue May 15, 2025 · 11 comments
Labels
C-discussion Category: Discussion or questions that doesn't represent real issues.

Comments

@ThomasHabets
Copy link

ThomasHabets commented May 15, 2025

In release mode assert_eq!() fails on two numbers that in fact are the same.

I tried this code

I have a case where I create two entangled particles. They have some shared Arc unknown state. If you ask the state, or each particle for their id, they will all reply with the same number, generated by the entangled state.

use std::sync::Arc;
struct Unknown {
}
impl Unknown {
    fn id(&self) -> u64 {
        &self as *const _ as u64
    }
}
struct Particle {
    s: Arc<Unknown>,
}
impl Particle {
    fn id(&self) -> u64 {
        self.s.id()
    }
}
fn create() -> (Particle, Particle) {
    let s = Arc::new(Unknown{});
    (Particle{s: s.clone()}, Particle{s})
}

fn main() {
    let (left, right) = create();
    assert_eq!(left.id(), right.id());
}

I expected to see this happen

The assert_eq! to succeed, since the numbers are definitely the same.

Instead, this happened

Works in debug build, asserts in release build:

$ cargo run --verbose
       Dirty bad v0.1.0 (/home/thomas/tmp/bad): the file `src/main.rs` has changed (1747289904.616906220s, 3m 43s after last build at 1747289681.264659271s)
   Compiling bad v0.1.0 (/home/thomas/tmp/bad)
     Running `/home/thomas/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/bin/rustc --crate-name bad --edition=2024 src/main.rs --error-format=json --json=diagnostic-rendered-ansi,artifacts,future-incompat --diagnostic-width=225 --crate-type bin --emit=dep-info,link -C embed-bitcode=no -C debuginfo=2 --check-cfg 'cfg(docsrs,test)' --check-cfg 'cfg(feature, values())' -C metadata=74ea956b76c91b92 -C extra-filename=-0fc67055542ffac8 --out-dir /home/thomas/tmp/bad/target/debug/deps -C incremental=/home/thomas/tmp/bad/target/debug/incremental -L dependency=/home/thomas/tmp/bad/target/debug/deps -Ctarget-cpu=native`
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.18s
     Running `target/debug/bad`
$ cargo run --release --verbose
       Dirty bad v0.1.0 (/home/thomas/tmp/bad): the file `src/main.rs` has changed (1747289904.616906220s, 3m 39s after last build at 1747289685.924665034s)
   Compiling bad v0.1.0 (/home/thomas/tmp/bad)
     Running `/home/thomas/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/bin/rustc --crate-name bad --edition=2024 src/main.rs --error-format=json --json=diagnostic-rendered-ansi,artifacts,future-incompat --diagnostic-width=225 --crate-type bin --emit=dep-info,link -C opt-level=3 -C embed-bitcode=no --check-cfg 'cfg(docsrs,test)' --check-cfg 'cfg(feature, values())' -C metadata=47dd865bb155b0dd -C extra-filename=-a3db58b646f52598 --out-dir /home/thomas/tmp/bad/target/release/deps -C strip=debuginfo -L dependency=/home/thomas/tmp/bad/target/release/deps -Ctarget-cpu=native`
    Finished `release` profile [optimized] target(s) in 0.09s
     Running `target/release/bad`

thread 'main' panicked at src/main.rs:24:5:
assertion `left == right` failed
  left: 140725163265000
 right: 140725163265000
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

These numbers are the same. I was going crazy for a bit. I even changed the code to:

 let (a,b) = (left.id(), right.id());
 assert_eq!(a,b, "the difference is: {}", a-b);

and then I get:

thread 'main' panicked at src/main.rs:25:5:
assertion `left == right` failed: the difference is: 0
  left: 140721109877640
 right: 140721109877640

Note the difference is: 0.

Meta

$ cargo version --verbose
rustc 1.86.0 (05f9846f8 2025-03-31)
binary: rustc
commit-hash: 05f9846f893b09a1be1fc8560e33fc3c815cfecb
commit-date: 2025-03-31
host: x86_64-unknown-linux-gnu
release: 1.86.0
LLVM version: 19.1.7

Also same thing on nightly:
$ cargo +nightly version --verbose 
cargo 1.89.0-nightly (7918c7eb5 2025-04-27)
release: 1.89.0-nightly
commit-hash: 7918c7eb59614c39f1c4e27e99d557720976bdd7
commit-date: 2025-04-27
host: x86_64-unknown-linux-gnu
libgit2: 1.9.0 (sys:0.20.0 vendored)
libcurl: 8.12.1-DEV (sys:0.4.80+curl-8.12.1 vendored ssl:OpenSSL/3.4.1)
ssl: OpenSSL 3.4.1 11 Feb 2025
os: Debian 12.0.0 (bookworm) [64-bit]

After a rustup update, problem remains:

cargo 1.89.0-nightly (056f5f4f3 2025-05-09)
release: 1.89.0-nightly
commit-hash: 056f5f4f3c100cb36b5e9aed2d20b9ea70aae295
commit-date: 2025-05-09
host: x86_64-unknown-linux-gnu
libgit2: 1.9.0 (sys:0.20.0 vendored)
libcurl: 8.12.1-DEV (sys:0.4.80+curl-8.12.1 vendored ssl:OpenSSL/3.4.1)
ssl: OpenSSL 3.4.1 11 Feb 2025
os: Debian 12.0.0 (bookworm) [64-bit]
@ThomasHabets ThomasHabets added the C-bug Category: This is a bug. label May 15, 2025
@rustbot rustbot added the needs-triage This issue may need triage. Remove it if it has been sufficiently triaged. label May 15, 2025
@theemathas
Copy link
Contributor

Your id() method creates a reference with type &&Unknown. It then casts this to a value of type *const &Unknown. This is a pointer pointing into the call stack where the argument of id() is stored. Then, you cast this pointer to an integer.

Presumably, this isn't intended, and you wanted to write self as *const _ as u64, without the &.

@theemathas
Copy link
Contributor

The part where equal numbers are compared as unequal is probably a variant of #107975

@ThomasHabets
Copy link
Author

ThomasHabets commented May 15, 2025

I've simplified the code a bit more to:

struct Unknown {}
impl Unknown {
    fn id(&self) -> u64 {
        &self as *const _ as u64
    }
}

fn main() {
    let u = Unknown {};
    let (a, b) = (u.id(), u.id());
    assert_eq!(a, b, "difference is: {} {}", a-b, b-a);
}

Yeah, you're right. Removing the & makes it work.

I'm not fully fluent in Rust undefined behaviour, but is my code (the original code) actually wrong? There's no warning, and it's not making sense.

Edit: well, of course it's wrong, as you point out. But should Rust allow this nonsense code to compile and do the wrong thing?

@theemathas
Copy link
Contributor

theemathas commented May 15, 2025

The original code doesn't have undefined behavior (although it does have a bug). Compiler warnings can't catch everything.

The strange behavior where equal numbers compare as unequal is a known rustc bug (unrelated to the bug in your code) which can occur when the addresses of two dangling pointers are compared with each other.

@saethlin
Copy link
Member

But should Rust allow this nonsense code to compile and do the wrong thing?

This code is only nonsense if you know what the programmer intent was. A lint in clippy for observing the address of locals might make sense, but I suspect the rate at which it catches bugs may not be very high as opposed to warning about weird but correct code.

@ThomasHabets
Copy link
Author

ThomasHabets commented May 15, 2025

This code is only nonsense if you know what the programmer intent was.

Fair enough. But I maintain that having two integer variables with the same value, whose difference is zero, simply must compare equal.

Otherwise, how far does this go? If I format!("{x}") on the two numbers, is it possible that their string representations, though exactly equal, also do not compare as equal?

This sounds like C++ UB, which I don't think is great.

@theemathas
Copy link
Contributor

I maintain that having two integer variables with the same value, whose difference is zero, simply must compare equal.

Yes, that part is a known rustc bug.

@pierzchalski
Copy link
Contributor

@ThomasHabets if you're curious as to why this sort of compiler bug could even happen in the first place, I find this to be a good introduction.

@jieyouxu jieyouxu added C-discussion Category: Discussion or questions that doesn't represent real issues. and removed C-bug Category: This is a bug. needs-triage This issue may need triage. Remove it if it has been sufficiently triaged. labels May 15, 2025
@ThomasHabets
Copy link
Author

@pierzchalski thanks, very interesting article.

I think it misses an important core point though: The example code is UB, right? So a lot of the discussion is about how one should correctly compile & optimize UB input?

For C/C++ I think the ship has sailed, but for Rust I would not expect to trigger this without unsafe.

I'm glad this is recognized as a bug in rustc, and I guess I should follow #107975 for that?

@theemathas
Copy link
Contributor

The example code does not have UB. It has a miscompilation. (UB means that the code is incorrect in such a way that a non-buggy compiler is allowed to compile the code into whatever it feels like.)

Yes, if/when this bug ever gets fixed, #107975 will be updated.

@ThomasHabets
Copy link
Author

Oh, you may be right. A surprising (to me) allowance in the spec that you can compare pointers one (and only one) element past the end of an array against the (potentially) following object.

Two pointers compare equal if and only if both are null pointers, both are pointers to the
same object (including a pointer to an object and a subobject at its beginning) or function,
both are pointers to one past the last element of the same array object, or one is a pointer
to one past the end of one array object and the other is a pointer to the start of a different
array object that happens to immediately follow the first array object in the address
space
[…]Two objects may be adjacent in memory because […] the implementation chose
to place them so, even though they are unrelated. [my emphasis]

Thanks for the correction.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
C-discussion Category: Discussion or questions that doesn't represent real issues.
Projects
None yet
Development

No branches or pull requests

6 participants