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:
- ensure no current accesses to the returned slices from the VMM side.
- 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
andVolatileSlice
, 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 aVolatileSlice
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 incopy_to_volatile_slice
?copy_to_volatile_slice
:
- is not marked unsafe
- takes a non-mutable
VolatileSlice
as the destination, and creating aVolatileSlice
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.