Repository navigation
Suspected miscompilation with bool as u32 #158206
Copy link
Copy link
Closed
Labels
A-mir-optArea: MIR optimizationsArea: MIR optimizationsC-bugCategory: This is a bug.Category: This is a bug.I-miscompileIssue: Correct Rust code lowers to incorrect machine codeIssue: Correct Rust code lowers to incorrect machine codeP-criticalCritical priorityCritical 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.regression-from-stable-to-stablePerformance or correctness regression from one stable version to another.Performance or correctness regression from one stable version to another.
Description
Godbolt link: https://godbolt.org/z/G4hxdn4Wa
I tried this code (simplified to a ~minimal version):
struct Store {
ring: [Kind; 32],
}
#[derive(Copy, Clone, Eq, PartialEq)]
enum Kind {
A,
B,
C
}
struct Ring(u32);
impl Ring {
fn peek(&self, store: &Store) -> Kind {
store.ring[(self.0 as usize) % 32]
}
pub fn peek_test(&self, store: &Store, expected: Kind) -> bool {
self.peek(store) == expected
}
#[inline]
pub fn consume_test(&mut self, store: &Store, expected: Kind) -> bool {
let x = self.peek_test(store, expected);
self.0 += if x { 1 } else { 0 };
x
}
// Uses a u32 cast instead of the
#[inline]
pub fn consume_test_borked(&mut self, store: &Store, expected: Kind) -> bool {
let x = self.peek_test(store, expected);
self.0 += x as u32;
x
}
}
#[unsafe(no_mangle)]
fn foo(store: &Store, mut lex: Ring) -> usize {
if !lex.consume_test(store, Kind::A) {
return 0;
}
match lex.peek(store) {
Kind::A => 17,
_ => 27,
}
}
#[unsafe(no_mangle)]
fn foo_borked(store: &Store, mut lex: Ring) -> usize {
if !lex.consume_test_borked(store, Kind::A) {
return 0;
}
match lex.peek(store) {
Kind::A => 17,
_ => 27,
}
}I expected foo and foo_borked to have identical behavior/codegen.
Instead, this happened: foo_borked compiled into a function that always takes the Kind::A branch, never returning 27.
; rustc -Copt-level=3 [also =2]
foo:
mov eax, esi
and eax, 31
cmp byte ptr [rdi + rax], 0
je .LBB0_2
xor eax, eax
ret
.LBB0_2:
inc esi
and esi, 31
cmp byte ptr [rdi + rsi], 0
mov ecx, 17
mov eax, 27
cmove rax, rcx
ret
foo_borked:
and esi, 31
xor ecx, ecx
cmp byte ptr [rdi + rsi], 0
mov eax, 17
cmovne rax, rcx
retLooking at the LLVM IR with -Cno-prepopulate-passes it appears that this is a rustc bug, and given that -Zmir-opt-level=0 produces valid code (foo_borked = foo) this is likely a Mir opt issue.
define noundef i64 @foo_borked(ptr noalias nofree noundef readonly captures(address, read_provenance) dereferenceable(32) %store, i32 noundef %0) unnamed_addr {
start:
%__self_discr.dbg.spill = alloca [8 x i8], align 8
%__arg1_discr.dbg.spill = alloca [8 x i8], align 8
%expected.dbg.spill1 = alloca [1 x i8], align 1
%expected.dbg.spill = alloca [1 x i8], align 1
%store.dbg.spill = alloca [8 x i8], align 8
%x = alloca [1 x i8], align 1
%_0 = alloca [8 x i8], align 8
%lex = alloca [4 x i8], align 4
store i32 %0, ptr %lex, align 4
store ptr %store, ptr %store.dbg.spill, align 8
store i8 0, ptr %expected.dbg.spill, align 1
store i8 0, ptr %expected.dbg.spill1, align 1
store i64 0, ptr %__arg1_discr.dbg.spill, align 8
call void @llvm.lifetime.start.p0(ptr %x)
%_14 = load i32, ptr %lex, align 4
%_13 = zext i32 %_14 to i64
%_12 = urem i64 %_13, 32
%_15 = icmp ult i64 %_12, 32
br i1 %_15, label %bb7, label %panic
bb7:
%1 = getelementptr inbounds nuw i8, ptr %store, i64 %_12
%_10 = load i8, ptr %1, align 1
%__self_discr = zext i8 %_10 to i64
store i64 %__self_discr, ptr %__self_discr.dbg.spill, align 8
; %x is being loaded here without a prior store, becoming a poison
%2 = load i8, ptr %x, align 1
%3 = trunc nuw i8 %2 to i1
%4 = icmp ule i1 %3, true
call void @llvm.assume(i1 %4)
%_8 = zext i1 %3 to i32
%5 = load i32, ptr %lex, align 4
%6 = add i32 %5, %_8
store i32 %6, ptr %lex, align 4
%7 = icmp eq i64 %__self_discr, 0
br i1 %7, label %bb1, label %bb2
panic:
call void @core[dcb47395e1cc1d61]::panicking::panic_bounds_check(i64 noundef %_12, i64 noundef 32, ptr noalias nofree noundef readonly align 8 captures(address, read_provenance) dereferenceable(24) @alloc_7414d1c37f862b99e65b1576388dc6f7) #4
unreachable
bb1:
call void @llvm.lifetime.end.p0(ptr %x)
%_19 = load i32, ptr %lex, align 4
%_18 = zext i32 %_19 to i64
%_17 = urem i64 %_18, 32
%_20 = icmp ult i64 %_17, 32
br i1 %_20, label %bb8, label %panic2
bb2:
store i64 0, ptr %_0, align 8
call void @llvm.lifetime.end.p0(ptr %x)
br label %bb6
bb8:
%8 = getelementptr inbounds nuw i8, ptr %store, i64 %_17
%_5 = load i8, ptr %8, align 1
%_7 = zext i8 %_5 to i64
%9 = icmp eq i64 %_7, 0
br i1 %9, label %bb4, label %bb3
panic2:
call void @core[dcb47395e1cc1d61]::panicking::panic_bounds_check(i64 noundef %_17, i64 noundef 32, ptr noalias nofree noundef readonly align 8 captures(address, read_provenance) dereferenceable(24) @alloc_7414d1c37f862b99e65b1576388dc6f7) #4
unreachable
bb4:
store i64 17, ptr %_0, align 8
br label %bb5
bb3:
store i64 27, ptr %_0, align 8
br label %bb5
bb5:
br label %bb6
bb6:
%10 = load i64, ptr %_0, align 8
ret i64 %10
}Meta
rustc --version --verbose:
rustc 1.98.0-nightly (bc2112ed5 2026-06-18)
binary: rustc
commit-hash: bc2112ed56c99fa649e09ab3ab286afab3d9059a
commit-date: 2026-06-18
host: x86_64-unknown-linux-gnu
release: 1.98.0-nightly
LLVM version: 22.1.7
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
A-mir-optArea: MIR optimizationsArea: MIR optimizationsC-bugCategory: This is a bug.Category: This is a bug.I-miscompileIssue: Correct Rust code lowers to incorrect machine codeIssue: Correct Rust code lowers to incorrect machine codeP-criticalCritical priorityCritical 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.regression-from-stable-to-stablePerformance or correctness regression from one stable version to another.Performance or correctness regression from one stable version to another.
Activity
tmiasko commentedon Jun 21, 2026
SimplifyComparisonIntegral removes a definition of
_3, even though it remains used after the transformation:hanna-kruppe commentedon Jun 21, 2026
The part of the pass that removes the definition of
_3reasons that theswitchIntmoved it, so "it cannot be used later on". Leaving aside the undecided semantic of move operands, that reasoning is plain wrong since the local can be used before theswitchIntand there's no check that the local is single-use. In fact, it seems desirable to apply the optimization even if the comparison result is used elsewhere.hanna-kruppe commentedon Jun 21, 2026
@rustbot claim
Rollup merge of rust-lang#158214 - hanna-kruppe:issue-158206, r=cjgillot
Rollup merge of rust-lang#158214 - hanna-kruppe:issue-158206, r=cjgillot
qti3e commentedon Jun 21, 2026
Thanks for the fast PR! Given that this is (imho) a very severe bug (silent miscompilation of perfectly safe code) is the fix going to be back ported to stable? I can reproduce the bug on 1.94, 1.95, 1.96 and nightly. (1.93 seems to be the latest version without the bug present)
9 remaining items
Rollup merge of #158214 - hanna-kruppe:issue-158206, r=cjgillot
jackh726 commentedon Jun 23, 2026
Keeping open until backport is resolved.
theemathas commentedon Jun 26, 2026
The beta backport has been merged in #158420.
Let's keep this open until the stable backport in #158331 is merged.
theemathas commentedon Jun 27, 2026
#158331 has been merged
theemathas commentedon Jul 1, 2026
Does this miscompile only affect code that uses
asto turn a boolean into an integer? Or does it affect other code too? Is there a short description of the rough kind of shape of code that might be affected by this miscompilation?hanna-kruppe commentedon Jul 1, 2026
The faulty optimization isn’t specific to bool-as-int casts. The relevant MIR pattern is:
moveoperand rather than acopyoperand (note that this excludes uses of the comparison result after the branch being optimized)charcomparison (SSA local equal/not-equal to a constant)It’s hard for me to describe exactly what source code patterns will lead to this MIR. The problematic pass runs very late in the optimization pipeline. Earlier passes may introduce the required ingredients even if they’re not apparent at the source level (e.g., GVN unifying two separate
x == Cexpressions). On the other hand, the requirements “moveoperand” and “SSA local” seems to save some simple source code patterns that look like they would be affected.Rather than trying to guess whether specific code may trigger this miscompilation, I’d suggest updating the compiler or disabling the pass via
-Zmir-passes.upgrade rust-overlay to 2026-07-10 (rustc 1.97.0), scrub pdx-nxst-001…
build: pin the toolchain to Rust 1.98.1