Repository navigation
Inlining causes miscompilation of code that mixes target features #116573
Description
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
Curve25519: Conservatively force noinline on ADX code paths.
saethlin commentedon Oct 9, 2023
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/WcMq14MPGRalfJung commentedon Oct 9, 2023
Okay so it's an LLVM bug then it seems. Cc @nikic
nikic commentedon Oct 9, 2023
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.)
Curve25519: Conservatively force noinline on ADX code paths.
61 remaining items
RalfJung commentedon Aug 16, 2024
Ah, try running the example without optimizations. That still reproduces it on the playground.
veluca93 commentedon Aug 16, 2024
To be on the safer side, it can be useful to add
#[inline(never)]to the extern "C" function:RalfJung commentedon Aug 19, 2024
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-featurerather than the attribute. Now is there a way to mix code from crates compiled with and without-Ctarget-featureto cause a problem?Cc @nikic @comex
target_feature_inline_always#145574 as a duplicate of this issuetmiasko commentedon Jun 25, 2026
#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.
laundmo commentedon Aug 13, 2026
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?
tmiasko commentedon Aug 13, 2026
@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.
tmiasko commentedon Aug 14, 2026
Fixed by update to LLVM 23:
RalfJung commentedon Aug 16, 2026
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
Jamesbarford commentedon Sep 8, 2026
I've given it another attempt; #162460