rust-lang / rust-lang/rust

Unintuitive pattern evaluation order of or-patterns and union patterns.

Open
#158,387 24 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

A-MIR A-patterns C-bug T-compiler T-lang T-lang-docs T-opsem
Dominant language
Rust
Stars
119k
Forks
16.1k
PR merge metrics
PR metrics pending

Description

View all comments

This issue is a consequence of https://github.com/rust-lang/unsafe-code-guidelines/issues/540.

When a user has a DIY discriminated union (e.g., for FFI), the user might have a struct containing an enum (the tag) and a union (the payload data). When dealing with such a construction, the reference recommends using pattern-matching to match both the tag and the data in the same pattern. In order for this to not be UB, this relies on the compiler checking the pattern from left to right. (In a struct pattern, it checks the fields in the same order they're mentioned, and short-circuits if anything doesn't match.)

However, as discovered by @steffahn , or-patterns can cause the compiler to change the order that things are matched. This is undocumented, and extremely surprising, if not outright a bug. When combined with the aforementioned construction with unions, this can cause benign-looking code to have UB.

For example, consider the following code:

mod module {
    #[repr(u8)]
    #[derive(Copy, Clone)]
    #[expect(dead_code)]
    enum Tag {
        Integer1,
        Integer2,
        Float,
    }

    #[repr(C)]
    #[derive(Copy, Clone)]
    union Data {
        integer: i64,
        float: f32,
    }

    // A Value is either:
    // * tagged with Integer1 and has integer data
    // * tagged with Integer2 and has integer data
    // * tagged with Float, and has float data
    #[repr(C)]
    #[derive(Copy, Clone)]
    pub struct Value {
        tag: Tag,
        data: Data,
    }

    pub fn is_integer_zero(value: Value) -> bool {
        unsafe {
            match value {
                Value {
                    tag: Tag::Integer1 | Tag::Integer2,
                    data: Data { integer: 0 },
                } => true,
                _ => false,
            }
        }
    }

    pub fn make_float(float: f32) -> Value {
        Value {
            tag: Tag::Float,
            data: Data { float },
        }
    }
}

// Imagine that the above code is in its own crate.

fn main() {
    let value = module::make_float(0.0_f32);
    let _ = module::is_integer_zero(value);
}

This code has code that I think looks reasonable, since the is_integer_zero function body is very similar to the aforementioned code pattern recommended by the reference. The only relevant difference is that it uses a single pattern to match against multiple tag values at once. This causes the compiler to emit a read to the integer field before ever checking the tag field. As a result, the above code has UB.

Miri output
error: Undefined Behavior: reading memory at alloc205[0x8..0x10], but memory is uninitialized at [0xc..0x10], and this operation requires initialized memory
  --> src/main.rs:31:13
   |
31 |             match value {
   |             ^^^^^^^^^^^ Undefined Behavior occurred here
   |
   = help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
   = help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
   = note: stack backtrace:
           0: module::is_integer_zero
               at src/main.rs:31:13: 31:24
           1: main
               at src/main.rs:53:13: 53:43

Uninitialized memory occurred at alloc205[0xc..0x10], in this allocation:
alloc205 (stack variable, size: 16, align: 8) {
    02 __ __ __ __ __ __ __ 00 00 00 00 __ __ __ __ │ .░░░░░░░....░░░░
}

note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace
MIR of is_integer_zero
fn is_integer_zero(_1: Value) -> bool {
    debug value => _1;
    let mut _0: bool;
    let mut _2: u8;

    bb0: {
        switchInt(copy ((_1.1: module::Data).0: i64)) -> [0: bb2, otherwise: bb1];
    }

    bb1: {
        _0 = const false;
        goto -> bb4;
    }

    bb2: {
        _2 = discriminant((_1.0: module::Tag));
        switchInt(move _2) -> [0: bb3, 1: bb3, 2: bb1, otherwise: bb5];
    }

    bb3: {
        _0 = const true;
        goto -> bb4;
    }

    bb4: {
        return;
    }

    bb5: {
        unreachable;
    }
}

cc @Nadrieril @RalfJung

Meta

Reproducible on the playground with version 1.98.0-nightly (2026-06-23 f28ac764c36004fa6a6e)

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reproducing the example on the playground with Rust 1.98.0-nightly and inspect the shown MIR for is_integer_zero, then compare it with the reference's union-pattern guidance. Done means the documented matching behavior no longer permits the or-pattern to read the union payload before the tag check, with the Miri failure addressed.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
compilers
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
38/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.