Skip to content

LLVM loop optimization can make safe programs crash #28728

Closed

Description

@RalfJung

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.

Activity

  1. added
    A-LLVMArea: Code generation parts specific to LLVM. Both correctness bugs and optimization-related issues.
    on Sep 29, 2015
  2. ranma42 commented on Sep 29, 2015

    @ranma42
    Contributor

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

  3. alexcrichton commented on Sep 29, 2015

    @alexcrichton
    Member

    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.

  4. added
    T-compilerRelevant to the compiler team, which will review and decide on the PR/issue.
    on Sep 29, 2015
  5. ranma42 commented on Sep 29, 2015

    @ranma42
    Contributor

    A way to prevent infinite loops from being optimised away is to add unsafe {asm!("" :::: "volatile")} inside of them. This is similar to the llvm.noop.sideeffect intrinsic 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.

  6. geofft commented on Sep 29, 2015

    @geofft
    Contributor

    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.

  7. arielb1 commented on Sep 29, 2015

    @arielb1
    Contributor

    @geofft

    It's the same issue.

  8. RalfJung commented on Sep 29, 2015

    @RalfJung
    MemberAuthor

    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.

  9. simnalamburt commented on Sep 29, 2015

    @simnalamburt
    Contributor

    👍

  10. ranma42 commented on Sep 29, 2015

    @ranma42
    Contributor
  11. nikomatsakis commented on Oct 1, 2015

    @nikomatsakis
    Contributor

    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:

    1. the loop will terminate OR
    2. the loop will have side-effects (I/O operations etc, I forget precisely how this is defined in C)

    Either of this should be enough to avoid undefined behavior.

  12. nikomatsakis commented on Oct 1, 2015

    @nikomatsakis
    Contributor

    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.

  13. 147 remaining items

  14. nikic commented on Jan 23, 2021

    @nikic
    Contributor

    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).

  15. oxalica commented on Jan 24, 2021

    @oxalica
    Contributor

    The upstream issue is marked as fixed now. https://bugs.llvm.org/show_bug.cgi?id=965

  16. bstrie commented on Jan 24, 2021

    @bstrie
    Contributor

    There's still a minor issue remaining (side-effects can be hoisted across infinite loops).

    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.

  17. nikic commented on Jan 24, 2021

    @nikic
    Contributor

    There's still a minor issue remaining (side-effects can be hoisted across infinite loops).

    Is there an LLVM bug to track this issue? (I presume it's not something rustc can address on its own?)

    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.)

    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.

    We shouldn't revert that yet as Rust also supports older LLVM versions, but we can limit sideeffect insertion to older versions at least.

  18. oberien commented on Feb 1, 2021

    @oberien
    Contributor

    I just found the following miscompilation resulting from this bug, which simply compiles to ud2 on stable, beta and nightly:

    pub fn oops() -> u32 {
        (0..).sum() // or .last().unwrap() instead of .sum()
    }

    It's a minified version of the following snippet where I mistyped take_while as filter:

    pub fn foo(end: u32) -> u32 {
        (0..)
            .map(|i| i * i)
            .filter(|&i| i < end)
            .sum()
    }
  19. Necior commented on Feb 9, 2021

    @Necior

    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:

    impl Worker {
        fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
            let thread = thread::spawn(|| loop {});
            Worker { id, thread }
        }
    }

    instead of (Listing 20-20):

    impl Worker {
        fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
            let thread = thread::spawn(move || loop {
                let job = receiver.lock().unwrap().recv().unwrap();
                println!("Worker {} got a job; executing.", id);
                job();
            });
            Worker { id, thread }
        }
    }

    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 🤞

  20. added a commit that references this issue on Feb 13, 2021
  21. nagisa commented on Mar 5, 2021

    @nagisa
    Member

    Closed by #81451

  22. daira commented on May 17, 2021

    @daira
    Contributor

    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.

  23. SimonSapin commented on May 17, 2021

    @SimonSapin
    Contributor

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

  24. RalfJung commented on May 17, 2021

    @RalfJung
    MemberAuthor

    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.)

  25. daira commented on May 17, 2021

    @daira
    Contributor

    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.sideeffect as 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.)

  26. Aaron1011 commented on May 17, 2021

    @Aaron1011
    Contributor

    It doesn't appear to be the only example: #52652

    That bug was 'fixed' very quickly, but the fix had to be reverted because it broke the rlua crate. The full fix required an RFC to add new -unwind ABIs 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.

  27. locked and limited conversation to collaborators on May 17, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

A-LLVMArea: Code generation parts specific to LLVM. Both correctness bugs and optimization-related issues.C-bugCategory: This is a bug.E-mediumCall for participation: Medium difficulty. Experience needed to fix: Intermediate.I-needs-decisionIssue: In need of a decision.I-unsoundIssue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/SoundnessP-mediumMedium priorityT-compilerRelevant to the compiler team, which will review and decide on the PR/issue.WG-embeddedWorking group: Embedded systems

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions