Skip to content

[RFC] AtomicPerByte (aka "atomic memcpy") - #3301

Open
m-ou-se wants to merge 9 commits into
rust-lang:masterfrom
m-ou-se:atomic-memcpy
Open

[RFC] AtomicPerByte (aka "atomic memcpy")#3301
m-ou-se wants to merge 9 commits into
rust-lang:masterfrom
m-ou-se:atomic-memcpy

Conversation

@m-ou-se

@m-ou-se m-ou-se commented Aug 14, 2022

Copy link
Copy Markdown
Member

@m-ou-se m-ou-se added the T-libs-api Relevant to the library API team, which will review and decide on the RFC. label Aug 14, 2022
@bjorn3

bjorn3 commented Aug 14, 2022

Copy link
Copy Markdown
Member

cc @ojeda

@ibraheemdev

ibraheemdev commented Aug 14, 2022

Copy link
Copy Markdown
Member

This could mention the atomic-maybe-uninit crate in the alternatives section (cc @taiki-e).

@ghost

ghost commented Aug 14, 2022

Copy link
Copy Markdown

With some way for the language to be able to express "this type is valid for any bit pattern", which project safe transmute presumably will provide (and that exists in the ecosystem as bytemuck and zerocopy and probably others), I'm wondering if it would be better to return an AtomicPerByteRead<T>(MaybeUninit<T>) which we/the ecosystem could provide a safe into_inner (returning a T) if T is valid for any bit pattern.

This would also require removing the safe uninit method. But you could always presumably do an AtomicPerByte<MaybeUninit<T>> with no runtime cost to passing MaybeUninit::uninit() to new.

That's extra complexity, but means that with some help from the ecosystem/future stdlib work, this can be used in 100% safe code, if the data is fine with being torn.

@Lokathor

Copy link
Copy Markdown
Contributor

The "uninit" part of MaybeUninit is essentially not a bit pattern though. That's the problem. Even if a value is valid "for all bit patterns", you can't unwrap uninit memory into that type.

not without the fabled and legendary Freeze Intrinsic anyway.

@T-Dark0

T-Dark0 commented Aug 14, 2022

Copy link
Copy Markdown

On the other hand, AnyBitPatternOrPointerFragment isn't a type we have, nor really a type we strictly need for this. Assuming tearing can't deinitialize initialized memory, then MaybeUninit would suffice I think?

@programmerjake

Copy link
Copy Markdown
Member

note that LLVM already implements this operation:
llvm.memcpy.element.unordered.atomic Intrinsic
with an additional fence operation for acquire/release.

@comex

comex commented Aug 15, 2022

Copy link
Copy Markdown

The trouble with that intrinsic is that unordered is weaker than monotonic aka Relaxed, and it can't easily be upgraded. There's no "relaxed fence" if the ordering you want is Relaxed; and even if the ordering you want is Acquire or Release, combining unordered atomic accesses with fences doesn't produce quite the same result. Fences provide additional guarantees regarding other memory accessed before/after the atomic access, but they don't do anything to restore the missing "single total order" per address of the atomic accesses themselves.

Comment thread text/3301-atomic-memcpy.md
Comment thread text/3301-atomic-memcpy.md
Comment on lines +180 to +181
- In order for this to be efficient, we need an additional intrinsic hooking into
special support in LLVM. (Which LLVM needs to have anyway for C++.)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do you plan to implement this until LLVM implements this?

I don't think it is necessary to explain the implementation details in the RFC, but if we provide an unsound implementation until the as yet unmerged C++ proposal is implemented in LLVM in the future, that seems to be a problem.

(Also, if the language provides the functionality necessary to implement this soundly in Rust, the ecosystem can implement this soundly as well without inline assembly.)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't looked into the details yet of what's possible today with LLVM. There's a few possible outcomes:

  • We wait until LLVM supports this. (Or contribute it to LLVM.) This feature is delayed until some point in the future when we can rely on an LLVM version that includes it.
  • Until LLVM supports it, we use a theoretically unsound but known-to-work-today hack like ptr::{read_volatile, write_volatile} combined with a fence. In the standard library we can more easily rely on implementation details of today's compiler.
  • We use the existing llvm.memcpy.element.unordered.atomic, after figuring out the consequences of the unordered property.
  • Until LLVM supports appears, we implement it in the library using a loop of AtomicUsize::load()/store()s and a fence, possibly using an efficient inline assembly alternative for some popular architectures.

I'm not fully sure yet which of these are feasible.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO, having the efficient assembly version for popular architectures is going to be important for adoption. If the cost of soundness is a 25% performance drop for bulk copies, people might well keep using an unsound version.

Comment thread text/3301-atomic-memcpy.md Outdated
@m-ou-se

m-ou-se commented Aug 15, 2022

Copy link
Copy Markdown
Member Author

The trouble with that intrinsic is that unordered is weaker than monotonic aka Relaxed, and it can't easily be upgraded. There's no "relaxed fence" if the ordering you want is Relaxed; and even if the ordering you want is Acquire or Release, combining unordered atomic accesses with fences doesn't produce quite the same result. Fences provide additional guarantees regarding other memory accessed before/after the atomic access, but they don't do anything to restore the missing "single total order" per address of the atomic accesses themselves.

I'm very familiar with the standard Rust and C++ memory orderings, but I don't know much about llvm's unordered ordering. Could you give an example of unexpected results we might get if we were to implement AtomicPerByte<T>::{read, write} using llvm's unordered primitive and a fence? Thanks!

(It seems monotonic is behaves identically to unordered for loads and stores?)

Comment thread text/3301-atomic-memcpy.md Outdated
Comment thread text/3301-atomic-memcpy.md
but it's easy to accidentally cause undefined behavior by using `load`
to make an extra copy of data that shouldn't be copied.

- Naming: `AtomicPerByte`? `TearableAtomic`? `NoDataRace`? `NotQuiteAtomic`?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given these options and considering what the C++ paper chose, AtomicPerByte sounds OK and has the advantage of having Atomic as a prefix.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AtomicPerByteMaybeUninit or AtomicPerByteManuallyDrop to also resolve the other concern around dropping? Those are terrible names though...

@ojeda

ojeda commented Aug 15, 2022

Copy link
Copy Markdown

cc @ojeda

Thanks! Cc'ing @wedsonaf since he will like it :)

@thomcc

thomcc commented Aug 15, 2022

Copy link
Copy Markdown
Member

Unordered is not monotonic (as in, it has no total order across all accesses), so LLVM is free to reorder loads/stores in ways it would not be allowed to with Relaxed (it behaves a lot more like a non-atomic variable in this sense)

In practical terms, in single-thread scenarios it behaves as expected, but when you load an atomic variable with unordered where the previous writer was another thread, you basically have to be prepared for it to hand you back any value previously written by that thread, due to the reordering allowed.

Concretely, I don't know how we'd implement relaxed ordering by fencing without having that fence have a cost on weakly ordered machines (e.g. without implementing it as an overly-strong acquire/release fence).

That said, I think we could add an intrinsic to LLVM that does what we want here. I just don't think it already exists.

(FWIW, another part of the issue is that this stuff is not that well specified, but it's likely described by the "plain" accesses explained in https://www.cs.tau.ac.il/~orilahav/papers/popl17.pdf)

@thomcc

thomcc commented Aug 15, 2022

Copy link
Copy Markdown
Member

CC @RalfJung who has stronger opinions on Unordered (and is the one who provided that link in the past).

I think we can easily implement this with relaxed in compiler-builtins though, but it should get a new intrinsic, since many platforms can implement it more efficiently.

@bjorn3

bjorn3 commented Aug 15, 2022

Copy link
Copy Markdown
Member

We already have unordered atomic memcpy intrinsics in compiler-builtins. For 1, 2, 4 and 8 byte access sizes.

@thomcc

thomcc commented Aug 15, 2022

Copy link
Copy Markdown
Member

I'm not sure we'd want unordered, as mentioned above...

@thomcc

thomcc commented Aug 16, 2022

Copy link
Copy Markdown
Member

To clarify on the difference between relaxed and unordered (in terms of loads and stores), if you have

static ATOM: AtomicU8 = AtomicU8::new(0);
const O: Ordering = ???;

fn thread1() {
    ATOM.store(1, O);
    ATOM.store(2, O);
}

fn thread2() {
    let a = ATOM.load(O);
    let b = ATOM.load(O);
    assert!(a <= b);
}

thread2 will never assert if O is Relaxed, but it could if O is (the hypothetical) Unordered.

In other words, for unordered, it would be legal for 2 to be stored before 1, or for b to be loaded before a. In terms of fences, there's no fence that "upgrades" unordered to relaxed, although I believe (but am not certain) that stronger fences do apply to it.

@programmerjake

Copy link
Copy Markdown
Member

something that could work but not be technically correct is:
compiler acquire fence
unordered atomic memcpy
compiler release fence

those fences are no-ops at runtime, but prevent the compiler from reordering the unordered atomics -- assuming your on any modern cpu (except Alpha iirc) it will behave like relaxed atomics because that's what standard load/store instructions do.

@thomcc

thomcc commented Aug 16, 2022

Copy link
Copy Markdown
Member

Those fences aren't always no-ops at runtime, they actually emit code on several platforms (rust-lang/rust#62256). It's also unclear what can and can't be reordered across compiler fences (rust-lang/unsafe-code-guidelines#347), certainly plain stores can in some cases (this is easy to show happening in godbolt).

Either way, my point has not been that we can't implement this. We absolutely can and it's probably even straightforward. My point is just that I don't really think those existing intrinsics help us do that.

@tschuett

Copy link
Copy Markdown

I like MaybeAtomic, but following C++ with AtomicPerByte sounds reasonable.
The LLVM guys started something similar in 2016:
https://reviews.llvm.org/D27133

Comment thread text/3301-atomic-memcpy.md
loop {
let s1 = self.seq.load(Acquire);
let data = read_data(&self.data, Acquire);
let s2 = self.seq.load(Relaxed);

@RalfJung RalfJung Aug 20, 2022

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's something very subtle here that I had not appreciated until a few weeks ago: we have to ensure that the load here cannot return an outdated value that would prevent us from noticing a seqnum bump.

The reason this is the case is that if there is a concurrent write, and if any
part of data reads from that write, then we have a release-acquire pair, so then we are guaranteed to see at least the first fetch_add from write, and thus we will definitely see a version conflict. OTOH if the s1 reads-from some second fetch_add in write, then that forms a release-acquire pair, and we will definitely see the full data.

So, all the release/acquire are necessary here. (I know this is not a seqlock tutorial, and @m-ou-se is certainly aware of this, but it still seemed worth pointing out -- many people reading this will not be aware of this.)

(This is related to this comment by @cbeuw.)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah exactly. This is why people are sometimes asking for a "release-load" operation. This second load operation needs to happen "after" the read_data() part, but the usual (incorrect) read_data implementation doesn't involve atomic operations or a memory ordering, so they attempt to solve this issue with a memory ordering on that final load, which isn't possible. The right solution is a memory ordering on the read_data() operation.

@ibraheemdev ibraheemdev Aug 23, 2022

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Under a reordering based atomic model (as CPUs use), a release load makes sense and works. Release loads don't really work unless they are also RMWs (fetch_add(0)) under the C11 model.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, the famous seqlock paper discusses "read dont-modify write" operations.

while the second one is basically a memory fence followed by series of `AtomicU8::store`s.
Except the implementation can be much more efficient.
The implementation is allowed to load/store the bytes in any order,
and doesn't have to operate on individual bytes.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "load/store bytes in any order" part is quite tricky, and I think means that the specification needs to be more complicated to allow for that.

I was originally thinking this would be specified as a series of AtomicU8 load/store with the respective order, no fence involved. That would still allow merging adjacent writes (I think), but it would not allow reordering bytes. I wonder if we could get away with that, or if implementations actually need the ability to reorder.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For a memcpy (meaning the two regions are exclusive) you generally want to copy using increasing address order ("forward") on all hardware I've ever heard of. Even if a forward copy isn't faster (which it often is), it's still the same speed as a reverse copy.

I suspect the "any order is allowed" is just left in as wiggle room for potentially strange situations where somehow a reverse order copy would improve performance.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "load/store bytes in any order" part is quite tricky, and I think means that the specification needs to be more complicated to allow for that.

A loop of relaxed load/store operations followed/preceded by an acquire/release fence already effectively allows for the relaxed operations to happen in any order, right?

I was originally thinking this would be specified as a series of AtomicU8 load/store with the respective order, no fence involved.

In the C++ paper they are basically as:

for (size_t i = 0; i < count; ++i) {
  reinterpret_cast<char*>(dest)[i] =
      atomic_ref<char>(reinterpret_cast<char*>(source)[i]).load(memory_order::relaxed);
}
atomic_thread_fence(order);

and

atomic_thread_fence(order);
for (size_t i = 0; i < count; ++i) {
  atomic_ref<char>(reinterpret_cast<char*>(dest)[i]).store(
      reinterpret_cast<char*>(source)[i], memory_order::relaxed);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A loop of relaxed load/store operations followed/preceded by an acquire/release fence already effectively allows for the relaxed operations to happen in any order, right?

Yes, relaxed loads/stores to different locations can be reordered, so specifying their order is moot under the as-if rule.

In the C++ paper they are basically as:

Hm... but usually fences and accesses are far from equivalent. If we specify them like this, calling code can rely on the presence of these fences. For example changing a 4-byte atomic acquire memcpy to an AtomicU32 acquire load would not be correct (even if we know everything is initialized and aligned etc).

Fence make all preceding/following relaxed accesses potentially induce synchronization, whereas release/acquire accesses only do that for that particular access.

@RalfJung

RalfJung commented Aug 20, 2022

Copy link
Copy Markdown
Member

CC @RalfJung who has stronger opinions on Unordered (and is the one who provided that link in the past).

Yeah, I don't think we should expose Unordered to users in any way until we are ready and willing to have our own concurrency memory model separate from that of C++ (or until C++ has something like unordered, and it's been shown to also make sense formally). There are some formal memory models with "plain" memory accesses, which are similar to unordered (no total mo order but race conditions allowed), but I have no idea if those are an accurate model of LLVM's unordered accesses. Both serve the same goal though, so there's a high chance they are at least related: both aim to model Java's regular memory accesses.

We already have unordered atomic memcpy intrinsics in compiler-builtins. For 1, 2, 4 and 8 byte access sizes.

Well I sure hope we're not using them in any way that actually becomes observable in program behavior, as that would be unsound.

@programmerjake

Copy link
Copy Markdown
Member

turns out mixed <= 64-bit atomics does work on x86 if you use a new enough cpu: rust-lang/unsafe-code-guidelines#345 (comment)

@arielb1

arielb1 commented May 21, 2025

Copy link
Copy Markdown
Contributor

The debate about freeze makes me more want to have separate "untrusting" and "trusting" versions of the AtomicPerByte memcpy, where the "untrusted" version is guaranteed to read only the memory that's in the address it's reading from - this doesn't have any meaning in the abstract machine - in the AM, it's nondeterminism - but in the concrete machine, it will be guaranteed that the "nondeterministic" value is equal to a value that physically existed in that address.

I don't think it makes sense for general-use primitives to have semantics that involve the concrete machine, which is why I think it makes sense for that to be a separate API.

@RalfJung

Copy link
Copy Markdown
Member

I don't understand what you mean by this. This operation is fully defined by saying that it behaves like a series of u8 relaxed (atomic) loads/stores. There's no new source of non-determinism here that doesn't already exist with atomics today.

@arielb1

arielb1 commented May 21, 2025

Copy link
Copy Markdown
Contributor

This operation is fully defined by saying that it behaves like a series of u8 relaxed (atomic) loads/stores.

If there are undefs in the memory being loaded from, then an AtomicU8 load would be UB, right? You could do a load of an AtomicMaybeUninitU8 (is that a thing? we probably want that to also allow copying provenance), but I think you want a "concrete machine" guarantee that if there is undef, the uninitialized memory is copied from the original memory location rather than being from e.g. the stack of the thread calling atomicperbyte memcpy (this does not do anything at the abstract machine level, but does affect the concrete machine).

Of course, you can argue that an untrusted program is not able to put undef in memory it controls, but a buggy Rust program can certainly write undef (e.g. by writing a struct with padding), and you want a version of AtomicPerByte memcpy that is robust against that.

@RalfJung

RalfJung commented May 21, 2025

Copy link
Copy Markdown
Member

Ah, I guess it's more of a series of MaybeUninit<u8> relaxed (atomic) loads/stores -- which is not currently a thing, but would become a thing with this RFC. Then you can even read uninitialized or partially initialized memory; it is up to you how to safely deal with that though. I don't think there should be any implicit freeze in this API.

Untrusted code is linked in at the assembly level, so it cannot write uninit into Rust memory anyway.

I think you want a "concrete machine" guarantee that if there is undef, the uninitialized memory is copied from the original memory location rather than being from e.g. the stack of the thread calling atomicperbyte memcpy (this does not do anything at the abstract machine level, but does affect the concrete machine).

I don't think such a guarantee makes sense. You are basically suggesting uninit should have provenance so we can define "where it comes from" and have that affect its value when frozen -- that'd be a complete nightmare for optimizations.

@arielb1

arielb1 commented May 21, 2025

Copy link
Copy Markdown
Contributor

I don't think such a guarantee makes sense. You are basically suggesting uninit should have provenance so we can define "where it comes from" and have that affect its value when frozen -- that'd be a complete nightmare for optimizations.

Assuming we have an AM way of doing an MaybeUninit atomic per byte memcpy. if you do an asm memcpy, that from an abstract machine is equivalent to an MaybeUninit atomic per byte memcpy followed by a freeze, you get this guarantee. It would be nice to get that guarantee without inline asm.

@RalfJung

Copy link
Copy Markdown
Member

I don't think it is a useful enough to guarantee to justify the enormous amounts of work it'd take to make this reasonably precise. The compiler is and should be allowed to entirely omit operations that boil down to "store uninit in this memory here", and such operations can easily cause your property to be violated.

If you want assembly-level guarantees, write assembly code.

@arielb1

arielb1 commented May 21, 2025

Copy link
Copy Markdown
Contributor

In that case I think it would be right to recommend that people use an asm memcpy crate instead of calling AtomicPerByte reads (but the existence of AtomicPerByte operations is still helpful since it makes this well-defined behavior in the non-undef case).

@SmnTin

SmnTin commented Oct 13, 2025

Copy link
Copy Markdown

Hi folks! I have wanted to implement a seqlock that safely works for any Copy type for years. Workarounds of using assembly memcpy or ensuring that a type has no padding bytes and modeling this op with a bunch of atomics aren't really nice. What stops this RFC from being accepted?

Comment on lines +24 to +51
// Incomplete example

pub struct SeqLock<T> {
seq: AtomicUsize,
data: UnsafeCell<T>,
}

unsafe impl Sync<T: Copy + Send> for SeqLock<T> {}

impl<T: Copy> SeqLock<T> {
/// Safety: Only call from one thread.
pub unsafe fn write(&self, value: T) {
self.seq.fetch_add(1, Relaxed);
write_data(&mut self.data, value, Release);
self.seq.fetch_add(1, Release);
}

pub fn read(&self) -> T {
loop {
let s1 = self.seq.load(Acquire);
let data = read_data(&self.data, Acquire);
let s2 = self.seq.load(Relaxed);
if s1 & 1 == 0 && s1 == s2 {
return unsafe { assume_valid(data) };
}
}
}
}

@steffahn steffahn Oct 19, 2025

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the SeqLock implementation here sound? Couldn’t seq overflow such that s1 == s2 succeeds even though what truly happened is that the counter wrapped around the whole usize space once?

[Edit: I think I meant to quote the other SeqLock code block, but the point applies either way.]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a known issue with seqlocks. The soundness relies on the overflow never happening in practice since it would require one thread to be suspended long enough for another thread to perform usize::MAX atomic increments.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should either be explained in a comment, or there should be an abort on overflow.

@DemiMarie DemiMarie left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It’s hard for me to tell if this API is sufficient without a lot of examples:

  1. When can one call .assume_init() or cast to a ordinary Rust slice?
  2. How does one get an AtomicPerByte from a raw pointer?

Some examples would be extremely useful here. Ones I would like to see:

  1. Loading a Copy type with all bit patterns valid and no padding (such as u32) from a pointer.
  2. Storing a Copy type with all bit patterns valid and no padding (such as u32) to a pointer.
  3. Copying to a slice of a type with all bit patterns valid and no padding.
  4. Copying from a slice of a type with all bit patterns valid and no padding.
  5. Copying from an ordinary raw pointer.
  6. Copying to an ordinary raw pointer.
  7. Copying between AtomicPerByte types, such as source and destination buffers that are both shared with untrusted code.
  8. Copying to/from an SIMD vector type.

All of these should be sound even if the pointer is concurrently modified by untrusted code.

Comment on lines +180 to +181
- In order for this to be efficient, we need an additional intrinsic hooking into
special support in LLVM. (Which LLVM needs to have anyway for C++.)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO, having the efficient assembly version for popular architectures is going to be important for adoption. If the cost of soundness is a 25% performance drop for bulk copies, people might well keep using an unsound version.

The `AtomicPerByte<T>` type can be thought of as
the `Sync` (data race free) equivalent of `MaybeUninit<T>`.
It can contain a `T`, but it might be invalid in various ways
due to concurrent store operations.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
due to concurrent store operations.
due to concurrent store operations. However, it will never be invalid
if all bit patterns are valid for the type.

Otherwise, what would be the correct way to use this with memory shared with untrusted code?

Comment on lines +170 to +193
impl<T> AtomicPerByte<T> {
pub const fn new(value: T) -> Self;
pub const fn uninit() -> Self;

pub fn store(&self, value: T, ordering: Ordering);
pub fn load(&self, ordering: Ordering) -> MaybeUninit<T>;

pub fn store_from(&self, src: &MaybeUninit<T>, ordering: Ordering);
pub fn load_to(&self, dest: &mut MaybeUninit<T>, ordering: Ordering);

pub fn store_from_slice(this: &[Self], src: &[MaybeUninit<T>], ordering: Ordering);
pub fn load_to_slice(this: &[Self], dest: &mut [MaybeUninit<T>], ordering: Ordering);

pub const fn into_inner(self) -> MaybeUninit<T>;

pub const fn as_ptr(&self) -> *const T;
pub const fn as_mut_ptr(&self) -> *mut T;

pub const fn get_mut(&mut self) -> &mut MaybeUninit<T>;
pub const fn get_mut_slice(this: &mut [Self]) -> &mut [MaybeUninit<T>];

pub const fn from_mut(value: &mut MaybeUninit<T>) -> &mut Self;
pub const fn from_mut_slice(slice: &mut [MaybeUninit<T>]) -> &mut [Self];
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the API is sufficient, but there are some areas where the documentation could use improvement:

  1. When can one call .assume_init() or cast to a ordinary Rust slice?
  2. How does one get an AtomicPerByte from a raw pointer?

Some examples would be extremely useful here. Ones I would like to see:

  1. Loading a Copy type with all bit patterns valid and no padding (such as u32) from a pointer.
  2. Storing a Copy type with all bit patterns valid and no padding (such as u32) to a pointer.
  3. Copying to a slice of a type with all bit patterns valid and no padding.
  4. Copying from a slice of a type with all bit patterns valid and no padding.
  5. Copying from an ordinary raw pointer.
  6. Copying to an ordinary raw pointer.
  7. Copying between AtomicPerByte types, such as source and destination buffers that are both shared with untrusted code.
  8. Copying to/from an SIMD vector type.

Comment on lines +123 to +127
The `MaybeUninit` type is used to represent the potentially invalid state
the data might be in, since it might be the result of tearing during a race.

Only after confirming that there was no race and the data is valid
can one safely use `MaybeUninit::assume_init` to get the actual `T` out.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn’t sufficient for interoperating with untrusted code. The untrusted code can cause tearing to happen at will, so one can never guarantee that there was no race. However, one can choose to only use types for which all bit patterns are valid.

Alternatively, one could define that POD types (no padding, all bit patterns are valid) never tear.

@programmerjake programmerjake Feb 22, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, one could define that POD types (no padding, all bit patterns are valid) never tear.

arbitrarily large types are POD, e.g. [u8; 1000000], never tearing is impossible for all POD types on any reasonable multi-core CPU design. (technically you could have a CPU that blocks all other cores for the duration of an atomic memcpy, but I don't think that's reasonable for multi-core CPUs).

imo the best fix is to not make torn reads UB when all bit patterns are valid. That may need to be combined with some freeze primitive to convert stores of undef to some initialized bit pattern.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

“never tear” in the sense that this is not UB. Obviously they can tear in hardware.

@amluto

amluto commented Apr 16, 2026

Copy link
Copy Markdown

A while back (2008!) I came up with a nifty algorithm for a seqlock with (for some use cases, anyway) dramatically better performance than the standard one:

https://link.springer.com/chapter/10.1007/978-3-540-92221-6_40

and I was contemplating implementing it in Rust. And I found this thread while wondering how people handle ordinary seqlocks in Rust. After trying to catch up on it, I'm wondering how any of the proposals here handle provenance. It seems to me that the ultimate goal is to have a safely-callable API to stick any Copy (Copy + Send, perhaps?) type into a magic datastructure and pull it out intact from elsewhere. In 2008, provenance wasn't very popular, but it's popular today, and it seems to me that one ought to be able to stick &T or *T into a seqlock-protected data structure. And I have a question that I'm not sure is well answered by the discussion above: what happens to the provenance associated with the data that round-trips through the proposed APIs? Or, more precisely, when is it valid to assume_init() or whatever the APIs ends up being on data that has been laundered through AtomicPerByte?

As far as I know, this is problem even in a single-threaded world: if I decompose some T that contains pointers or references into bytes or integer words or anything of the sort (via transmute or really basically any similar mechanism) and then reassemble it into a bitwise-identical block of memory, then turning it back into a reference is UB and turning it back into a pointer and dereferencing the pointer is UB.

I can imagine this being solved be careful language in the spec, but I don't see any language about this at all in the RFC. Or would it actually need to be visible at the API level with some actual object, zero-sized or otherwise, that carries the provenance through the seqlock container?

At least the MaybeUninit docs stringly suggest that references can live in a MaybeUninit (copied here):

use std::mem::MaybeUninit;

// Create an explicitly uninitialized reference. The compiler knows that data inside
// a `MaybeUninit<T>` may be invalid, and hence this is not UB:
let mut x = MaybeUninit::<&i32>::uninit();
// Set it to a valid value.
x.write(&0);
// Extract the initialized data -- this is only allowed *after* properly
// initializing `x`!
let x = unsafe { x.assume_init() };

But it's one thing to say that &0 in this example and another thing entirely to imagine that one can copy, possibly corruptly, some bytes and somehow do the right incantation to preserve the provenance in them. As food for thought, imagine a very different implementation of a seqlock-like structure: a pile of bytes and a 256-bit mutex-protected (or outright hardware-supplied atomic) register containing a cryptographic hash. To write, you atomically update the hash register and you write the bytes with whatever weakly ordered primitive you like. To read, you read the bytes, read the hash, check the hash, and, if it matches, assume_init().

I'm suspicious that this construction does not work -- it's basically the same as the classic if (x == y) demonstration where x points to one object and y points one past the end of another: mere equality of the raw bytes backing two objects does not mean they have equivalent provenance. But, on the contrary side, using inline asm to copy a T: Copy seems like it's probably fine even if the contents of the asm treat T as if it were made up of integers.

A possibly silly solution would be to introduce a PhantomProvenance<T> that would be a zero-sized encoding of the provenance of a T. And to either have AtomicPhantomProvenance<T> or otherwise allow atomic operations on a PhantomProvenance<T>. (I'm not convinced that anything stronger than relaxed actually makes sense, but I haven't tried to think through all the implications. Certainly one cannot read 0 bytes on common architectures and use that read to synchronize between threads in a manner that actually does anything.) And then the low-level operation that turns a pile-of-bytes into a T would also take a PhantomProvenance<T> as input. And then anyone doing a hash-based lock would need to read the hash and the provenance together.

(I'm not sure that "phantom" is the right word. I'm also not sure how this would fit in with architectures and runtimes that have real provenance, CHERI or Fil-C-style.)

(I should see if I'm allowed to post a preprint or something of this paper somewhere that isn't paywalled, although the paper has nothing whatsoever to say about implementing any of this in a real programming language.)

@RalfJung

RalfJung commented Apr 17, 2026

Copy link
Copy Markdown
Member

As far as I know, this is problem even in a single-threaded world:

Indeed your question seems to have nothing to do with concurrency, so this isn't the right place to discuss it. Please have a look at https://doc.rust-lang.org/nightly/std/ptr/index.html#provenance and https://doc.rust-lang.org/nightly/std/mem/union.MaybeUninit.html#validity and if there are still questions remaining, you can find us on Zulip. But note that a "zero-sized type that holds provenance" can not exist in Rust. Provenance is always attached to bytes.

@wyfo

wyfo commented May 9, 2026

Copy link
Copy Markdown

I've just written a post on IRLO about racy reads, and it actually gave me an idea about how to solve the Drop issue of the current AtomicPerByte.

Let me introduce AtomicCell<T>!

struct AtomicCell<T>(UnsafeCell<T>);

unsafe impl<T: Send> Sync for AtomicCell<T> {}

impl<T> AtomicCell<T> {
    pub const fn new(value: T) -> Self { Self(UnsafeCell::new(value)) }
    /// Safety
    /// 
    /// Calls to `store` must be serialized, 
    /// i.e. two thread shall not call `store` concurrently.
    pub unsafe fn store(&self, value: T, ordering: Ordering) { /* .. */ }
    pub fn load(&self, ordering: Ordering) -> MaybeUninit<T> { /* .. */ }
    pub const fn get_mut(&mut self) -> &mut T { self.0.get_mut() }
    pub const fn as_ptr(&self) -> *mut T { self.0.get() }
}

The key difference with AtomicPerByte is that store is unsafe and requires to be serialized. This contract forces the cell to always contain a valid value. As a result, the value inside is dropped with the cell (as with any other cells). I think it's a big argument in favor of AtomicCell. That's also quite consistent with other cell types which require writes to be serialized; AtomicCell just comes with a relaxed constraint on read.

AtomicPerByte API is more complete and powerful, but the question is: do we need this power? In the case of SeqLock (and in my own use case), definitely not. And AtomicCell is definitely less error-prone. Nothing prevents to add AtomicPerByte to the stdlib and use it internally inside AtomicCell, but there would be no need to expose it for now.

@DemiMarie

Copy link
Copy Markdown

I’m much more interested in the raw low-level copy APIs. The reason is that some operations, such as copying from one process or VM to another, need this flexibility. This is because both reads and writes can race and the size of the operation is not known at compile-time.

@wyfo

wyfo commented May 10, 2026

Copy link
Copy Markdown

I’m much more interested in the raw low-level copy APIs. The reason is that some operations, such as copying from one process or VM to another, need this flexibility. This is because both reads and writes can race and the size of the operation is not known at compile-time.

In the general case, unless your type is a POD with no invariant, racing stores will result into unusable garbage content. And even in the case of a POD with no invariant, the result would be so unpredictable that I wouldn't know how to use it. Could you elaborate a concrete use case where racing stores would be a desired feature?
I'm just thinking, maybe store safety contract could be relaxed if the wrapped type is MaybeUninit<T>. In this case, concurrent stores resulting in torn values could be allowed.

Regarding arbitrary size, I didn't include store_from_slice/load_from_slice to keep it simple, but atomic memcpy could be achieve with AtomicCell<MaybeUninit<u8>>::store_from_slice, the same it's done with AtomicPerByte.

@elfenpiff

Copy link
Copy Markdown

I’m one of the maintainers of iceoryx2 and here we use lock-free data structures in shared memory and need the equivalent of an “atomic memcpy” for some of them.

Our data structures must be crash-resilient: if one process crashes in the middle of a modification, the data structure must remain valid and usable by other processes. Consider a queue where a process is terminated during a push operation. Another process must still be able to safely consume the remaining elements using pop.

Since we are also working toward safety certification, undefined behavior caused by using core::ptr::copy concurrently is a no-go. We therefore implemented ByteAtomic to address this issue on our side.

@FerdinandSpitzschnueffler recently published an article about the implementation journey, including the challenges posed by padding and uninitialized bytes:

https://ekxide.io/blog/byte-wise-atomic-wrapper-to-prevent-ub

@DemiMarie

Copy link
Copy Markdown

Could you elaborate a concrete use case where racing stores would be a desired feature?

Writing to memory shared with untrusted code needs this. The untrusted code should not write the memory (if it does, it’s buggy), but if it does write, this must not cause a security hole. (The contents of the memory after the conflicting writes are irrelevant.)

@RalfJung

RalfJung commented Aug 5, 2026

Copy link
Copy Markdown
Member

Sharing memory with untrusted code is an interesting use case but IMO is out-of-scope in this RFC as it is a way harder problem than what this RFC is trying to solve.

@DemiMarie

Copy link
Copy Markdown

Sharing memory with untrusted code is an interesting use case but IMO is out-of-scope in this RFC as it is a way harder problem than what this RFC is trying to solve.

Why is it harder? Genuine question. The straightforward desugaring to inline assembly works fine, so long as the memory is never visible to the AM (which, in my use-cases, it never will be).

@RalfJung

RalfJung commented Aug 6, 2026

Copy link
Copy Markdown
Member

Keeping the memory entirely outside the AM is an option, but then you have to do everything with inline asm or volatile accesses (and this RFC is irrelevant for you). But I am not aware of any work exploring concurrency semantics where data races are not UB, and as you said that is a requirement for sharing memory with untrusted code where that memory actually becomes AM memory.

See rust-lang/unsafe-code-guidelines#607 for details.

@DemiMarie

This comment was marked as off-topic.

@RalfJung

RalfJung commented Aug 6, 2026

Copy link
Copy Markdown
Member

Please keep this discussion on-topic for atomic memcpy. There are many things I want that are not atomic memcpy but that's irrelevant here.

@RalfJung

RalfJung commented Aug 6, 2026

Copy link
Copy Markdown
Member

I’m one of the maintainers of iceoryx2 and here we use lock-free data structures in shared memory and need the equivalent of an “atomic memcpy” for some of them.

That's a very neat way of doing seqlocks. :) Shows that with some restrictions on T you can already do them. This RFC would let us do them for all types.
FWIW if doing all these copies byte-for-byte is a problem then there are further tricks one could pull, though that should probably be discussed elsewhere.

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

Labels

T-libs-api Relevant to the library API team, which will review and decide on the RFC.

Projects

None yet

Development

Successfully merging this pull request may close these issues.