Skip to content

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

Description

@theemathas

View all comments

This issue is a consequence of rust-lang/unsafe-code-guidelines#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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-MIRArea: Mid-level IR (MIR) - https://blog.rust-lang.org/2016/04/19/MIR.htmlA-patternsRelating to patterns and pattern matchingC-bugCategory: This is a bug.I-lang-docs-nominatedNominated for discussion during a lang-docs team meeting.T-compilerRelevant to the compiler team, which will review and decide on the PR/issue.T-langRelevant to the language teamT-lang-docsRelevant to the lang-docs team.T-opsemRelevant to the opsem teamneeds-triageThis issue may need triage. Remove it if it has been sufficiently triaged.

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions