Join GitHub today
GitHub is home to over 28 million developers working together to host and review code, manage projects, and build software together.
Sign upLLVM loop optimization can make safe programs crash #28728
Comments
steveklabnik
added
the
A-LLVM
label
Sep 29, 2015
This comment has been minimized.
This comment has been minimized.
|
The LLVM IR of the optimised code is ; Function Attrs: noreturn nounwind readnone uwtable
define internal void @_ZN4main20h5ec738167109b800UaaE() unnamed_addr #0 {
entry-block:
unreachable
}This kind of optimisation breaks the main assumption that should normally hold on uninhabited types: it should be impossible to have a value of that type. |
This comment has been minimized.
This comment has been minimized.
|
triage: I-nominated Seems bad! If LLVM doesn't have a way to say "yes, this loop really is infinite" though then we may just have to sit-and-wait for the upstream discussion to settle. |
rust-highfive
added
the
I-nominated
label
Sep 29, 2015
alexcrichton
added
the
T-compiler
label
Sep 29, 2015
This comment has been minimized.
This comment has been minimized.
|
A way to prevent infinite loops from being optimised away is to add |
This comment has been minimized.
This comment has been minimized.
|
Is this related to #18785? That one's about infinite recursion to be UB, but it sounds like the fundamental cause might be similar: LLVM doesn't consider not halting to be a side effect, so if a function has no side effects other than not halting, it's happy to optimize it away. |
This comment has been minimized.
This comment has been minimized.
|
It's the same issue. |
This comment has been minimized.
This comment has been minimized.
|
Yes, looks like it's the same. Further down that issue, they show how to get |
This comment has been minimized.
This comment has been minimized.
|
|
This comment has been minimized.
This comment has been minimized.
|
Crash, or, possibly even worse heartbleed https://play.rust-lang.org/?gist=15a325a795244192bdce&version=stable |
bluss
added
the
I-wrong
label
Sep 29, 2015
This comment has been minimized.
This comment has been minimized.
|
So I've been wondering how long until somebody reports this. :) In my opinion, the best solution would of course be if we could tell LLVM not to be so aggressive about potentially infinite loops. Otherwise, the only thing I think we can do is to do a conservative analysis in Rust itself that determines whether:
Either of this should be enough to avoid undefined behavior. |
This comment has been minimized.
This comment has been minimized.
|
triage: P-medium We'd like to see what LLVM will do before we invest a lot of effort on our side, and this seems relatively unlikely to cause problems in practice (though I have personally hit this while developing the compiler as well). There are no backwards incomatibility issues to be concerned about. |
rust-highfive
added
P-medium
and removed
I-nominated
labels
Oct 1, 2015
This comment has been minimized.
This comment has been minimized.
|
Quoting from the LLVM mailing list discussion:
|
This comment has been minimized.
This comment has been minimized.
|
@dotdash The excerpt you are quoting comes from the C++ specification; it is basically the answer to "how it [having side effects] is defined in C" (also confirmed by the standard committee: http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1528.htm ). Regarding what is the expected behaviour of the LLVM IR there is some confusion. https://llvm.org/bugs/show_bug.cgi?id=24078 shows that there seems to be no accurate & explicit specification of the semantics of infinite loops in LLVM IR. It aligns with the semantics of C++, most likely for historical reasons and for convenience (I only managed to track down https://groups.google.com/forum/#!topic/llvm-dev/j2vlIECKkdE which apparently refers to a time when infinite loops were not optimised away, some time before the C/C++ specs were updated to allow it). From the thread it is clear that there is the desire to optimise C++ code as effectively as possible (i.e. also taking into account the opportunity to remove infinite loops), but in the same thread several developers (including some that actively contribute to LLVM) have shown interest in the ability to preserve infinite loops, as they are needed for other languages. |
This comment has been minimized.
This comment has been minimized.
|
@ranma42 I'm aware of that, I just quoted that for reference, because one possibility to work-around this would be to detect such loops in rust and add one of the above to it to stop LLVM from performing this optimization. |
This comment has been minimized.
This comment has been minimized.
|
Is this a soundness issue? If so, we should tag it as such. |
This comment has been minimized.
This comment has been minimized.
|
Yes, following @ranma42's example, this way shows how it readily defeats array bounds checks. playground link |
bluss
added
I-unsound 💥
and removed
I-wrong
labels
Nov 30, 2015
arielb1
added
I-wrong
and removed
I-unsound 💥
labels
Dec 2, 2015
This comment has been minimized.
This comment has been minimized.
|
The policy is that wrong-code issues that are also soundness issues (i.e. most of them) should be tagged |
brson
added
I-unsound 💥
E-hard
and removed
E-hard
labels
Aug 4, 2016
This comment has been minimized.
This comment has been minimized.
So? The only thing that matters is that the example program, which is basically |
This comment has been minimized.
This comment has been minimized.
I don't know why you're saying this like it resolves anything. LLVM considering infinite recursion and loops UB and optimizing accordingly, yet it being safe in Rust, is the entire point of this whole issue! |
This comment has been minimized.
This comment has been minimized.
|
Author of https://reviews.llvm.org/rL317729 here, confirming that I have not yet implemented the follow-up patch. You can insert an Obviously it would be nicer to have the second patch in place, so that it's not necessary to do this. I don't know when I'll get back to implementing that. |
This comment has been minimized.
This comment has been minimized.
|
There's some small difference, but I'm not sure if it's material or not. Recursion#[allow(unconditional_recursion)]
#[inline(never)]
pub fn via_recursion<T>() -> T {
via_recursion()
}
fn main() {
let a: String = via_recursion();
}define internal void @_ZN10playground4main17h1daf53946e45b822E() unnamed_addr #2 personality i32 (i32, i32, i64, %"unwind::libunwind::_Unwind_Exception"*, %"unwind::libunwind::_Unwind_Context"*)* @rust_eh_personality {
_ZN4core3ptr13drop_in_place17h95538e539a6968d0E.exit:
ret void
}Loop#[inline(never)]
pub fn via_loop<T>() -> T {
loop {}
}
fn main() {
let b: String = via_loop();
}define internal void @_ZN10playground4main17h1daf53946e45b822E() unnamed_addr #2 {
start:
unreachable
}MetaRust 1.29.1, compiling in release mode, viewing the LLVM IR. |
This comment has been minimized.
This comment has been minimized.
|
I don't think that we can, in general, detect recursion (trait objects, C FFI, etc.), so we would have to use |
This comment has been minimized.
This comment has been minimized.
|
@gnzlbg That's true, though you could put the @llvm.sideeffects at the entries of functions, rather than at the call sites. |
This comment has been minimized.
This comment has been minimized.
|
Strangely, I cannot reproduce the SEGFAULT in @SergioBenitez' test case locally. Also, for a stack overflow, shouldn't there be a different error message? I thought we had some code to print "The stack overflowed" or so? |
This comment has been minimized.
This comment has been minimized.
|
@RalfJung did you try in debug mode? (I can reliably reproduce the stack overflow in debug mode in my machines and the playground, so maybe you need to fill a bug if for you this is not the case). In
It's hard to tell what the best way forward would be without knowing exactly which optimizations trait Foo {
fn foo(&self) { self.bar() }
fn bar(&self);
}
struct A;
impl Foo for A {
fn bar(&self) {} // not recursive
}
struct B;
impl Foo for B {
fn bar(&self) { self.foo() } // recursive
}
fn main() {
let a = A;
a.bar(); // Ok - no @llvm.sideeffect needed anywhere
let b = B;
b.bar(); // We need @llvm.sideeffect on this call site
let c: &[&dyn Foo] = &[&a, &b];
for i in c {
i.bar(); // We need @lvm.sideeffect here too
}
}AFAICT, we have to put |
This comment has been minimized.
This comment has been minimized.
Sure, but in debug mode LLVM doesn't do the loop optimizations so there is no problem. If the program stackoverflows in debug mode, that should not give LLVM license to create UB. The issue is figuring out whether the final program has UB, and from staring at the IR I cannot tell. It segfaults, but I do not know why. But it does seem like a bug to me to "optimize" a stackoverflowing program into one that segfaults. |
This comment has been minimized.
This comment has been minimized.
In C, a thread of execution is assumed to terminate, perform volatile memory accesses, I/O, or a synchronizing atomic operation. It would be surprising to me if LLVM-IR did not evolve to have the same semantics either by accident or by design. The Rust code contains a thread of execution that never terminates, and does not perform any of the operations required for this to not be UB in C. I'd suspect that we generate the same LLVM-IR that a C program with undefined behavior would, so I don't think it is surprising that LLVM is misoptimizing this Rust program.
LLVM removes the infinite recursion, so as @SergioBenitez mentioned above, the program then proceeds to:
The part of the program that does it is this one: let x = Container::from("hello"); // invalid reference created here
println!("{} {}", x.string, x.num); // invalid reference dereferenced herewhere |
This comment has been minimized.
This comment has been minimized.
I was under the impression that you argued above that this is not the same bug as the infinite loop issue. It seems I misread your messages then. Sorry for the confusion. So, seems like a good next step would be to sprinkle some
Btw, this is not entirely correct -- a loop with a constant conditional (such as |
This comment has been minimized.
This comment has been minimized.
The behavior of non-terminating Rust programs is always defined, while the behavior of non-terminating LLVM-IR programs is only defined if certain conditions are met. I thought that this issue is about fixing the implementation of Rust for infinite loops such that the behavior of the generated LLVM-IR becomes defined, and for this, @SergioBenitez mentioned that one can also create non-terminating Rust programs using recursion, and @rkruppe argued that infinite recursion and infinite loops are equivalent, so that these are both the same bug. I don't disagree that these two issues are related, or even that they are the same bug, but for me, these two issues look slightly different:
|
This comment has been minimized.
This comment has been minimized.
The analysis required to show that a loop body actually has side effects (not just potentially, as with calls to external functions) and thus doesn't need a |
This comment has been minimized.
This comment has been minimized.
|
If I understand the problem correctly, to fix the infinite-loop case, it should be sufficient to insert I don't know what to do about the infinite recursion, but I agree with RalfJung that optimizing an infinite recursion into an unrelated segfault is not desirable behavior. |
This comment has been minimized.
This comment has been minimized.
I don't think it's that simple, e.g., |
This comment has been minimized.
This comment has been minimized.
Hm, yes, that's troublesome. But we don't have to be perfect, just conservatively correct. A loop like while spinlock.load(Ordering::SeqCst) != 0 {}(from the loop {
if /* runtime-variable condition */ { break }
/* more stuff */
}should also not be troublesome. In fact, is there any case that the "no break expressions in the body of the loop" rule gets wrong besides loop {
if /* provably false at compile time */ { break }
}? |
This comment has been minimized.
This comment has been minimized.
Fair enough. However, as you said, the issue (the mismatch between Rust semantics and LLVM semantics) is actually about non-termination, not about loops. So I think that's what we should be tracking here.
What you describe holds for C. In Rust, any loop is allowed to diverge. Everything else would just be unsound to do. So, for example while test_fermats_last_theorem_on_some_random_number() { }is an okay program in Rust (but neither in C nor C++), and it will loop forever without causing a side-effect. So, it has to be all loops, except for those we can prove will terminate. |
This comment has been minimized.
This comment has been minimized.
It's not only
Consider: fn foo(x: bool) { loop { if x { break; } } }where If that makes sense, one transformation that LLVM would be allowed to do is replacing fn foo_opt(x: bool) { if x { foo(true) } else { foo(false) } }where both branches are optimized independently, and the second branch would be mis-optimized if we don't use That is, to be able to omit |
This comment has been minimized.
This comment has been minimized.
Ekleog
commented
Oct 1, 2018
|
Everything about this bug sounds to me like it'd be much easier to solve from LLVM than from As I understand it, the fix from LLVM would be changing optimizations from running on (prove non-termination || can't prove either) to running only when non-termination can be proven (or the opposite). I'm not saying this is easy (in any way), but LLVM already (I guess) includes code to try to prove (non-)termination of loops. On the other hand, So I would think the path forward would be:
What do you think about this? I hope the performance impact of step 1 wouldn't be too horrible, though, even if it's meant to vanish once 2 is implemented… |
This comment has been minimized.
This comment has been minimized.
|
@Ekleog that's what @sunfishcode second patch might be about: https://lists.llvm.org/pipermail/llvm-dev/2017-October/118595.html
|
This comment has been minimized.
This comment has been minimized.
|
To be fair to LLVM, compiler writers don't approach this topic from the perspective of "I'm going to write an optimization that proves loops are non-terminating, so that I can pedantically optimize them away!" Instead, the assumption that loops will either terminate or have side effects just arises naturally in some common compiler algorithms. Fixing this isn't just a tweak to existing code; it'll require a significant amount of new complexity. Consider the following algorithm for testing whether a function body "has no side effects": if any instruction in the body has potential side effects, then the function body may have side effects. Nice and simple. Then later, calls to functions "with no side effects" are deleted. Cool. Except, branch instructions are considered to have no side effects, so a function containing only branches will appear to have no side effects, even though it may contain an infinite loop. Oops. It is fixable. If anyone else is interested in looking into this, my basic idea is to split the concept of "has side effects" into independent concepts of "has actual side effects" and "may be non-terminating". And then go through the whole optimizer and find all the places that care about "has side effects" and figure out which concept(s) they actually need. And then teach the loop passes to add metadata to branches that aren't part of a loop, or the loops they're in are provably finite, in order to avoid pessimizations. A possible compromise might be to have rustc insert @llvm.sideeffect when a user literally writes an empty |
This comment has been minimized.
This comment has been minimized.
It is entirely unnatural though if you even start to think about correctness of those transformations. To be frank I still think it was a huge mistake of C to ever allow this assumption, but well.
There's a good reason that "non-termination" is typically considered an effect when you start to look at things formally. (Haskell isn't pure, it has two effects: Non-termination and exceptions.)
As you noted yourself, this is still incorrect. I do not think we should accept a "solution" which we know to be incorrect. Compilers are such an integral part of our infrastructure, we shouldn't just hope that nothing goes wrong. This is no way to build a solid foundation. What happened here is that the notion of correctness was built around what compilers did, instead of starting with "What do we want from our compilers" and then making that their specification. A correct compiler does not turn a program that always diverges into one that terminates, period. I find this rather self-evident, but with Rust having a reasonable type system, this is even clearly witnessed in the types, which is why the issue is surfacing regularly. Given the constraints we are working with (namely, LLVM), what we should do is start by adding |
This comment has been minimized.
This comment has been minimized.
|
To make my point more precise, I think the following is a sound Rust crate, with fn test_fermats_last_theorem() -> bool {
let x = pick_a_number_greater_2();
let y = pick_a_number_greater_2();
let z = pick_a_number_greater_2();
let n = pick_a_number_greater_2();
// x^n + y^n = z^n is impossible for n > 2
pow(x, n) + pow(y, n) != pow(z, n)
}
pub fn diverge() -> ! {
while test_fermats_last_theorem() { }
// This code is unreachable, as proven by Andrew Wiles
unsafe { mem::transmute(()) }
}If we compile away that diverging loop, that's a bug and it should be fixed. We don't even have numbers so far for how much performance it would cost to fix this naively. Until we do, I see no reason to deliberately break programs like the above. |
This comment has been minimized.
This comment has been minimized.
|
In practice, Would it make sense to have an |
This comment has been minimized.
This comment has been minimized.
|
I absolutely agree we need to fix this soundness issue for good. However, the way we go about that should be mindful of the possibility that "add
Admittedly, this perspective is informed by the fact that this issue has been standing for years. If it was a fresh regression, I would be more open to fixing it more quickly or reverting the PR that introduced it. |
This comment has been minimized.
This comment has been minimized.
NeoLegends
commented
Jan 4, 2019
•
|
Meanwhile, should this be mentioned in https://doc.rust-lang.org/beta/reference/behavior-considered-undefined.html as long as this issue is open? |
RalfJung commentedSep 29, 2015
The following snippet crashes when compiled in release mode on current stable, beta and nightly:
https://play.rust-lang.org/?gist=1f99432e4f2dccdf7d7e&version=stable
This is based on the following example of LLVM removing a loop that I was made aware of: https://github.com/simnalamburt/snippets/blob/master/rust/src/bin/infinite.rs.
What seems to happen is that since C allows LLVM to remove endless loops that have no side-effect, we end up executing a
matchthat has to arms.