| // Copyright 2022 The ChromiumOS Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #![deny(missing_docs)] |
| |
| use std::fs::File; |
| use std::fs::OpenOptions; |
| use std::os::unix::fs::FileExt; |
| use std::os::unix::fs::OpenOptionsExt; |
| use std::path::Path; |
| |
| use base::error; |
| use base::MemoryMapping; |
| use base::MemoryMappingBuilder; |
| use base::MmapError; |
| use base::Protection; |
| use data_model::VolatileMemory; |
| use data_model::VolatileMemoryError; |
| use data_model::VolatileSlice; |
| use thiserror::Error as ThisError; |
| |
| use crate::pagesize::bytes_to_pages; |
| use crate::pagesize::is_page_aligned; |
| use crate::pagesize::pages_to_bytes; |
| |
| pub type Result<T> = std::result::Result<T, Error>; |
| |
| #[derive(ThisError, Debug)] |
| pub enum Error { |
| #[error("failed to io: {0}")] |
| Io(std::io::Error), |
| #[error("failed to mmap operation: {0}")] |
| Mmap(MmapError), |
| #[error("failed to volatile memory operation: {0}")] |
| VolatileMemory(VolatileMemoryError), |
| #[error("index is out of range")] |
| OutOfRange, |
| #[error("data size is invalid")] |
| InvalidSize, |
| } |
| |
| impl From<std::io::Error> for Error { |
| fn from(e: std::io::Error) -> Self { |
| Self::Io(e) |
| } |
| } |
| |
| impl From<MmapError> for Error { |
| fn from(e: MmapError) -> Self { |
| Self::Mmap(e) |
| } |
| } |
| |
| impl From<VolatileMemoryError> for Error { |
| fn from(e: VolatileMemoryError) -> Self { |
| Self::VolatileMemory(e) |
| } |
| } |
| |
| /// SwapFile stores active pages in a memory region. |
| /// |
| /// TODO(kawasin): The file structure is straightforward and is not optimized yet. |
| /// Each page in the file corresponds to the page in the memory region. |
| /// |
| /// The swap file is created as `O_TMPFILE` from the specified directory. As benefits: |
| /// |
| /// * it has no chance to conflict and, |
| /// * it has a security benefit that no one (except root) can access the swap file. |
| /// * it will be automatically deleted by the kernel when crosvm exits/dies or on reboot if the |
| /// device panics/hard-resets while crosvm is running. |
| #[derive(Debug)] |
| pub struct SwapFile { |
| file: File, |
| file_mmap: MemoryMapping, |
| // TODO(kawasin): convert vec with a bit vector. |
| state_list: Vec<bool>, |
| } |
| |
| impl SwapFile { |
| /// Creates an initialized [SwapFile] for a memory region. |
| /// |
| /// This creates the swapping file. If the file exists, it is truncated. |
| /// |
| /// The all pages are marked as empty at first time. |
| /// |
| /// # Arguments |
| /// |
| /// * `dir_path` - path to the directory to create a swap file from. |
| /// * `num_of_pages` - the number of pages in the region. |
| pub fn new(dir_path: &Path, num_of_pages: usize) -> Result<Self> { |
| let file = OpenOptions::new() |
| .read(true) |
| .write(true) |
| .custom_flags(libc::O_TMPFILE | libc::O_EXCL) |
| .mode(0o000) // other processes with the same uid can't open the file |
| .open(dir_path)?; |
| let file_mmap = MemoryMappingBuilder::new(pages_to_bytes(num_of_pages)) |
| .from_file(&file) |
| .protection(Protection::read()) |
| .build()?; |
| Ok(Self { |
| file, |
| file_mmap, |
| state_list: vec![false; num_of_pages], |
| }) |
| } |
| |
| /// Returns the total count of managed pages. |
| pub fn num_pages(&self) -> usize { |
| self.state_list.len() |
| } |
| |
| /// Returns a content of the page corresponding to the index. |
| /// |
| /// Returns [Option::None] if no content in the file. |
| /// |
| /// Returns [Error::OutOfRange] if the `idx` is out of range. |
| /// |
| /// # Arguments |
| /// |
| /// * `idx` - the index of the page from the head of the pages. |
| pub fn page_content(&self, idx: usize) -> Result<Option<VolatileSlice>> { |
| match self.state_list.get(idx) { |
| Some(is_present) => { |
| if *is_present { |
| let slice = self |
| .file_mmap |
| .get_slice(pages_to_bytes(idx), pages_to_bytes(1))?; |
| Ok(Some(slice)) |
| } else { |
| Ok(None) |
| } |
| } |
| None => Err(Error::OutOfRange), |
| } |
| } |
| |
| /// Clears the page in the file corresponding to the index. |
| /// |
| /// # Arguments |
| /// |
| /// * `idx` - the index of the page from the head of the pages. |
| pub fn clear(&mut self, idx: usize) -> Result<()> { |
| match self.state_list.get_mut(idx) { |
| Some(is_present) => { |
| if *is_present { |
| *is_present = false; |
| // TODO(kawasin): punch a hole to the cleared page in the file. |
| // TODO(kawasin): free the page cache for the page. |
| } |
| Ok(()) |
| } |
| None => Err(Error::OutOfRange), |
| } |
| } |
| |
| /// Writes the contents to the swap file. |
| /// |
| /// # Arguments |
| /// |
| /// * `idx` - the index of the head page of the content from the head of the pages. |
| /// * `mem_slice` - the page content(s). this can be more than 1 page. the size must align with |
| /// the pagesize. |
| pub fn write_to_file(&mut self, idx: usize, mem_slice: &[u8]) -> Result<()> { |
| // validate |
| if !is_page_aligned(mem_slice.len()) { |
| // mem_slice size must align with page size. |
| return Err(Error::InvalidSize); |
| } |
| let num_pages = bytes_to_pages(mem_slice.len()); |
| if idx + num_pages > self.state_list.len() { |
| return Err(Error::OutOfRange); |
| } |
| |
| let byte_offset = (pages_to_bytes(idx)) as u64; |
| self.file.write_all_at(mem_slice, byte_offset)?; |
| for i in idx..(idx + num_pages) { |
| self.state_list[i] = true; |
| } |
| Ok(()) |
| } |
| |
| /// Returns all present pages in the swap file. |
| pub fn all_present_pages(&self) -> PresentPagesIterator { |
| PresentPagesIterator { |
| swap_file: self, |
| idx: 0, |
| } |
| } |
| } |
| |
| /// [Iterator] traversing all present pages in the swap file. |
| pub struct PresentPagesIterator<'a> { |
| swap_file: &'a SwapFile, |
| idx: usize, |
| } |
| |
| /// Subsequent pages present in a swap file. |
| pub struct Pages<'a> { |
| pub base_idx: usize, |
| pub content: VolatileSlice<'a>, |
| } |
| |
| impl<'a> Iterator for PresentPagesIterator<'a> { |
| type Item = Pages<'a>; |
| |
| fn next(&mut self) -> Option<Self::Item> { |
| let mut head_idx = self.idx; |
| loop { |
| if head_idx >= self.swap_file.state_list.len() { |
| self.idx = head_idx; |
| return None; |
| } else if self.swap_file.state_list[head_idx] { |
| break; |
| } else { |
| head_idx += 1; |
| } |
| } |
| let mut idx = head_idx + 1; |
| while idx < self.swap_file.state_list.len() && self.swap_file.state_list[idx] { |
| idx += 1; |
| } |
| self.idx = idx; |
| let num_of_pages = idx - head_idx; |
| // The offset and count must be correct and never cause [VolatileMemoryError]. |
| let slice = self |
| .swap_file |
| .file_mmap |
| .get_slice(pages_to_bytes(head_idx), pages_to_bytes(num_of_pages)) |
| .unwrap(); |
| Some(Pages { |
| base_idx: head_idx, |
| content: slice, |
| }) |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use std::path::PathBuf; |
| use std::slice; |
| |
| use base::pagesize; |
| |
| use super::*; |
| |
| #[test] |
| fn new_success() { |
| let dir_path = tempfile::tempdir().unwrap(); |
| |
| assert_eq!(SwapFile::new(dir_path.path(), 200).is_ok(), true); |
| } |
| |
| #[test] |
| fn new_fails_to_open_file() { |
| let dir_path = PathBuf::from("/invalid/invalid/invalid"); |
| assert_eq!(SwapFile::new(&dir_path, 200).is_err(), true); |
| } |
| |
| #[test] |
| fn len() { |
| let dir_path = tempfile::tempdir().unwrap(); |
| let swap_file = SwapFile::new(dir_path.path(), 200).unwrap(); |
| |
| assert_eq!(swap_file.num_pages(), 200); |
| } |
| |
| #[test] |
| fn page_content_default_is_none() { |
| let dir_path = tempfile::tempdir().unwrap(); |
| let swap_file = SwapFile::new(dir_path.path(), 200).unwrap(); |
| |
| assert_eq!(swap_file.page_content(0).unwrap().is_none(), true); |
| } |
| |
| #[test] |
| fn page_content_returns_content() { |
| let dir_path = tempfile::tempdir().unwrap(); |
| let mut swap_file = SwapFile::new(dir_path.path(), 200).unwrap(); |
| |
| let data = &vec![1; pagesize()]; |
| swap_file.write_to_file(0, data).unwrap(); |
| |
| let page = swap_file.page_content(0).unwrap().unwrap(); |
| let result = unsafe { slice::from_raw_parts(page.as_ptr() as *const u8, pagesize()) }; |
| assert_eq!(result, data); |
| } |
| |
| #[test] |
| fn page_content_out_of_range() { |
| let dir_path = tempfile::tempdir().unwrap(); |
| let swap_file = SwapFile::new(dir_path.path(), 200).unwrap(); |
| |
| assert_eq!(swap_file.page_content(199).is_ok(), true); |
| match swap_file.page_content(200) { |
| Err(Error::OutOfRange) => {} |
| _ => unreachable!("not out of range"), |
| } |
| } |
| |
| fn assert_page_content(swap_file: &SwapFile, idx: usize, data: &[u8]) { |
| let page = swap_file.page_content(idx).unwrap().unwrap(); |
| let result = unsafe { slice::from_raw_parts(page.as_ptr() as *const u8, pagesize()) }; |
| assert_eq!(result, data); |
| } |
| |
| #[test] |
| fn write_to_file_swap_file() { |
| let dir_path = tempfile::tempdir().unwrap(); |
| let mut swap_file = SwapFile::new(dir_path.path(), 200).unwrap(); |
| |
| let buf1 = &vec![1; pagesize()]; |
| let buf2 = &vec![2; 2 * pagesize()]; |
| swap_file.write_to_file(0, buf1).unwrap(); |
| swap_file.write_to_file(2, buf2).unwrap(); |
| |
| // page_content() |
| assert_page_content(&swap_file, 0, buf1); |
| assert_page_content(&swap_file, 2, &buf2[0..pagesize()]); |
| assert_page_content(&swap_file, 3, &buf2[pagesize()..2 * pagesize()]); |
| } |
| |
| #[test] |
| fn write_to_file_invalid_size() { |
| let dir_path = tempfile::tempdir().unwrap(); |
| let mut swap_file = SwapFile::new(dir_path.path(), 200).unwrap(); |
| |
| let buf = &vec![1; pagesize() + 1]; |
| match swap_file.write_to_file(0, buf) { |
| Err(Error::InvalidSize) => {} |
| _ => unreachable!("not invalid size"), |
| }; |
| } |
| |
| #[test] |
| fn write_to_file_out_of_range() { |
| let dir_path = tempfile::tempdir().unwrap(); |
| let mut swap_file = SwapFile::new(dir_path.path(), 200).unwrap(); |
| |
| let buf1 = &vec![1; pagesize()]; |
| let buf2 = &vec![2; 2 * pagesize()]; |
| match swap_file.write_to_file(200, buf1) { |
| Err(Error::OutOfRange) => {} |
| _ => unreachable!("not out of range"), |
| }; |
| match swap_file.write_to_file(199, buf2) { |
| Err(Error::OutOfRange) => {} |
| _ => unreachable!("not out of range"), |
| }; |
| } |
| |
| #[test] |
| fn clear() { |
| let dir_path = tempfile::tempdir().unwrap(); |
| let mut swap_file = SwapFile::new(dir_path.path(), 200).unwrap(); |
| |
| let data = &vec![1; pagesize()]; |
| swap_file.write_to_file(0, data).unwrap(); |
| swap_file.clear(0).unwrap(); |
| |
| assert_eq!(swap_file.page_content(0).unwrap().is_none(), true); |
| } |
| |
| #[test] |
| fn clear_out_of_range() { |
| let dir_path = tempfile::tempdir().unwrap(); |
| let mut swap_file = SwapFile::new(dir_path.path(), 200).unwrap(); |
| |
| assert_eq!(swap_file.clear(199).is_ok(), true); |
| match swap_file.clear(200) { |
| Err(Error::OutOfRange) => {} |
| _ => unreachable!("not out of range"), |
| }; |
| } |
| |
| #[test] |
| fn all_present_pages_empty() { |
| let dir_path = tempfile::tempdir().unwrap(); |
| let swap_file = SwapFile::new(dir_path.path(), 200).unwrap(); |
| |
| let mut iter = swap_file.all_present_pages(); |
| |
| assert_eq!(iter.next().is_none(), true); |
| } |
| |
| #[test] |
| fn all_present_pages_return_pages() { |
| let dir_path = tempfile::tempdir().unwrap(); |
| let mut swap_file = SwapFile::new(dir_path.path(), 200).unwrap(); |
| |
| swap_file |
| .write_to_file(1, &vec![1_u8; 2 * pagesize()]) |
| .unwrap(); |
| // whole pages are cleared |
| swap_file |
| .write_to_file(10, &vec![2_u8; 2 * pagesize()]) |
| .unwrap(); |
| swap_file.clear(10).unwrap(); |
| swap_file.clear(11).unwrap(); |
| // pages are partially cleared. |
| swap_file |
| .write_to_file(13, &vec![3_u8; 3 * pagesize()]) |
| .unwrap(); |
| swap_file.clear(13).unwrap(); |
| // pages are splitted |
| swap_file |
| .write_to_file(17, &vec![4_u8; 3 * pagesize()]) |
| .unwrap(); |
| swap_file.clear(18).unwrap(); |
| // pages are combined |
| swap_file |
| .write_to_file(30, &vec![5_u8; pagesize()]) |
| .unwrap(); |
| swap_file |
| .write_to_file(31, &vec![6_u8; pagesize()]) |
| .unwrap(); |
| |
| let mut iter = swap_file.all_present_pages(); |
| |
| let pages = iter.next().unwrap(); |
| assert_eq!(pages.base_idx, 1); |
| assert_eq!(pages.content.size(), 2 * pagesize()); |
| assert_eq!(unsafe { *pages.content.as_ptr() }, 1_u8); |
| let pages = iter.next().unwrap(); |
| assert_eq!(pages.base_idx, 14); |
| assert_eq!(pages.content.size(), 2 * pagesize()); |
| assert_eq!(unsafe { *pages.content.as_ptr() }, 3_u8); |
| let pages = iter.next().unwrap(); |
| assert_eq!(pages.base_idx, 17); |
| assert_eq!(pages.content.size(), pagesize()); |
| assert_eq!(unsafe { *pages.content.as_ptr() }, 4_u8); |
| let pages = iter.next().unwrap(); |
| assert_eq!(pages.base_idx, 19); |
| assert_eq!(pages.content.size(), pagesize()); |
| assert_eq!(unsafe { *pages.content.as_ptr() }, 4_u8); |
| let pages = iter.next().unwrap(); |
| assert_eq!(pages.base_idx, 30); |
| assert_eq!(pages.content.size(), 2 * pagesize()); |
| assert_eq!(unsafe { *pages.content.as_ptr() }, 5_u8); |
| assert_eq!( |
| unsafe { *pages.content.offset(pagesize()).unwrap().as_ptr() }, |
| 6_u8 |
| ); |
| } |
| } |