I'd like to ask on rules for working on a memory shared with other processes. Shared memory IPC is known as the fastest, but the most error-prone, and I wonder about rules to work with it safely.
As far as I understand, ordinary Rust memory-access cannot be used on a shared memory because the compiler assumes that the outside process cannot access the process memory and may perform optimizations that break the program if ordinary memory access is performed on a shared memory
Documentation for core::ptr::read_volatile() and core::ptr::write_volatile() say that they can be used for memory outside of Rust abstract machine control, so they can be used for shared memory, but it also says that volatile memory access must not trap. I wonder, what counts as trapping? In Linux, accessing a shared memory can result in SIGBUS signal if accessed location no longer exists because the underlying memory-mapped file was truncated. Does that mean that a process must take measures to prevent this from happening in the first place (for example, on Linux, by memory-mapping an anonymous file created with memfd_create() and the sealing it)? Or it can be mitigated by installing a SIGBUS handler that restores the file size?
I also wonder about data races with volatile accesses. It's obvious what does it mean with memory the abstract machine is aware of (if one thread calls read_volatile() on a pointer to the content of UnsafeCell<T> and the other thread simultaneously calls write_volatile() on the same cell, behavior is undefined), but how does it affect memory that the abstract machine is unaware of? Can a Rust program call read_volatile() on a pointer to the memory-mapped file that the other process is writing (through volatile access to memory-mapped file or through the filesystem API)?
I'd also like to ask about atomics in the shared memory. One of the way to atomically access a memory locaiton in Rust is to use the core::sync::atomic module, by calling the from_ptr() associated function on the pointer to the object. I wonder, can it be used with shared memory? The Rust documentation says that memory which is outside the control of the Rust abstract machine is considered exposed, and therefore a pointer to it can be created with core::ptr::with_exposed_provenance() function. However, does it make the memory considered to be managed by the Rust abstract machine? What happens if some process accesses this memory non-atomically or through the filesystem API? And what happens if the shared memory atomic disappears because the file is truncated? Can the SIGBUS handler help in this case, or is behavior undefined in this case regardless if the signal is caught?
As far as I understand, putting pointers or references inside a shared memory is pointless or even dangerous, because memory addresses have consistent meaning only inside a process (other process may have something different on that address).
From discussions on this forum and internals.rust-lang.org, I've learned that atomic does not necessarily mean volatile. That means that a compiler may still introduce optimizations that are incorrect for shared memory between processes. As far as I understand, right now in Rust, the only way to make memory accesses that are both atomic and volatile is using inline assembly.
I also wonder about data races with non-atomic volatile accesses. I know it's undefined behavior to have data-racing access to the variable controlled by the abstract machine (that the compiler is "aware of") regardless of volatility, but what about memory that is the abstract machine is unaware of (such as mmap()ed file)?
If your use case would allow ptr::read_volatile and ptr::write_volatile you will be fine with just ptr::read and ptr::write along with proper atomic synchronization, which you are going to need either way. The optimizations allowed by the non-volatile versions are the ones that atomics are meant to avoid. The only issue you could run in to are related reference aliasing, but this is also common to both versions of the functions.
Shared memory falls into the first category listed in the read_volatile documentation. From the Allocation documentation page:
An allocation is a subset of program memory which is addressable from Rust, and within which pointer arithmetic is possible. Examples of allocations include heap allocations, stack-allocated variables, statics, and consts. The safety preconditions of some Rust operations - such as offset and field projections (expr.field) - are defined in terms of the allocations on which they operate.
An allocation has a base address, a size, and a set of memory addresses. It is possible for an allocation to have zero size, but such an allocation will still have a base address. The base address of an allocation is not necessarily unique. While it is currently the case that an allocation always has a set of memory addresses which is fully contiguous (i.e., has no “holes”), there is no guarantee that this will not change in the future.
Allocations must behave like “normal” memory: in particular, reads must not have side-effects, and writes must become visible to other threads using the usual synchronization primitives.
Shared memory meets all of these requirements. Actually being allocated by Rust itself isn't needed, it only needs to be addressable. The only issues you could run into are the same ones you would have with a C FFI function returning memory allocated in C.