Skip to content

Commit 2fdaa83

Browse files
committed
Add VolatileRead/VolatileWrite traits
These are essentially clones of the Read/Write traits from the standard library, but instead of operating on &[u8] and &mut [u8], they operate on VolatileSlices. We cannot use the stdlib traits to operate on guest memory due to unsoundness concerns, see rust-vmm/vm-memory#217 and 228. A default implementation is provided for `File` and `UnixStream` (as these are the types for which firecracker needs them). I have done experiements with instead providing a blanket implementation for `T: AsRawFd`, however ran into some problems with trait coherence rules, as such a blanket implementation makes the implementation of Read/WriteVolatile for &[u8]/&mut [u8] impossible. This allows us to also potentially choose different implementations for different kind of `AsRawFd`s (e.g. using `recv` for sockets). This can all be revised later though. These traits would fit nicely into rust-vmm, and I'll put out feelers about interest regarding these traits after our release tasks are done. Signed-off-by: Patrick Roy <[email protected]>
1 parent 799533e commit 2fdaa83

File tree

1 file changed

+354
-3
lines changed

1 file changed

+354
-3
lines changed

src/utils/src/vm_memory.rs

Lines changed: 354 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,17 @@
55
// Use of this source code is governed by a BSD-style license that can be
66
// found in the THIRD-PARTY file.
77

8-
use std::io::Error as IoError;
8+
use std::io::{Error as IoError, ErrorKind};
99
use std::os::unix::io::AsRawFd;
1010

1111
use vm_memory::bitmap::AtomicBitmap;
12-
pub use vm_memory::bitmap::Bitmap;
12+
pub use vm_memory::bitmap::{Bitmap, BitmapSlice};
1313
use vm_memory::mmap::{check_file_offset, NewBitmap};
1414
pub use vm_memory::mmap::{MmapRegionBuilder, MmapRegionError};
1515
pub use vm_memory::{
1616
address, Address, ByteValued, Bytes, Error, FileOffset, GuestAddress, GuestMemory,
1717
GuestMemoryError, GuestMemoryRegion, GuestUsize, MemoryRegionAddress, MmapRegion,
18-
VolatileMemory, VolatileMemoryError,
18+
VolatileMemory, VolatileMemoryError, VolatileSlice,
1919
};
2020

2121
pub type GuestMemoryMmap = vm_memory::GuestMemoryMmap<Option<AtomicBitmap>>;
@@ -142,6 +142,212 @@ pub fn mark_dirty_mem(mem: &GuestMemoryMmap, addr: GuestAddress, len: usize) {
142142
});
143143
}
144144

145+
pub trait ReadVolatile {
146+
fn read_volatile<B: BitmapSlice>(
147+
&mut self,
148+
buf: &mut VolatileSlice<B>,
149+
) -> Result<usize, VolatileMemoryError>;
150+
151+
fn read_exact_volatile<B: BitmapSlice>(
152+
&mut self,
153+
buf: &mut VolatileSlice<B>,
154+
) -> Result<(), VolatileMemoryError> {
155+
// Implementation based on https://github.com/rust-lang/rust/blob/7e7483d26e3cec7a44ef00cf7ae6c9c8c918bec6/library/std/src/io/mod.rs#L465
156+
157+
// Doing the trick the stdlib does where in each loop, the given slice is adjusted does not
158+
// work for us as VolatileSlice::offset returns an owned copy, but `buf` is a
159+
// reference. This means the first iteration of the loop would have to work with a
160+
// reference, and all following iterations with an owned copy. This is not (easily)
161+
// expressible in rust's type system, which is why we use this approach. Note that the total
162+
// number of `.offset` calls are the same, meaning we do not incur a performance impact (as
163+
// VolatileSlice::offset is a O(1) operation).
164+
let mut bytes_read_so_far = 0;
165+
166+
while bytes_read_so_far < buf.len() {
167+
match self.read_volatile(&mut buf.offset(bytes_read_so_far)?) {
168+
Err(VolatileMemoryError::IOError(err)) if err.kind() == ErrorKind::Interrupted => {
169+
continue
170+
}
171+
Ok(0) => {
172+
return Err(VolatileMemoryError::IOError(std::io::Error::new(
173+
ErrorKind::UnexpectedEof,
174+
"failed to fill whole buffer",
175+
)))
176+
}
177+
Ok(bytes_read) => bytes_read_so_far += bytes_read,
178+
Err(err) => return Err(err),
179+
}
180+
}
181+
182+
Ok(())
183+
}
184+
}
185+
186+
pub trait WriteVolatile {
187+
fn write_volatile<B: BitmapSlice>(
188+
&mut self,
189+
buf: &VolatileSlice<B>,
190+
) -> Result<usize, VolatileMemoryError>;
191+
192+
fn write_all_volatile<B: BitmapSlice>(
193+
&mut self,
194+
buf: &VolatileSlice<B>,
195+
) -> Result<(), VolatileMemoryError> {
196+
// Based on https://github.com/rust-lang/rust/blob/7e7483d26e3cec7a44ef00cf7ae6c9c8c918bec6/library/std/src/io/mod.rs#L1570
197+
198+
// See comment in ReadVolatile::read_exact_volatile for reasons for this variable.
199+
let mut bytes_written_so_far = 0;
200+
201+
while bytes_written_so_far < buf.len() {
202+
match self.write_volatile(&buf.offset(bytes_written_so_far)?) {
203+
Err(VolatileMemoryError::IOError(err)) if err.kind() == ErrorKind::Interrupted => {
204+
continue
205+
}
206+
Ok(0) => {
207+
return Err(VolatileMemoryError::IOError(std::io::Error::new(
208+
ErrorKind::WriteZero,
209+
"failed to write whole buffer",
210+
)))
211+
}
212+
Ok(bytes_written) => bytes_written_so_far += bytes_written,
213+
Err(err) => return Err(err),
214+
}
215+
}
216+
217+
Ok(())
218+
}
219+
}
220+
221+
impl ReadVolatile for std::fs::File {
222+
fn read_volatile<B: BitmapSlice>(
223+
&mut self,
224+
buf: &mut VolatileSlice<B>,
225+
) -> Result<usize, VolatileMemoryError> {
226+
read_volatile_raw_fd(self, buf)
227+
}
228+
}
229+
230+
impl ReadVolatile for std::os::unix::net::UnixStream {
231+
fn read_volatile<B: BitmapSlice>(
232+
&mut self,
233+
buf: &mut VolatileSlice<B>,
234+
) -> Result<usize, VolatileMemoryError> {
235+
read_volatile_raw_fd(self, buf)
236+
}
237+
}
238+
239+
fn read_volatile_raw_fd(
240+
raw_fd: &mut impl AsRawFd,
241+
buf: &mut VolatileSlice<impl BitmapSlice>,
242+
) -> Result<usize, VolatileMemoryError> {
243+
let fd = raw_fd.as_raw_fd();
244+
let dst = buf.as_ptr().cast::<libc::c_void>();
245+
246+
buf.bitmap().mark_dirty(0, buf.len());
247+
248+
// SAFETY: We got a valid file descriptor from `AsRawFd`. The memory pointed to by `dst` is
249+
// valid for writes of length `buf.len() by the invariants upheld by the constructor
250+
// of `VolatileSlice`.
251+
let bytes_read = unsafe { libc::read(fd, dst, buf.len()) };
252+
253+
if bytes_read < 0 {
254+
Err(VolatileMemoryError::IOError(std::io::Error::last_os_error()))
255+
} else {
256+
Ok(bytes_read.try_into().unwrap())
257+
}
258+
}
259+
260+
impl WriteVolatile for std::fs::File {
261+
fn write_volatile<B: BitmapSlice>(
262+
&mut self,
263+
buf: &VolatileSlice<B>,
264+
) -> Result<usize, VolatileMemoryError> {
265+
write_volatile_raw_fd(self, buf)
266+
}
267+
}
268+
269+
impl WriteVolatile for std::os::unix::net::UnixStream {
270+
fn write_volatile<B: BitmapSlice>(
271+
&mut self,
272+
buf: &VolatileSlice<B>,
273+
) -> Result<usize, VolatileMemoryError> {
274+
write_volatile_raw_fd(self, buf)
275+
}
276+
}
277+
278+
fn write_volatile_raw_fd(
279+
raw_fd: &mut impl AsRawFd,
280+
buf: &VolatileSlice<impl BitmapSlice>,
281+
) -> Result<usize, VolatileMemoryError> {
282+
let fd = raw_fd.as_raw_fd();
283+
let src = buf.as_ptr().cast::<libc::c_void>();
284+
285+
// SAFETY: We got a valid file descriptor from `AsRawFd`. The memory pointed to by `src` is
286+
// valid for reads of length `buf.len() by the invariants upheld by the constructor
287+
// of `VolatileSlice`.
288+
let bytes_written = unsafe { libc::write(fd, src, buf.len()) };
289+
290+
if bytes_written < 0 {
291+
Err(VolatileMemoryError::IOError(std::io::Error::last_os_error()))
292+
} else {
293+
Ok(bytes_written.try_into().unwrap())
294+
}
295+
}
296+
297+
impl WriteVolatile for &mut [u8] {
298+
fn write_volatile<B: BitmapSlice>(
299+
&mut self,
300+
buf: &VolatileSlice<B>,
301+
) -> Result<usize, VolatileMemoryError> {
302+
// NOTE: The duality of read <-> write here is correct. This is because we translate a call
303+
// "slice.write(buf)" (e.g. write into slice from buf) into "buf.read(slice)" (e.g. read
304+
// from buffer into slice). Both express data transfer from the buffer to the slice
305+
buf.read(self, 0)
306+
}
307+
308+
fn write_all_volatile<B: BitmapSlice>(
309+
&mut self,
310+
buf: &VolatileSlice<B>,
311+
) -> Result<(), VolatileMemoryError> {
312+
// Based on https://github.com/rust-lang/rust/blob/f7b831ac8a897273f78b9f47165cf8e54066ce4b/library/std/src/io/impls.rs#L376-L382
313+
if self.write_volatile(buf)? == buf.len() {
314+
Ok(())
315+
} else {
316+
Err(VolatileMemoryError::IOError(std::io::Error::new(
317+
ErrorKind::WriteZero,
318+
"failed to write whole buffer",
319+
)))
320+
}
321+
}
322+
}
323+
324+
impl ReadVolatile for &[u8] {
325+
fn read_volatile<B: BitmapSlice>(
326+
&mut self,
327+
buf: &mut VolatileSlice<B>,
328+
) -> Result<usize, VolatileMemoryError> {
329+
// NOTE: the duality of read <-> write here is correct. This is because we translate a call
330+
// "slice.read(buf)" (e.g. "read from slice into buf") into "buf.write(slice)" (e.g. write
331+
// into buf from slice)
332+
buf.write(self, 0)
333+
}
334+
335+
fn read_exact_volatile<B: BitmapSlice>(
336+
&mut self,
337+
buf: &mut VolatileSlice<B>,
338+
) -> Result<(), VolatileMemoryError> {
339+
// Based on https://github.com/rust-lang/rust/blob/f7b831ac8a897273f78b9f47165cf8e54066ce4b/library/std/src/io/impls.rs#L282-L302
340+
if buf.len() > self.len() {
341+
return Err(VolatileMemoryError::IOError(std::io::Error::new(
342+
ErrorKind::UnexpectedEof,
343+
"failed to fill whole buffer",
344+
)));
345+
}
346+
347+
self.read_volatile(buf).map(|_| ())
348+
}
349+
}
350+
145351
pub mod test_utils {
146352
use super::*;
147353

@@ -193,6 +399,8 @@ pub mod test_utils {
193399
#[cfg(test)]
194400
mod tests {
195401
#![allow(clippy::undocumented_unsafe_blocks)]
402+
use std::io::{Read, Seek, Write};
403+
196404
use super::*;
197405
use crate::get_page_size;
198406
use crate::tempfile::TempFile;
@@ -444,4 +652,147 @@ mod tests {
444652
.unwrap();
445653
}
446654
}
655+
656+
#[test]
657+
fn test_read_volatile() {
658+
let test_cases = [
659+
([1u8, 2].as_ref(), [1u8, 2, 0, 0, 0]),
660+
([1, 2, 3, 4].as_ref(), [1, 2, 3, 4, 0]),
661+
// ensure we don't have a buffer overrun
662+
([5, 6, 7, 8, 9].as_ref(), [5, 6, 7, 8, 0]),
663+
];
664+
665+
for (mut input, output) in test_cases {
666+
// ---- Test ReadVolatile for &[u8] ----
667+
//
668+
// Test read_volatile for &[u8] works
669+
let mut memory = vec![0u8; 5];
670+
671+
assert_eq!(
672+
input
673+
.read_volatile(&mut VolatileSlice::from(&mut memory[..4]))
674+
.unwrap(),
675+
input.len().min(4)
676+
);
677+
assert_eq!(&memory, &output);
678+
679+
// Test read_exact_volatile for &[u8] works
680+
let mut memory = vec![0u8; 5];
681+
let result = input.read_exact_volatile(&mut VolatileSlice::from(&mut memory[..4]));
682+
683+
// read_exact fails if there are not enough bytes in input to completely fill
684+
// memory[..4]
685+
if input.len() < 4 {
686+
result.unwrap_err();
687+
assert_eq!(memory, vec![0u8; 5]);
688+
} else {
689+
result.unwrap();
690+
assert_eq!(&memory, &output);
691+
}
692+
693+
// ---- Test ReadVolatile for File ----
694+
695+
let mut temp_file = TempFile::new().unwrap().into_file();
696+
temp_file.write_all(input).unwrap();
697+
temp_file.rewind().unwrap();
698+
699+
// Test read_volatile for File works
700+
let mut memory = vec![0u8; 5];
701+
702+
assert_eq!(
703+
temp_file
704+
.read_volatile(&mut VolatileSlice::from(&mut memory[..4]))
705+
.unwrap(),
706+
input.len().min(4)
707+
);
708+
assert_eq!(&memory, &output);
709+
710+
temp_file.rewind().unwrap();
711+
712+
// Test read_exact_volatile for File works
713+
let mut memory = vec![0u8; 5];
714+
715+
let read_exact_result =
716+
temp_file.read_exact_volatile(&mut VolatileSlice::from(&mut memory[..4]));
717+
718+
if input.len() < 4 {
719+
read_exact_result.unwrap_err();
720+
} else {
721+
read_exact_result.unwrap();
722+
}
723+
assert_eq!(&memory, &output);
724+
}
725+
}
726+
727+
#[test]
728+
fn test_write_volatile() {
729+
let test_cases = [
730+
(vec![1u8, 2], [1u8, 2, 0, 0, 0]),
731+
(vec![1, 2, 3, 4], [1, 2, 3, 4, 0]),
732+
// ensure we don't have a buffer overrun
733+
(vec![5, 6, 7, 8, 9], [5, 6, 7, 8, 0]),
734+
];
735+
736+
for (mut input, output) in test_cases {
737+
// ---- Test WriteVolatile for &mut [u8] ----
738+
//
739+
// Test write_volatile for &mut [u8] works
740+
let mut memory = vec![0u8; 5];
741+
742+
assert_eq!(
743+
(&mut memory[..4])
744+
.write_volatile(&VolatileSlice::from(input.as_mut_slice()))
745+
.unwrap(),
746+
input.len().min(4)
747+
);
748+
assert_eq!(&memory, &output);
749+
750+
// Test write_all_volatile for &mut [u8] works
751+
let mut memory = vec![0u8; 5];
752+
753+
let result =
754+
(&mut memory[..4]).write_all_volatile(&VolatileSlice::from(input.as_mut_slice()));
755+
756+
if input.len() > 4 {
757+
result.unwrap_err();
758+
// This quirky behavior of writing to the slice even in the case of failure is also
759+
// exhibited by the stdlib
760+
assert_eq!(&memory, &output);
761+
} else {
762+
result.unwrap();
763+
assert_eq!(&memory, &output);
764+
}
765+
766+
// ---- Test ẂriteVolatile for File works
767+
// Test write_volatile for File works
768+
let mut temp_file = TempFile::new().unwrap().into_file();
769+
770+
temp_file
771+
.write_volatile(&VolatileSlice::from(input.as_mut_slice()))
772+
.unwrap();
773+
temp_file.rewind().unwrap();
774+
775+
let mut written = vec![0u8; input.len()];
776+
temp_file.read_exact(written.as_mut_slice()).unwrap();
777+
778+
assert_eq!(input, written);
779+
// check no excess bytes were written to the file
780+
assert_eq!(temp_file.read(&mut [0u8]).unwrap(), 0);
781+
782+
// Test write_all_volatile for File works
783+
let mut temp_file = TempFile::new().unwrap().into_file();
784+
785+
temp_file
786+
.write_all_volatile(&VolatileSlice::from(input.as_mut_slice()))
787+
.unwrap();
788+
temp_file.rewind().unwrap();
789+
790+
let mut written = vec![0u8; input.len()];
791+
temp_file.read_exact(written.as_mut_slice()).unwrap();
792+
793+
assert_eq!(input, written);
794+
// check no excess bytes were written to the file
795+
assert_eq!(temp_file.read(&mut [0u8]).unwrap(), 0);
796+
}
797+
}
447798
}

0 commit comments

Comments
 (0)