Stream: t-lang/wg-unsafe-code-guidelines

Topic: Move of special unions in const-eval


HeroicKatora (Feb 26 2020 at 16:22, on Zulip):

This is a question for clarification which I didn't quite want to make into an issue on the main repo, miri or unsafe-code-guidelines, and it is partially addressed by a thread in the repository already.

The constant evaluation of a move of some unions seems to differ from the semantics of non-constant evaluation. The union instance is not regarded as a bag-of-bits as aluded to in the reference and also this unsafe-code-guidelines issue. This occurs only if the unions has a single scalar scalar field and otherwise ZSTs, which is pretty specific but occurs for MaybeUninit if the type parameter is a scalar. If the field is of other types such as arrays ([scalar; N]) or if the other field is not a ZST then the semantics seem to agree.

I tried this code:

#![feature(const_if_match)] // Only related to make_1u8_bag, all of them set by Miri
#![feature(const_fn)]
#![feature(const_mut_refs)]
#![feature(const_panic)]
#![feature(const_raw_ptr_deref)]

// Or, equivalently: `MaybeUninit`.
pub union BagOfBits<T: Copy> {
    uninit: (),
    _storage: T,
}

pub const fn make_1u8_bag<T: Copy>() -> BagOfBits<T> {
    assert!(core::mem::size_of::<T>() >= 1);
    let mut bag = BagOfBits { uninit: () };
    unsafe { (&mut bag as *mut _ as *mut u8).write(1); };
    bag
}

pub fn check_bag<T: Copy>(bag: &BagOfBits<T>) {
    let val = unsafe { (bag as *const _ as *const u8).read() };
    assert_eq!(val, 1);
}

fn main() {
    check_bag(&make_1u8_bag::<[usize; 1]>()); // Fine
    check_bag(&make_1u8_bag::<usize>()); // Fine

    const CONST_ARRAY_BAG: BagOfBits<[usize; 1]> = make_1u8_bag();
    check_bag(&CONST_ARRAY_BAG); // Fine.
    const CONST_USIZE_BAG: BagOfBits<usize> = make_1u8_bag();
    check_bag(&CONST_USIZE_BAG); // Panics
}

Playground

I expected to see this happen: All assertions succeed.

Instead, this happened: The check where a union of type BagOfBits<usize> is moved during constant evaluation, panics. When putting everything through Miri it panics with the message of trying to read an uninitialized byte. The check where the union of type BagOfBits<[usize; 1]> is used instead runs fine in both cases.

error: Miri evaluation error: attempted to read undefined bytes
  --> src/main.rs:21:5
   |
21 |     assert_eq!(val, 1);
   |     ^^^^^^^^^^^^^^^^^^^ attempted to read undefined bytes

As far as I am aware, when constant evaluation reads scalars then it propagates uninitialized bytes. If that were to happen then it would explain the observed behaviour but it seems weird why the union copy would use a scalar read.

Now, the question is: Are there any stable guarantees that this behaviour violates? If not, are there any that are likely to be given in the future? And if not, is the behaviour for a field of type [usize; 1] or a second non-ZST field stably sound?

RalfJ (Feb 26 2020 at 16:38, on Zulip):

@HeroicKatora interesting find! I think this is more in const-eval territory than UCG. And I think it is a bug.
What happens, most likely, is that const eval realizes that BagOfBits<usize> is a Scalar, and scalars get some special treatment that "amplifies" a partially uninitialized result into being fully uninitialized.
But before we start having a discussion here that will get lost -- could you please open an issue, and Cc me? This is worth tracking.

HeroicKatora (Feb 26 2020 at 16:44, on Zulip):

If you too think this is a bug, then I'll open a tracking issue :smiley:

Last update: Jun 05 2020 at 23:10UTC