Repository navigation
LLVM loop optimization can make safe programs crash #28728
Copy link
Copy link
Closed
Labels
A-LLVMArea: Code generation parts specific to LLVM. Both correctness bugs and optimization-related issues.Area: Code generation parts specific to LLVM. Both correctness bugs and optimization-related issues.C-bugCategory: This is a bug.Category: This is a bug.E-mediumCall for participation: Medium difficulty. Experience needed to fix: Intermediate.Call for participation: Medium difficulty. Experience needed to fix: Intermediate.I-needs-decisionIssue: In need of a decision.Issue: In need of a decision.I-unsoundIssue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/SoundnessIssue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/SoundnessP-mediumMedium priorityMedium priorityT-compilerRelevant to the compiler team, which will review and decide on the PR/issue.Relevant to the compiler team, which will review and decide on the PR/issue.WG-embeddedWorking group: Embedded systemsWorking group: Embedded systems
Description
The following snippet crashes when compiled in release mode on current stable, beta and nightly:
enum Null {}
fn foo() -> Null { loop { } }
fn create_null() -> Null {
let n = foo();
let mut i = 0;
while i < 100 { i += 1; }
return n;
}
fn use_null(n: Null) -> ! {
match n { }
}
fn main() {
use_null(create_null());
}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/12e73f45f3/rust/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 match that has no arms.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
A-LLVMArea: Code generation parts specific to LLVM. Both correctness bugs and optimization-related issues.Area: Code generation parts specific to LLVM. Both correctness bugs and optimization-related issues.C-bugCategory: This is a bug.Category: This is a bug.E-mediumCall for participation: Medium difficulty. Experience needed to fix: Intermediate.Call for participation: Medium difficulty. Experience needed to fix: Intermediate.I-needs-decisionIssue: In need of a decision.Issue: In need of a decision.I-unsoundIssue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/SoundnessIssue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/SoundnessP-mediumMedium priorityMedium priorityT-compilerRelevant to the compiler team, which will review and decide on the PR/issue.Relevant to the compiler team, which will review and decide on the PR/issue.WG-embeddedWorking group: Embedded systemsWorking group: Embedded systems
Activity
ranma42 commentedon Sep 29, 2015
The LLVM IR of the optimised code is
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.
rust-lang/rfcs#1216 proposes to explicitly handle such types in Rust. It might be effective in ensuring that LLVM never has to handle them and in injecting the appropriate code to ensure divergence when needed (IIUIC this could be achieved with appropriate attributes or intrinsic calls).
This topic has also been recently discussed in the LLVM mailing list: http://lists.llvm.org/pipermail/llvm-dev/2015-July/088095.html
alexcrichton commentedon Sep 29, 2015
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.
ranma42 commentedon Sep 29, 2015
A way to prevent infinite loops from being optimised away is to add
unsafe {asm!("" :::: "volatile")}inside of them. This is similar to thellvm.noop.sideeffectintrinsic that has been proposed in the LLVM mailing list, but it might prevent some optimisations.In order to avoid the performance loss and to still guarantee that diverging functions/loops are not optimised away, I believe that it should be sufficient to insert an empty non-optimisable loop (i.e.
loop { unsafe { asm!("" :::: "volatile") } }) if uninhabited values are in scope.If LLVM optimises the code which should diverge to the point that it does not diverge anymore, such loops will ensure that the control flow is still unable to proceed.
In "lucky" case in which LLVM is unable to optimise the diverging code, such loop will be removed by DCE.
geofft commentedon Sep 29, 2015
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.
arielb1 commentedon Sep 29, 2015
@geofft
It's the same issue.
RalfJung commentedon Sep 29, 2015
Yes, looks like it's the same. Further down that issue, they show how to get
undef, from which I assume it's not hard to make a (seemingly safe) program crash.simnalamburt commentedon Sep 29, 2015
👍
ranma42 commentedon Sep 29, 2015
Crash, or, possibly even worse heartbleed https://play.rust-lang.org/?gist=15a325a795244192bdce&version=stable
nikomatsakis commentedon Oct 1, 2015
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.
nikomatsakis commentedon Oct 1, 2015
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.
147 remaining items
nikic commentedon Jan 23, 2021
This should be mostly fixed in LLVM 12 by https://reviews.llvm.org/D94106. There's still a minor issue remaining (side-effects can be hoisted across infinite loops).
oxalica commentedon Jan 24, 2021
The upstream issue is marked as fixed now. https://bugs.llvm.org/show_bug.cgi?id=965
bstrie commentedon Jan 24, 2021
Is there an LLVM bug to track this issue? (I presume it's not something rustc can address on its own?)
Also, if this issue were fully addressed, would we want to revert #77972? If so, that would be worth tracking somewhere. Even though it appears to cause no noticeable regressions, not doing unnecessary work seems valuable.
nikic commentedon Jan 24, 2021
This issue is fixed by https://reviews.llvm.org/D95288. There might be more places with problematic assumptions, but at least I'm not aware of any. (In default-enabled passes that is. The attributor has known issues.)
We shouldn't revert that yet as Rust also supports older LLVM versions, but we can limit sideeffect insertion to older versions at least.
oberien commentedon Feb 1, 2021
I just found the following miscompilation resulting from this bug, which simply compiles to
ud2on stable, beta and nightly:It's a minified version of the following snippet where I mistyped
take_whileasfilter:Necior commentedon Feb 9, 2021
I just want to notice that it's a bug on which beginner Rustaceans may stumble upon when working on the final exercise from The Book (Building a Multithreaded Web Server).
When I was playing with the web server's code, for a moment I had an empty loop body:
instead of (Listing 20-20):
Running the first version ends with
Illegal instruction (core dumped). This example boils down to #46076 which was closed as a duplicate of #28728 (this issue). It's also similar to #28728 (comment).I keep my fingers crossed for solving this issue 🤞
Use asm::nop in empty loops
nagisa commentedon Mar 5, 2021
Closed by #81451
daira commentedon May 17, 2021
Thankyou to everyone who helped to get this fixed.
However, I would like to note that it took over five years from the original report, and four years after LLVM devs said they would fix it on their side (#28728 (comment)), for this memory-unsafe soundness bug to actually be fixed in Rust.
Is this the expected length of time for fixes to memory-unsafe soundness bugs? :-/
It could, of course, have been fixed much earlier by just adding a side-effect into infinite loops [edit: I know that this is a strong statement but I have seen nothing that contradicts it], and then removing that workaround as a performance optimization after LLVM fixed their bug.
It doesn't appear to be the only example: #52652 (which also could be triggered from safe code as noted in #52652 (comment)) took over three years to be fixed, measuring from the original attempted fix in #46833.
Based on the evidence of this bug and #52652, there appears to be a de facto policy to leave tricky unsoundness bugs unfixed for long periods, whenever fixing them in the most obvious way could cause any potential performance regression or break any (no matter how obscure) code. I would like to register my objection to that de facto policy, which I do not think well serves the majority of Rust users.
SimonSapin commentedon May 17, 2021
@daira While I think you make good points, closed issues are generally considered resolved and not in need of attention, so new comments there are easy to miss. Consider starting a new discussion elsewhere (though to be honest I’m not sure which venue would be best in this case.)
RalfJung commentedon May 17, 2021
https://internals.rust-lang.org/ might be a reasonable place.
(FWIW, I disagree with your assessment @daira, in particular about "just" adding a side-effect into infinite loops -- it's not that simple; as usual, one has to be careful with "why don't you just"-style questions.)
daira commentedon May 17, 2021
I used "just" advisedly. It really wouldn't have been hard, as compiler bugs go, to use that approach. Specifically, my understanding is that adding
@llvm.sideeffectas originally proposed in #18785 would have been sufficient. No compiler bug of this kind is trivial, but they should not take 5 years to fix. (Correction: over 6 years, counting from #18785; that bug wasn't specific to loops but it was essentially the same problem.)Aaron1011 commentedon May 17, 2021
That bug was 'fixed' very quickly, but the fix had to be reverted because it broke the
rluacrate. The full fix required an RFC to add new-unwindABIs to the language. While it's definitely a good idea to prioritize fixing soundness issues, it's not always a matter of making an invisible change to the compiler backend.