Repository navigation
Opsem extension proposal: atomic volatile accesses #615
Description
Our current volatile APIs do not let people write semantically correct MMIO with a DMA device. The interaction with such a device typically looks as follows:
- set up some data somewhere in regular memory using regular stores
- emit a kind of release fence
- write a single value to an MMIO register to tell the device that the data is ready
If the final write is just volatile but not atomic, then the data writes and the MMIO write are not guaranteed to happen in any particular order in the real (concrete) execution. A volatile write is observably-ordered wrt all other observable events (volatile accesses and I/O), but not observably-ordered wrt non-volatile memory accesses. A release fence only does something when there's a data write before the fence and an atomic write after the fence. So to get the desired semantics,
we need to make that write atomic.
Note that here I am using "atomic" in the sense of the C++ memory model. This is separate from the question of whether the write can tear or not. An atomic operation must be compiled in a way that avoids tearing, but the converse does not hold -- an operation can be non-torn while still also being non-atomic.
Another way to view this: a volatile store is basically a syscall, so the compiler treats it kind of like an opaque function call -- except that the compiler makes a lot of assumptions about the possible side-effects of that call: it cannot read or write any Rust-accessible memory except for the locations that the store overwrites. It also cannot read or write the "synchronization state" of the current thread, i.e. the set of memory events that that thread has observed or that can participate in fence-based synchronization. (In LLVM terms, volatile accesses are nosync.) However, to explain what happens in the DMA setting, we want the volatile MMIO write to interact with the synchronization state (specifically, the set of memory events in the "release fence" state of the current thread is made available to anyone reading from this volatile write). We need to tell the compiler that this operation may synchronize (i.e., we need to get rid of the nosync). The way we typically do this is by making an operation atomic, so we should have atomic volatile accesses.
So, I propose that t-opsem should decide that Rust shall have atomic volatile accesses: operations that are both observable (i.e., like I/O) and participate in the concurrency memory model. The exact API is left to t-libs-api; there is an ACP for it at rust-lang/libs-team#801.
Activity
RalfJung commentedon Jun 16, 2026
@rfcbot fcp merge opsem
rust-rfcbot commentedon Jun 16, 2026
@RalfJung has proposed to merge this. The next step is review by the rest of the tagged team members:
No concerns currently listed.
Once a majority of reviewers approve (and at most 2 approvals are outstanding), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up!
See this document for info about what commands tagged team members can give me.
Lokathor commentedon Jun 16, 2026
I do want to point out that you can get correct semantics for your DMA under current Rust as long as you use volatile writes to fill your source buffer before you use the volatile write that activates the DMA.
This is not ideal in probably the majority of cases, and so we should do this opsem extension, but I thought it's worth noting down.
CAD97 commentedon Jun 16, 2026
This is an operation serving a need and doesn't impose any significant burden on the opsem to support. I concur; the Rust AM should provide this operation.
@rustbot reviewed
As an aside, I want to note that describing typical volatile writes as not being (implicit: strongly) ordered w.r.t. other writes was a confusing point to me a few times; operations in a single thread are always sequenced-before later operations in the same thread. The actual break in ordering is the (in)ability of the nonatomic volatile write to happens-before a "later" observing read.
This may just be a difference in the shape of how the relations are established. My mental model doesn't actually have a name for the effectively stronger sequenced-before relation between two writes if the latter write is release-ordered, since it's largely equivalent to the C++ description of the memory ordering model.
Rather than say "there's not really any ordering," I think I would have phrased it "can't establish any ordering [to a reading thread]."
This is the case if the reading side records the externally observable events and never reads Rust memory, as no ordering with the memory (even the addresses previously volatile written to) has been established. I think that caveat cannot break your as-if story1, but the logic could be somewhat tenuous. A release-ordered trigger is clearly the correct model, anyway.
Footnotes
The entire story of volatile writes to standard Rust memory is weird. The write is considered externally observable, but the only effect of the write is to change the value in Rust memory, and the only way to observe what the value changed to is to perform a Rust read, which is required to fit in the standard ordering that ignores volatile entirely. So I guess the external observer knows that a write happened, but to know what value got written requires additional synchronization?
That is why I worded that caveat the way I did. You'd need observable writes to write an observable value, to record them, and then read from your side table, to avoid reading from Rust memory. Then you would show that reading from the physical memory cannot possibly produce a different result and use that proof to remove the side table.
Or just use atomic aquire-release synchronization directly. ↩
Diggsey commentedon Jun 16, 2026
I think you clearly justified the need for an atomic volatile access, but haven't explained why non-atomic volatiles are useful. I'm sure they are, but it might be worth mentioning a typical use-case where a non-atomic volatile is both correct and necessary performance-wise?
bjorn3 commentedon Jun 16, 2026
At least one example for where non-atomic volatiles are valid is devices that don't do DMA. For example a UART where you read or write a byte at a time from a device register rather than pointing the UART to a memory location. As for performance, I have no clue if it matters.
RalfJung commentedon Jun 16, 2026
I was talking about the order in which the operations actually happen on the hardware level. That order does not have to respect happens-before. For example, it is legal for the compiler to reorder two non-atomic writes to different locations despite the fact that there is a happens-before relationship between them.
This order is only really meaningful between two observable events (volatile access or I/O), so maybe a more correct version of what I said would be that non-volatile accesses do not participate in this order and it is hence meaningless to ask whether, in the concrete execution, a non-volatile store occurs before or after a volatile store.
Indeed I have not. Those accesses already exist and we cannot remove them due to backwards compatibility, so discussing their motivation here seems off-topic to me.
RalfJung commentedon Jun 16, 2026
Yes indeed. There is a world in which volatile writes are only permitted to non-Rust memory. But at this point I think that would be too much of a breaking change.
Diggsey commentedon Jun 17, 2026
Removing them would be, but making the existing volatile accesses atomic would not be a backwards incompatible change.
RalfJung commentedon Jun 17, 2026
Yes it would be... or you'll have to clarify what you mean.
Adding the
atomicbit to these operations in the LLVM IR we generate would break all volatile loads and stores that don't have the right size or alignment to be made into atomic operation.Diggsey commentedon Jun 17, 2026
Well that answers the question I had: both operations are needed because regular volatile operations have weaker size and alignment requirements, so both variants are required.
programmerjake commentedon Jun 17, 2026
RalfJung commentedon Jun 17, 2026
Please keep the discussion on-topic. This is not a thread for random proposals of new operations, this thread has a very clearly defined scope.
1 remaining item
programmerjake commentedon Jun 23, 2026
Note that IIRC RMW operations aren't planned to be implemented as part of this issue/ACP/etc., just the atomic volatile loads/stores.
Rust has several subsets of atomics: atomic load/store (
cfg(target_has_atomic_load_store = "N")for bit-width N), atomic compare_exchange (cfg(target_has_atomic = "N")), and atomics having the same alignment as the underlying non-atomic type (cfg(target_has_atomic_primitive_alignment = "N").It seems reasonable for volatile atomics to also have
cfgs for those, and maybe a more detailed set of feature flags (e.g. on x86 you can do a volatile atomicfetch_add, but not a volatile atomicfetch_maxsince there's a single instruction for the former but none for the latter, other architectures are different). Also, as mentioned in the Zulip thread, some devices do actually support RMW atomics (PCIe supports atomic operations).The way I see it, if you're trying to do MMIO to a device that only supports some operations, it's your job as the device driver author to not use the wrong volatile atomic operations, this is the same as you having to not use a too small or too large volatile load/store. e.g. if you do a 64-bit read from a memory-mapped serial port that only works with byte-sized accesses, that's your own fault as a driver author. similarly to if you try to do a compare-exchange.
RalfJung commentedon Jun 23, 2026
That statement is not well-typed. Atomic operations include atomic loads, atomic stores, and atomic RMWs (read-modify-write) such as swap and compare-exchange. So saying that one atomic operation allows another does not make sense.
The first ACP that this refers to only includes volatile atomic loads and stores. If we ever add volatile atomic RMWs, they will be guaranteed to lower to a single instruction, not an ll/sc loop.
Yes, it will be up to the user to emit the right fence via inline asm. The operation needs to be atomic to be able to participate in fence-based synchronization at all; that does not mean that a regular fence is guaranteed to be sufficient to synchronize with DMA observers. The documentation for this will need to be very careful and explicit about that point.
Lokathor commentedon Jun 23, 2026
Question: if this is accepted, would we want to continue to have non-atomic volatile accesses? Or perhaps put another way: is there an example we could put in the docs of when you should and shouldn't use atomics with your volatile.
RalfJung commentedon Jun 23, 2026
If you need to do a volatile access that's not of the right size or alignment for one of our atomic types, you have to continue using the old non-atomic volatile accesses for now. Eventually we'll hopefully have atomic bytewise accesses and then we can also have atomic volatile bytewise accesses at which point I think there's no longer any reason to use the old non-atomic volatile accesses.
Lokathor commentedon Jun 23, 2026
Are you suggesting that the API is that a user would have to use an atomic type such as AtomicU16, or whatever type, and then that type would have the new
load_volatileandstore_volatilemethods? That would... Not be quite so ergonomic a transition from how people currently do things, though not impossible.It's also fine to not currently have any specific API in mind for how we would do this, I'm just curious how you see it working in user code, if you do have something in mind.
RalfJung commentedon Jun 23, 2026
The API is out of scope for this issue (this issue is just about the basic opsem capability). The issue description mentions the ACP where the API is sketched out and discussed: rust-lang/libs-team#801.
rust-rfcbot commentedon Jun 25, 2026
🔔 This is now entering its final comment period, as per the review above. 🔔
rust-rfcbot commentedon Jul 5, 2026
The final comment period, with a disposition to merge, as per the review above, is now complete.
As the automated representative of the governance process, I would like to thank the author for their work and everyone else who contributed.
RalfJung commentedon Jul 8, 2026
Tracking issue: rust-lang/rust#158947