Skip to content

match on uninhabited type does not trigger UB in Miri #142394

@theemathas

Description

@theemathas
Contributor

I tried this code:

#![allow(deref_nullptr)]

enum Never {}

fn main() {
    unsafe {
        match *std::ptr::null::<Result<Never, Never>>() {
            Ok(_) => {
                lol();
            }
            Err(_) => {
                wut();
            }
        }
    }
}

fn lol() {
    println!("lol");
}

fn wut() {
    println!("wut");
}

I expected to see this happen: Running this with Miri detects undefined behavior.

Instead, this happened: Running this with Miri outputs "lol", and program exits successfully.

It seems like rustc is discarding the second match arm entirely in the MIR?

MIR
// WARNING: This output format is intended for human consumers only
// and is subject to change without notice. Knock yourself out.
// HINT: See also -Z dump-mir for MIR at specific points during compilation.
fn main() -> () {
    let mut _0: ();
    let mut _1: *const std::result::Result<Never, Never>;
    let _2: ();

    bb0: {
        _1 = null::<Result<Never, Never>>() -> [return: bb1, unwind continue];
    }

    bb1: {
        _2 = lol() -> [return: bb2, unwind continue];
    }

    bb2: {
        return;
    }
}

// MIR for lol() and wut() omitted for brevity

Meta

Reproducible on the playground with 1.89.0-nightly (2025-06-10 1677d46cb128cc8f285d)

@rustbot labels +A-patterns

Activity

added
needs-triageThis issue may need triage. Remove it if it has been sufficiently triaged.
A-patternsRelating to patterns and pattern matching
and removed on Jun 12, 2025
changed the title [-]`match` on a null place with uninhabited type has strange behavior[/-] [+]`match` on uninhabited type has strange behavior[/+] on Jun 12, 2025
xizheyin

xizheyin commented on Jun 12, 2025

@xizheyin
Contributor

This may because, if either type parameter of Result is Never, the Rust compiler treats the type as uninhabitable and optimizes away the code, so dereferencing a null pointer doesn't cause a crash; but with Result<i32, i32>, the type is inhabited, so dereferencing null will lead to a segmentation fault.

theemathas

theemathas commented on Jun 12, 2025

@theemathas
ContributorAuthor

Miri is supposed to run on unoptimized code.

saethlin

saethlin commented on Jun 12, 2025

@saethlin
Member

This may because, if either type parameter of Result is Never, the Rust compiler treats the type as uninhabitable and optimizes away the code, so dereferencing a null pointer doesn't cause a crash; but with Result<i32, i32>, the type is inhabited, so dereferencing null will lead to a segmentation fault.

No. The fact that the pointer here is null is an unnecessary distraction that I didn't realize last night. Result<Never, Never> is a ZST, and our documentation clearly states:

For memory accesses of size zero, every pointer is valid, including the null pointer.

Moreover, if you look at the MIR you can see that we never even emit a read. The dereference is fine.

This code has the same strange lowering:

enum Never {}

fn main(p: *const Result<Never, Never>) {
    unsafe {
        match *p {
            Ok(_) => {
                lol();
            }
            Err(_) => {
                wut();
            }
        }
    }
}

fn lol() {
    println!("lol");
}

fn wut() {
    println!("wut");
}
kpreid

kpreid commented on Jun 12, 2025

@kpreid
Contributor

This seems closely related to rust-lang/unsafe-code-guidelines#540, and arguably corresponds to a gap in the definition of what is UB — what are the steps executing a pattern match actually performs? Which one of those steps is is UB when an uninhabited value is matched — not ever produced, yet matched?

Here is a simpler program that also passes Miri:

enum Never {} // also works with ! type

fn main() {
    unsafe {
        match *std::ptr::dangling::<Never>() {
            _ => println!("never happened"),
        }
    }
}

This one lacks the puzzle of “which of the match arms should be run? neither, of course”, though, and one could perhaps argue that it shouldn’t be UB since there is no branch and Rust prefers not to have entirely gratuitous UB. Yet, what semantics justifies that?

kpreid

kpreid commented on Jun 12, 2025

@kpreid
Contributor

Here are a couple of possible high-level statements of where the UB should be (these do not reflect the current documented semantics of Rust, but could in the future):

  1. The discriminant of Result<Never, Never> is, itself, uninhabited. Therefore, reading it to match it is “producing an invalid value” UB for std::mem::Discriminant<Result<Never, Never>> or an equivalent internal construct.
  2. A match arm whose pattern is uninhabited should be unreachable; its body should never execute. Therefore, none of the arms of these matches match, and control flow falls off the end of the match, which should be defined as UB.
RalfJung

RalfJung commented on Jun 13, 2025

@RalfJung
Member

I agree this should be UB -- I'd have expected a discriminant read that causes UB here. But somehow we don't get one?
Cc @Nadrieril

changed the title [-]`match` on uninhabited type has strange behavior[/-] [+]`match` on uninhabited type does not trigger UB in Miri[/+] on Jun 13, 2025
RalfJung

RalfJung commented on Jun 13, 2025

@RalfJung
Member

The MIR is already odd immediately after being built:

fn main(_1: *const Result<Never, Never>) -> () {
    debug p => _1;
    let mut _0: ();
    let _2: ();
    let _3: ();

    bb0: {
        PlaceMention((*_1));
        falseEdge -> [real: bb3, imaginary: bb1];
    }

    bb1: {
        StorageLive(_3);
        _3 = wut() -> [return: bb5, unwind: bb7];
    }

    bb2: {
        FakeRead(ForMatchedPlace(None), (*_1));
        unreachable;
    }

    bb3: {
        StorageLive(_2);
        _2 = lol() -> [return: bb4, unwind: bb7];
    }

Note the entire lack of a discriminant check.

Nadrieril

Nadrieril commented on Jun 13, 2025

@Nadrieril
Member

Yep, this is something we're about to fix. Today we skip discriminant reads for enums based on the presence of empty variants; this was considered incorrect and t-lang FCP-approved a fix here (which is apparently waiting on me?? oops).

RalfJung

RalfJung commented on Jun 13, 2025

@RalfJung
Member

I recall having discussions around this, but... that PR mentions closures and there are no closures involved here?

added
T-compilerRelevant to the compiler team, which will review and decide on the PR/issue.
A-MIRArea: Mid-level IR (MIR) - https://blog.rust-lang.org/2016/04/19/MIR.html
and removed
needs-triageThis issue may need triage. Remove it if it has been sufficiently triaged.
on Jun 13, 2025
Nadrieril

Nadrieril commented on Jun 20, 2025

@Nadrieril
Member

Ah yeah you're right. That PR is only the first step towards fixing that, but we intend to fix that eventually.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-MIRArea: Mid-level IR (MIR) - https://blog.rust-lang.org/2016/04/19/MIR.htmlA-miriArea: The miri toolA-patternsRelating to patterns and pattern matchingC-bugCategory: This is a bug.T-compilerRelevant to the compiler team, which will review and decide on the PR/issue.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @RalfJung@kpreid@theemathas@Nadrieril@saethlin

        Issue actions

          `match` on uninhabited type does not trigger UB in Miri · Issue #142394 · rust-lang/rust