Skip to content

Suspected miscompilation with bool as u32 #158206

Closed

Description

@qti3e

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
        ret

Looking 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

Activity

  1. added
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    on Jun 21, 2026
  2. added
    T-compilerRelevant to the compiler team, which will review and decide on the PR/issue.
    A-mir-optArea: MIR optimizations
    I-miscompileIssue: Correct Rust code lowers to incorrect machine code
    on Jun 21, 2026
  3. added
    I-prioritizeIssue needs a team member to assess the impact. Will be replaced by P-{low,medium,high,critical}
    on Jun 21, 2026
  4. tmiasko commented on Jun 21, 2026

    @tmiasko
    Contributor

    SimplifyComparisonIntegral removes a definition of _3, even though it remains used after the transformation:

    @@ -1,4 +1,4 @@
    -// MIR for `foo_borked` before SimplifyComparisonIntegral
    +// MIR for `foo_borked` after SimplifyComparisonIntegral
     
     fn foo_borked(_1: &Store, _2: Ring) -> usize {
         debug store => _1;
    @@ -52,6 +52,7 @@
         }
     
         bb1: {
    +        StorageDead(_12);
             StorageDead(_3);
             StorageLive(_4);
             StorageLive(_16);
    @@ -68,6 +69,7 @@
         }
     
         bb2: {
    +        StorageDead(_12);
             _0 = const 0_usize;
             StorageDead(_3);
             goto -> bb6;
    @@ -98,14 +100,14 @@
             StorageDead(_11);
             StorageLive(_12);
             _12 = discriminant(_7);
    -        _3 = Eq(copy _12, const 0_isize);
    -        StorageDead(_12);
    +        nop;
    +        nop;
             StorageDead(_7);
             StorageLive(_6);
             _6 = copy _3 as u32 (IntToInt);
             (_2.0: u32) = Add(copy (_2.0: u32), move _6);
             StorageDead(_6);
    -        switchInt(move _3) -> [0: bb2, otherwise: bb1];
    +        switchInt(copy _12) -> [0: bb1, otherwise: bb2];
         }
     
         bb8: {
  5. hanna-kruppe commented on Jun 21, 2026

    @hanna-kruppe
    Contributor

    The part of the pass that removes the definition of _3 reasons that the switchInt moved 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 the switchInt and 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.

  6. hanna-kruppe commented on Jun 21, 2026

    @hanna-kruppe
    Contributor

    @rustbot claim

  7. removed
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    on Jun 21, 2026
  8. deleted a comment from ipezygj on Jun 21, 2026
  9. added 2 commits that reference this issue on Jun 21, 2026
    3c9acdf
    5030e61
  10. qti3e commented on Jun 21, 2026

    @qti3e
    ContributorAuthor

    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)

  11. 9 remaining items

  12. added a commit that references this issue on Jun 22, 2026
    8513af5
  13. jackh726 commented on Jun 23, 2026

    @jackh726
    Member

    Keeping open until backport is resolved.

  14. reopened this on Jun 25, 2026
  15. theemathas commented on Jun 26, 2026

    @theemathas
    Contributor

    The beta backport has been merged in #158420.

    Let's keep this open until the stable backport in #158331 is merged.

  16. theemathas commented on Jun 27, 2026

    @theemathas
    Contributor

    #158331 has been merged

  17. theemathas commented on Jul 1, 2026

    @theemathas
    Contributor

    Does this miscompile only affect code that uses as to 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?

  18. hanna-kruppe commented on Jul 1, 2026

    @hanna-kruppe
    Contributor

    The faulty optimization isn’t specific to bool-as-int casts. The relevant MIR pattern is:

    • A single basic block ending with a branch on a boolean
    • The branch condition is a move operand rather than a copy operand (note that this excludes uses of the comparison result after the branch being optimized)
    • That boolean is computed earlier in the same basic block as integer or char comparison (SSA local equal/not-equal to a constant)
    • That comparison result is also used for any other computation in the same basic block before the branch. Note that this excludes “another branch on the same bool” as that generally means another basic block. Casting to integer is the most likely but not only option here.

    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 == C expressions). On the other hand, the requirements “move operand” 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.

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

Metadata

Metadata

Assignees

Labels

A-mir-optArea: MIR optimizationsC-bugCategory: This is a bug.I-miscompileIssue: Correct Rust code lowers to incorrect machine codeP-criticalCritical priorityT-compilerRelevant 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.

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions