Skip to content

Operational semantics for AtomicMaybeUninit::compare_exchange #449

@RalfJung

Description

@RalfJung

Rust currently has no way to do atomic operations on types that may contain uninitialized bytes. crossbeam's AtomicCell resorts to using the standard library Atomic* types instead, but that doesn't entirely work: load will be UB if there is any uninitialized byte, and store requires transmuting the to-be-stored T into an integer which is likewise UB if there are any uninitialized bytes. (Plus if there is pointer provenance, that is getting lost.)

(Note that (u16, u8) anyway cannot be used with atomic operations since it is insufficiently aligned. Generally atomic operations have alignment requirements equal to their size, so the only way for other types to have the right size and alignment is for them to be newtypes, or to use repr(align).)

So there is a clear demand for something like AtomicMaybeUninit. There even is an inline assembly implementation of it. It would be great if Rust could provide this natively. Mostly this is a libs-api question of how to best expose these operations, but for read-modify-write operations things are more interesting: what is AtomicMaybeUninit::compare_exchange supposed to behave like? Usually comparing uninit values leads to UB, even on the LLVM level.

Avoid the uninit comparison

One option would be to try and somehow entirely avoid comparisons of uninit memory. This would also avoid having to get any new guarantees from LLVM. An AtomicMaybeUninit type could have the invariant that the data inside it is always frozen; compare_exchange could freeze the provided value-to-compare, and then the comparison works on entirely well-defined bits.

However, this invariant is broken by get_mut, since that function could be used to de-initialize padding bytes. So maybe we don't have such an invariant but freeze the in-memory data before comparison? But then that freezing must somehow be atomic, and what if the comparison fails -- usually this would be just a read-only access, but now the freeze made this a write somehow?

Allow the uninit comparison

We could say that comparing data with uninit contents just leads non-deterministic results (and try to convince LLVM to add that to their concurrency spec). However this introduces liveness issues: always returning "inequal" would be a legal implementation if any of the in-memory bytes is uninitialized. And it seems like the C++ wording for compare_exchange_strong on unions indeed allows comparisons to always fail. For padding outside union types however C++ guarantees that comparison will ignore padding bytes; I have no idea how compilers are implementing this -- they might have to use locking?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions