Skip to content

derive(PartialEq) on enums is unsound with user-defined attribute macros. #148423

@theemathas

Description

@theemathas

dep/src/lib.rs:

use proc_macro::TokenStream;

#[proc_macro_attribute]
pub fn discard(_: TokenStream, _: TokenStream) -> TokenStream {
    TokenStream::new()
}

src/main.rs

#![allow(unused)]

#[derive(PartialEq)]
#[dep::discard]
enum Thing {
    One(i32),
    Two(i32),
}

enum Thing {
    One(i32),
    Two(i32),
    Three(i32),
}

fn main() {
    Thing::Three(1) == Thing::Three(1);
}

The above code causes undefined behavior in safe code. Miri output below:

error: Undefined Behavior: entering unreachable code
  --> src/main.rs:3:10
   |
 3 | #[derive(PartialEq)]
   |          ^^^^^^^^^ 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: BACKTRACE:
   = note: inside `<Thing as std::cmp::PartialEq>::eq` at src/main.rs:3:10: 3:19
note: inside `main`
  --> src/main.rs:17:5
   |
17 |     Thing::Three(1) == Thing::Three(1);
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace

error: aborting due to 1 previous error

This happens because the derive(PartialEq) macro expands to code that assumes that the enum has exactly two variants. However, the discard macro discarded the enum definition seen by PartialEq, so the expanded code instead refers to the second definition of Foo, which has three variants.

Output of cargo +nightly rustc -- -Zunpretty=expanded:

#![feature(prelude_import)]
#![allow(unused)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;

#[automatically_derived]
impl ::core::marker::StructuralPartialEq for Thing { }
#[automatically_derived]
impl ::core::cmp::PartialEq for Thing {
    #[inline]
    fn eq(&self, other: &Thing) -> bool {
        let __self_discr = ::core::intrinsics::discriminant_value(self);
        let __arg1_discr = ::core::intrinsics::discriminant_value(other);
        __self_discr == __arg1_discr &&
            match (self, other) {
                (Thing::One(__self_0), Thing::One(__arg1_0)) =>
                    __self_0 == __arg1_0,
                (Thing::Two(__self_0), Thing::Two(__arg1_0)) =>
                    __self_0 == __arg1_0,
                _ => unsafe { ::core::intrinsics::unreachable() }
            }
    }
}

enum Thing { One(i32), Two(i32), Three(i32), }

fn main() { Thing::Three(1) == Thing::Three(1); }

This bug could plausibly be hit in real code if an attribute macro adds a new variant to an enum.

See also #148277

Meta

rustc --version --verbose:

rustc 1.93.0-nightly (b15a874aa 2025-11-02)
binary: rustc
commit-hash: b15a874aafe7eab9ea3ac2c1d59c7b03e1425027
commit-date: 2025-11-02
host: aarch64-apple-darwin
release: 1.93.0-nightly
LLVM version: 21.1.3

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-macrosArea: All kinds of macros (custom derive, macro_rules!, proc macros, ..)A-proc-macrosArea: Procedural macrosC-bugCategory: This is a bug.I-lang-radarItems that are on lang's radar and will need eventual work or consideration.I-unsoundIssue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/SoundnessP-highHigh priorityT-compilerRelevant to the compiler team, which will review and decide on the PR/issue.T-langRelevant to the language teamT-libsRelevant to the library 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

    Issue actions