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)
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:
This code has code that I think looks reasonable, since the
is_integer_zerofunction 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 theintegerfield before ever checking thetagfield. As a result, the above code has UB.Miri output
MIR of is_integer_zero
cc @Nadrieril @RalfJung
Meta
Reproducible on the playground with version
1.98.0-nightly (2026-06-23 f28ac764c36004fa6a6e)