xous-core/services/pddb/src/backend/dictionary.rs
bunnie 679435c5f4 format sevices/pddb
wow the formatter hated this code.

hope i can recognize it later on when it needs work,
nobody else probably wants to touch it. :-/
2024-01-14 22:35:23 +08:00

1862 lines
92 KiB
Rust

use core::mem::size_of;
use core::ops::{Deref, DerefMut};
use std::cmp::{Ordering, Reverse};
use std::collections::{BTreeSet, BinaryHeap, HashMap};
use std::io::{Error, ErrorKind, Result};
use std::num::NonZeroU32;
use aes_gcm_siv::Aes256GcmSiv;
use bitfield::bitfield;
#[cfg(feature = "perfcounter")]
use perflib::{PERFMETA_ENDBLOCK, PERFMETA_NONE, PERFMETA_STARTBLOCK};
use super::*;
use crate::api::*;
#[cfg(feature = "perfcounter")]
use crate::FILE_ID_SERVICES_PDDB_SRC_DICTIONARY;
bitfield! {
#[derive(Copy, Clone, PartialEq, Eq)]
pub struct DictFlags(u32);
impl Debug;
pub valid, set_valid: 0;
}
/// RAM based copy of the dictionary structures on disk. Most of the methods on this function operate on
/// keys within the Dictionary. Operations on the Dictionary itself originate from the containing Basis
/// structure.
pub(crate) struct DictCacheEntry {
/// Use this to compute the virtual address of the dictionary's location
/// multiply this by DICT_VSIZE to get at the virtual address. This /could/ be a
/// NonZeroU32 type as it should never be 0. Maybe that's a thing to fix later on.
pub(crate) index: NonZeroU32,
/// A cache of the keys within the dictionary. If the key does not exist in
/// the cache, one should consult the on-disk copy, assuming the record is clean.
pub(crate) keys: HashMap<String, KeyCacheEntry>,
/// count of total keys in the dictionary -- may be equal to or larger than the number of elements in
/// `keys`
pub(crate) key_count: u32,
/// actual count of keys found -- this drifts from the actual key count due to write errors and
/// ungraceful powerdowns
pub(crate) found_key_count: u32,
/// track the pool of free key indices. Wrapped in a refcell so we can work the index mechanism while
/// updating the keys HashMap
pub(crate) free_keys: BinaryHeap<Reverse<FreeKeyRange>>,
/// hint for when to stop doing a brute-force search for the existence of a key in the disk records.
/// This field is set to the max count on a new, naive record; and set only upon a sync() or a fill()
/// call.
pub(crate) last_disk_key_index: u32,
/// set if synced to disk. should be cleared if the dict is modified, and/or if a subordinate key
/// descriptor is modified.
pub(crate) clean: bool,
/// track modification count
pub(crate) age: u32,
/// copy of the flags entry on the Dict on-disk
pub(crate) flags: DictFlags,
/// small pool data. index corresponds to portion on disk. This structure is built up as the dictionary
/// is read in, and is the "master" for tracking purposes. We always fill this from index 0 and go
/// up; if a KeySmallPool goes completely empty, the entry should still exist but indicate that it's
/// got space. Thus if a key was found allocated to the Nth index position, but the previous N-1
/// positions are empty, the only way we could have gotten there was if we had allocated lots of
/// small data, filled up the pool to the Nth position, and then deleted all of that prior data. This
/// situation could create pathologies in the memory usage overhead of the small_pool, which until we
/// have a "defrag" operation for the small pool, we may just have to live with.
pub(crate) small_pool: Vec<KeySmallPool>,
/// free space of each small pool element. It's a collection of free space along with the Vec index of
/// the small_pool. We don't keep the KeySmallPool itself in the small_pool_free directly because
/// it's presumed to be more common that we want to index the data, than it is to need to ask the
/// question of who has the most space free. This stays in lock-step with the small_pool data because
/// we do a .pop() to get the target vector from the small_pool_free, then we modify the pool item,
/// and then we .push() it back into the heap (or if it doesn't fit at all we allocate a new
/// entry and return the original item plus the new one to the heap).
pub(crate) small_pool_free: BinaryHeap<KeySmallPoolOrd>,
/// copy of our AAD, for convenience
pub(crate) aad: Vec<u8>,
/// ticktimer reference, for managing atimes
pub(crate) created: std::time::Instant,
}
impl DictCacheEntry {
pub fn new(dict: Dictionary, index: usize, aad: &Vec<u8>) -> DictCacheEntry {
let mut my_aad = Vec::<u8>::new();
for &b in aad.iter() {
my_aad.push(b);
}
let mut free_keys = BinaryHeap::<Reverse<FreeKeyRange>>::new();
free_keys.push(Reverse(FreeKeyRange {
start: dict.free_key_index,
run: KEY_MAXCOUNT as u32 - 1 - dict.free_key_index,
}));
DictCacheEntry {
index: NonZeroU32::new(index as u32).unwrap(),
keys: HashMap::<String, KeyCacheEntry>::new(),
key_count: dict.num_keys,
found_key_count: dict.num_keys,
free_keys,
last_disk_key_index: dict.free_key_index,
clean: false,
age: dict.age,
flags: dict.flags,
small_pool: Vec::<KeySmallPool>::new(),
small_pool_free: BinaryHeap::<KeySmallPoolOrd>::new(),
aad: my_aad,
created: std::time::Instant::now(),
}
}
/// Populates cache entries, reporting the maximum extent of large alloc data seen so far.
/// Requires a descriptor for the hardware, and our virtual to physical page mapping.
/// Does not overwrite existing cache entries, if they already exist -- only loads in ones that are
/// missing. Todo: condense routines in common with ensure_key_entry() to make it easier to maintain.
///
/// ASSUMES: all small/large pools have been sync'd prior to calling this
pub fn fill(
&mut self,
hw: &mut PddbOs,
v2p_map: &HashMap<VirtAddr, PhysPage>,
cipher: &Aes256GcmSiv,
cleanup: bool,
) -> VirtAddr {
let mut try_entry = 1;
let mut key_count = 0;
let mut alloc_top = VirtAddr::new(LARGE_POOL_START).unwrap();
// prune invalid entries, then count how many we have
self.keys.retain(|_name, entry| entry.flags.valid());
let valid_keys = self.keys.len();
assert!(
valid_keys <= self.key_count as usize,
"Inconsistency in key count. See note on basis/dict_count for logic on why this assert should be true..."
);
if valid_keys == self.key_count as usize {
// we've got an entry for every key, so we can safely skip the deep index search
let mut smallkeys_to_fill = Vec::<String>::new(); // work around inner mutability problem by copying the names of keys to fill
for (name, key) in self.keys.iter() {
// #[cfg(feature="perfcounter")]
// hw.perf_entry(FILE_ID_SERVICES_PDDB_SRC_DICTIONARY, PERFMETA_NONE, key_count,
// std::line!());
if key.flags.valid() {
if key.start + key.reserved > alloc_top.get() {
// if the key is within the large pool space, note its allocation for the basis
// overall
alloc_top = VirtAddr::new(key.start + key.reserved).unwrap();
// nothing else needs to be done -- we don't pre-cache large key data.
} else {
if key.data.is_none() {
smallkeys_to_fill.push(name.to_string());
}
}
}
}
// the cache may not be very effectively used because the key order is random, but let's try
// anyways.
let mut data_cache = PlaintextCache { data: None, tag: None };
for key_to_fill in smallkeys_to_fill.iter() {
#[cfg(feature = "perfcounter")]
hw.perf_entry(FILE_ID_SERVICES_PDDB_SRC_DICTIONARY, PERFMETA_NONE, key_count, std::line!());
self.try_fill_small_key(hw, v2p_map, cipher, &mut data_cache, key_to_fill);
}
} else {
let mut index_cache = PlaintextCache { data: None, tag: None };
let mut data_cache = PlaintextCache { data: None, tag: None };
let mut errcnt = 0;
while try_entry < KEY_MAXCOUNT && (cleanup || (key_count < self.key_count)) {
#[cfg(feature = "perfcounter")]
hw.perf_entry(FILE_ID_SERVICES_PDDB_SRC_DICTIONARY, PERFMETA_NONE, key_count, std::line!());
// cache our decryption data -- there's about 32 entries per page, and the scan is largely
// linear/sequential, so this should be a substantial savings in effort.
let req_vaddr = dict_indices_to_vaddr(self.index, try_entry);
index_cache.fill(hw, v2p_map, cipher, &self.aad, VirtAddr::new(req_vaddr).unwrap());
if index_cache.data.is_none() || index_cache.tag.is_none() {
// somehow we hit a page where nothing was allocated (perhaps it was previously deleted?),
// or less likely, the data was corrupted. Note the isuse, skip past it.
if (errcnt < 4) || (errcnt % 8192 == 0) {
log::warn!(
"Dictionary fill: encountered unallocated page at {} in the dictionary map. {}/{}",
try_entry,
key_count,
self.key_count
);
}
errcnt += 1;
try_entry += DK_PER_VPAGE;
} else {
let cache_pp = index_cache
.tag
.as_ref()
.expect("PP should be in existence, it was already checked...");
let pp = v2p_map
.get(&VirtAddr::new(req_vaddr).unwrap())
.expect("dictionary PP should be in existence");
assert!(pp.valid(), "v2p returned an invalid page");
assert!(cache_pp.page_number() == pp.page_number(), "cache inconsistency error");
let cache =
index_cache.data.as_ref().expect("Cache should be full, it was already checked...");
let mut keydesc = KeyDescriptor::default();
let start = size_of::<JournalType>() + (try_entry % DK_PER_VPAGE) * DK_STRIDE;
for (&src, dst) in
cache[start..start + DK_STRIDE].iter().zip(keydesc.deref_mut().iter_mut())
{
*dst = src;
}
if keydesc.flags.valid() {
let kcache = KeyCacheEntry {
start: keydesc.start,
len: keydesc.len,
reserved: keydesc.reserved,
flags: keydesc.flags,
age: keydesc.age,
descriptor_index: NonZeroU32::new(try_entry as u32).unwrap(),
clean: true,
data: None,
atime: self.created.elapsed().as_millis() as u64,
};
let kname = std::str::from_utf8(&keydesc.name.data[..keydesc.name.len as usize])
.expect("key is not valid utf-8");
let key_exists_and_valid = if let Some(kcache) = self.keys.get(kname) {
kcache.flags.valid()
} else {
false
};
if !key_exists_and_valid {
self.keys.insert(kname.to_string(), kcache);
if keydesc.start + keydesc.reserved > alloc_top.get() {
// if the key is within the large pool space, note its allocation for the
// basis overall
alloc_top = VirtAddr::new(keydesc.start + keydesc.reserved).unwrap();
// nothing else needs to be done -- we don't pre-cache large key data.
} else {
// try to fill the small key cache entry details
self.try_fill_small_key(hw, v2p_map, cipher, &mut data_cache, &kname);
}
} else {
log::trace!("fill: entry already present {}", kname);
}
key_count += 1;
}
try_entry += 1;
}
}
// note where the scan left off, so we don't have to brute-force it in the future
self.last_disk_key_index = try_entry as u32;
// note the actual number of keys found
self.found_key_count = key_count;
// now build the small_pool_free binary heap structure
#[cfg(feature = "perfcounter")]
hw.perf_entry(FILE_ID_SERVICES_PDDB_SRC_DICTIONARY, PERFMETA_NONE, key_count, std::line!());
self.rebuild_free_pool();
#[cfg(feature = "perfcounter")]
hw.perf_entry(FILE_ID_SERVICES_PDDB_SRC_DICTIONARY, PERFMETA_NONE, key_count, std::line!());
}
alloc_top
}
/// merges the list of keys in this dict cache entry into a merge_list.
/// The `merge_list` is used because keys are presented as a union across all open basis.
pub(crate) fn key_list(
&mut self,
hw: &mut PddbOs,
v2p_map: &HashMap<VirtAddr, PhysPage>,
cipher: &Aes256GcmSiv,
merge_list: &mut BTreeSet<String>,
) {
#[cfg(feature = "perfcounter")]
hw.perf_entry(FILE_ID_SERVICES_PDDB_SRC_DICTIONARY, PERFMETA_STARTBLOCK, 0, std::line!());
// ensure that the key cache is filled
if self.keys.len() < self.key_count as usize {
self.fill(hw, v2p_map, cipher, false);
}
#[cfg(feature = "perfcounter")]
hw.perf_entry(FILE_ID_SERVICES_PDDB_SRC_DICTIONARY, PERFMETA_NONE, 0, std::line!());
for (key, kcache) in self.keys.iter() {
if kcache.flags.valid() {
merge_list.insert(key.to_string());
}
}
#[cfg(feature = "perfcounter")]
hw.perf_entry(FILE_ID_SERVICES_PDDB_SRC_DICTIONARY, PERFMETA_ENDBLOCK, 0, std::line!());
}
/// Simply ensures we have the description of a key in cache. Only tries to load small key data.
/// Required by meta-operations on the keys that operate only out of the cache.
/// This shares a lot of code with the fill() routine -- we should condense the common routines
/// to make this easier to maintain. Returns false if the disk was searched and no key was found; true
/// if cache is hot or key was found on search.
pub(crate) fn ensure_key_entry(
&mut self,
hw: &mut PddbOs,
v2p_map: &mut HashMap<VirtAddr, PhysPage>,
cipher: &Aes256GcmSiv,
name_str: &str,
) -> bool {
let mut data_cache = PlaintextCache { data: None, tag: None };
// only fill if the key isn't in the cache, or if the data section has been evicted
let needs_fill = if let Some(entry) = self.keys.get(name_str) {
if entry.flags.valid() {
if entry.data.is_none() {
self.try_fill_small_key(hw, v2p_map, cipher, &mut data_cache, name_str);
// it should be filled, so no further processing is needed
false
} else {
// data is already there
false
}
} else {
// invalid entry
false
}
} else {
// entry doesn't exist
true
};
if needs_fill {
log::debug!("searching for key {}", name_str);
let mut try_entry = 1;
let mut key_count = 0;
let mut index_cache = PlaintextCache { data: None, tag: None };
let mut warn_count = 0;
while try_entry < KEY_MAXCOUNT
&& key_count < self.key_count
&& try_entry <= self.last_disk_key_index as usize
{
// cache our decryption data -- there's about 32 entries per page, and the scan is largely
// linear/sequential, so this should be a substantial savings in effort.
// Determine the absolute virtual address of the requested entry. It's written a little weird
// because DK_PER_VPAGE is 32, which optimizes cleanly and removes an
// expensive division step
let req_vaddr = dict_indices_to_vaddr(self.index, try_entry);
index_cache.fill(hw, v2p_map, cipher, &self.aad, VirtAddr::new(req_vaddr).unwrap());
if index_cache.data.is_none() || index_cache.tag.is_none() {
// this case "should not happen" in practice, because the last_disk_key_index would either
// be correctly set as short by a dict_add(), or a mount() operation
// would have limited the extent of the search. if we are hitting
// this, that means the last_disk_key_index operator was not managed correctly.
if warn_count < 4 || (warn_count % 8192 == 0) {
log::warn!("expensive search op");
}
warn_count += 1;
try_entry += DK_PER_VPAGE;
} else {
let cache =
index_cache.data.as_ref().expect("Cache should be full, it was already checked...");
let cache_pp = index_cache
.tag
.as_ref()
.expect("PP should be in existence, it was already checked...");
let pp = v2p_map
.get(&VirtAddr::new(req_vaddr).unwrap())
.expect("dictionary PP should be in existence");
assert!(pp.valid(), "v2p returned an invalid page");
assert!(cache_pp.page_number() == pp.page_number(), "cache inconsistency error");
let mut keydesc = KeyDescriptor::default();
let start = size_of::<JournalType>() + (try_entry % DK_PER_VPAGE) * DK_STRIDE;
// there is 1 byte of extra padding that causes the slices of keydesc.deref_mut() to be
// 128 bytes long instead of 127 bytes...
keydesc.deref_mut()[..DK_STRIDE].copy_from_slice(&cache[start..start + DK_STRIDE]);
let kname = std::str::from_utf8(&keydesc.name.data[..keydesc.name.len as usize])
.expect("key is not valid utf-8");
if keydesc.flags.valid() {
if kname == name_str {
log::debug!("found {} at entry {}/{}", name_str, try_entry, start);
let kcache = KeyCacheEntry {
start: keydesc.start,
len: keydesc.len,
reserved: keydesc.reserved,
flags: keydesc.flags,
age: keydesc.age,
descriptor_index: NonZeroU32::new(try_entry as u32).unwrap(),
clean: true,
data: None,
atime: self.created.elapsed().as_millis() as u64,
};
self.keys.insert(kname.to_string(), kcache);
self.try_fill_small_key(hw, v2p_map, cipher, &mut data_cache, &kname);
return true;
}
key_count += 1;
}
try_entry += 1;
}
}
false
} else {
// the key is in the cache, but is it valid?
if self.keys.get(name_str).expect("inconsistent state").flags.valid() {
true
} else {
// not valid -- it's an erased key, but waiting to be synced to disk. Return that the key
// wasn't found.
false
}
}
}
fn try_fill_small_key(
&mut self,
hw: &mut PddbOs,
v2p_map: &HashMap<VirtAddr, PhysPage>,
cipher: &Aes256GcmSiv,
data_cache: &mut PlaintextCache,
key_name: &str,
) {
let mut filled_index: Option<usize> = None;
if let Some(kcache) = self.keys.get_mut(key_name) {
if !kcache.flags.valid() {
// nothing to fill, the key entry isn't valid
return;
}
if let Some(pool_index) = small_storage_index_from_key(&kcache, self.index) {
// if the key is within the small pool space, create a bookkeeping record for it, and
// pre-cache its data. generate the index within the small pool based on the
// address
while self.small_pool.len() < pool_index + 1 {
// fill in the pool with blank entries. In general, we should have a low amount of blank
// entries, but one situation where we could get a leak is if we
// allocate a large amount of small data, and then delete all but the
// most recently allocated one, leaving an orphan at a high index, which is then
// subsequently treated as read-only so none of the subsequent
// write/update ops would have occassion to move it. This would
// need to be remedied with a "defrag" operation, but for now, we don't have that.
let ksp = KeySmallPool::new();
self.small_pool.push(ksp);
}
let ksp = &mut self.small_pool[pool_index];
if !ksp.clean || ksp.evicted {
if !ksp.contents.contains(&key_name.to_string()) {
log::trace!("creating ksp entry for {}", key_name);
ksp.contents.push(key_name.to_string());
assert!(
kcache.reserved >= kcache.len,
"Reserved amount is less than length, this is an error!"
);
assert!(
kcache.reserved <= VPAGE_SIZE as u64,
"Reserved amount is not appropriate for the small pool. Logic error in prior PDDB operation!"
);
log::trace!("avail: {} reserved: {}", ksp.avail, kcache.reserved);
assert!(
(ksp.avail as u64) >= kcache.reserved,
"Total amount allocated to a small pool chunk is incorrect; suspect logic error in prior PDDB operation!"
);
ksp.avail -= kcache.reserved as u16;
// note: small_pool_free is updated only after all the entries have been read in
} else {
// if the entry was previously created, don't update the metadata.
// This might not be the best place for this note, but, as a reminder to myself,
// the small pool code is written assuming you scan through all the keys and fill it
// into RAM at least once. Once you've done that, you can evict cached data to free up
// some space, but it's sort of dumb that you have to do that
// in the first place. This is going to lead to a crisis once
// the small pool data exceeds the available RAM -- which isn't too far from now.
// This will be tracked in issue #109.
log::trace!("refilling data only for {}", key_name);
}
// now grab the *data* referred to by this key. Maybe this is a "bad" idea -- this can
// really eat up RAM fast to hold all the small pool data right away,
// but let's give it a try and see how it works. Later on we can always skip this.
// manage a separate small cache for data blocks, under the theory that small keys tend to
// be packed together
let data_vaddr = small_storage_base_vaddr_from_indices(self.index, pool_index);
data_cache.fill(hw, v2p_map, cipher, &self.aad, VirtAddr::new(data_vaddr).unwrap());
if let Some(page) = data_cache.data.as_ref() {
let start_offset =
size_of::<JournalType>() + (kcache.start % VPAGE_SIZE as u64) as usize;
let mut data = page[start_offset..start_offset + kcache.len as usize].to_vec();
data.reserve_exact((kcache.reserved - kcache.len) as usize);
kcache.data = Some(KeyCacheData::Small(KeySmallData { clean: true, data }));
filled_index = Some(pool_index);
} else {
log::error!(
"Key {}'s data region at pp: {:x?} va: {:x} is unreadable",
key_name,
data_cache.tag,
kcache.start
);
}
} else {
log::debug!("pool is clean for key {} / index {}, skipping fill", key_name, pool_index);
}
}
} else {
log::error!(
"try_fill_small_key() can only be called after the key cache entry has been allocated"
);
panic!("consistency error");
}
if let Some(index) = filled_index {
// check to see if this fill can clear the eviction status of the entire pool
let ksp = &mut self.small_pool[index];
let mut all_present = true;
for key in ksp.contents.iter() {
if let Some(kcache) = self.keys.get(key) {
if kcache.flags.valid() {
if kcache.data.is_none() {
all_present = false;
break;
}
}
}
}
if all_present {
ksp.evicted = false;
}
}
}
/// To be called only to re-fill key entries whose data have been evicted by a prune.
pub(crate) fn refill_small_key(
&mut self,
hw: &mut PddbOs,
v2p_map: &HashMap<VirtAddr, PhysPage>,
cipher: &Aes256GcmSiv,
data_cache: &mut PlaintextCache,
key_name: &str,
) {
if let Some(kcache) = self.keys.get_mut(key_name) {
if kcache.flags.valid() {
// invalid keys were previously deleted, and have nothing to fill
if let Some(pool_index) = small_storage_index_from_key(&kcache, self.index) {
let ksp = &mut self.small_pool[pool_index];
assert!(
ksp.contents.contains(&key_name.to_string()),
"refill called on a non-existent key. This is an illegal state."
);
// now grab the *data* referred to by this key. Maybe this is a "bad" idea -- this can
// really eat up RAM fast to hold all the small pool data right away,
// but let's give it a try and see how it works. Later on we can always skip this.
// manage a separate small cache for data blocks, under the theory that small keys tend to
// be packed together
let data_vaddr = small_storage_base_vaddr_from_indices(self.index, pool_index);
data_cache.fill(hw, v2p_map, cipher, &self.aad, VirtAddr::new(data_vaddr).unwrap());
if let Some(page) = data_cache.data.as_ref() {
let start_offset =
size_of::<JournalType>() + (kcache.start % VPAGE_SIZE as u64) as usize;
let mut data = page[start_offset..start_offset + kcache.len as usize].to_vec();
data.reserve_exact((kcache.reserved - kcache.len) as usize);
kcache.data = Some(KeyCacheData::Small(KeySmallData { clean: true, data }));
} else {
log::error!(
"Key {}'s data region at pp: {:x?} va: {:x} is unreadable",
key_name,
data_cache.tag,
kcache.start
);
}
}
}
} else {
log::error!("refill_small_key() can only be called after the key cache entry has been allocated");
panic!("consistency error");
}
}
/// Update a key entry. If the key does not already exist, it will create a new one.
///
/// Assume: the caller has called ensure_fast_space_alloc() to make sure there is sufficient space for the
/// inserted key before calling.
///
/// `key_update` will write `data` starting at `offset`, and will grow the record if data
/// is larger than the current allocation. If `truncate` is false, the existing data past the end of
/// the `data` written is preserved; if `truncate` is true, the excess data past the end of the written
/// data is removed.
///
/// For small records, a `key_update` call would just want to replace the entire record, so it would have
/// an `offset` of 0, `truncate` is true, and the data would be the new data. However, the `offset` and
/// `truncate` records are particularly useful for updating very large file streams, which can't be
/// held entirely in RAM.
///
/// Note: it is up to the higher level Basis disambiguation logic to decide the cross-basis update policy:
/// it could either be to update only the dictionary in the latest open basis, update all
/// dictionaries, or update a specific dictionary in a named basis. In all of these cases, the Basis
/// resolver will have had to find the correct DictCacheEntry and issue the `key_update` to it; for
/// multiple updates, then multiple calls to multiple DictCacheEntry are required.
pub fn key_update(
&mut self,
hw: &mut PddbOs,
v2p_map: &mut HashMap<VirtAddr, PhysPage>,
cipher: &Aes256GcmSiv,
name: &str,
data: &[u8],
offset: usize,
alloc_hint: Option<usize>,
truncate: bool,
large_alloc_ptr: PageAlignedVa,
) -> Result<PageAlignedVa> {
self.age = self.age.saturating_add(1);
self.clean = false;
if self.ensure_key_entry(hw, v2p_map, cipher, name) {
let kcache = self.keys.get_mut(name).expect("Entry was assured, but then not there!");
kcache.set_atime(self.created.elapsed().as_millis() as u64);
kcache.clean = false;
// the update isn't going to fit in the reserved space, remove it, and re-insert it with an
// entirely new entry.
if kcache.reserved < (data.len() + offset) as u64 {
if kcache.start < SMALL_POOL_END {
// this started life as a small key. the algorithm is to remove and retry upon extend.
// allocate a new vector that contains the *entire* data contents (not just the updated
// portion), and re-insert it
let mut update_data = Vec::<u8>::with_capacity(data.len() + offset);
if let Some(KeyCacheData::Small(old_data)) = &kcache.data {
for &b in &old_data.data {
update_data.push(b);
}
} else {
log::error!("expected a small key, but the data type was incorrect or not in cache");
panic!("expected a small key, but the data type was incorrect or not in cache");
}
// extend the vector out to the expected length, so the indexed slice in the next step
// doesn't panic
while update_data.len() < offset + data.len() {
update_data.push(0);
}
for (&src, dst) in data.iter().zip(update_data[offset..].iter_mut()) {
*dst = src
}
log::debug!("update/extend: removing {}", name);
// now remove the old key entirely
self.key_remove(hw, v2p_map, cipher, name, false);
self.sync_small_pool(hw, v2p_map, cipher);
if update_data.len() > 4 {
// just make sure that this log call doesn't fail on an index violation...
log::debug!(
"update/extend: re-adding {} with data len {}: {:x?}...",
name,
update_data.len(),
&update_data[..4]
);
}
// and re-add it with the extended data; if it's no longer a small key after this, it'll
// be handled inside this call.
return self.key_update(
hw,
v2p_map,
cipher,
name,
&update_data,
0,
alloc_hint,
truncate,
large_alloc_ptr,
);
} else {
// large data sets will need more physical pages to be allocated for the new file length.
// It's a hard error if the requested size goes beyond the
// pre-allocated virtual memory space limit.
if (offset + data.len()) as u64 > LARGE_FILE_MAX_SIZE {
log::error!(
"Requested file update would exceed our file size limit. Asked: {}, limit: {}",
offset + data.len(),
LARGE_FILE_MAX_SIZE
);
panic!("Updated file size is greater than the large file size limit.");
}
assert!(
(kcache.start + kcache.reserved) % VPAGE_SIZE as u64 == 0,
"large space allocation rules were violated"
);
let new_reservation_abs_addr =
PageAlignedVa::from(kcache.start + offset as u64 + data.len() as u64).as_u64();
for vpage_addr in
((kcache.start + kcache.reserved)..new_reservation_abs_addr).step_by(VPAGE_SIZE)
{
// ensures that a physical page entry exists for every new virtual address required by
// the extended key
v2p_map.entry(VirtAddr::new(vpage_addr).unwrap()).or_insert_with(|| {
let mut ap = hw
.try_fast_space_alloc()
.expect("No free space to allocate additional large key storage");
ap.set_valid(true);
ap
});
}
kcache.reserved = new_reservation_abs_addr - kcache.start; // convert absolute address to an actual length
kcache.clean = false;
// now retry the call, with the new physical page reservations in place
return self.key_update(
hw,
v2p_map,
cipher,
name,
data,
offset,
alloc_hint,
truncate,
large_alloc_ptr,
);
}
}
// the key exists, *and* there's sufficient space for the data
if kcache.start < SMALL_POOL_END {
log::debug!("doing data update of {}", name);
kcache.age = kcache.age.saturating_add(1);
kcache.clean = false;
// it's a small key; note that we didn't consult the *size* of the key to determine its pool
// type: small-sized keys can still end up in the "large" space if the small
// pool allocation is exhausted.
match kcache.data.as_mut() {
Some(KeyCacheData::Small(cache_data)) => {
cache_data.clean = false;
// grow the data cache to accommodate the necessary length; this should be efficient
// because we reserved space when the vector was allocated
while cache_data.data.len() < data.len() + offset {
cache_data.data.push(0);
}
for (&src, dst) in data.iter().zip(cache_data.data[offset..].iter_mut()) {
*dst = src;
}
}
Some(KeyCacheData::Large(_)) => {
panic!("Key allocated to small area but its cache data was not of the small type");
}
None => {
// cache data was evicted, fill it
assert!(kcache.flags.valid(), "Entry to be filled should be valid");
let pool_index =
small_storage_index_from_key(&kcache, self.index).expect("pool should be valid");
let ksp = &mut self.small_pool[pool_index];
if !ksp.contents.contains(&name.to_string()) {
log::warn!(
"illegal state: key_update fill key pool contents: {:?}, missing {}",
ksp.contents,
name
);
panic!("Entry to fill isn't in the expected pool");
}
let data_vaddr = small_storage_base_vaddr_from_indices(self.index, pool_index);
let mut data_cache = PlaintextCache { data: None, tag: None };
data_cache.fill(hw, v2p_map, cipher, &self.aad, VirtAddr::new(data_vaddr).unwrap());
if let Some(page) = data_cache.data.as_ref() {
let start_offset =
size_of::<JournalType>() + (kcache.start % VPAGE_SIZE as u64) as usize;
let mut data = page[start_offset..start_offset + kcache.len as usize].to_vec();
data.reserve_exact((kcache.reserved - kcache.len) as usize);
kcache.data = Some(KeyCacheData::Small(KeySmallData { clean: true, data }));
} else {
let error = format!(
"Key {}'s data region at pp: {:x?} va: {:x} is unreadable",
name, data_cache.tag, kcache.start
);
panic!("{}", error);
}
}
}
// check if we grew the length
if kcache.len < (data.len() + offset) as u64 {
kcache.len = (data.len() + offset) as u64;
} else if truncate {
kcache.len = (data.len() + offset) as u64;
}
// mark the storage pool entry as dirty, too.
let pool_index = small_storage_index_from_key(&kcache, self.index).expect("index missing");
self.small_pool[pool_index].clean = false;
// note: there is no need to update small_pool_free because the reserved size did not change.
} else {
// it's a large key
if let Some(_kcd) = &kcache.data {
unimplemented!("caching is not yet implemented for large data sets");
} else {
kcache.age = kcache.age.saturating_add(1);
kcache.clean = false;
/* // this was for debugging a patching bug -- OK to remove
if data.len() == 4 {
use std::convert::TryInto;
log::info!("patching checksum: {:x} at offset {}", u32::from_le_bytes(data.try_into().unwrap()), offset);
}*/
// 1. handle unaligned start offsets
let mut written: usize = 0;
if ((kcache.start + offset as u64 + written as u64) % VPAGE_SIZE as u64) != 0 {
let start_vpage_addr =
((kcache.start + offset as u64) / VPAGE_SIZE as u64) * VPAGE_SIZE as u64;
let pp = v2p_map
.get(&VirtAddr::new(start_vpage_addr).unwrap())
.expect("large key data allocation missing");
assert!(pp.valid(), "v2p returned an invalid page");
let mut pt_data = match hw.data_decrypt_page(&cipher, &self.aad, pp) {
Some(data) => data,
None => {
// this case is triggered by the following circumstance:
// - we reserved data that includes this current page
// - up until now, we've only written data into the previous page (so this
// page is not initialized -- it's garbage)
// - we just issued an update that causes the data to touch this page for the
// first time
// in response to this, we allocate a fresh page of 0's.
log::debug!(
"Reserved and uninitialized page encountered updating large block: {} {:x}..{}->{}; update @{}..{}",
name,
kcache.start,
kcache.len,
kcache.reserved,
offset,
data.len()
);
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
}
};
if offset > 0 {
log::trace!(
"patching offset {}, total length {}, data length {}",
offset % VPAGE_SIZE,
kcache.len,
data.len()
);
}
for (&src, dst) in data[written..]
.iter()
.zip(pt_data[size_of::<JournalType>() + (offset % VPAGE_SIZE)..].iter_mut())
{
*dst = src;
written += 1;
}
if written < data.len() {
assert!(
(kcache.start + offset as u64 + written as u64) % VPAGE_SIZE as u64 == 0,
"alignment algorithm failed"
);
}
hw.data_encrypt_and_patch_page(cipher, &self.aad, &mut pt_data, &pp);
}
// 2. do the rest
while written < data.len() {
let vpage_addr = ((kcache.start + written as u64 + offset as u64)
/ VPAGE_SIZE as u64)
* VPAGE_SIZE as u64;
let pp = v2p_map
.get(&VirtAddr::new(vpage_addr).unwrap())
.expect("large key data allocation missing");
assert!(pp.valid(), "v2p returned an invalid page");
if data.len() - written >= VPAGE_SIZE {
// overwrite whole pages without decryption
let mut block = [0u8; VPAGE_SIZE + size_of::<JournalType>()];
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
data[written..].iter().zip(block[size_of::<JournalType>()..].iter_mut())
{
*dst = src;
written += 1;
}
hw.data_encrypt_and_patch_page(cipher, &self.aad, &mut block, pp);
} else {
// handle partial trailing pages
if let Some(pt_data) = hw.data_decrypt_page(&cipher, &self.aad, pp).as_mut() {
for (&src, dst) in
data[written..].iter().zip(pt_data[size_of::<JournalType>()..].iter_mut())
{
*dst = src;
written += 1;
}
hw.data_encrypt_and_patch_page(cipher, &self.aad, pt_data, pp);
} else {
// page didn't exist, initialize it with 0's and merge the tail end.
let mut pt_data = [0u8; VPAGE_SIZE + size_of::<JournalType>()];
for (&src, dst) in (hw.trng_u32() % JOURNAL_RAND_RANGE)
.to_le_bytes()
.iter()
.zip(pt_data[..size_of::<JournalType>()].iter_mut())
{
*dst = src;
}
for (&src, dst) in
data[written..].iter().zip(pt_data[size_of::<JournalType>()..].iter_mut())
{
*dst = src;
written += 1;
}
hw.data_encrypt_and_patch_page(cipher, &self.aad, &mut pt_data, pp);
}
}
}
log::trace!("data written: {}, data requested to write: {}", written, data.len());
assert!(
written == data.len(),
"algorithm problem -- didn't write all the data we thought we would"
);
// 3. truncate or extend
// check if we grew the length; extend the length by exactly enough if so.
if kcache.len < (data.len() + offset) as u64 {
kcache.len = (data.len() + offset) as u64;
} else if truncate {
// discard all whole pages after written+offset, and reset the reserved field to the
// smaller size.
log::trace!("PageAligned VA components: {}, {}", written, offset);
let vpage_end_offset = PageAlignedVa::from((written + offset) as u64);
if (vpage_end_offset.as_u64() - kcache.start) > kcache.reserved {
for vpage in (vpage_end_offset.as_u64()..kcache.start + kcache.reserved)
.step_by(VPAGE_SIZE)
{
if let Some(pp) = v2p_map.get_mut(&VirtAddr::new(vpage).unwrap()) {
assert!(pp.valid(), "v2p returned an invalid page");
log::trace!("fast_space_free key_update {} before", pp.journal());
hw.fast_space_free(pp);
assert!(pp.valid() == false, "pp is still marked as valid!");
}
}
kcache.reserved = vpage_end_offset.as_u64() - kcache.start;
kcache.clean = false;
kcache.len = (data.len() + offset) as u64;
}
}
}
}
} else {
// key does not exist (or was previously erased) -- create one or replace the erased one.
// try to fit the key in the small pool first
if ((data.len() + offset) < SMALL_CAPACITY)
&& (alloc_hint.unwrap_or(DEFAULT_ALLOC_HINT) < SMALL_CAPACITY)
{
log::debug!("creating small key {}", name);
// handle the case that we're a brand new dictionary and no small keys have ever been stored
// before.
if self.small_pool.len() == 0 {
self.small_pool.push(KeySmallPool::new());
self.rebuild_free_pool();
}
let pool_candidate =
self.small_pool_free.pop().expect("Free pool was allocated & rebuilt, but still empty.");
let mut reservation = if alloc_hint.unwrap_or(DEFAULT_ALLOC_HINT) > data.len() + offset {
alloc_hint.unwrap_or(DEFAULT_ALLOC_HINT)
} else {
data.len() + offset
};
if reservation == 0 {
// this case happens if someone tries to just create an empty key entry without giving an
// alloc hint
reservation = 1; // at least make a reservation for 1 byte of data
}
let index = if pool_candidate.avail as usize >= reservation {
// it fits in the current candidate slot, use this as the index
let ksp = &mut self.small_pool[pool_candidate.index];
ksp.contents.push(name.to_string());
ksp.avail -= reservation as u16;
ksp.clean = false;
log::debug!("ksp.clean = false {}", name);
self.small_pool_free
.push(KeySmallPoolOrd { avail: ksp.avail, index: pool_candidate.index });
pool_candidate.index
} else {
self.small_pool_free.push(pool_candidate);
// allocate a new small pool slot
let mut ksp = KeySmallPool::new();
ksp.contents.push(name.to_string());
ksp.avail -= reservation as u16;
ksp.clean = false;
log::debug!("ksp.clean = false {}", name);
// update the free pool with the current candidate
// we don't subtract 1 from len because we're about to push the ksp onto the end of the
// small_pool, consuming it
self.small_pool_free
.push(KeySmallPoolOrd { avail: ksp.avail, index: self.small_pool.len() });
self.small_pool.push(ksp);
// the actual location is at len-1 now because we have done the push
self.small_pool.len() - 1
};
let mut kf = KeyFlags(0);
kf.set_valid(true);
kf.set_unresolved(true);
let mut alloc_data = Vec::<u8>::new();
for _ in 0..offset {
alloc_data.push(0);
}
for &b in data {
alloc_data.push(b);
}
let descriptor_index = if let Some(di) = self.get_free_key_index() {
di
} else {
return Err(Error::new(ErrorKind::OutOfMemory, "Ran out of key indices in dictionary"));
};
/* log::info!("storing in index {:?}", descriptor_index);
for fk in self.free_keys.iter() {
log::info!("fk start: {} run: {}", fk.0.start, fk.0.run);
} */
let kcache = KeyCacheEntry {
start: small_storage_base_vaddr_from_indices(self.index, index),
len: (data.len() + offset) as u64,
reserved: reservation as u64,
flags: kf,
age: 0,
descriptor_index,
clean: false,
data: Some(KeyCacheData::Small(KeySmallData { clean: false, data: alloc_data })),
atime: self.created.elapsed().as_millis() as u64,
};
self.keys.insert(name.to_string(), kcache);
self.key_count += 1;
} else {
log::debug!("creating large key: {}", data.len());
// it didn't fit in the small pool, stick it in the big pool.
let reservation =
PageAlignedVa::from(if alloc_hint.unwrap_or(DEFAULT_ALLOC_HINT) > data.len() + offset {
alloc_hint.unwrap_or(DEFAULT_ALLOC_HINT)
} else {
data.len() + offset
});
let mut kf = KeyFlags(0);
kf.set_valid(true);
let descriptor_index = if let Some(di) = self.get_free_key_index() {
di
} else {
return Err(Error::new(ErrorKind::OutOfMemory, "Ran out of key indices in dictionary"));
};
let kcache = KeyCacheEntry {
start: large_alloc_ptr.as_u64(),
len: (data.len() + offset) as u64,
reserved: reservation.as_u64(),
flags: kf,
age: 0,
descriptor_index,
clean: false,
data: None, // no caching implemented yet for large keys
atime: self.created.elapsed().as_millis() as u64,
};
self.keys.insert(name.to_string(), kcache);
self.key_count += 1;
// 1. allocate all the pages up to the reservation limit
for vpage_addr in (large_alloc_ptr.as_u64()..large_alloc_ptr.as_u64() + reservation.as_u64())
.step_by(VPAGE_SIZE)
{
let pp = hw.try_fast_space_alloc().ok_or(Error::new(
ErrorKind::OutOfMemory,
"couldn't allocate memory for large key",
))?;
assert!(pp.valid(), "didn't receive a valid page in large space alloc");
log::debug!("pp alloc v{:x}->p{:x?}", vpage_addr, pp);
v2p_map.insert(VirtAddr::new(vpage_addr).unwrap(), pp);
}
// 2. Recurse. Now, the key should exist, and it should go through the "write the data out"
// section of the algorithm.
// note that the alloc pointer at this point sets the ultimate limit for the large file size
// (32GiB as of writing).
return self.key_update(
hw,
v2p_map,
cipher,
name,
data,
offset,
alloc_hint,
truncate,
large_alloc_ptr + PageAlignedVa::from(LARGE_FILE_MAX_SIZE),
);
}
}
Ok(large_alloc_ptr)
}
#[allow(dead_code)]
pub fn key_contains(&self, name: &str) -> bool { self.keys.contains_key(&String::from(name)) }
fn rebuild_free_pool(&mut self) {
self.small_pool_free.clear();
for (index, ksp) in self.small_pool.iter().enumerate() {
self.small_pool_free.push(KeySmallPoolOrd { index, avail: ksp.avail })
}
}
/// Used to remove a key from the dictionary. If you call it with a non-existent key,
/// the routine has no effect, and does not report an error. Small keys are not immediately
/// overwritten in paranoid mode, but large keys are.
pub fn key_remove(
&mut self,
hw: &mut PddbOs,
v2p_map: &mut HashMap<VirtAddr, PhysPage>,
cipher: &Aes256GcmSiv,
name_str: &str,
paranoid: bool,
) {
log::debug!("removing key {}", name_str);
if paranoid {
// large records are paranoid-erased, by default, because of the pool-reuse problem.
unimplemented!(
"Paranoid erase for small records not yet implemented. Calling sync after an update, however, effectively does a paranoid erase."
);
}
// this call will check the disk to see if there's key data that's not in cache.
if self.ensure_key_entry(hw, v2p_map, cipher, name_str) {
let name = String::from(name_str);
let mut need_rebuild = false;
let mut need_free_key: Option<u32> = None;
if let Some(kcache) = self.keys.get_mut(&name) {
if !kcache.flags.valid() {
log::debug!("ensure of invalid key: {}", name_str);
assert!(kcache.clean == false, "keys that are invalidated should be marked as not clean");
// key was previously deleted, already in cache, but not flushed. Nothing to do here.
return;
}
self.age = self.age.saturating_add(1);
self.clean = false;
kcache.age = kcache.age.saturating_add(1);
kcache.flags.set_valid(false);
kcache.clean = false;
if let Some(small_index) = small_storage_index_from_key(kcache, self.index) {
// handle the small pool case
let ksp = &mut self.small_pool[small_index];
let err_static = format!(
"Small pool did not contain the element we expected: {}, len: {}",
&name, kcache.len
);
log::debug!("ksp swap_remove({}) flags: {:?}", name, kcache.flags);
ksp.contents
.swap_remove(ksp.contents.iter().position(|s| *s == name).expect(&err_static));
assert!(kcache.reserved <= SMALL_CAPACITY as u64, "error in small key entry size");
ksp.avail += kcache.reserved as u16;
assert!(ksp.avail <= SMALL_CAPACITY as u16, "bookkeeping error in small pool capacity");
ksp.clean = false; // this will also effectively cause the record to be deleted on disk once the small pool data is synchronized
need_rebuild = true;
} else {
// handle the large pool case
// mark the entry as invalid and dirty; virtual space is one huge memory leak...
// ...but we remove the virtual pages from the page pool, effectively reclaiming the
// physical space.
for vpage in kcache.large_pool_vpages() {
if let Some(pp) = v2p_map.get_mut(&vpage) {
assert!(pp.valid(), "v2p returned an invalid page");
{
// this slows things down but it prevents data from leaking back into other
// basis data structures when the sector is re-allocated
// in other words, i think it's probably very unsafe to not always
// secure-erase large data as it's de-allocated.
let mut noise = [0u8; PAGE_SIZE];
hw.trng_slice(&mut noise);
hw.patch_data(&noise, pp.page_number() * PAGE_SIZE as u32);
}
log::trace!("fast_space_free key_remove {} before", pp.journal());
hw.fast_space_free(pp);
assert!(pp.valid() == false, "pp is still marked as valid!");
}
}
}
need_free_key = Some(kcache.descriptor_index.get());
}
// free up the key index in the dictionary, if necessary
if let Some(key_to_free) = need_free_key {
log::debug!("freeing key: {}", key_to_free);
self.put_free_key_index(key_to_free);
self.key_count -= 1;
log::debug!("key_count: {}", self.key_count);
}
if need_rebuild {
// no stable "retain" api, so we have to clear the heap and rebuild it https://github.com/rust-lang/rust/issues/71503
self.rebuild_free_pool();
}
// we don't remove the cache entry, because it hasn't been synchronized to disk.
// at this point:
// - in-memory representation will return an entry, but with its valid flag set to false.
// - disk still contains a key entry that claims we have a valid key
// a call to sync is necessary to completely flush things, but, we don't sync every time we remove
// because it's inefficient.
} else {
log::debug!("key_remove() key does not exist: {}", name_str);
}
// if there's no key....we're done!
}
/// used to remove a key from the dictionary, syncing 0's to the disk in the key's place
/// sort of less relevant now that the large keys have a paranoid mode; probably this routine should
/// actually be a higher-level function that catches the paranoid request and does an "update" of 0's
/// to the key then does a disk sync and then calls remove
pub fn key_erase(&mut self, _name: &str) {
unimplemented!();
}
/// estimates the amount of space needed to sync the dict cache. Pass this to ensure_fast_space_alloc()
/// before calling a sync. estimate can be inaccurate under pathological allocation conditions.
pub(crate) fn alloc_estimate_small(&self) -> usize {
let mut data_estimate = 0;
let mut index_estimate = 0;
for ksp in &self.small_pool {
if !ksp.clean {
for keyname in &ksp.contents {
let kce = self.keys.get(keyname).expect("data allocated but no index entry");
if kce.flags.unresolved() && kce.flags.valid() {
data_estimate += SMALL_CAPACITY - ksp.avail as usize;
index_estimate += 1;
}
}
}
}
let index_avail = DK_PER_VPAGE - self.keys.len() % DK_PER_VPAGE;
let index_req = if index_estimate > index_avail {
((index_estimate - index_avail) / DK_PER_VPAGE) + 1
} else {
0
};
(data_estimate / VPAGE_SIZE) + 1 + index_req
}
/// Synchronize a given small pool key to disk. Make sure there is adequate space in the fastspace
/// pool by using self.alloc_estimate_small + hw.ensure_fast_space_alloc. Following this call,
/// one should call `dict_sync` and `pt_sync` as soon as possible to keep everything consistent.
///
/// Observation: given the dictionary index and the small key pool index, we know exactly
/// the virtual address of the data pool.
pub(crate) fn sync_small_pool(
&mut self,
hw: &mut PddbOs,
v2p_map: &mut HashMap<VirtAddr, PhysPage>,
cipher: &Aes256GcmSiv,
) -> bool {
let mut data_cache = PlaintextCache { data: None, tag: None };
for (index, entry) in self.small_pool.iter_mut().enumerate() {
if !entry.clean {
let pool_vaddr =
VirtAddr::new(small_storage_base_vaddr_from_indices(self.index, index)).unwrap();
if !hw.fast_space_has_pages(1) {
return false;
}
let pp = v2p_map
.entry(pool_vaddr)
.or_insert_with(|| {
let mut ap =
hw.try_fast_space_alloc().expect("No free space to allocate small key storage");
ap.set_valid(true);
ap
})
.clone();
assert!(pp.valid(), "v2p returned an invalid page");
// WARNING - we don't read back the journal number before loading data into the page!
// we /could/ do that, but it incurs an expensive full-page decryption when we plan to nuke
// all the data. I'm a little worried the implementation as-is is going to be
// too slow, so let's try the abbreviated method and see how it fares. This
// incurs a risk that we lose data if we have a power outage or panic just after
// the page is erased but before all the PTEs and pointers are synced.
//
// If it turns out this is an issue, here's how you'd fix it:
// 1. decrypt the old page (if it exists) and extract the journal rev
// 2. de-allocate the old phys page, returning it to the fastspace pool; it'll likely not be
// returned on the next step
// 3. allocate a new page
// 4. write data to the new page (which increments the old journal rev)
// 5. sync the page tables
// This implementation just skips to step 3.
let mut page = [0u8; VPAGE_SIZE + size_of::<JournalType>()];
for (&src, dst) in (hw.trng_u32() % JOURNAL_RAND_RANGE)
.to_le_bytes()
.iter()
.zip(page[..size_of::<JournalType>()].iter_mut())
{
*dst = src;
}
let mut pool_offset = 0;
// visit the entries in arbitrary order, but back them in optimally tightly packed
for key_name in &entry.contents {
let kcache = self.keys.get_mut(key_name).expect("data record without index");
if kcache.flags.valid() {
// only sync valid keys, not ones that were marked for deletion but not yet synced.
log::debug!(
"sync {}/{}:0x{:x}..{}->{}",
key_name,
index,
kcache.start,
kcache.len,
kcache.reserved
);
let old_start = kcache.start;
kcache.start = pool_vaddr.get() + pool_offset as u64;
kcache.age = kcache.age.saturating_add(1);
kcache.clean = false;
kcache.flags.set_unresolved(false);
kcache.flags.set_valid(true);
match kcache.data.as_mut() {
Some(KeyCacheData::Small(data)) => {
data.clean = true;
page[size_of::<JournalType>() + pool_offset
..size_of::<JournalType>() + pool_offset + kcache.len as usize]
.copy_from_slice(&data.data[..kcache.len as usize]);
}
None => {
// the entry was pruned and must be loaded
// fill() will only do the expensive decryption operation once per page, and
// refer to the cached value on other calls
log::debug!("Small key sync filling {}", key_name);
data_cache.fill(
hw,
v2p_map,
cipher,
&self.aad,
VirtAddr::new(pool_vaddr.get()).unwrap(),
);
if let Some(old_page) = data_cache.data.as_ref() {
let start_offset =
size_of::<JournalType>() + (old_start % VPAGE_SIZE as u64) as usize;
// the delta of len-reserved bytes is assumed zeroed by the original space
// allocator; we just copy that
// here. However, if this assumption is broken, we could have leakage
// between keys in the reserved area!!!
page[size_of::<JournalType>() + pool_offset
..size_of::<JournalType>() + pool_offset + kcache.len as usize]
.copy_from_slice(
&old_page[start_offset..start_offset + kcache.len as usize],
);
// now take the previous data and shove it back in our cache
let data =
old_page[start_offset..start_offset + kcache.len as usize].to_vec();
kcache.data =
Some(KeyCacheData::Small(KeySmallData { clean: true, data }));
} else {
log::error!(
"Key {}'s data region at pp: {:x?} va: {:x} is unreadable",
key_name,
data_cache.tag,
kcache.start
);
}
}
_ => {
// the type returned was large, which is patently incorrect
panic!("Incorrect data cache type for small key entry.");
}
}
pool_offset += kcache.reserved as usize;
}
}
// now commit the sector to disk
hw.data_encrypt_and_patch_page(cipher, &self.aad, &mut page, &pp);
entry.clean = true;
entry.evicted = false;
log::debug!("Key pool[{}] is now clean, and not evicted: {:?}", index, entry.contents);
} else {
log::debug!("Key pool[{}] was clean, no need to sync: {:?}", index, entry.contents);
}
}
// we now have a bunch of dirty kcache entries. You should call `dict_sync` shortly after this to
// synchronize those entries to disk.
true
}
/// No data cache to flush yet...large pool caches not implemented!
pub(crate) fn sync_large_pool(&self) {}
/// Finds the next available slot to store the key metadata (not the data itself). It also
/// does bookkeeping to bound brute-force searches for keys within the dictionary's index space.
pub(crate) fn get_free_key_index(&mut self) -> Option<NonZeroU32> {
if let Some(free_key) = self.free_keys.pop() {
let index = free_key.0.start;
if free_key.0.run > 0 {
self.free_keys.push(Reverse(FreeKeyRange { start: index + 1, run: free_key.0.run - 1 }))
}
if index >= self.last_disk_key_index {
// if the new index is outside the currently known set, raise the search extent for the
// brute-force search
self.last_disk_key_index = index + 1;
}
NonZeroU32::new(index as u32)
} else {
log::warn!("Ran out of dict index space");
None
}
}
/// Returns a key's metadata storage to the index pool.
pub(crate) fn put_free_key_index(&mut self, index: u32) {
let free_keys = std::mem::replace(&mut self.free_keys, BinaryHeap::<Reverse<FreeKeyRange>>::new());
let mut free_key_vec = free_keys.into_sorted_vec();
// I know what you're going to say! It'd be more efficient to have a custom Ord implementation!
// I tried! It got really confusing! Some calls were coming out sorted and others weren't. Something
// subtle was wrong in the Ord implementation. While this is less efficient, it at least
// works, and I can reason through it. You're welcome to fix it. Just be sure to pass the unit
// tests.
free_key_vec.reverse();
// this is a bit weird because we have three cases:
// - the new key is more than 1 away from any element, in which case we insert it as a singleton
// (start = index, run = 0)
// - the new key is adjacent to exactly once element, in which case we put it either on the top or
// bottom (merge into existing record)
// - the new key is adjacent to exactly two elements, in which case we merge the new key and other two
// elements together, add its length to the new overall run
let mut skip = false;
let mut placed = false;
log::trace!("inserting free key {}", index);
for i in 0..free_key_vec.len() {
if skip {
// this happens when we merged into the /next/ record, and we reduced the total number of
// items by one
skip = false;
continue;
}
if !placed {
match free_key_vec[i].0.arg_compared_to_self(index) {
FreeKeyCases::LessThan => {
self.free_keys.push(Reverse(FreeKeyRange { start: index as u32, run: 0 }));
placed = true;
self.free_keys.push(free_key_vec[i]);
}
FreeKeyCases::LeftAdjacent => {
self.free_keys.push(Reverse(FreeKeyRange {
start: index as u32,
run: free_key_vec[i].0.run + 1,
}));
placed = true;
}
FreeKeyCases::Within => {
log::error!("Double-free error in free_keys()");
log::info!("free_key_vec[i].0: {:?}", free_key_vec[i].0);
log::info!("index: {}", index);
panic!(
"Double-free error in free_keys(). free_key_vec[i].0: {:?}, index: {}, free_key_vec.len(): {}",
free_key_vec[i].0,
index,
free_key_vec.len()
);
}
FreeKeyCases::RightAdjacent => {
// see if we should merge to the right
if i + 1 < free_key_vec.len() {
log::trace!(
"middle insert {}: {:?}, {:?}",
index,
free_key_vec[i],
free_key_vec[i + 1]
);
if free_key_vec[i + 1].0.arg_compared_to_self(index as u32)
== FreeKeyCases::LeftAdjacent
{
self.free_keys.push(Reverse(FreeKeyRange {
start: free_key_vec[i].0.start,
run: free_key_vec[i].0.run + free_key_vec[i + 1].0.run + 2,
}));
skip = true;
placed = true;
continue;
}
}
self.free_keys.push(Reverse(FreeKeyRange {
start: free_key_vec[i].0.start,
run: free_key_vec[i].0.run + 1,
}));
placed = true;
}
FreeKeyCases::GreaterThan => {
self.free_keys.push(free_key_vec[i]);
}
}
} else {
self.free_keys.push(free_key_vec[i]);
}
}
}
/// a cache entry doesn't actually know it's own name -- it's the key associated with the cache entry
/// so you must provide it to create the full record. It also doesn't know the name of its containing
/// basis.
pub(crate) fn to_dict_attributes(&self, name: &str, basis_name: &str) -> DictAttributes {
DictAttributes {
flags: self.flags,
age: self.age,
num_keys: self.key_count,
free_key_index: self.last_disk_key_index,
name: name.to_string(),
clean: self.clean,
small_key_count: self.small_pool.len(),
basis: basis_name.to_string(),
}
}
/// removes a key cache entry from a dictionary cache, returning the amount of space liberated with
/// the eviction. 0 means the key was either 0-sized, or it could not be evicted (possibly
/// because it was dirty; you need to call sync before evicting anything from the cache)
pub(crate) fn evict_keycache_entry(&mut self, key: &str) -> usize {
if let Some(kcache) = self.keys.get_mut(key) {
// if the cache entry is dirty, abort
if kcache.flags.valid() && !kcache.clean {
return 0;
}
if let Some(KeyCacheData::Small(_)) = kcache.data {
// can only prune small records
let pruned = kcache.size();
kcache.data.take(); // this effectively frees up the key cache data
// mark the key's pool as unclean, so it is processed for filling
let pool_index = small_storage_index_from_key(&kcache, self.index).expect("index missing");
self.small_pool[pool_index].evicted = true;
log::debug!("pruned {} bytes from key {} / evicted ksp index: {}", pruned, key, pool_index);
pruned
} else {
0
}
} else {
0
}
}
}
/// converts a dictionary index -- which is a 1-offset number of dictionaries -- plus a key
/// metadata index (not the key number; but, the enumerated set of potential key slots, also
/// 1-offset), and creates a virtual address for the location of this combination
/// It's written a little weird because DK_PER_VPAGE is 32, which optimizes cleanly and removes
/// an expensive division step.
fn dict_indices_to_vaddr(dict_index: NonZeroU32, key_meta_index: usize) -> u64 {
assert!(key_meta_index != 0, "key metadata index is 1-offset");
dict_index.get() as u64 * DICT_VSIZE + ((key_meta_index / DK_PER_VPAGE) as u64) * VPAGE_SIZE as u64
}
/// Derives the index of a Small Pool storage block given the key cache entry and the dictionary index.
/// The index maps into the small_pool array, which itself maps 1:1 onto blocks inside the small pool
/// memory space.
pub(crate) fn small_storage_index_from_key(kcache: &KeyCacheEntry, dict_index: NonZeroU32) -> Option<usize> {
let storage_base = (dict_index.get() - 1) as u64 * SMALL_POOL_STRIDE + SMALL_POOL_START;
let storage_end = storage_base + SMALL_POOL_STRIDE;
if (kcache.start >= storage_base) && ((kcache.start + kcache.reserved) < storage_end) {
let index_base = kcache.start - storage_base;
Some(index_base as usize / SMALL_CAPACITY)
} else {
None
}
}
/// derive the virtual address of a small storage block from the dictionary and the index of the small storage
/// pool
pub(crate) fn small_storage_base_vaddr_from_indices(dict_index: NonZeroU32, base_index: usize) -> u64 {
SMALL_POOL_START + (dict_index.get() - 1) as u64 * DICT_VSIZE + base_index as u64 * SMALL_CAPACITY as u64
}
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
#[repr(C, align(8))]
pub struct DictName {
pub len: u8,
pub data: [u8; DICT_NAME_LEN - 1],
}
impl DictName {
pub fn try_from_str(name: &str) -> Result<DictName> {
let mut alloc = [0u8; DICT_NAME_LEN - 1];
let bytes = name.as_bytes();
if bytes.len() > (DICT_NAME_LEN - 1) {
Err(Error::new(ErrorKind::InvalidInput, "dict name is too long"))
} else {
for (&src, dst) in bytes.iter().zip(alloc.iter_mut()) {
*dst = src;
}
Ok(DictName {
len: bytes.len() as u8, // this as checked above to be short enough
data: alloc,
})
}
}
}
impl Default for DictName {
fn default() -> DictName { DictName { len: 0, data: [0; DICT_NAME_LEN - 1] } }
}
#[derive(Debug)]
/// On-disk representation of the dictionary header. This structure is mainly for archival/unarchival
/// purposes. To "functionalize" a stored disk entry, it needs to be deserialized into a DictionaryCacheEntry.
#[repr(C, align(8))]
pub(crate) struct Dictionary {
/// Reserved for flags on the record entry
pub(crate) flags: DictFlags,
/// Access count to the dicitionary
pub(crate) age: u32,
/// Number of keys in the dictionary
pub(crate) num_keys: u32,
/// Free index starting space. While this is a derived parameter, its value is recorded to avoid
/// an expensive, long search operation during the creation of a dictionary cache record. 0 is an invalid
/// index, as this is where the header goes. Maybe this should be a NonZeroU32.
pub(crate) free_key_index: u32,
/// Name. Length should pad out the record to exactly 127 bytes.
pub(crate) name: DictName,
}
impl Default for Dictionary {
fn default() -> Dictionary {
let mut flags = DictFlags(0);
flags.set_valid(true);
Dictionary { flags, age: 0, num_keys: 0, free_key_index: 1, name: DictName::default() }
}
}
impl Deref for Dictionary {
type Target = [u8];
fn deref(&self) -> &[u8] {
unsafe {
core::slice::from_raw_parts(
self as *const Dictionary as *const u8,
core::mem::size_of::<Dictionary>(),
) as &[u8]
}
}
}
impl DerefMut for Dictionary {
fn deref_mut(&mut self) -> &mut [u8] {
unsafe {
core::slice::from_raw_parts_mut(
self as *mut Dictionary as *mut u8,
core::mem::size_of::<Dictionary>(),
) as &mut [u8]
}
}
}
#[allow(dead_code)]
#[derive(Debug)]
pub(crate) struct DictAttributes {
pub(crate) flags: DictFlags,
pub(crate) age: u32,
pub(crate) num_keys: u32,
pub(crate) free_key_index: u32,
pub(crate) name: String,
pub(crate) clean: bool,
pub(crate) small_key_count: usize,
pub(crate) basis: String,
}
/// This structure "enforces" the 127-byte stride of dict/key vpage entries
#[derive(Copy, Clone)]
pub(crate) struct DictKeyEntry {
pub(crate) data: [u8; DK_STRIDE],
}
impl Default for DictKeyEntry {
fn default() -> DictKeyEntry { DictKeyEntry { data: [0; DK_STRIDE] } }
}
/// This structure helps to bookkeep which slices within a DictKey virtual page need to be updated
pub(crate) struct DictKeyVpage {
pub(crate) elements: [Option<DictKeyEntry>; VPAGE_SIZE / DK_STRIDE],
}
impl<'a> Default for DictKeyVpage {
fn default() -> DictKeyVpage { DictKeyVpage { elements: [None; VPAGE_SIZE / DK_STRIDE] } }
}
#[derive(PartialEq, Eq, Debug)]
pub(crate) enum FreeKeyCases {
LeftAdjacent,
RightAdjacent,
Within,
LessThan,
GreaterThan,
}
#[derive(Eq, Copy, Clone, Debug)]
pub(crate) struct FreeKeyRange {
/// This index should be free. Smallest value is 1; 0-index is for the dict header. Maybe this should be
/// a NonZeroU32?
pub(crate) start: u32,
/// Additional free keys after the start one. Run = 0 means just the start key is free, and the
/// next one should be used. Run = 2 means {start, start+1} are free, etc.
pub(crate) run: u32,
}
impl FreeKeyRange {
/// returns the result of "index compared to self", so,
/// if the index is smaller than me, the result is LessThan.
pub(crate) fn arg_compared_to_self(&self, index: u32) -> FreeKeyCases {
if self.start > 1 && (index < (self.start - 1)) {
FreeKeyCases::LessThan
} else if self.start > 0 && (index == self.start - 1) {
FreeKeyCases::LeftAdjacent
} else if (index >= self.start) && (index <= (self.start + self.run)) {
FreeKeyCases::Within
} else if index == (self.start + self.run + 1) {
FreeKeyCases::RightAdjacent
} else {
FreeKeyCases::GreaterThan
}
}
}
impl Ord for FreeKeyRange {
fn cmp(&self, other: &Self) -> Ordering { self.start.cmp(&other.start) }
}
impl PartialOrd for FreeKeyRange {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(&other)) }
}
impl PartialEq for FreeKeyRange {
fn eq(&self, other: &Self) -> bool { self.start == other.start }
}
/// stashed copy of a decrypted page. The copy here must always match
/// what's actually on disk; do not mutate it and expect it to sync with the disk.
/// Remember to invalidate this if the data are
/// This is stored with the journal number on top.
/// What the four possibilities of cache vs pp mean:
/// Some(cache) & Some(cache_pp) -> valid cache and pp
/// None(cache) & Some(cache_pp) -> the page was allocated; but never used, or was erased (it's free for you
/// to use it); alternately, it was corrupted Some(cache) & None(cache_pp) -> invalid, internal error
/// None(cache) & None(cache_pp) -> the basis mapping didn't exist: we've never requested this page before.
pub(crate) struct PlaintextCache {
/// a page of data, stored with the Journal rev on top
pub(crate) data: Option<Vec<u8>>,
/// the page the cache corresponds to
pub(crate) tag: Option<PhysPage>,
}
impl PlaintextCache {
pub fn fill(
&mut self,
hw: &mut PddbOs,
v2p_map: &HashMap<VirtAddr, PhysPage>,
cipher: &Aes256GcmSiv,
aad: &[u8],
req_vaddr: VirtAddr,
) {
if let Some(pp) = v2p_map.get(&req_vaddr) {
assert!(pp.valid(), "v2p returned an invalid page");
let mut fill_needed = false;
if let Some(tag) = self.tag {
if tag.page_number() != pp.page_number() {
fill_needed = true;
}
} else if self.tag.is_none() {
fill_needed = true;
}
if fill_needed {
self.data = hw.data_decrypt_page(&cipher, &aad, pp);
self.tag = Some(*pp);
}
} else {
self.data = None;
self.tag = None;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn clone_bheap<T: Clone + Ord + Copy>(heap: &mut BinaryHeap<Reverse<T>>) -> BinaryHeap<Reverse<T>> {
let heap_copy = std::mem::replace(heap, BinaryHeap::<Reverse<T>>::new());
let mut heap_clone = BinaryHeap::<Reverse<T>>::new();
let heap_vec = heap_copy.into_sorted_vec();
for &i in heap_vec.iter() {
heap_clone.push(i.clone());
heap.push(i);
}
heap_clone
}
#[test]
fn test_free_key_index() {
let mut d = DictCacheEntry::new(Dictionary::default(), 1, &Vec::<u8>::new());
let init_max = KEY_MAXCOUNT as u32 - 1 - 1;
// 1..131070
let c = clone_bheap(&mut d.free_keys);
let mut f = c.into_sorted_vec();
f.reverse();
assert!(f.len() == 1);
assert!(f[0].0.start == 1);
assert!(f[0].0.run == init_max);
for i in 0..10 {
assert!(d.get_free_key_index().unwrap().get() == i + 1);
}
// 11...131060
let c = clone_bheap(&mut d.free_keys);
let mut f = c.into_sorted_vec();
f.reverse();
assert!(f.len() == 1);
assert!(f[0].0.start == 11);
assert!(f[0].0.run == init_max - 10);
d.put_free_key_index(4);
let c = clone_bheap(&mut d.free_keys);
let mut f = c.into_sorted_vec();
f.reverse();
println!("insert 4: {:?}", f);
assert!(f.len() == 2);
assert!(f[0].0.start == 4);
assert!(f[0].0.run == 0);
assert!(f[1].0.start == 11);
assert!(f[1].0.run == init_max - 10);
d.put_free_key_index(9);
let c = clone_bheap(&mut d.free_keys);
let mut f = c.into_sorted_vec();
f.reverse();
println!("insert 9: {:?}", f);
assert!(f.len() == 3);
assert!(f[0].0.start == 4);
assert!(f[0].0.run == 0);
assert!(f[1].0.start == 9);
assert!(f[1].0.run == 0);
assert!(f[2].0.start == 11);
assert!(f[2].0.run == init_max - 10);
d.put_free_key_index(10);
let c = clone_bheap(&mut d.free_keys);
let mut f = c.into_sorted_vec();
f.reverse();
println!("insert 10: {:?}", f);
assert!(f.len() == 2);
assert!(f[0].0.start == 4);
assert!(f[0].0.run == 0);
assert!(f[1].0.start == 9);
assert!(f[1].0.run == init_max - 8);
d.put_free_key_index(3);
let c = clone_bheap(&mut d.free_keys);
let mut f = c.into_sorted_vec();
f.reverse();
println!("insert 3: {:?}", f);
assert!(f.len() == 2);
assert!(f[0].0.start == 3);
assert!(f[0].0.run == 1);
assert!(f[1].0.start == 9);
assert!(f[1].0.run == init_max - 8);
d.put_free_key_index(5);
let c = clone_bheap(&mut d.free_keys);
let mut f = c.into_sorted_vec();
f.reverse();
println!("insert 5: {:?}", f);
assert!(f.len() == 2);
assert!(f[0].0.start == 3);
assert!(f[0].0.run == 2);
assert!(f[1].0.start == 9);
assert!(f[1].0.run == init_max - 8);
d.put_free_key_index(8);
let c = clone_bheap(&mut d.free_keys);
let mut f = c.into_sorted_vec();
f.reverse();
println!("insert 8: {:?}", f);
assert!(f.len() == 2);
assert!(f[0].0.start == 3);
assert!(f[0].0.run == 2);
assert!(f[1].0.start == 8);
assert!(f[1].0.run == init_max - 7);
let k = d.get_free_key_index().unwrap().get();
let c = clone_bheap(&mut d.free_keys);
let mut f = c.into_sorted_vec();
f.reverse();
println!("get [3]: {:?}", f);
assert!(k == 3);
assert!(f.len() == 2);
assert!(f[0].0.start == 4);
assert!(f[0].0.run == 1);
assert!(f[1].0.start == 8);
assert!(f[1].0.run == init_max - 7);
assert!(d.get_free_key_index().unwrap().get() == 4);
let c = clone_bheap(&mut d.free_keys);
let mut f = c.into_sorted_vec();
f.reverse();
println!("get [4]: {:?}", f);
assert!(f.len() == 2);
assert!(f[0].0.start == 5);
assert!(f[0].0.run == 0);
assert!(f[1].0.start == 8);
assert!(f[1].0.run == init_max - 7);
assert!(d.get_free_key_index().unwrap().get() == 5);
let c = clone_bheap(&mut d.free_keys);
let mut f = c.into_sorted_vec();
f.reverse();
println!("get [5]: {:?}", f);
assert!(f.len() == 1);
assert!(f[0].0.start == 8);
assert!(f[0].0.run == init_max - 7);
}
}