Using Atomics with Enums and unitialized memory errors

I opened an issue on crossbeam for this yesterday, but I wanted to try and see if there was a work around and understand it better. Essentially the issue I am running into is that I can't store an enum in an atomic because Rust doesn't initialize the payload on fieldless enum variants. It is UB to read unitialized memory, and Miri catches this error. Let me give a concrete example

use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering;

#[allow(dead_code)]
#[repr(align(8))]
#[derive(Copy, Clone, Debug)]
enum Test {
    Field(u32),
    FieldLess,
}

fn main() {
    let bits: usize = unsafe { std::mem::transmute(Test::FieldLess) };
    let atomic = AtomicUsize::new(bits);
    let bits = atomic.load(Ordering::Acquire);

    unsafe {
        let a: Test = std::mem::transmute(bits); // error here
        println!("{:?}", a);
    }
}

This example I would expect to work fine, but when I run it with cargo +nightly miri run it reports that I am reading uninitialized memory (the enum payload) with transmute. This is the same issue crossbeam is seeing. I have two questions.

  1. What is the correct way to store this enum in an atomic that does not trigger UB?
  2. According to the rust reference reading uninitialized memory is acceptable in only two places: unions and struct padding. If reading uninitialized struct padding is considered defined behavior, why is enum padding not? It seems like they should both follow the same rules.

The place where the error happens seems wrong; the first transmute should cause the error, since the usize now contains some uninitialized data, but that makes the whole number uninitialized.

And effectively, running this through miri, we see the following wording:

error: Undefined Behavior: type validation failed at .<enum-tag>: encountered uninitialized bytes, but expected a valid enum tag
  --> src/main.rs:18:23
   |
18 |         let a: Test = std::mem::transmute(bits); // error here
   |                       ^^^^^^^^^^^^^^^^^^^^^^^^^ type validation failed at .<enum-tag>: encountered uninitialized bytes, but expected a valid enum tag
   |

Pointing to the bits being uninitialized as I suspected.

As to how I'd do this, I'd probably deconstruct and reconstruct it manually, as though as far as I know there's no way to do this safely.


A side note, in a perfect world, we'd have some way to do the following both on stable and without it erroring:

#![feature(core_intrinsics)]

use std::intrinsics::{atomic_load};
use std::cell::UnsafeCell;

#[allow(dead_code)]
#[repr(align(8))]
#[derive(Copy, Clone, Debug)]
enum Test {
    Field(u32),
    FieldLess,
}

fn main() {
    let ucell = UnsafeCell::new(Test::FieldLess);
    
    let loaded = unsafe {
        atomic_load(ucell.get())
    };
}

[quote="OptimisticPeach, post:2, topic:66243"]
As to how I'd do this, I'd probably deconstruct and reconstruct it manually
[/quotes]

How would you deconstruct an enum? As far as I know, they are fairly opaque.

fn test_to_u64(t: &Test) -> u64 {
    match t {
        Test::FieldLess => 0,
        Test::Field(v) => ((*v as u64) << 32) + 1,
    }
}

fn u64_to_test(n: u64) -> Test {
    match n {
        0 => Test::FieldLess,
        n => Test::Field((n >> 32) as u32),
    }
}

And you can store it in the AtomicU64 type.

2 Likes

I believe that this would fail, | 0x100 is only reasonable if we're dealing with a u8.

Instead I'd do | 1 << 32 and conversely & ((1 << 32) - 1).

Right. I fixed the discriminant handling.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.