Skip to content
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

Inconsistent behavior when comparing function pointers in release #54685

Closed
repnop opened this issue Sep 30, 2018 · 33 comments · Fixed by #86398
Closed

Inconsistent behavior when comparing function pointers in release #54685

repnop opened this issue Sep 30, 2018 · 33 comments · Fixed by #86398
Labels
A-LLVM Area: Code generation parts specific to LLVM. Both correctness bugs and optimization-related issues. C-bug Category: This is a bug. E-needs-test Call for participation: An issue has been fixed and does not reproduce, but no test has been added. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.

Comments

@repnop
Copy link
Contributor

repnop commented Sep 30, 2018

Playground link: https://play.rust-lang.org/?gist=62d362e2bf72001bd3f3c4a3ed0c842c&version=stable&mode=release&edition=2015

Switching to debug causes it to print both addresses twice (expected behavior), but in release both functions are optimized into one, so x and y are given the same address. @CryZe took a look at the assembly, and LLVM removed the first comparison (and evaluated it to a constant false), but not the second, which then evaluates to true. This is very reminiscent of undefined behavior in C and C++.

Thanks to @memoryruins for sending the original link which allowed me to discover this.

fn foo(i: i32) -> i32 {
    1
}
fn bar(i: i32) -> i32 {
    1
}

fn main() {
    let x: fn(i32) -> i32 = foo;
    let y: fn(i32) -> i32 = bar;
    
    if x == y {
        println!("true")
    } else {
        println!("{:?}, {:?}", x, y);
    }

    if x == y {
        println!("true")
    } else {
        println!("{:?}, {:?}", x, y);
    }
}

Debug output:

0x562a7b399600, 0x562a7b399620
0x562a7b399600, 0x562a7b399620

Release output:

0x560d1e3a1310, 0x560d1e3a1310
true

stable rustc 1.29.1 (b801ae6 2018-09-20)

@repnop
Copy link
Contributor Author

repnop commented Sep 30, 2018

Also worth noting (pointed out by @memoryruins) that MIRI throws an error in the comparison saying it dereferenced a function pointer.

@ishitatsuyuki
Copy link
Contributor

We run mergefunc pass on release builds. Anyway, can you explain why this is a problem? Rust does not make any guarantee on pointer values.

@CryZe
Copy link
Contributor

CryZe commented Sep 30, 2018

It seems like LLVM starts comparing the pointer of bar to bar instead of comparing bar to foo. This very much looks like LLVM is messing up in some way.
https://cdn.discordapp.com/attachments/273539705595756544/495820067091513347/unknown.png
https://i.imgur.com/dvWMTjE.png

Unless LLVM is allowed to break function pointers entirely.

Actually that may be what mergefunc is doing.

@Havvy
Copy link
Contributor

Havvy commented Sep 30, 2018

@ishitatsuyuki The problem is that they shouldn't produce an x == y expression that is false in one statement and then true in the next. They should either always be equal or never be equal, but this code snippet shows that this is not always true.

@rep-nop Can you include the source code from the playground and the compilation results (under release and debug) into your opening comment?

@eddyb
Copy link
Member

eddyb commented Sep 30, 2018

(came here from #54695)

According to @rkruppe, this could be caused by constant-folding &a == &b to false in LLVM (when a and b are different globals).
IMO this optimization shouldn't apply to functions and/or any unnamed_addr globals.

cc @rust-lang/compiler

@VictorKoenders
Copy link

Insanity Is Doing the Same Thing Over and Over Again and Expecting Different Results

@strega-nil
Copy link
Contributor

strega-nil commented Sep 30, 2018

This is definitely a bug in LLVM, afaict - this expression should never be true in C++. It may be that they never set unnamed_addr on functions?

@VictorKoenders
Copy link

  • Still happens with no_mangle
  • Still happens with pub
  • Still happens with extern "C"

@hanna-kruppe
Copy link
Contributor

@ubsan Yeah Clang doesn't add unnamed_addr anywhere as aggressively as rustc, and unnamed_addr is the one that allows completely merging the functions (which is required for the comparison to return true).

@strega-nil
Copy link
Contributor

@rkruppe seems like someone added an optimization that assumes that functions are always distinct, even when unnamed_addr. We might want to open a bug on LLVM?

@hanna-kruppe
Copy link
Contributor

I don't see why this would be an LLVM bug. The observed behavior seems perfectly in line with the meaning of unnamed_addr: the address is not significant, so address comparisons can evaluate either way. If a frontend does not want that, it shouldn't add unnamed_addr to functions whose address is observed. (Not saying that's what rustc should do, if it came down to it I would argue for the current behavior, just describing what would probably be a reasonable position of LLVM developers.)

@RalfJung
Copy link
Member

RalfJung commented Oct 1, 2018

Oh this is beautiful. ;) Finally we have a real-life example of pointer comparison being non-deterministic. It's not at all how I expected that example to look, though...

Non-determinism does not mean that we have UB, so I do not think there is a huge problem here. But non-deterministic comparison is certainly surprising. It means, for example, that the PartialEq implementation for function pointers does not satisfy the PartialEq contract, which is something we should keep in mind.

I suppose we could also have an analysis to see which functions have their address taken, and not set unnamed_addr for them?

@arielb1
Copy link
Contributor

arielb1 commented Oct 1, 2018

@RalfJung

What would be a problem would be if LLVM also assumes that equality is deterministic and duplicates 2 occurrences of the comparison.

@hanna-kruppe
Copy link
Contributor

@arielb1

What would be a problem would be if LLVM also assumes that equality is deterministic and duplicates 2 occurrences of the comparison.

That is a very good point. I don't think there is anything preventing that, and in principle that could lead to unsoundness. However, this problem applies to all comparisons that might be non-deterministic, right? So "just don't use unnamed_addr" wouldn't solve it.

@RalfJung

I suppose we could also have an analysis to see which functions have their address taken, and not set unnamed_addr for them?

LLVM already does that, or more precisely, it adds unnamed_addr to functions that are guaranteed to not have their address taken. This is only knowable for internal functions, so it's a significant downgrade from the status quo in non-LTO builds. It also can't distinguish e.g. between only putting a function in a vtable (which is not accessible to users and only ever used for calling) and reifying a function pointer in user code. The latter could be solved by an analysis in rustc, but the former is a fundamental limitation of separate compilation.

@RalfJung
Copy link
Member

RalfJung commented Oct 1, 2018

@arielb1 Ah right, forgot about that... so, is it UB under LLVM IR to take the address of a function tagged unnamed_addr?

However, this problem applies to all comparisons that might be non-deterministic, right?

True, but it is not clear if there are any. I cannot give a satisfactory definition of LLVM semantics that makes ptr comparison deterministic, but I also was unable to find an example that would demonstrate that it is not deterministic.

@hanna-kruppe
Copy link
Contributor

is it UB under LLVM IR to take the address of a function tagged unnamed_addr?

I see no reason for that and it seems unnecessarily restrictive (again consider function pointers in vtables, their address is taken and must be considered to escape, but the actual address is never observed). It ought to be enough if comparisons, pointer->integer casts, and and anything else that leaks information about the address is undef or poison.

@RalfJung
Copy link
Member

RalfJung commented Oct 1, 2018

Ack -- just taking a reference and calling it is fine indeed.

However, in Rust, with fn ptr comparison being a safe operation, that doesn't really help us all that much.

@hanna-kruppe
Copy link
Contributor

Yes, for Rust we may have to stop applying unnamed_addr to most functions ☹️

@eddyb
Copy link
Member

eddyb commented Oct 1, 2018

I don't think we should stop using unnamed_addr, and aside from this (overly, IMO) aggressive constant-folding optimization, I expect function pointers to behave as boringly as those obtained from Box::leak, just that sometimes you might happen to have two pointers to the same machine code function, despite them originating from distinct Rust functions.

That is, I expect the place where non-determinism happens, to be on reification to fn pointer.
It's possible for two distinct Rust functions to map to the same machine code function, or the same Rust function to map to two machine code functions.
That's what we don't guarantee - that mapping. foo as fn() != foo as fn() is fine.

Meanwhile, once you have a pointer, you should be able to expect it to behave in certain ways.
If you have let x = foo as fn(); then I certainly expect x == x and other properties to hold.
And if you black box its creation you totally can. It's just this optimization.

@eddyb
Copy link
Member

eddyb commented Oct 1, 2018

@arielb1 said:

@RalfJung What would be a problem would be if LLVM also assumes that equality is deterministic and duplicates 2 occurrences of the comparison.

In terms of "pure operations on SSA values", I think it's reasonable to expect that the same operation applied to the same inputs, would be always deterministic.
Doesn't GVN rely on that? In fact, I'm shocked GVN doesn't hide the constant-folding here.

Oh, I figured out why we can even observe the difference! Printing takes a reference, and LLVM doesn't know it's a read-only reference, and assumes the pointer can change.

@VictorKoenders
Copy link

If you have let x = foo as fn(); then I certainly expect x == x and other properties to hold.

Especially if you have the statement if x == y being executed twice giving different results. Unless x or y changes when you compare it (which shouldn't happen, but technically is possible), you'd expect a std/core comparison to be deterministic.

@RalfJung
Copy link
Member

RalfJung commented Oct 1, 2018

In terms of "pure operations on SSA values", I think it's reasonable to expect that the same operation applied to the same inputs, would be always deterministic.

That's only true if the operation is, well, deterministic. The game is still open about whether that is the case for pointer comparison (think: comparing one-past-the-end pointers). But those subtleties are likely not an issue here.

Also one could make the same argument about casting a function to a fn ptr, I do not see a good reason why that would be any less deterministic than other operations.

But, concretely, you seem to propose to pass fn ptrs through black_box after their creation? That would likely fix this case, but it'd also kill LLVM's ability to replace dynamic calls by static ones.

@eddyb
Copy link
Member

eddyb commented Oct 1, 2018

But, concretely, you seem to propose to pass fn ptrs through black_box after their creation?

No, I'm not. I'm just saying that the resulting behavior is what I expect, for integer operations on fn().

Also one could make the same argument about casting a function to a fn ptr, I do not see a good reason why that would be any less deterministic than other operations.

The compilation model means that we can't always easily deduplicate instances of the same function with the same type parameters, if that's what you mean.
Think about two sibling crates that both happen to instantiate Vec::<[u8; 3_141_592]>::new - if you're generating machine code, how would you guarantee that you get the same function pointer?

@RalfJung
Copy link
Member

RalfJung commented Oct 1, 2018

Think about two sibling crates that both happen to instantiate Vec::<[u8; 3_141_592]>::new - if you're generating machine code, how would you guarantee that you get the same function pointer?

That's a different case though. Here, both casts are in the same function even. Seems reasonable to expect something deterministic there.

No, I'm not. I'm just saying that the resulting behavior is what I expect, for integer operations on fn().

I do not understand what you are proposing then. Also, ptr equality is not the same as equality of the resulting integers. We are talking about comparison at fn() type here, not at usize. The latter definitely should be deterministic. (However, LLVM has some bugs where it removes ptr-int-casts, leading to miscompilations.)

@eddyb
Copy link
Member

eddyb commented Oct 1, 2018

@RalfJung I guess the confusing bit that led me to say "integer operations" is that LLVM uses icmp for comparing pointers (albeit without casting to integers first).

Also, I found the code responsible for making the decision that the const-fold is valid:

static ICmpInst::Predicate areGlobalsPotentiallyEqual(const GlobalValue *GV1,
                                                      const GlobalValue *GV2) {
  auto isGlobalUnsafeForEquality = [](const GlobalValue *GV) {
    if (GV->hasExternalWeakLinkage() || GV->hasWeakAnyLinkage())
      return true;
// ...
    if (!isGlobalUnsafeForEquality(GV1) && !isGlobalUnsafeForEquality(GV2))
      return ICmpInst::ICMP_NE;

So it seems like patching LLVM to get the more boring behavior only involves adding an extra check for unnamed_addr, right next to the hasExternalWeakLinkage and hasWeakAnyLinkage check.
(Note that this problem also affects constant globals, not just functions)

@RalfJung
Copy link
Member

RalfJung commented Oct 1, 2018

Ah yes, icmp is used for both integer and pointer comparisons. I never even thought about the fact that the i might be for "integer". ;)

@strega-nil
Copy link
Contributor

@eddyb there already exists the guarantee that

template <typename T>
void foo();

foo<int> == foo<int>, no matter where these pointers were gotten. That's what inline linkage is for.

@jonas-schievink jonas-schievink added T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. C-bug Category: This is a bug. labels Jan 27, 2019
bors added a commit to rust-lang/rust-clippy that referenced this issue Mar 30, 2020
Lint unnamed address comparisons

Functions and vtables have an insignificant address. Attempts to compare such addresses will lead to very surprising behaviour. For example: addresses of different functions could compare equal; two trait object pointers representing the same object and the same type could be unequal.

Lint against unnamed address comparisons to avoid issues like those in rust-lang/rust#69757 and rust-lang/rust#54685.

Changelog: New lints: [`fn_address_comparisons`] [#5294](#5294), [`vtable_address_comparisons`] [#5294](#5294)
bors added a commit to rust-lang/rust-clippy that referenced this issue Mar 30, 2020
Lint unnamed address comparisons

Functions and vtables have an insignificant address. Attempts to compare such addresses will lead to very surprising behaviour. For example: addresses of different functions could compare equal; two trait object pointers representing the same object and the same type could be unequal.

Lint against unnamed address comparisons to avoid issues like those in rust-lang/rust#69757 and rust-lang/rust#54685.

changelog: New lints: [`fn_address_comparisons`] [#5294](#5294), [`vtable_address_comparisons`] [#5294](#5294)
@tmiasko
Copy link
Contributor

tmiasko commented May 23, 2021

This issue was fixed in https://reviews.llvm.org/D87123

@nikomatsakis nikomatsakis added the E-needs-test Call for participation: An issue has been fixed and does not reproduce, but no test has been added. label May 24, 2021
@nikomatsakis
Copy link
Contributor

Tagging as "needs test" -- do we have a minimal example?

yerke added a commit to yerke/rust that referenced this issue Jun 18, 2021
JohnTitor added a commit to JohnTitor/rust that referenced this issue Jun 21, 2021
…ark-Simulacrum

Add regression test for issue rust-lang#54685

Closes rust-lang#54685

Took the test from rust-lang#54685 and modified it a bit to use assertion. Made sure that the updated test catches the original issue on 1.50 by running on Compiler Explorer (https://godbolt.org/z/E64onYeT5).
bors added a commit to rust-lang-ci/rust that referenced this issue Jun 22, 2021
Rollup of 11 pull requests

Successful merges:

 - rust-lang#85054 (Revert SGX inline asm syntax)
 - rust-lang#85182 (Move `available_concurrency` implementation to `sys`)
 - rust-lang#86037 (Add `io::Cursor::{remaining, remaining_slice, is_empty}`)
 - rust-lang#86114 (Reopen rust-lang#79692 (Format symbols under shared frames))
 - rust-lang#86297 (Allow to pass arguments to rustdoc-gui tool)
 - rust-lang#86334 (Resolve type aliases to the type they point to in intra-doc links)
 - rust-lang#86367 (Fix comment about rustc_inherit_overflow_checks in abs().)
 - rust-lang#86381 (Add regression test for issue rust-lang#39161)
 - rust-lang#86387 (Remove `#[allow(unused_lifetimes)]` which is now unnecessary)
 - rust-lang#86398 (Add regression test for issue rust-lang#54685)
 - rust-lang#86493 (Say "this enum variant takes"/"this struct takes" instead of "this function takes")

Failed merges:

r? `@ghost`
`@rustbot` modify labels: rollup
@bors bors closed this as completed in fdb1daa Jun 22, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-LLVM Area: Code generation parts specific to LLVM. Both correctness bugs and optimization-related issues. C-bug Category: This is a bug. E-needs-test Call for participation: An issue has been fixed and does not reproduce, but no test has been added. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging a pull request may close this issue.