rust-vmm / vm-memory

Virtual machine's guest memory crate

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Remove as_slice from GuestMemoryRegion

zachreizner opened this issue · comments

The as_slice/as_mut_slice method inside of GuestMemoryRegion is not safe to use even though it is marked unsafe. This is because a Rust primitive slice assumes that a const slice will not have mutable references (which there are in a guest) or that a mutable slice is exclusive (but the guest can modify it).

It's marked unsafe because of "possible aliasing", so it's a special purposed slice and the caller needs to take care of possible aliasing.

There are two reasons why the functions can not ensure safely, even though it is marked unsafe. The first is that the read_from function will pass the slice returned from as_mut_slice to a generic io::Write implementation which may not respect the unsafe nature of the slice it was given. The second is that creating primitive slices from memory that does not obey the Rust rules:

  • A mutable reference cannot be aliased
  • Safe Rust can not cause a data race

is inherently undefined behavior because their is no defined proper usage. Similar to how mem::uninitialized was too undefined to use properly, instantiating slices that don't adhere to Rust's safety rules is "instantaneously" UB.

It's definitely true that current design/implementation may cause UB, so it's treat off between complex, performance and call responsibilities. Current implementation prefers performance/simplicity by shifting the responsibilities to callers to:

  1. ensure no current accesses to the returned slices from the VMM side.
  2. ensure no current accesses to the returned slices from the VMM and guest side.

It does break the rust memory safety rules, improvements are welcomed:)

How is as_mut_slice returning a possibly-aliased reference different from writing &mut *a where a is a mutable pointer? (serious question, not rhetorical).

Answering @bonzini's question: with respect to Rust safety around aliasing is concerned, there is no difference. If a is a pointer to guest memory, it violate Rust safety (and is therefore UB), to have the expression &mut *a.

Then it is up to the user to use as_mut_slice safely, as with all unsafe functions. It is for example safe to use it before the guest has started. Also, the whole idea behind VolatileMemory is that, for ByteValued types, accesses to *mut u8 are "not entirely" undefined behavior:

VolatileRef and VolatileSlice, along with types that [...] implement
VolatileMemory, allow us to sidestep that rule by wrapping pointers that absolutely have to be
accessed volatile.

The head comment of src/volatile_memory.rs goes on to say "For the purposes of maintaining safety [...] No references or slices to volatile memory [...] Access should always been done with a volatile read or write" but these rules do not apply to unsafe code.

With respect to "not entirely" undefined behavior, see also this FIXME comment:

        // Safe because the pointers are range-checked when the slices
        // are created, and they never escape the VolatileSlices.
        // FIXME: ... however, is it really okay to mix non-volatile
        // operations such as copy with read_volatile and write_volatile?
        copy(self.addr, slice.addr, min(self.size, slice.size));

There is unsafety possible in the use of GuestMemory::read_from, which itself is not marked unsafe. Consider this pseudo-code:

let addr = ....;
let mem = GuestMemoryMmap::new(....);
let mem_cloned = mem.clone();
spawn(|| mem_cloned.read_from(addr, &mut SafeReader, 100);
mem.read_from(addr, &mut SafeReader, 100);

struct SafeReader;
impl Read for SafeReader {
    fn read(&mut self, buf: &mut [u8]) -> Result<usize> {
        let x = buf[0];
        buf[0] = x;
        assert_eq!(x, buf[0]);
        Ok(1)
    }
}

Because read_from is called twice over the same section of shared memory in a concurrent fashion, the result of the assertion in the SafeReader implementation is undefined because of the race condition. I hope we can agree that two concurrent threads with a mutable reference to the same data is never acceptable in Rust.

I also want to stress the point that unsafe does not mean we are allowed to create aliased mutable references, even though unsafe allows such an event to occur. Violating certain invariants in Rust invokes undefined behavior, which we don't want to do. Merely creating aliased mutable references violates the pointer aliasing rule, which means there is no way to call as_mut_slice without invoking undefined behavior. It doesn't even matter how we use it after, the damage would have been done.

I hope we can agree that two concurrent threads with a mutable reference to the same data is never acceptable in Rust.

You do have two concurrent threads with a mutable reference, if one thread accesses it through VolatileMemory and the other does it in KVM_RUN. So my understanding is that VolatileMemory knowingly bends that rule more than a little bit.

I also want to stress the point that unsafe does not mean we are allowed to create aliased mutable references, even though unsafe allows such an event to occur

I agree, but isn't this case more or less the same as the copy case in copy_to_volatile_slice? copy_to_volatile_slice:

  • is not marked unsafe

  • takes a non-mutable VolatileSlice as the destination, and creating a VolatileSlice can create multiple aliasing *mut u8

  • dereferences a *mut u8 in such a way that you would have two concurrent pointer copies.

Hi everyone! A couple of questions come to mind looking at this discussion. First of all, there are some (apparently older) Rust docs that mention aliasing rules should hold for raw pointers as well, whereas others state (FWIW) that raw pointers are allowed to ignore many of the rules references have to follow. Is there a way to conclude whether raw pointers and refs/slices are subject to the same aliasing rules or not?

Second, can we determine if aliasing for raw data bytes in particular actually poses any risk? I imagine the compiler/optimizer/etc is free to do a lot of things with a &mut when it gets one, but is there any danger when it's "just" a &mut [u8] for example? I imagine this is implementation specific, but maybe there's a way to know for sure. I've been digging for a little while, but no success so far :(

I agree, but isn't this case more or less the same as the copy case in copy_to_volatile_slice? copy_to_volatile_slice:

  • is not marked unsafe
  • takes a non-mutable VolatileSlice as the destination, and creating a VolatileSlice can create multiple aliasing *mut u8
  • dereferences a *mut u8 in such a way that you would have two concurrent pointer copies.

The issue is with aliased mutable primitive references (i.e. &mut T), which allows UB in safe code. VolatileSlice should not be bending rules about primitive references because it doesn't use any, using *mut u8 instead. AFAICT, raw pointers in Rust do not have rules around aliasing and the Rust language does not have any implicit rules w.r.t. VolatileSlice that could lead to UB.

Concurrency is the issue. Data races have undefined behavior so two concurrent std::ptr::copy operations to the same destination are UB.

That said, while I believe that as_slice can live (as an unsafe method at least), the volatile memory part is definitely incomplete in rust-vmm. At the very least we need the volatile read/write traits from crosvm.

Resolved by #217.