xous-core/services/pddb/src/backend/basis.rs
2022-03-29 16:33:40 +08:00

1616 lines
87 KiB
Rust

/// # The Organization of Basis Data
///
/// ## Overview
/// In basis space, the BasisRoot is located at VPAGE #1 (VPAGE #0 is always invalid).
/// A VPAGE is 0xFE0 (4,064) bytes long, which is equal to a PAGE of 4k minus 32 bytes of encryption+journal
/// overhead.
///
/// *NB: The factors of 4,064 are: 1, 2, 4, 8, 16, 32, 127, 254, 508, 1016, 2032, 4064*
///
/// AAD associated with the BasisRoot consist of a bytewise concatenation of:
/// - Basis name
/// - version number (should match version inside; complicates downgrade attacks)
/// - FPGA's silicon DNA number (makes a naive raw-copy of the data to another device unusable;
/// but of course, the DNA ID can be forged minor efforts)
///
/// Usage Assumptions:
/// - Most mutability happens on the data keys themselves (keys are read/write/modify routinely).
/// - Dictionary modifications (key addition or removal) are about 20x less frequent than key mods.
/// - Basis modifications (creation/removal of dictionaries) is about 10x less frequent than dictionary mods.
/// - According to https://www.pdl.cmu.edu/PDL-FTP/HECStorage/Yifan_Final.pdf, 0.01% of files (1 in 10,000)
/// require a name over 100 bytes long; 0.1% require longer than 64 bytes. There longest filename identified
/// was 143 bytes long. Study surveys ~14M files on the LANL network.
/// - Same study says 99.9% of directories have under 1k files, 99.999% under 10k
///
/// The root basis structure takes up the first valid VPAGE in the virtual memory space.
/// It contains a count of the number of valid dictionaries in the Basis. Dictionaries are found at
/// fixed offsets starting at 0xFE_0000 and repeating every 0xFE_0000 intervals, with up to 16383 dictionaries
/// allowed. A naive linear search is used to scan for dictionaries, starting at the lowest address,
/// scanning every 0xFE_0000, until the number of valid dictionaries have been found that matches the valid
/// dictionary count prescribed in the Basis root. A dictionary can be effectively deleted by just marking its
/// descriptor as invalid.
///
/// A stride of 0xFE_0000 means that dictionary descriptors can be up to 4096 VPAGEs long. A dictionary
/// descriptor consists of a `DictDescriptor` header, some bookkeeping data, plus a count of the number
/// of keys in the dictionary. Following the header is a list of key descriptors. Similar to the DictDescriptors,
/// the key descriptors are stored at a stride of 127 (or 32 per VPAGE); they can be deleted by being marked
/// as invalid, and a linear scan is used to identify all the entries. A KeyDescriptor contains the name
/// of the key, flags, its age, and pointers to the key data in virtual memory space + its length.
/// This leads to a name length restriction of roughly 115 characters for keys and dictionaries, which is
/// about half of what most filesystems allow, but accommodates roughly 99.99% of the use cases.
///
/// Thus adding a new dictionary always consumes at least one 4k page, but you can have up to 15 keys
/// in that dictionary with no extra bookkeeping cost once the dictionary is added.
///
///
/// ## Basis Virtual Memory Layout
///```Text
/// | Start Address | |
/// |------------------------|-------------------------------------------|
/// | 0x0000_0000_0000_0000 | Invalid -- VPAGE 0 reserved for Option<> |
/// | 0x0000_0000_0000_0FE0 | Basis root page |
/// | 0x0000_0000_00FE_0000 | Dictionary[0] |
/// | +0 | - Dict header (127 bytes) |
/// | +7F | - Maybe key entry (127 bytes) |
/// | +FE | - Maybe key entry (127 bytes) |
/// | +FD_FF02 | - Last key entry start (128k possible) |
/// | 0x0000_0000_01FC_0000 | Dictionary[1] |
/// | 0x0000_003F_7F02_0000 | Dictionary[16382] |
/// | 0x0000_003F_8000_0000 | Small data pool start (~256GiB) |
/// | | - Dict[0] pool = 16MiB (4k vpages) |
/// | | - SmallPool[0] |
/// | +FE0 | - SmallPool[1]
/// | 0x0000_003F_80FE_0000 | - Dict[1] pool = 16MiB |
/// | 0x0000_007E_FE04_0000 | - Dict[16383] pool |
/// | 0x0000_007E_FF02_0000 | Unused |
/// | 0x0000_007F_0000_0000 | Medium data pool start |
/// | | - TBD |
/// | 0x0000_FE00_0000_0000 | Large data pool start (~16mm TiB) |
/// | | - Demand-allocated, bump-pointer |
/// | | currently no defrag |
/// ```
///
/// Note that each Basis has its own memory section, and you can have "many" orthogonal Basis without
/// a collision -- the AES keyspace is 128 bits, so you have a decent chance of no collisions
/// even with a few billion Basis concurrently existing in the filesystem.
///
/// ## Memory Pools
///
/// Key data is split into three categories of sizes: small, medium, and large; but the implementation
/// currently only deals with small and large keys. The thresholds are subject to tuning, but
/// roughly speaking, small data are keys <4k bytes; large keys are everything else.
///
/// Large keys are the simplest - each key starts at a VPAGE-aligned address, and allocates
/// up from there. Any unused amount is wasted, but with a ~32k threshold you'll have no worse
/// than 12.5% unused space, probably closer to ~7%-ish if all your data hovered around the threshold.
/// The allocation is a simple pointer that just keeps going up. De-allocated space is never defragmented,
/// and we just rely on the space being "huge" to save us.
///
/// Small keys are kept in VPAGE-sized pools of data, and compacted together in RAM. The initial, naive
/// implementation simply keeps all small keys in a HashMap in RAM, and when it comes time to sync them
/// to disk, they are sorted by update count, and written to disk in ascending order.
///
/// Medium keys have a TBD implementation, and are currently directed to the large pool for now.
///
/// ## The Alignment and Serialization Chronicles
///
/// We're using Repr(C) and alignment to 64-bits to create a consistent "FFI" layout; we use an unsafe cast
/// to [u8] as our method to serialize the structure, which means we could be subject to breakage if the Rust
/// compiler decides to change its Repr(C) FFI (it's not guaranteed, but I think at this point in the lifecycle
/// with simple primitive types it's hard to see it changing). This puts some requirements on the ordering of
/// fields in the struct below. Note that the serialization is all double-checked by the pddbdbg.py script.
///
/// In coming to the choice to use Repr(C), I experimented with rkyv and bincode. bincode relies on the serde
/// crate, which, as of Nov 2021, has troubles taking in const generics, and thus barfs on our fixed-sized
/// string allocations that are longer than 32 bytes. Version 2.0 of bincode /might/ do this better, but as
/// of the design of this crate, it's in "alpha" with no official release to crates.io, so we're avoiding it;
/// but for sure 1.3.3 of bincode (latest stable as of the design) cannot do the job, and there's a few other
/// users reporting the issue so I'm pretty sure it's not "user error" on my part.
///
/// rkyv handles const generics well, and it perhaps very reasonably shuffles around the order of structures
/// in the struct to improve the packing efficiency. However, this has the property that rkyv ser will never break
/// rkyv deser, but unfortunately you can't interoperate with anything that isn't rkyv (e.g., describing the data
/// layout to someone who wants to do a C implementation). There's also a risk that if we are forced to
/// upgrade rkyv later on we might break compatibility with what's stored on disk, although I'm pretty sure the
/// maintainer of rkyv tries to avoid that as much as possible.
///
/// Repr(C), while also not guaranteed to be stable, has pressure from the CFFI users at least to keep
/// things as stable as possible, and it is by definition inter-operable with C. Repr(C) is native to Rust,
/// with no additional dependencies to pull in, which helps reduce the code base size overall.
/// So, we're using a repr(C) with an align(8), and then carefully checking our structure organization and
/// elements to keep things "in spec" with what C can natively understand, in an effort to create a disk
/// storage structure that can persist through future versions of Rust and also other implementations in other
/// languages.
///
/// ## Known Repr(C) footguns:
/// - When you start laying in 64-bit types, stuff has to be 64-bit aligned, or else you'll start to get
/// uninitialized padding data inserted, which can leak stack data in the serialization process.
/// - Don't use anything that's not native to C. In particular, for primitives that we want to be "Option"
/// wrapped, we're using a NonZeroU64 format. The compiler knows how to turn that into a 64-bit C-friendly
/// structure and serialize/deserialize that into the correct Rust structure. See
/// https://doc.rust-lang.org/nomicon/other-reprs.html for a citation on that.
use crate::api::*;
use super::*;
use core::ops::{Deref, DerefMut};
use core::mem::size_of;
use std::convert::TryInto;
use aes_gcm_siv::{Aes256GcmSiv, Key};
use aes_gcm_siv::aead::NewAead;
use aes::Aes256;
use aes::cipher::{NewBlockCipher, generic_array::GenericArray};
use std::iter::IntoIterator;
use std::collections::{BinaryHeap, HashMap, HashSet};
use std::io::{Result, Error, ErrorKind};
use std::cmp::Reverse;
use core::num::NonZeroU32;
pub(crate) const SMALL_POOL_START: u64 = 0x0000_003F_8000_0000;
pub(crate) const SMALL_POOL_END: u64 = 0x0000_007E_FF02_0000;
pub(crate) const SMALL_POOL_STRIDE: u64 = 0xFE_0000;
/// we don't want this bigger than VPAGE_SIZE, because a key goal of the small pool is to
/// reduce # of writes to the disk of small data. While we could get some gain in memory efficiency
/// if we made this larger than a VPAGE_SIZE, we don't get much gain in terms of write reduction,
/// and it greatly complicates the implementation. So, SMALL_CAPACITY should be less than VPAGE_SIZE.
pub(crate) const SMALL_CAPACITY: usize = VPAGE_SIZE;
pub(crate) const LARGE_POOL_START: u64 = 0x0000_FE00_0000_0000;
pub(crate) const KEY_MAXCOUNT: usize = 131_071; // 2^17 - 1
/// This is a size limit on the biggest file you can create. It's currently 32GiB. No, this is not
/// web scale, but it's big enough to hold a typical blu-ray movie as a single file. You can adjust
/// this constant up or down, and the trade-off is, you get more or less total number of large files
/// allocated over the life of the filesystem. We simply "increment a pointer" when a new large file
/// is added to create the next virtual memory spot for the large file. So at 32GiB, you can create
/// a lifetime total of about 200 million files (this includes files you've previously deleted, until
/// we create a mechanism for sweeping through the memory space and tracking de-allocations). Note that
/// a "large" file includes anything over 4kiB, so if you create a 5kiB file, it can potentially grow to
/// 32 GiB without bumping into the next large file. This is a very "lazy" way to deal with large files.
/// Given that the PDDB is designed for a 32-bit device with only 128MiB of memory and a read/write lifetime
/// of 100k cycles for the FLASH, 200 million file allocations is probably greater than the lifetime of
/// the device itself. If the PDDB migrates to a larger handphone-style application, I think it'll probably
/// still hold up OK with 200 million total large file allocations over the device lifetime and a limit
/// of 32GiB. That's about 73k files created per day for 10 years, or about 50 files per minute -- roughly
/// one new file per second for 10 years straight before the PDDB runs out of virtual memory space.
/// A web server creating a >4k temporary log file for every client that hit and then deleting it
/// would probably crush this limit in months. So don't use the PDDB to back a high volume web server.
/// But it's probably OK for a consumer electronics device with a typical lifetime of less than 10 years.
/// If you really think you want larger files and also more write life, you'd need to implement an in-memory
/// "free" file allocator, but honestly, this is not something I think we need to burn resources on for
/// the initial target of the PDDB (that is, a 100MiB device with 100k read/write endurance lifetime).
/// Anyways, the code is written so you can just slide this constant up or down and change the behavior
/// of the system; it's recommended you reformat when you do that but I /think/ it should actually be OK
/// if you made a change "on the fly".
///
/// Also note that in practice, a file size is limited to 4GiB on a 32-bit Precursor device anyways
/// because the usize type isn't big enough. Recompiling for a 64-bit target, however, should give
/// you access to the 32GiB file size limit.
pub(crate) const LARGE_FILE_MAX_SIZE: u64 = 0x0000_0008_0000_0000;
/// The chosen "stride" of a dict/key entry. Drives a lot of key parameters in the database's characteristics.
/// This is chosen such that 32 of these entries fit evenly into a VPAGE.
pub(crate) const DK_STRIDE: usize = 127;
//// DK_STRIDES per VPAGE
pub(crate) const DK_PER_VPAGE: usize = VPAGE_SIZE / DK_STRIDE; // should be 32 - use this for computing modulus on dictionary indices
/// size of a dictionary region in virtual memory
pub(crate) const DICT_VSIZE: u64 = 0xFE_0000;
/// maximum number of dictionaries in a system
pub(crate) const DICT_MAXCOUNT: usize = 16383;
/// This is the format of the Basis as stored on disk
#[derive(PartialEq, Debug, Default)]
#[repr(C, align(8))]
pub(crate) struct BasisRoot {
pub(crate) magic: [u8; 4],
pub(crate) version: u32,
/// increments every time the BasisRoot is modified. This field must saturate, not roll over.
pub(crate) age: u32,
/// number of dictionaries.
pub(crate) num_dictionaries: u32,
/* at this point, we are aligned to a 64-bit boundary. All data must stay aligned to this boundary from here out! */
/// 64-byte name; aligns to 64-bits
pub(crate) name: BasisRootName,
}
impl BasisRoot {
pub(crate) fn aad(&self, dna: u64) -> Vec::<u8> {
let mut aad = Vec::<u8>::new();
aad.extend_from_slice(&self.name.data[..self.name.len as usize]);
aad.extend_from_slice(&PDDB_VERSION.to_le_bytes());
aad.extend_from_slice(&dna.to_le_bytes());
aad
}
}
impl Deref for BasisRoot {
type Target = [u8];
fn deref(&self) -> &[u8] {
unsafe {
core::slice::from_raw_parts(self as *const BasisRoot as *const u8, core::mem::size_of::<BasisRoot>())
as &[u8]
}
}
}
impl DerefMut for BasisRoot {
fn deref_mut(&mut self) -> &mut [u8] {
unsafe {
core::slice::from_raw_parts_mut(self as *mut BasisRoot as *mut u8, core::mem::size_of::<BasisRoot>())
as &mut [u8]
}
}
}
/// A list of open Basis that we can use to search and operate upon. Sort of the "root" data structure of the PDDB.
///
/// Note to self: it's tempting to integrate the "hw" parameter (the pointer to the PddbOs structure). However, this
/// results in interior mutability problems. I guess we could wrap it in a Rc or RefCell or something like that; but
/// the inconvenience of passing the hw structure around doesn't seem too bad so far...
pub(crate) struct BasisCache {
/// the cache entries themselves
cache: Vec::<BasisCacheEntry>,
}
impl BasisCache {
pub(crate) fn new() -> Self {
BasisCache { cache: Vec::new() }
}
fn select_basis(&mut self, basis_name: Option<&str>) -> Option<usize> {
if self.cache.len() == 0 {
log::error!("Can't select basis: PDDB is not mounted");
return None
}
if let Some(n) = basis_name {
self.cache.iter().position(|bc| bc.name == n)
} else {
Some(self.cache.len() - 1)
}
}
pub(crate) fn basis_count(&self) -> usize {self.cache.len()}
/// Adds a dictionary with `name` to:
/// - if `basis_name` is None, the most recently opened basis
/// - if `basis_name` is Some, searches for the given basis and adds the dictionary to that.
/// If the dictionary already exists, it returns an informative error.
pub(crate) fn dict_add(&mut self, hw: &mut PddbOs, name: &str, basis_name: Option<&str>) -> Result<()> {
if !hw.ensure_fast_space_alloc(2, &self.cache) {
return Err(Error::new(ErrorKind::OutOfMemory, "No free space to allocate dict"));
}
if let Some(basis_index) = self.select_basis(basis_name) {
let basis = &mut self.cache[basis_index];
basis.age = basis.age.saturating_add(1);
if basis.ensure_dict_in_cache(hw, name) {
return Err(Error::new(ErrorKind::AlreadyExists, "Dictionary already exists"));
}
basis.clean = false;
// allocate a vpage offset for the dictionary
let dict_index = basis.dict_get_free_offset(hw);
let dict_offset = VirtAddr::new(dict_index as u64 * DICT_VSIZE).unwrap();
let pp = basis.v2p_map.entry(dict_offset).or_insert_with(|| {
let mut ap = hw.try_fast_space_alloc().expect("No free space to allocate dict");
ap.set_valid(true);
ap
});
assert!(pp.valid(), "v2p returned an invalid page");
// create the cache entry
let mut dict_name = [0u8; DICT_NAME_LEN];
for (src, dst) in name.bytes().into_iter().zip(dict_name.iter_mut()) {
*dst = src;
}
let mut my_aad = Vec::<u8>::new();
for &b in basis.aad.iter() {
my_aad.push(b);
}
let mut init_flags = DictFlags(0);
init_flags.set_valid(true);
let mut free_keys = BinaryHeap::<Reverse<FreeKeyRange>>::new();
free_keys.push(Reverse(FreeKeyRange{start: 1, run: KEY_MAXCOUNT as u32 - 1}));
let dict_cache = DictCacheEntry {
index: NonZeroU32::new(dict_index).unwrap(),
keys: HashMap::<String, KeyCacheEntry>::new(),
clean: false,
age: 0,
free_keys,
last_disk_key_index: 1, // we know we don't have to search past this in a new dictionary
flags: init_flags,
key_count: 0,
small_pool: Vec::<KeySmallPool>::new(),
small_pool_free: BinaryHeap::<KeySmallPoolOrd>::new(),
aad: my_aad,
};
log::debug!("adding dictionary {}", name);
basis.dicts.insert(String::from(name), dict_cache);
basis.num_dicts += 1;
// encrypt and write the dict entry to disk
basis.dict_sync(hw, name)?;
// sync the root basis structure as well, while we're at it...
basis.basis_sync(hw);
// finally, sync the page tables.
basis.pt_sync(hw);
Ok(())
} else {
Err(Error::new(ErrorKind::NotFound, "Requested basis not found, or PDDB not mounted."))
}
}
/// Returns a list of all the known dictionaries, across all the basis. A HashSet is returned
/// because you can have the same-named dictionary in multiple basis, and what we're asking for
/// is the union of all the dictionary names, without duplicates.
pub(crate) fn dict_list(&mut self, hw: &mut PddbOs, basis_name: Option<&str>) -> HashSet::<String> {
let mut dict_set = HashSet::<String>::new();
if basis_name.is_some() {
if let Some(basis_index) = self.select_basis(basis_name) {
let basis = &mut self.cache[basis_index];
basis.populate_caches(hw);
for (key, dcache) in basis.dicts.iter() {
if dcache.flags.valid() {
dict_set.insert(String::from(key));
}
}
}
} else {
for basis in self.cache.iter_mut() {
basis.populate_caches(hw);
for (key, dcache) in basis.dicts.iter() {
if dcache.flags.valid() {
dict_set.insert(String::from(key));
}
}
}
}
dict_set
}
pub(crate) fn key_list(&mut self, hw: &mut PddbOs, dict: &str, basis_name: Option<&str>) -> Result<HashSet::<String>> {
let mut merge_list = HashSet::<String>::new();
let mut found_dict = false;
if basis_name.is_some() {
if let Some(basis_index) = self.select_basis(basis_name) {
let basis = &mut self.cache[basis_index];
basis.populate_caches(hw);
if let Some(dcache) = basis.dicts.get_mut(dict) {
dcache.key_list(hw, &basis.v2p_map, &basis.cipher, &mut merge_list);
found_dict = true;
}
}
} else {
for basis in self.cache.iter_mut() {
basis.populate_caches(hw);
if let Some(dcache) = basis.dicts.get_mut(dict) {
dcache.key_list(hw, &basis.v2p_map, &basis.cipher, &mut merge_list);
found_dict = true;
}
}
}
if found_dict {
Ok(merge_list)
} else {
return Err(Error::new(ErrorKind::NotFound, "dictionary not found"))
}
}
/// This version of the call only removes one instance of a dictionary from the specified basis.
/// Perhaps there also needs to be a `dict_remove_all` call which iterates through every basis
/// makes sure the dictionary is removed from all the possible known basis. Anyways, that function
/// would be a variant of this targeted version.
pub(crate) fn dict_remove(&mut self,
hw: &mut PddbOs, dict: &str, basis_name: Option<&str>, paranoid: bool
) -> Result<()> {
if let Some(basis_index) = self.select_basis(basis_name) {
log::debug!("deleting dict {}", dict);
let basis = &mut self.cache[basis_index];
basis.age = basis.age.saturating_add(1);
basis.clean = false;
basis.dict_delete(hw, dict, paranoid)?;
basis.basis_sync(hw);
basis.pt_sync(hw);
Ok(())
} else {
Err(Error::new(ErrorKind::NotFound, "Requested basis not found, or PDDB not mounted."))
}
}
pub(crate) fn key_read(&mut self, hw: &mut PddbOs, dict: &str, key: &str, data: &mut [u8],
offset: Option<usize>, basis_name:Option<&str>
) -> Result<usize> {
if let Some(basis_index) = self.select_basis(basis_name) {
let basis = &mut self.cache[basis_index];
if !basis.ensure_dict_in_cache(hw, dict) {
return Err(Error::new(ErrorKind::NotFound, "dictionary not found"));
}
if let Some(dict_entry) = basis.dicts.get_mut(dict) {
if dict_entry.ensure_key_entry(hw, &mut basis.v2p_map, &basis.cipher, key) {
let kcache = dict_entry.keys.get_mut(key).expect("Entry was assured, but then not there!");
// the key exists, *and* there's sufficient space for the data
if kcache.start < SMALL_POOL_END {
// small pool fetch
// for now, the rule is, if we have a small key, we also have its data in cache.
if let KeyCacheData::Small(cache_data) = kcache.data.as_mut().expect("small pool should all have their data 'hot' if the index entry is also in cache") {
let mut bytes_read = 0;
if offset.unwrap_or(0) as u64 > kcache.len {
return Err(Error::new(ErrorKind::UnexpectedEof, "offest requested is beyond the key length"));
}
for (&src, dst) in cache_data.data[offset.unwrap_or(0)..kcache.len as usize].iter().zip(data.iter_mut()) {
*dst = src;
bytes_read += 1;
}
if bytes_read != data.len() {
log::debug!("Key shorter than read buffer {}:{} ({}/{})", dict, key, bytes_read, data.len());
}
return Ok(bytes_read)
} else {
panic!("Key allocated to small area but its cache data was not of the small type");
}
} else {
if offset.unwrap_or(0) as u64 > kcache.len {
return Err(Error::new(ErrorKind::UnexpectedEof, "offest requested is beyond the key length"));
}
if data.len() == 0 { // mostly because i don't want to have to think about this case in the later logic.
return Ok(0)
}
// large pool fetch
let mut cur_offset = offset.unwrap_or(0) as u64;
let mut blocks_read = 0;
loop {
let start_vpage_addr = ((kcache.start + cur_offset) / VPAGE_SIZE as u64) * VPAGE_SIZE as u64;
// handle the case of terminating the last block read nicely:
// we can either end because we hit the end of the key data (so the data readback buffer is too large)
let key_endstop = if (kcache.len - cur_offset) < VPAGE_SIZE as u64 {
(kcache.len - cur_offset) as usize + size_of::<JournalType>()
} else {
VPAGE_SIZE + size_of::<JournalType>()
};
// or we can hit the end because we ran out of datareadback buffer
let data_endstop = if (data.len() as u64 - cur_offset) < VPAGE_SIZE as u64 {
(data.len() as u64 - cur_offset) as usize + size_of::<JournalType>()
} else {
VPAGE_SIZE + size_of::<JournalType>()
};
// of the two possible end criteria, pick the nearest one
let endstop = if key_endstop < data_endstop {
key_endstop
} else {
data_endstop
};
log::debug!("reading offset {}->{}/{}:{}", key, cur_offset, endstop, kcache.len);
let mut read_offset = (cur_offset % VPAGE_SIZE as u64) as usize;
if let Some(pp) = basis.v2p_map.get(&VirtAddr::new(start_vpage_addr).unwrap()) {
assert!(pp.valid(), "v2p returned an invalid page");
let pt_data = hw.data_decrypt_page(&basis.cipher, &basis.aad, pp).expect("Decryption auth error");
if blocks_read != 0 {
assert!(read_offset == 0, "algorithm error in handling offset data");
}
for (&src, dst) in
pt_data[
size_of::<JournalType>() // always this fixed offset per block
+ read_offset // this should be 0 after the first block
..endstop
].iter().zip(data[cur_offset as usize..].iter_mut()) {
*dst = src;
cur_offset += 1;
read_offset += 1;
}
blocks_read += 1;
} else {
log::warn!("Not enough bytes available to read for key {}:{} ({}/{})", dict, key, cur_offset, data.len());
return Ok(cur_offset as usize)
}
// cur_offset won't hit (start + len) if we're hitting an endstop, so the loop won't exit.
// if we hit an endstop, we should exit the read loop.
if endstop != VPAGE_SIZE + size_of::<JournalType>() {
break;
}
// break if we've hit or exceeded our endstop
if read_offset >= endstop {
break;
}
// also exit if we're "just nice", e.g. the size of the data happened to be exactly a multiple of the length of our block size
if cur_offset == kcache.len {
break;
}
}
return Ok(cur_offset as usize)
}
} else {
return Err(Error::new(ErrorKind::NotFound, "key not found"));
}
} else {
return Err(Error::new(ErrorKind::NotFound, "dictionary not found"));
}
} else {
Err(Error::new(ErrorKind::NotFound, "Requested basis not found, or PDDB not mounted."))
}
}
pub(crate) fn key_remove(&mut self,
hw: &mut PddbOs, dict: &str, key: &str, basis_name: Option<&str>, paranoid: bool
) -> Result<()> {
if let Some(basis_index) = self.select_basis(basis_name) {
let basis = &mut self.cache[basis_index];
if !basis.ensure_dict_in_cache(hw, dict) {
return Err(Error::new(ErrorKind::NotFound, "dictionary not found"));
}
if let Some(dict_entry) = basis.dicts.get_mut(dict) {
basis.age = basis.age.saturating_add(1);
basis.clean = false;
if dict_entry.ensure_key_entry(hw, &mut basis.v2p_map, &basis.cipher, key) {
if !paranoid {
dict_entry.key_remove(hw, &mut basis.v2p_map, &basis.cipher, key, false);
} else {
// this implementation is still in progress
dict_entry.key_erase(key);
// encrypt and write the dict entry to disk
basis.dict_sync(hw, dict)?;
// sync the root basis structure as well, while we're at it...
basis.basis_sync(hw);
// finally, sync the page tables.
basis.pt_sync(hw);
}
return Ok(())
} else {
return Err(Error::new(ErrorKind::NotFound, "key not found"));
}
} else {
return Err(Error::new(ErrorKind::NotFound, "dictionary not found"));
}
} else {
Err(Error::new(ErrorKind::NotFound, "Requested basis not found, or PDDB not mounted."))
}
}
/// Updates a key in a dictionary; if it doesn't exist, creates it. User can specify a basis,
/// or rely upon the auto-basis select algorithm.
pub(crate) fn key_update(&mut self,
hw: &mut PddbOs, dict: &str, key: &str, data: &[u8], offset: Option<usize>,
alloc_hint: Option<usize>, basis_name: Option<&str>, truncate: bool) -> Result<()> {
// we have to estimate how many pages are needed *before* we do anything, because we can't
// mutate the page table to allocate data while we're accessing the page table. This huge gob of code
// computes the pages needed. :-/
let mut pages_needed = 2; // things go badly when no space is available so make sure there's always at least 1 spot
let reserved = if data.len() + offset.unwrap_or(0) > alloc_hint.unwrap_or(0) {
data.len() + offset.unwrap_or(0)
} else {
alloc_hint.unwrap_or(0)
};
let reserved_pages = if reserved % VPAGE_SIZE == 0 {
reserved / VPAGE_SIZE
} else {
(reserved / VPAGE_SIZE) + 1
};
if let Some(basis_index) = self.select_basis(basis_name) {
let basis = &mut self.cache[basis_index];
if !basis.ensure_dict_in_cache(hw, dict) {
pages_needed += 1;
pages_needed += reserved_pages;
}
if let Some(dict_entry) = basis.dicts.get(dict) {
// see if we need to make a kcache entry
if let Some(kcache) = dict_entry.keys.get(key) {
if kcache.flags.valid() {
// now check for data reservations
if let Some(key_index) = small_storage_index_from_key(&kcache, dict_entry.index) {
// it's probably going in the small pool.
// index exists, see if the page exists
if reserved == 0 { // something went wrong here
log::debug!("reserved {}", reserved);
log::debug!("{}:{} - [{}]{:x}+{}->{}", dict, key, kcache.descriptor_index, kcache.start, kcache.len, kcache.reserved);
}
if dict_entry.small_pool.len() > key_index {
log::trace!("resolved key index {}, small pool len: {}", key_index, dict_entry.small_pool.len());
// see if the pool's address exists in the page table
let pool_vaddr = VirtAddr::new(small_storage_base_vaddr_from_indices(dict_entry.index, key_index)).unwrap();
if !basis.v2p_map.contains_key(&pool_vaddr) {
pages_needed += 1;
}
} else {
// we're definitely going to need another small pool page
pages_needed += 1;
}
} else {
// it's a large block. see if its address have been mapped
// large pool start addresses should always be vpage aligned
for vpage in (kcache.start..kcache.start + kcache.reserved).step_by(VPAGE_SIZE) {
if !basis.v2p_map.contains_key(&VirtAddr::new(vpage).unwrap()) {
pages_needed += 1;
}
}
}
} else {
pages_needed += reserved_pages;
}
} else {
// there's no key cache entry. For simplicity, let's just assume this means none of the data
// has been allocated, and ask for it to be available.
//
// At least in v1 of the code, this is the case. Later on maybe you could
// evict a key key cache entry to trim memory usage; if we do this, we'll have to implement a
// routine that is like ensure_key_entry() but doesn't do any allocations - it just does the search
// for the key record and if it doesn't exist reports that we'll need one, but if the key does exist
// and simply wasn't loaded into cache, then don't ask for the reservation.
//
// But this should be a "last resort" move: preferably, if we have memory pressure, we should take
// the data entries in the key cache and turn them into None to reduce data pressure, rather than
// evicting the key indices themselves (because it causes bookkeeping problems like this). I guess
// if someone wanted to allocate thousand and thousands of keys, we'd eventually consume a megabyte of
// RAM, which is relatively precious, but....anyways. Maybe the answer is "don't do that" on a small memory
// machine and expect it to work?
pages_needed += reserved_pages;
}
}
} else {
return Err(Error::new(ErrorKind::NotFound, "Requested basis not found, or PDDB not mounted."));
}
// make the reservation
log::debug!("reserving {} pages - {}", pages_needed, reserved_pages);
if !hw.ensure_fast_space_alloc(pages_needed, &self.cache) {
return Err(Error::new(ErrorKind::OutOfMemory, "No free space to allocate dict"));
}
// now actually do the update
if let Some(basis_index) = self.select_basis(basis_name) {
let dict_found = (&mut self.cache[basis_index]).ensure_dict_in_cache(hw, dict);
if !dict_found { // now that we're clear of the deep search, mutate the basis if we are sure it's not there
self.dict_add(hw, dict, basis_name).expect("couldn't add dictionary");
}
// at this point, the dictionary should definitely be in cache
// pre-flight & allocatefree space requirements
if let Some(dict_entry) = self.cache[basis_index].dicts.get(dict) {
hw.ensure_fast_space_alloc(dict_entry.alloc_estimate_small(), &self.cache);
// large pools don't have caching implemented, so we don't have to check for free space for them
}
// refetch the basis here to avoid the re-borrow problem, now that all the potential dict cache mutations are done
let basis = &mut self.cache[basis_index];
// bumping this every key update affects performance *a lot* -- don't think this is worth it.
// the bases should only "age" when dicts or keys are modified, not when any data in it is updated for any reason.
// basis.age = basis.age.saturating_add(1);
// basis.clean = false;
// now do the sync
if let Some(dict_entry) = basis.dicts.get_mut(dict) {
let updated_ptr = dict_entry.key_update(hw, &mut basis.v2p_map,
&basis.cipher, key, data,
offset.unwrap_or(0),
alloc_hint,
truncate,
basis.large_alloc_ptr.unwrap_or(PageAlignedVa::from(LARGE_POOL_START))
)?;
basis.large_alloc_ptr = Some(updated_ptr);
if !dict_entry.sync_small_pool(hw, &mut basis.v2p_map, &basis.cipher) {
return Err(Error::new(ErrorKind::OutOfMemory, "Ran out of memory syncing small pool"));
}
// we don't have large pool caches yet, but this is a placeholder to remember to do "something" at this point,
// once we do have them.
dict_entry.sync_large_pool();
// encrypt and write the dict entry to disk
basis.dict_sync(hw, dict)?;
// sync the root basis structure as well, while we're at it...
basis.basis_sync(hw);
// finally, sync the page tables.
basis.pt_sync(hw);
} else {
return Err(Error::new(ErrorKind::NotFound, "Requested dictionary not found, or could not be allocated."));
}
Ok(())
} else {
Err(Error::new(ErrorKind::NotFound, "Requested basis not found, or PDDB not mounted."))
}
}
pub(crate) fn key_attributes(&mut self, hw: &mut PddbOs, dict: &str, key: &str, basis_name: Option<&str>) -> Result<KeyAttributes> {
if basis_name.is_none() {
for basis in self.cache.iter_mut().rev() {
if !basis.ensure_dict_in_cache(hw, dict) {
continue;
} else {
let dict_entry = basis.dicts.get_mut(dict).expect("Entry was assured, but not there!");
if dict_entry.ensure_key_entry(hw, &mut basis.v2p_map, &basis.cipher, key) {
let kcache = match dict_entry.keys.get_mut(key) {
Some(kc) => kc,
None => continue,
};
return Ok(KeyAttributes {
len: kcache.len as usize,
reserved: kcache.reserved as usize,
age: kcache.age as usize,
dict: dict.to_string(),
basis: (&basis.name).to_string(),
flags: kcache.flags,
index: kcache.descriptor_index,
})
} else {
return Err(Error::new(ErrorKind::NotFound, "key not found"));
}
}
}
Err(Error::new(ErrorKind::NotFound, "key not found"))
} else {
if let Some(basis_index) = self.select_basis(basis_name) {
let basis = &mut self.cache[basis_index];
if !basis.ensure_dict_in_cache(hw, dict) {
return Err(Error::new(ErrorKind::NotFound, "dictionary not found"));
} else {
let dict_entry = basis.dicts.get_mut(dict).expect("Entry was assured, but not there!");
if dict_entry.ensure_key_entry(hw, &mut basis.v2p_map, &basis.cipher, key) {
let kcache = dict_entry.keys.get_mut(key).expect("Entry was assured, but then not there!");
Ok(KeyAttributes {
len: kcache.len as usize,
reserved: kcache.reserved as usize,
age: kcache.age as usize,
dict: dict.to_string(),
basis: (&basis.name).to_string(),
flags: kcache.flags,
index: kcache.descriptor_index,
})
} else {
return Err(Error::new(ErrorKind::NotFound, "key not found"));
}
}
} else {
Err(Error::new(ErrorKind::NotFound, "Requested basis not found, or PDDB not mounted."))
}
}
}
pub(crate) fn dict_attributes(&mut self, hw: &mut PddbOs, dict: &str, basis_name: Option<&str>) -> Result<DictAttributes> {
if basis_name.is_none() {
for basis in self.cache.iter_mut().rev() {
if !basis.ensure_dict_in_cache(hw, dict) {
continue;
} else {
let dict_entry = basis.dicts.get_mut(dict).expect("Entry was assured, but not there!");
return Ok(dict_entry.to_dict_attributes(dict, &basis.name));
}
}
return Err(Error::new(ErrorKind::NotFound, "dictionary not found"));
} else {
if let Some(basis_index) = self.select_basis(basis_name) {
let basis = &mut self.cache[basis_index];
if !basis.ensure_dict_in_cache(hw, dict) {
return Err(Error::new(ErrorKind::NotFound, "dictionary not found"));
} else {
let dict_entry = basis.dicts.get_mut(dict).expect("Entry was assured, but not there!");
Ok(dict_entry.to_dict_attributes(dict, &basis.name))
}
} else {
Err(Error::new(ErrorKind::NotFound, "Requested basis not found, or PDDB not mounted."))
}
}
}
/// this largely copies code from the pddb_mount() routine. Perhaps this should be modified a little bit to
/// re-use that code. However, there are material differences in how the passwords are handled between
/// these two methods, so the API calls are different. pddb_mount mounts the system basis with the intention
/// of making it persistent, and assuming you're coming up from a blank slate. This routine makes no such
/// assumptions and allows one to specify a persistence.
pub(crate) fn basis_unlock(&mut self, hw: &mut PddbOs, name: &str, password: &str,
policy: BasisRetentionPolicy) -> Option<BasisCacheEntry> {
let basis_key = hw.basis_derive_key(name, password);
if let Some(basis_map) = hw.pt_scan_key(&basis_key, name) {
let aad = hw.data_aad(name);
if let Some(root_page) = basis_map.get(&VirtAddr::new(VPAGE_SIZE as u64).unwrap()) {
let vpage = match hw.data_decrypt_page_with_commit(&basis_key, &aad, root_page) {
Some(data) => data,
None => {log::error!("Could not find basis {} root", name); return None;},
};
let mut basis_root = BasisRoot::default();
for (&src, dst) in vpage[size_of::<JournalType>()..].iter().zip(basis_root.deref_mut().iter_mut()) {
*dst = src;
}
if basis_root.magic != PDDB_MAGIC {
log::error!("Basis root did not deserialize correctly, unrecoverable error.");
return None;
}
if basis_root.version != PDDB_VERSION {
log::error!("PDDB version mismatch in system basis root. Unrecoverable error.");
return None;
}
let basis_name = std::str::from_utf8(&basis_root.name.data[..basis_root.name.len as usize]).expect("basis name is not valid utf-8");
if basis_name != name {
log::error!("PDDB mount requested {}, but got {}; aborting.", name, basis_name);
return None;
}
log::debug!("Basis {} record found, generating cache entry", name);
BasisCacheEntry::mount(hw, &basis_name, &basis_key, false, policy)
} else {
None
}
} else {
None
}
}
/// this creates a basis entry in the PDDB, which should then be "mounted" to activate it.
/// similar to how you might use fdisk to create a partition, but you still must call mount to access it.
pub(crate) fn basis_create(&mut self, hw: &mut PddbOs, name: &str, password: &str) -> Result<()> {
if !hw.fast_space_ensure_next_log() {
return Err(Error::new(ErrorKind::OutOfMemory, "No free space to create basis"));
};
if !hw.ensure_fast_space_alloc(1, &self.cache) {
return Err(Error::new(ErrorKind::OutOfMemory, "No free space to create basis"));
};
let basis_key = hw.basis_derive_key(name, password);
let mut basis_v2p_map = HashMap::<VirtAddr, PhysPage>::new();
let basis_root = BasisRoot {
magic: PDDB_MAGIC,
version: PDDB_VERSION,
name: BasisRootName::try_from_str(name).unwrap(),
age: 0,
num_dictionaries: 0
};
// allocate one page for the basis root
if let Some(alloc) = hw.try_fast_space_alloc() {
let mut rpte = alloc.clone();
rpte.set_clean(true); // it's not clean _right now_ but it will be by the time this routine is done...
rpte.set_valid(true);
let va = VirtAddr::new((1 * VPAGE_SIZE) as u64).unwrap(); // page 1 is where the root goes, by definition
log::debug!("adding basis {}: va {:x?} with pte {:?}", name, va, rpte);
basis_v2p_map.insert(va, rpte);
} else {
return Err(Error::new(ErrorKind::Other, "Space reservation failed"));
}
let aad = basis_root.aad(hw.dna());
let pp = basis_v2p_map.get(&VirtAddr::new(1 * VPAGE_SIZE as u64).unwrap()).unwrap();
assert!(pp.valid(), "v2p returned an invalid page");
let journal_bytes = (hw.trng_u32() % JOURNAL_RAND_RANGE).to_le_bytes();
let slice_iter =
journal_bytes.iter() // journal rev
.chain(basis_root.as_ref().iter());
let mut block = [0 as u8; KCOM_CT_LEN];
for (&src, dst) in slice_iter.zip(block.iter_mut()) {
*dst = src;
}
hw.data_encrypt_and_patch_page_with_commit(&basis_key, &aad, &mut block, &pp);
let cipher = Aes256::new(GenericArray::from_slice(&basis_key));
for (&virt, phys) in basis_v2p_map.iter_mut() {
hw.pt_patch_mapping(virt, phys.page_number(), &cipher);
// mark the entry as clean, as it has been sync'd to disk
phys.set_clean(true);
}
Ok(())
}
pub(crate) fn basis_list(&self) -> Vec<String> {
let mut ret = Vec::new();
for bcache in &self.cache {
ret.push(bcache.name.clone());
}
ret
}
pub(crate) fn basis_latest(&mut self) -> Option<String> {
if let Some(basis_index) = self.select_basis(None) {
let basis = &mut self.cache[basis_index];
Some(basis.name.clone())
} else {
None
}
}
pub(crate) fn basis_add(&mut self, basis: BasisCacheEntry) {
self.cache.push(basis);
}
pub(crate) fn basis_unmount(&mut self, hw: &mut PddbOs, basis_name: &str) -> Result<()> {
if let Some(basis_index) = self.select_basis(Some(basis_name)) {
let basis = &mut self.cache[basis_index];
basis.sync(hw)?;
self.cache.retain(|x| x.name != basis_name);
Ok(())
} else {
Err(Error::new(ErrorKind::NotFound, "Basis not found"))
}
}
/// note: you can "delete" a basis simply by forgetting its password, but this is more thorough.
/// there might also need to be a variant to make which is a "change my password" function, but that is actually
/// surprisingly hard.
pub(crate) fn basis_delete(&mut self, hw: &mut PddbOs, basis_name: &str) -> Result<()> {
if let Some(basis_index) = self.select_basis(Some(basis_name)) {
let basis = &mut self.cache[basis_index];
let mut temp: [u8; PAGE_SIZE] = [0; PAGE_SIZE];
for page in basis.v2p_map.values_mut() {
hw.trng_slice(&mut temp);
hw.patch_data(&temp, page.page_number() * PAGE_SIZE as u32);
log::trace!("fast_space_free basis delete {} before", page.journal());
hw.fast_space_free(page);
}
basis.pt_sync(hw);
Ok(())
} else {
Err(Error::new(ErrorKind::NotFound, "Basis not found"))
}
}
pub(crate) fn sync(&mut self, hw: &mut PddbOs, basis_name: Option<&str>) -> Result<()> {
if basis_name.is_some() {
if let Some(basis_index) = self.select_basis(basis_name) {
self.cache[basis_index].sync(hw)?
}
} else {
for basis in self.cache.iter_mut() {
basis.sync(hw)?;
}
}
Ok(())
}
pub(crate) fn suspend(&mut self, hw: &mut PddbOs) {
self.sync(hw, None).expect("couldn't sync on suspend");
let mut lock_list = Vec::<String>::new();
for basis in self.cache.iter_mut() {
match basis.policy {
BasisRetentionPolicy::Persist => (),
BasisRetentionPolicy::ClearAfterSleeps(sleeps) => {
basis.policy_state += 1;
if basis.policy_state >= sleeps {
lock_list.push(basis.name.clone());
}
}
/*
BasisRetentionPolicy::TimeOutSecs(secs) => {
if secs < basis.policy_state {
lock_list.push(basis.name.clone());
}
}*/
}
}
for basis in lock_list {
log::info!("unmounting basis on sleep: {}", &basis);
self.basis_unmount(hw, &basis).ok();
}
}
}
/// This is the RAM cached copy of a basis as maintained in the PDDB.
pub(crate) struct BasisCacheEntry {
/// the name of this basis
pub name: String,
/// set if synched to what's on disk
pub clean: bool,
/// last sync time, in systicks, if any. Updated by any sync of basis, dict, or pt.
pub last_sync: Option<u64>,
/// Number of dictionaries. This should be greater than or equal to the number of elements in dicts.
/// If dicts has less elements than num_dicts, it means there are still dicts on-disk that haven't been cached.
pub num_dicts: u32,
/// dictionary array.
pub dicts: HashMap::<String, DictCacheEntry>,
/// A cached copy of the absolute offset of the next free dictionary slot,
/// expressed as a number that needs to be multiplied by DICT_VSIZE to arrive at a virtual address
pub free_dict_offset: Option<u32>,
/// the cipher for the basis
pub cipher: Aes256GcmSiv,
/// derived cipher for encrypting PTEs -- cache it, so we can save the time cost of constructing the cipher key schedule
pub cipher_ecb: Aes256,
/// raw AES key -- needed because we have to use this to derive commitment keys for the basis root record, to work around AES-GCM-SIV salamanders
pub key: GenericArray<u8, cipher::consts::U32>,
/// the AAD associated with this Basis
pub aad: Vec::<u8>,
/// modification count
pub age: u32,
/// virtual to physical page map. It's the reverse mapping of the physical page table on disk.
pub v2p_map: HashMap<VirtAddr, PhysPage>,
/// the last journal rev written to disk
pub journal: u32,
/// current allocation pointer for the "large" pool. This just keeps incrementing "forever".
pub large_alloc_ptr: Option<PageAlignedVa>,
/// retiention policy
pub policy: BasisRetentionPolicy,
// rention state
pub policy_state: u32,
}
impl BasisCacheEntry {
/// given a pointer to the hardware, name of the basis, and its cryptographic key, try to derive
/// the basis. If `lazy` is true, it stops with the minimal amount of effort to respond to a query.
/// If it `lazy` is false, it will populate the dictionary cache and key cache entries, as well as
/// discover the location of the `large_alloc_ptr`.
pub(crate) fn mount(hw: &mut PddbOs, name: &str, key: &[u8; AES_KEYSIZE], lazy: bool, policy: BasisRetentionPolicy) -> Option<BasisCacheEntry> {
if let Some(basis_map) = hw.pt_scan_key(key, name) {
let cipher = Aes256GcmSiv::new(Key::from_slice(key));
let aad = hw.data_aad(name);
// get the first page, where the basis root is guaranteed to be
if let Some(root_page) = basis_map.get(&VirtAddr::new(VPAGE_SIZE as u64).unwrap()) {
let vpage = match hw.data_decrypt_page_with_commit(key, &aad, root_page) {
Some(data) => data,
None => {log::error!("System basis decryption did not authenticate. Unrecoverable error."); return None;},
};
// if the below assertion fails, you will need to re-code this to decrypt more than one VPAGE and stripe into a basis root struct
assert!(size_of::<BasisRoot>() <= VPAGE_SIZE, "BasisRoot has grown past a single VPAGE, this routine needs to be re-coded to accommodate the extra bulk");
let mut basis_root = BasisRoot::default();
for (&src, dst) in vpage[size_of::<JournalType>()..].iter().zip(basis_root.deref_mut().iter_mut()) {
*dst = src;
}
if basis_root.magic != PDDB_MAGIC {
log::error!("Basis root did not deserialize correctly, unrecoverable error.");
return None;
}
if basis_root.version != PDDB_VERSION {
log::error!("PDDB version mismatch in system basis root. Unrecoverable error.");
return None;
}
let basis_name = std::str::from_utf8(&basis_root.name.data[..basis_root.name.len as usize]).expect("basis name is not valid utf-8");
if basis_name != String::from(name) {
log::error!("Discovered basis name does not match the requested name: {}; aborting mount operation.", basis_name);
return None;
}
let mut bcache = BasisCacheEntry {
name: basis_name.to_string(),
clean: true,
last_sync: Some(hw.timestamp_now()),
num_dicts: basis_root.num_dictionaries,
dicts: HashMap::<String, DictCacheEntry>::new(),
cipher,
cipher_ecb: Aes256::new(GenericArray::from_slice(key)),
key: GenericArray::clone_from_slice(key),
aad,
age: basis_root.age,
free_dict_offset: None,
v2p_map: basis_map,
journal: u32::from_le_bytes(vpage[..size_of::<JournalType>()].try_into().unwrap()),
large_alloc_ptr: None,
policy,
policy_state: policy.derive_init_state(),
};
if !lazy {
bcache.populate_caches(hw);
}
log::info!("Basis {} found and reconstructed", name);
return Some(bcache);
} else {
// i guess technically we could try a brute-force search for the page if it went missing, but meh.
log::error!("Basis {} did not contain a root page -- unrecoverable error.", name);
return None;
}
} else {
log::error!("Basis {} has no page table entries -- maybe a bad password?", name);
None
}
}
/// called during the initial basis scan to track where the large allocation pointer end should be.
/// basically try to find the maximal extent of already allocated data, and start allocating from there.
pub(crate) fn large_pool_update(&mut self, maybe_end: u64) {
if maybe_end > self.large_alloc_ptr.unwrap_or(PageAlignedVa::from(LARGE_POOL_START)).as_u64() {
self.large_alloc_ptr = Some(PageAlignedVa::from(maybe_end));
}
}
/// do a deep scan of all the dictionaries and keys and attempt to populate all the caches
pub(crate) fn populate_caches(&mut self, hw: &mut PddbOs) {
// count number of valid dictionaries
let mut num_valid = 0;
for dict in self.dicts.values() {
if dict.flags.valid() {
num_valid += 1;
}
}
// note to self: this should be true, because even if we didn't read all the dictionaries in from the basis,
// and then say, deleted a bunch of dictionaries and then added a bunch more, the num_dicts field should
// move up and down with the additions/deletions, and thus should relatively be larger than or equal to the num_valid
// at all times. In other words, the difference of self.num_dicts vs num_valid should be precisely the number
// of dictionary entries on disk that we haven't loaded into the cache.
assert!(num_valid <= self.num_dicts, "Inconsistency in number of dictionaries in the basis");
if num_valid == self.num_dicts { // no need to scan the index, just refresh the dictionaries themselves
let mut largest_extent = 0;
for dict in self.dicts.values_mut() {
if dict.flags.valid() {
let extent = dict.fill(hw, &mut self.v2p_map, &self.cipher).get();
if extent > largest_extent {
largest_extent = extent;
}
}
}
self.large_pool_update(largest_extent);
} else { // scan the full index
let mut try_entry = 1;
let mut dict_count = 0;
while try_entry <= DICT_MAXCOUNT && dict_count < self.num_dicts {
let dict_vaddr = VirtAddr::new(try_entry as u64 * DICT_VSIZE).unwrap();
if let Some(pp) = self.v2p_map.get(&dict_vaddr) {
assert!(pp.valid(), "v2p returned an invalid page");
if let Some(dict) = self.dict_decrypt(hw, &pp) {
if dict.flags.valid() {
let dict_name = std::str::from_utf8(&dict.name.data[..dict.name.len as usize]).expect("dict name is not valid utf-8").to_string();
let dict_present_and_valid = if let Some(d) = self.dicts.get(&dict_name) {
d.flags.valid()
} else {
false
};
if !dict_present_and_valid {
let mut dcache = DictCacheEntry::new(dict, try_entry, &self.aad);
let max_large_alloc = dcache.fill(hw, &self.v2p_map, &self.cipher);
self.dicts.insert(dict_name.to_string(), dcache);
self.large_pool_update(max_large_alloc.get());
} else {
let dcache = self.dicts.get_mut(&dict_name).expect("dict should be present, as we checked for it already...");
let extent = dcache.fill(hw, &mut self.v2p_map, &self.cipher).get();
self.large_pool_update(extent);
}
dict_count += 1;
} else {
// this is an empty dictionary entry. we could stick a dictionary in here later on, take note if we haven't already computed that
if self.free_dict_offset.is_none() { self.free_dict_offset = Some(try_entry as u32); }
}
} else {
if self.free_dict_offset.is_none() { self.free_dict_offset = Some(try_entry as u32); }
}
} else {
if self.free_dict_offset.is_none() { self.free_dict_offset = Some(try_entry as u32); }
}
try_entry += 1;
}
if try_entry <= DICT_MAXCOUNT {
if self.free_dict_offset.is_none() { self.free_dict_offset = Some(try_entry as u32); }
}
}
}
/// If `paranoid` is true, it recurses through each key and replaces its data with random junk.
/// Otherwise, it does a "shallow" delete and just removes the directory entry, which is much
/// more performant. Note that the intended "fast" way to secure-erase data is to store sensitive
/// data in its own Basis, and then remove the Basis itself. This is much faster than picking
/// through compounded data and re-writing partias sectors, and because of this, initially,
/// the `paranoid` erase is `unimplemented`.
pub(crate) fn dict_delete(&mut self, hw: &mut PddbOs, name: &str, paranoid: bool) -> Result<()> {
if self.ensure_dict_in_cache(hw, name) {
let dcache = self.dicts.get_mut(name).expect("entry was ensured, but somehow missing");
log::trace!("dcache {} index {}, key_count {}", name, dcache.index, dcache.key_count);
// ensure all the keys are in RAM
dcache.fill(hw, &self.v2p_map, &self.cipher);
// allocate a copy of the key list, to avoid interior mutability problems with the next remove step
let mut key_list = Vec::<String>::new();
for (key, entry) in dcache.keys.iter() {
if entry.flags.valid() {
key_list.push(key.to_string());
}
}
for key in key_list {
log::debug!("removing {}:{}", name, key);
// this will wipe any large pools if paranoid is set
dcache.key_remove(hw, &mut self.v2p_map, &self.cipher, &key, paranoid);
}
// wipe & de-allocate any small pages
for index in 0..dcache.small_pool.len() {
let pool_vaddr = VirtAddr::new(small_storage_base_vaddr_from_indices(dcache.index, index)).unwrap();
if let Some(pp) = self.v2p_map.get_mut(&pool_vaddr) {
assert!(pp.valid(), "v2p returned an invalid page");
{ // always nuke old data
let mut random = [0u8; PAGE_SIZE];
hw.trng_slice(&mut random);
hw.patch_data(&random, pp.page_number() * PAGE_SIZE as u32);
}
log::trace!("fast_space_free small page delete {} before", pp.journal());
hw.fast_space_free(pp);
assert!(pp.valid() == false, "pp is still marked as valid!");
}
}
/* for pp in self.v2p_map.values() {
log::info!("v2p entry: {:x?}", pp);
} */
// erase the entire dictionary + key allocation area by writing over with random data. It's important to
// do a comprehensive erase, because if the dictionary slot is re-used, the previously allocated key entries
// decrypt correctly, are interpreted as valid keys, and thus cause consistency errors.
let key_pages = 1 + (dcache.last_disk_key_index + 1) as usize / DK_PER_VPAGE;
for page in 0..key_pages {
// note: check this code against dict_indices_to_vaddr() -- it's recoded here because we go by page, not by index, but this needs to be consistent with that function
let dk_vaddr = VirtAddr::new(dcache.index.get() as u64 * DICT_VSIZE as u64 + page as u64 * VPAGE_SIZE as u64).unwrap();
if let Some(pp) = self.v2p_map.get_mut(&dk_vaddr) {
assert!(pp.valid(), "v2p returned an invalid page");
log::info!("erasing dk page 0x{:x}/0x{:x}", dk_vaddr, pp.page_number() as usize * PAGE_SIZE);
let mut random = [0u8; PAGE_SIZE];
hw.trng_slice(&mut random);
hw.patch_data(&random, pp.page_number() * PAGE_SIZE as u32);
log::trace!("fast_space_free dict_delete {} before", pp.journal());
hw.fast_space_free(pp);
assert!(pp.valid() == false, "pp is still marked as valid!");
} else {
log::warn!("Inconsistent internal state: requested dictionary didn't have a mapping in the page table.");
}
}
// mark data for re-use
self.free_dict_offset = Some(dcache.index.get());
dcache.flags.set_valid(false); // this shouldn't be necessary because we're removing the entry, but, it's "correct"
self.num_dicts -= 1;
// remove the cache entry
self.dicts.remove(name);
Ok(())
} else {
Err(Error::new(ErrorKind::NotFound, "Dictionary not found"))
}
}
/// Called to compute the next free index. Normally this will get filled during a disk search for
/// cache entries, but it could be None if a new dict entry was allocated and no other entries were loaded.
pub(crate) fn dict_get_free_offset(&mut self, hw: &mut PddbOs) -> u32 {
if let Some(offset) = self.free_dict_offset.take() {
return offset;
} else {
let mut try_entry = 1;
while try_entry <= DICT_MAXCOUNT {
let dict_vaddr = VirtAddr::new(try_entry as u64 * DICT_VSIZE).unwrap();
if let Some(pp) = self.v2p_map.get(&dict_vaddr) {
assert!(pp.valid(), "v2p returned an invalid page");
if self.dict_decrypt(hw, &pp).is_none() {
// mapping exists, but the data is invalid. It's a free entry.
return try_entry as u32
}
} else {
// mapping doesn't exist yet, that's a free entry
return try_entry as u32
}
try_entry += 1;
}
}
// maybe we should handle this better? but, I think this should be super-rare, as we allow 16384 dictionaries per Basis,
// so if we got here I'm guessing it's more likely due to a coding error than an actually full Basis.
panic!("Basis full, can't allocate dictionary");
}
/// Returns a tuple of "dictionary offset" , "dictionary"; the "dictionary offset" needs to be multiplied by DICT_VSIZE to arrive
/// at a fully expanded virtual address.
pub(crate) fn dict_deep_search(&mut self, hw: &mut PddbOs, name: &str) -> Option<(u32, Dictionary)> {
let mut try_entry = 1;
let mut dict_count = 0;
while try_entry <= DICT_MAXCOUNT && dict_count < self.num_dicts {
let dict_vaddr = VirtAddr::new(try_entry as u64 * DICT_VSIZE).unwrap();
if let Some(pp) = self.v2p_map.get(&dict_vaddr) {
assert!(pp.valid(), "v2p returned an invalid page");
if let Some(dict) = self.dict_decrypt(hw, &pp) {
let dict_name = std::str::from_utf8(&dict.name.data[..dict.name.len as usize]).expect("dict name is not valid utf-8");
if (dict_name == name) && dict.flags.valid() {
return Some((try_entry as u32, dict))
}
dict_count += 1;
} else {
// this is an empty dictionary entry. we could stick a dictionary in here later on, take note if we haven't already computed that
if self.free_dict_offset.is_none() {
self.free_dict_offset = Some(try_entry as u32);
}
}
} else {
// this is an empty dictionary entry. we could stick a dictionary in here later on, take note if we haven't already computed that
if self.free_dict_offset.is_none() {
self.free_dict_offset = Some(try_entry as u32);
}
}
try_entry += 1;
}
None
}
/// The `pp` must be the resolved physical page storing the top of the given
/// dictionary index for this to work.
pub(crate) fn dict_decrypt(&self, hw: &mut PddbOs, pp: &PhysPage) -> Option<Dictionary> {
if let Some(data) = hw.data_decrypt_page(&self.cipher, &self.aad, &pp) {
let mut dict = Dictionary::default();
for (&src, dst) in data[size_of::<JournalType>()..].iter().zip(dict.deref_mut().iter_mut()) {
*dst = src;
}
Some(dict)
} else {
None
}
}
/// Looks for dirty entries in the page table, and flushes them to disk.
pub(crate) fn pt_sync(&mut self, hw: &mut PddbOs) {
self.last_sync = Some(hw.timestamp_now());
let mut kill_list = Vec::<VirtAddr>::new();
// iterate once to delete old entries
for (&virt, phys) in self.v2p_map.iter_mut() {
if !phys.valid() {
// erase the entry
log::debug!("deleting pte va: {:x?} pa: {:x?}", virt, phys);
kill_list.push(virt);
hw.pt_erase(phys.page_number());
}
}
// have to do this in a second phase due to interior mutability problems doing it inside the first iterator
for kill in kill_list {
if self.v2p_map.remove(&kill).is_none() {
log::warn!("went to remove PTE from v2p map but it wasn't there: {:x}", kill);
}
}
// iterate a second time to write new entries -- can't do this in a single loop because the
// order of visitation is arbitrary and we can delete after writing an entry if we put these
// two in the same loop!
for (&virt, phys) in self.v2p_map.iter_mut() {
if !phys.clean() {
log::debug!("syncing dirty pte va: {:x?} pa: {:x?}", virt, phys);
hw.pt_patch_mapping(virt, phys.page_number(), &self.cipher_ecb);
phys.set_clean(true);
}
}
}
/// This will sync the named Dictionary header + dirty *key descriptors*. It does not
/// sync the key data itself. Note this does not mark the surrounding basis structure as clean
/// when it exits, even if there are no more dirty entries within.
///
/// Significantly, this routine assumes that every dictionary entry has already had a v2p mapping
/// allocated. You can try calling it without pre-allocating the entries, but if the FastSpace structure
/// doesn't have enough space, the routine will return an error indicating we're out of memory.
/// You could then try to allocate more FastSpace, and re-try the sync operation.
pub(crate) fn dict_sync(&mut self, hw: &mut PddbOs, name: &str) -> Result<()> {
self.last_sync = Some(hw.timestamp_now());
if let Some(dict) = self.dicts.get_mut(&String::from(name)) {
let dict_offset = VirtAddr::new(dict.index.get() as u64 * DICT_VSIZE).unwrap();
if !dict.clean {
let dict_name = DictName::try_from_str(name).or(Err(Error::new(ErrorKind::InvalidInput, "dictionary name invalid: invalid utf-8 or length")))?;
let dict_disk = Dictionary {
flags: dict.flags,
age: dict.age,
num_keys: dict.key_count,
free_key_index: dict.last_disk_key_index,
name: dict_name,
};
log::debug!("syncing dict {} with {} keys", name, dict.key_count);
// log::info!("raw: {:x?}", dict_disk.deref());
// observation: all keys to be flushed to disk will be in the KeyCacheEntry. Some may be clean,
// but definitely all the dirty ones are in there (if they aren't, where else would they be??)
// this is the virtual page within the dictionary region that we're currently serializing
let mut vpage_num = 0;
let mut loopcheck= 0;
let mut sync_count = 0;
loop {
loopcheck += 1;
if loopcheck == 256 {
log::warn!("potential infinite loop detected sync dict {}", name);
}
// 1. resolve the virtual address to a target page
let cur_vpage = VirtAddr::new(dict_offset.get() + (vpage_num as u64 * VPAGE_SIZE as u64)).unwrap();
let pp = self.v2p_map.entry(cur_vpage).or_insert_with(|| {
let mut ap = hw.try_fast_space_alloc().expect("FastSpace empty");
ap.set_valid(true);
ap
});
//if name.contains("dict2") {
// log::debug!("TRACING: {}/{} | {:x?}", name, vpage_num, pp);
//}
assert!(pp.valid(), "v2p returned an invalid page");
// 2(a). fill in the target vpage with data: header special case
let mut dk_vpage = DictKeyVpage::default();
// the dict always occupies the first entry of the first vpage in the dictionary region
if vpage_num == 0 {
let mut dk_entry = DictKeyEntry::default();
for (&src, dst) in dict_disk.deref().iter().zip(dk_entry.data.iter_mut()) {
*dst = src;
}
dk_vpage.elements[0] = Some(dk_entry);
}
// 2(b). fill in the target vpage with data: general key case
// Scan the DictCacheEntry.keys record for dirty keys within the current target vpage's range
// this is not a terribly efficient operation right now, because the DictCacheEntry is designed to
// be searched by name, but in this case, we want to check for an index range. There's a lot
// of things we could do to optimize this, depending on the memory/time trade-off we want to
// make, but for now, let's do it with a dumb O(N) scan through the KeyCacheEntry, running under
// the assumption that the KeyCacheEntry doesn't ever get to a very large N.
let next_vpage = VirtAddr::new(cur_vpage.get() + VPAGE_SIZE as u64).unwrap();
for (key_name, key) in dict.keys.iter_mut() {
/*if key_name.contains("dict2|key6|len2347") {
log::warn!("TRACING: {}", key_name);
log::warn!("start: {:x}, clean: {}, flags: {:?}, desciptor: {:?}",
key.start, key.clean, key.flags, key.descriptor_index,
);
}*/
if !key.clean && key.flags.valid() {
if key.descriptor_vaddr(dict_offset) >= cur_vpage &&
key.descriptor_vaddr(dict_offset) < next_vpage {
log::debug!("merging in key {}", key_name);
// key is within the current page, add it to the target list
let mut dk_entry = DictKeyEntry::default();
let kn = KeyName::try_from_str(key_name).or(Err(Error::new(ErrorKind::InvalidInput, "key name invalid: invalid utf-8 or length")))?;
let key_desc = KeyDescriptor {
start: key.start,
len: key.len,
reserved: key.reserved,
flags: key.flags,
age: key.age,
name: kn,
};
for (&src, dst) in key_desc.deref().iter().zip(dk_entry.data.iter_mut()) {
*dst = src;
}
dk_vpage.elements[key.descriptor_index.get() as usize % DK_PER_VPAGE] = Some(dk_entry);
key.clean = true;
} else {
log::debug!("proposed key fell outside of our vpage: {} vpage{:x}/vaddr{:x}", key_name, cur_vpage.get(), key.descriptor_vaddr(dict_offset));
}
}
}
// 3. merge the vpage modifications into the disk
let mut page = if let Some(data) = hw.data_decrypt_page(&self.cipher, &self.aad, &pp) {
log::trace!("merging dictionary data into existing page");
data
} else {
log::trace!("existing data invalid, creating a new page");
// the existing data was invalid (this happens e.g. on the first time a dict is created). Just overwrite the whole page.
let mut d = vec![0u8; VPAGE_SIZE + size_of::<JournalType>()];
for (&src, dst) in (hw.trng_u32() % JOURNAL_RAND_RANGE).to_le_bytes().iter().zip(d[..size_of::<JournalType>()].iter_mut()) {
*dst = src;
}
d
};
for (index, stride) in page[size_of::<JournalType>()..].chunks_mut(DK_STRIDE).enumerate() {
if let Some(elem) = dk_vpage.elements[index] {
for (&src, dst) in elem.data.iter().zip(stride.iter_mut()) {
*dst = src;
}
}
}
// generate nonce and write out
hw.data_encrypt_and_patch_page(&self.cipher, &self.aad, &mut page, &pp);
// 4. Check for dirty keys, if there are still some, update vpage_num to target them; otherwise
// exit the loop
let mut found_next = false;
for key in dict.keys.values() {
if !key.clean && key.flags.valid() {
found_next = true;
// note: we don't care *which* vpage we do next -- so we just break after finding the first one
vpage_num = key.descriptor_vpage_num();
break;
}
}
if !found_next {
log::debug!("breaking after syncing {} keys", sync_count);
break;
}
sync_count += 1;
}
log::debug!("done syncing dict");
dict.clean = true;
}
Ok(())
} else {
Err(Error::new(ErrorKind::NotFound, "dict_sync called with an invalid dictionary name"))
}
}
/// Runs through the dictionary listing in a basis and compacts them. Call when the
/// the dictionary space becomes sufficiently fragmented that accesses are becoming
/// inefficient.
#[allow(dead_code)]
pub(crate) fn dict_compact(&self, _basis_name: Option<&str>) -> Result<()> {
unimplemented!();
}
/// Syncs *only* the basis header to disk.
pub(crate) fn basis_sync(&mut self, hw: &mut PddbOs) {
self.last_sync = Some(hw.timestamp_now());
if !self.clean {
let basis_root = BasisRoot {
magic: PDDB_MAGIC,
version: PDDB_VERSION,
name: BasisRootName::try_from_str(&self.name).unwrap(),
age: self.age,
num_dictionaries: self.num_dicts,
};
let pp = self.v2p_map.get(&VirtAddr::new(1 * VPAGE_SIZE as u64).unwrap())
.expect("Internal consistency error: Basis exists, but its root map was not allocated!");
assert!(pp.valid(), "basis page was invalid");
let journal_bytes = self.journal.to_le_bytes(); // journal gets bumped by the patching function now
let slice_iter =
journal_bytes.iter() // journal rev
.chain(basis_root.as_ref().iter());
let mut block = [0 as u8; KCOM_CT_LEN];
for (&src, dst) in (hw.trng_u32() % JOURNAL_RAND_RANGE).to_le_bytes().iter().zip(block[..size_of::<JournalType>()].iter_mut()) {
*dst = src;
}
for (&src, dst) in slice_iter.zip(block.iter_mut()) {
*dst = src;
}
hw.data_encrypt_and_patch_page_with_commit(self.key.as_slice(), &self.aad, &mut block, &pp);
self.clean = true;
}
}
/// This function ensures a dictionary is in the cache; if not, it will load its entry.
pub(crate) fn ensure_dict_in_cache(&mut self, hw: &mut PddbOs, name: &str) -> bool {
let mut dict_found = false;
let dict_in_cache_and_valid =
if let Some(dict) = self.dicts.get(name) {
dict.flags.valid()
} else {
false
};
if !dict_in_cache_and_valid {
log::debug!("dict: key not in cache {}", name);
// if the dictionary doesn't exist in our cache it doesn't necessarily mean it
// doesn't exist. Do a comprehensive search if our cache isn't complete.
if let Some((index, dict_record)) = self.dict_deep_search(hw, name) {
let dict_name = std::str::from_utf8(&dict_record.name.data[..dict_record.name.len as usize]).expect("dict name is not valid utf-8").to_string();
let dcache = DictCacheEntry::new(dict_record, index as usize, &self.aad);
self.dicts.insert(dict_name, dcache);
dict_found = true;
}
} else {
dict_found = true;
}
dict_found
}
pub(crate) fn sync(&mut self, hw: &mut PddbOs) -> Result<()> {
// this is a bit awkward, but we have to make a copy of all the dictionary names
// because otherwise we borrow self as immutable to enumerate the names, and then
// we borrow it as mutable to do the sync. This deep-copy works around the issue.
let mut dictnames = Vec::<String>::new();
for (dict, entry) in self.dicts.iter() {
if entry.flags.valid() {
dictnames.push(dict.to_string());
}
}
for dict in dictnames {
match self.dict_sync(hw, &dict) {
Ok(_) => {},
Err(e) => {
log::error!("Error encountered syncing dict {}: {:?}", dict, e);
return Err(Error::new(ErrorKind::Other, e.to_string()));
}
}
}
self.basis_sync(hw);
self.pt_sync(hw);
Ok(())
}
// allocate a pointer data in the large pool, of length `amount`. "always" succeeds because...
// there's 16 million terabytes of large pool to allocate before you run out?
/*
// this function is actually implemented inside the "key_update()" code - as a large key is allocated,
// the pointer is bumped along within the update code. AFAIK, nobody else should be calling this, so, let's
// comment it out, and if it becomes necessary for some reason let's take a good hard look at assumptions.
// In particular, this function might become necessary if disk caching is implemented for large keys.
pub(crate) fn large_pool_alloc(&mut self, amount: u64) -> u64 {
let alloc_ptr = self.large_alloc_ptr.unwrap_or(PageAlignedVa::from(LARGE_POOL_START));
self.large_alloc_ptr = Some(self.large_alloc_ptr.unwrap_or(PageAlignedVa::from(LARGE_POOL_START)) + PageAlignedVa::from(amount));
return alloc_ptr.as_u64()
}*/
}
// ****
// Beginning of serializers for the data structures in this file.
// ****
/// Newtype for BasisRootName so we can give it a default initializer.
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
pub struct BasisRootName {
pub len: u8,
pub data: [u8; BASIS_NAME_LEN - 1],
}
impl BasisRootName {
pub fn try_from_str(name: &str) -> Result<BasisRootName> {
let mut alloc = [0u8; BASIS_NAME_LEN - 1];
let bytes = name.as_bytes();
if bytes.len() > (BASIS_NAME_LEN - 1) {
Err(Error::new(ErrorKind::InvalidInput, "basis name is too long")) // FileNameTooLong is still nightly :-/
} else {
for (&src, dst) in bytes.iter().zip(alloc.iter_mut()) {
*dst = src;
}
Ok(BasisRootName {
len: bytes.len() as u8, // this as checked above to be short enough
data: alloc,
})
}
}
}
impl Default for BasisRootName {
fn default() -> BasisRootName {
BasisRootName{
len: 0,
data: [0; BASIS_NAME_LEN - 1]
}
}
}