wasefire_store/
file.rs

1// Copyright 2022 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! File-backed persistent flash storage for virtual authenticator.
16//!
17//! [`FileStorage`] implements the flash [`Storage`] interface but doesn't interface with an
18//! actual flash storage. Instead it uses a host-based file to persist the storage state.
19
20use alloc::borrow::Cow;
21use core::cell::RefCell;
22use std::fs::{File, OpenOptions};
23use std::io::{Read, Seek, SeekFrom, Write};
24use std::path::Path;
25
26use wasefire_error::Error;
27
28use crate::{Storage, StorageIndex};
29
30/// Simulates a flash storage using a host-based file.
31///
32/// This is usable for emulating authenticator hardware on VM hypervisor's host OS.
33pub struct FileStorage {
34    // Options of the storage.
35    options: FileOptions,
36
37    /// File for persisting contents of the storage.
38    ///
39    /// Reading data from File requires mutable reference, as seeking and reading data
40    /// changes file's current position.
41    ///
42    /// All operations on backing file internally always first seek to needed position,
43    /// so it's safe to borrow mutable reference to backing file for the time of operation.
44    file: RefCell<File>,
45}
46
47/// Options for file-backed storage.
48pub struct FileOptions {
49    /// Size of a word in bytes.
50    pub word_size: usize,
51
52    /// Size of a page in bytes.
53    pub page_size: usize,
54
55    /// Number of pages in storage.
56    pub num_pages: usize,
57}
58
59impl FileStorage {
60    pub fn new(path: &Path, options: FileOptions) -> Result<FileStorage, Error> {
61        let mut file_ref = RefCell::new(
62            OpenOptions::new().read(true).write(true).create(true).truncate(false).open(path)?,
63        );
64        let file = file_ref.get_mut();
65        let file_len = file.metadata()?.len();
66        let store_len: u64 = (options.page_size * options.num_pages) as u64;
67
68        if file_len == 0 {
69            file.seek(SeekFrom::Start(0))?;
70            let buf = vec![0xff; options.page_size];
71            for _ in 0 .. options.num_pages {
72                file.write_all(&buf)?;
73            }
74        } else if file_len != store_len {
75            // FileStorage buffer should be of fixed size, opening previously saved file
76            // from storage of different size is not supported
77            panic!("Invalid file size {file_len}, should be {store_len}");
78        }
79        Ok(FileStorage { options, file: file_ref })
80    }
81}
82
83impl Storage for FileStorage {
84    fn word_size(&self) -> usize {
85        self.options.word_size
86    }
87
88    fn page_size(&self) -> usize {
89        self.options.page_size
90    }
91
92    fn num_pages(&self) -> usize {
93        self.options.num_pages
94    }
95
96    fn max_word_writes(&self) -> usize {
97        // We can write an unlimited amount of times in a file, but the store arithmetic
98        // uses `Nat` so the value should fit in a `Nat`.
99        u32::MAX as usize
100    }
101
102    fn max_page_erases(&self) -> usize {
103        // We can "erase" an unlimited amount of times in a file, but the store format
104        // encodes the number of erase cycles on 16 bits.
105        u16::MAX as usize
106    }
107
108    fn read_slice(&self, index: StorageIndex, length: usize) -> Result<Cow<'_, [u8]>, Error> {
109        let mut file = self.file.borrow_mut();
110        file.seek(SeekFrom::Start(index.range(length, self)?.start as u64))?;
111        let mut buf = vec![0u8; length];
112        file.read_exact(&mut buf)?;
113        Ok(Cow::Owned(buf))
114    }
115
116    fn write_slice(&mut self, index: StorageIndex, value: &[u8]) -> Result<(), Error> {
117        let mut file = self.file.borrow_mut();
118        file.seek(SeekFrom::Start(index.range(value.len(), self)?.start as u64))?;
119        file.write_all(value)?;
120        Ok(())
121    }
122
123    fn erase_page(&mut self, page: usize) -> Result<(), Error> {
124        let mut file = self.file.borrow_mut();
125        let index = StorageIndex { page, byte: 0 };
126        file.seek(SeekFrom::Start(index.range(self.page_size(), self)?.start as u64))?;
127        file.write_all(&vec![0xff; self.page_size()][..])?;
128        Ok(())
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use std::path::PathBuf;
135
136    use tempfile::TempDir;
137
138    use super::*;
139
140    const BLANK_WORD: &[u8] = &[0xff, 0xff, 0xff, 0xff];
141    const DATA_WORD: &[u8] = &[0xee, 0xdd, 0xbb, 0x77];
142
143    const FILE_NAME: &str = "storage.bin";
144
145    const OPTIONS: FileOptions = FileOptions { word_size: 4, page_size: 0x1000, num_pages: 20 };
146
147    fn make_tmp_dir() -> PathBuf {
148        TempDir::new().unwrap().keep()
149    }
150
151    fn remove_tmp_dir(tmp_dir: &Path) {
152        std::fs::remove_dir_all(tmp_dir).unwrap();
153    }
154
155    fn temp_storage(tmp_dir: &Path) -> FileStorage {
156        FileStorage::new(&tmp_dir.join(FILE_NAME), OPTIONS).unwrap()
157    }
158
159    #[test]
160    fn read_write_persist_ok() {
161        let index = StorageIndex { page: 0, byte: 0 };
162        let next_index = StorageIndex { page: 0, byte: 4 };
163
164        let tmp_dir = make_tmp_dir();
165        {
166            let mut file_storage = temp_storage(&tmp_dir);
167            assert_eq!(file_storage.read_slice(index, 4).unwrap(), BLANK_WORD);
168            file_storage.write_slice(index, DATA_WORD).unwrap();
169            assert_eq!(file_storage.read_slice(index, 4).unwrap(), DATA_WORD);
170            assert_eq!(file_storage.read_slice(next_index, 4).unwrap(), BLANK_WORD);
171        }
172        // Reload and check the data from previously persisted storage
173        {
174            let file_storage = temp_storage(&tmp_dir);
175            assert_eq!(file_storage.read_slice(index, 4).unwrap(), DATA_WORD);
176            assert_eq!(file_storage.read_slice(next_index, 4).unwrap(), BLANK_WORD);
177        }
178        remove_tmp_dir(&tmp_dir);
179    }
180}