rust-vmm / vm-memory

Virtual machine's guest memory crate
Apache License 2.0
299 stars 97 forks source link

Unsoundness in `get_atomic_ref` in `volatile_memory.rs` #281

Closed cblichmann closed 6 months ago

cblichmann commented 7 months ago

Hi everyone!

During an internal "unsafe" review, we stumbled across this line: https://github.com/rust-vmm/vm-memory/blob/c1b23a2a82eddcbc049f434ed9466f137a9623ae/src/volatile_memory.rs#L274

(with T being AtomicInteger).

Now the std docs say:

You must adhere to the Memory model for atomic accesses. In particular, it is not allowed to mix atomic and non-atomic accesses, or atomic accesses of different sizes, without synchronization.

This doesn't appear to be either enforced or documented as a safety invariant.

So this should either be clearly stated in the SAFETY comment or the code should make sure that memory accesses are not mixed.

bonzini commented 7 months ago

Indeed, the purpose of the whole VolatileMemory/ByteValued mechanism is to provide safe mechanisms that very closely skirt undefined behavior.

The problem is that a VMM's memory accesses (both reads and writes) are driven by whatever configuration the guests puts in the device registers, and it's not really possible to enforce that the configuration makes sense. For example you could program a device to operate from address 0x12345000, while another unrelated device DMAs from 0x12300000 to 0x123FFFFFF.

VolatileMemory/ByteValued therefore have to make some assumptions, with varying degrees of soundness. From definitely sound to least sound:

And indeed, this last point is not entirely sound. Because only very specific synchronization patterns are employed between host and guest, there should be no undefined behavior for well-behaving guests.

However, it simply is not possible to write a data-driven program such as a VMM in a way that is 100% compliant with the Rust memory model. Even for requests serviced in the CPU thread, there's always the possibility of a multi-processor guest writing to memory at the same time. Again, vm-memory tries to protect against that by making methods like aligned_as_ref<T: ByteValued> unsafe. Whenever atomics cannot be used, safe code can use volatile accesses because volatile has similar optimization guarantees to atomics; either VolatileRef (get_ref<T: ByteValued> is likewise safe), or the Bytes trait (all implementations Bytes ultimately call into VolatileSlice's implementation of the trait).

So, for an ill-behaved guest that tries actively to cause data races, we make an assumption that is true at the processor level: that this is no different from dealing with a guest that writes random data to guest memory without causing data races. And that this constrains the kind of optimization that the compiler can perform.

In fact this is not specific to virtualization, it can happen with cross-process atomics, or it can happen at the kernel-userspace boundary for Rust code that is part of Linux or another OS.

[1] By the way, "it is not allowed to mix atomic and non-atomic accesses" is not really true: it is not allowed to mix atomic and non-atomic accesses, or atomic accesses of different sizes, if at least one is a write. If all of the accesses are reads there are no data races in either C, C++ or Rust.

[2] https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4455.html

bonzini commented 6 months ago

@cblichmann is the answer above satisfactory? Can this issue be closed?

cblichmann commented 6 months ago

Thanks for the detailed explanation! Yes, let's close :)