Skip to content

Inlining causes miscompilation of code that mixes target features #116573

Closed

Description

@RalfJung

View all comments

The following code ought to be completely fine and UB-free:

use std::mem::transmute;
#[cfg(target_arch = "x86")]
use std::arch::x86::*;
#[cfg(target_arch = "x86_64")]
use std::arch::x86_64::*;

extern "C" fn no_target_feature(_dummy: f32, x: __m256) {
    let val = unsafe { transmute::<_, [u32; 8]>(x) };
    dbg!(val);
}

#[inline(always)] 
fn no_target_feature_intermediate(dummy: f32, x: __m256) {
    no_target_feature(dummy, x);
}

#[target_feature(enable = "avx")]
unsafe fn with_target_feature(x: __m256) {
  // Critical call: caller and callee have different target features.
  // However, we use the Rust ABI, so this is fine.
  no_target_feature_intermediate(0.0, x);
}

fn main() {
    assert!(is_x86_feature_detected!("avx"));
    // SAFETY: we checked that the `avx` feature is present.
    unsafe {
        with_target_feature(transmute([1; 8]));
    }
}

There's some unsafe going on, but the safety comment explains why that is okay. We are even taking care to follow the target-feature related ABI rules (see #115476); all calls between functions with different target-features use the "Rust" ABI.

And yet, this prints (when built without optimizations)

[src/main.rs:9] val = [
    1,
    1,
    1,
    1,
    538976288,
    538976288,
    538976288,
    538976288,
]

The value got clobbered while being passed through the various functions.

Replacing inline(always) by inline(never) makes the issue disappear. But inline attributes must never cause miscompilation, so there's still a soundness bug here.

I don't know if this is the MIR inliner (Cc @rust-lang/wg-mir-opt) or the LLVM inliner going wrong.

Here's an LLVM issue for the problem: llvm/llvm-project#70563

Activity

  1. added
    I-unsoundIssue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/Soundness
    on Oct 9, 2023
  2. added
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    I-prioritizeIssue needs a team member to assess the impact. Will be replaced by P-{low,medium,high,critical}
    on Oct 9, 2023
  3. saethlin commented on Oct 9, 2023

    @saethlin
    Member

    This still miscompiles with -Zmir-opt-level=0. The playground does not let you pass flags (rust-lang/rust-playground#781), so I generally advise against using it. godbolt supports flags, execution, setting environment variables, and picking among old toolchains: https://godbolt.org/z/WcMq14MPG

  4. added
    A-LLVMArea: Code generation parts specific to LLVM. Both correctness bugs and optimization-related issues.
    T-compilerRelevant to the compiler team, which will review and decide on the PR/issue.
    C-bugCategory: This is a bug.
    and removed
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    on Oct 9, 2023
  5. RalfJung commented on Oct 9, 2023

    @RalfJung
    MemberAuthor

    Okay so it's an LLVM bug then it seems. Cc @nikic

  6. nikic commented on Oct 9, 2023

    @nikic
    Contributor

    Is there a way to reproduce this without #[inline(always)]? Forcing inlining disables target-feature safety checks in LLVM.

    (Incidentally, there was an attempt to not do that in LLVM 17, but this was reverted due to the large amount of regressions it caused. People rely on that a lot, including in Rust.)

  7. 61 remaining items

  8. RalfJung commented on Aug 16, 2024

    @RalfJung
    MemberAuthor

    Ah, try running the example without optimizations. That still reproduces it on the playground.

    [src/main.rs:9:5] val = [
        1,
        1,
        1,
        1,
        0,
        0,
        8192,
        0,
    ]
    
  9. veluca93 commented on Aug 16, 2024

    @veluca93
    Contributor

    To be on the safer side, it can be useful to add #[inline(never)] to the extern "C" function:

    #[cfg(target_arch = "x86")]
    use std::arch::x86::*;
    #[cfg(target_arch = "x86_64")]
    use std::arch::x86_64::*;
    use std::mem::transmute; 
    
    #[inline(never)]                                                           
    extern "C" fn no_target_feature(_dummy: f32, x: __m256) {
        let val = unsafe { transmute::<_, [u32; 8]>(x) };
        dbg!(val);
    }
    
    #[inline(always)]
    fn no_target_feature_intermediate(dummy: f32, x: __m256) {
        no_target_feature(dummy, x);
    }
    
    #[target_feature(enable = "avx")]
    unsafe fn with_target_feature(x: __m256) {
        // Critical call: caller and callee have different target features.
        // However, we use the Rust ABI, so this is fine.
        no_target_feature_intermediate(0.0, x);
    }
    
    fn main() {
        assert!(is_x86_feature_detected!("avx"));
        // SAFETY: we checked that the `avx` feature is present.
        unsafe {
            with_target_feature(transmute([1; 8]));
        }
    }
    
  10. RalfJung commented on Aug 19, 2024

    @RalfJung
    MemberAuthor

    Okay, so the issue still exists. But the question remains whether there's a reproducer that works even with #127731, i.e. a reproducer that only uses vector types on extern "C" functions that have the right feature flags enabled.

    The original reproducer was a "no target feature" function taking a vector and having its call site incorrectly inlined into a "with target feature" function. That can't happen any more, so we need a "with target feature" function and coerce LLVM into inlining its call site into a "no target feature" function. It will only do that when the "with target feature" function has inline(always), which means the target feature must be enabled via -Ctarget-feature rather than the attribute. Now is there a way to mix code from crates compiled with and without -Ctarget-feature to cause a problem?

    Cc @nikic @comex

  11. added
    A-target-featureArea: Enabling/disabling target features like AVX, Neon, etc.
    on Aug 27, 2024
  12. tmiasko commented on Jun 25, 2026

    @tmiasko
    Contributor

    #145574 (comment) contains a working reproducer as of rustc 1.98.0-nightly (91fe22da8 2026-06-21).

    It is an updated version of those discussed earlier in the thread, where LLVM argument promotion was introducing calls with ABI depending on target features (they stopped reproducing at some point, most likely due to changes in LLVM pass ordering).

    It demonstrates that frontend checks are poor substitute for missing backend inlining checks, since they are separated by an arbitrary sequence of LLVM optimizations. The caller and callee examined in the frontend differs from those used at inlining time, so checks suffer from time-of-check to time-of-use bugs.

    llvm/llvm-project#205113 will stop inlining functions with incompatible target features.

  13. added
    llvm-fixed-upstreamIssue expected to be fixed by the next major LLVM upgrade, or backported fixes
    on Jul 7, 2026
  14. laundmo commented on Aug 13, 2026

    @laundmo
    Contributor

    from what i can tell, many usages of runtime function multiversioning rely on inline/inline(always) on functions without target-feature attributes to cause those functions to be inlined into the multiple versions of the calling function, and their code subsequently compiled with each target feature enabled.

    Would the LLVM change break this strategy?

  15. tmiasko commented on Aug 13, 2026

    @tmiasko
    Contributor

    @laundmo it depends on specifics. rustc nightly is based on LLVM 23, which includes fixes for both inliner pass and always inliner pass, so you can find out experimentally. If inlining isn't performed but would be sound to perform, please open a new issue.

  16. RalfJung commented on Aug 16, 2026

    @RalfJung
    MemberAuthor

    Nice. :)

    So, can we then experiment with unstably allowing inline(always) on target-feature functions?
    (On LLVM 22 and older we should still implicitly downgrade this to just inline.)
    Cc @Jamesbarford

  17. Jamesbarford commented on Sep 8, 2026

    @Jamesbarford
    Contributor

    I've given it another attempt; #162460

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-LLVMArea: Code generation parts specific to LLVM. Both correctness bugs and optimization-related issues.A-codegenArea: Code generationA-target-featureArea: Enabling/disabling target features like AVX, Neon, etc.C-bugCategory: This is a bug.I-miscompileIssue: Correct Rust code lowers to incorrect machine codeI-unsoundIssue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/SoundnessP-highHigh priorityT-compilerRelevant to the compiler team, which will review and decide on the PR/issue.WG-llvmWorking group: LLVM backend code generationllvm-fixed-upstreamIssue expected to be fixed by the next major LLVM upgrade, or backported fixes

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions