Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PM-18100] Add mlock and memfd_secret implementations #125

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions crates/bitwarden-crypto/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ wasm-bindgen = { workspace = true, optional = true }
zeroize = { version = ">=1.7.0, <2.0", features = ["derive", "aarch64"] }
zeroizing-alloc = ">=0.1.0, <0.2"

[target.'cfg(all(not(target_arch = "wasm32"), not(windows)))'.dependencies]
memsec = { version = "0.7.0", features = ["alloc_ext"] }

[dev-dependencies]
criterion = "0.5.1"
rand_chacha = "0.3.1"
Expand All @@ -68,3 +71,7 @@ required-features = ["no-memory-hardening"]

[lints]
workspace = true

[package.metadata.cargo-udeps.ignore]
# This is unused when using --all-features, as that disables memory-hardening
normal = ["memsec"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
use std::{mem::MaybeUninit, ptr::NonNull, sync::OnceLock};

use super::{KeyId, SliceBackend, SliceLike};

// This is an in-memory key store that is protected by memfd_secret on Linux 5.14+.
// This should be secure against memory dumps from anything except a malicious kernel driver.
// Note that not all 5.14+ systems have support for memfd_secret enabled, so
// LinuxMemfdSecretKeyStore::new returns an Option.
pub(crate) type LinuxMemfdSecretBackend<Key> = SliceBackend<Key, MemfdSecretImplKeyData>;

pub(crate) struct MemfdSecretImplKeyData {
ptr: std::ptr::NonNull<[u8]>,
capacity: usize,
}

// For Send+Sync to be safe, we need to ensure that the memory is only accessed mutably from one
// thread. To do this, we have to make sure that any funcion in `MemfdSecretImplKeyData` that
// accesses the pointer mutably is defined as &mut self, and that the pointer is never copied or
// moved outside the struct.
unsafe impl Send for MemfdSecretImplKeyData {}
unsafe impl Sync for MemfdSecretImplKeyData {}

impl Drop for MemfdSecretImplKeyData {
fn drop(&mut self) {
unsafe {
memsec::free_memfd_secret(self.ptr);
}
}
}

impl<Key: KeyId> SliceLike<Key> for MemfdSecretImplKeyData {
fn is_available() -> bool {
static IS_SUPPORTED: OnceLock<bool> = OnceLock::new();

*IS_SUPPORTED.get_or_init(|| unsafe {
let Some(ptr) = memsec::memfd_secret_sized(1) else {
return false;
};
memsec::free_memfd_secret(ptr);
true
})
}

fn with_capacity(capacity: usize) -> Self {
let entry_size = std::mem::size_of::<Option<(Key, Key::KeyValue)>>();

unsafe {
let ptr: NonNull<[u8]> = memsec::memfd_secret_sized(capacity * entry_size)
.expect("memfd_secret_sized failed");

// Initialize the array with Nones using MaybeUninit
let uninit_slice: &mut [MaybeUninit<_>] = std::slice::from_raw_parts_mut(
ptr.as_ptr() as *mut MaybeUninit<Option<(Key, Key::KeyValue)>>,
capacity,
);
for elem in uninit_slice {
elem.write(None);
}

MemfdSecretImplKeyData { ptr, capacity }
}
}

fn get_key_data(&self) -> &[Option<(Key, Key::KeyValue)>] {
let ptr = self.ptr.as_ptr() as *const Option<(Key, Key::KeyValue)>;
// SAFETY: The pointer is valid and points to a valid slice of the correct size.
// This function is &self so it only takes a immutable *const pointer.
unsafe { std::slice::from_raw_parts(ptr, self.capacity) }
}

fn get_key_data_mut(&mut self) -> &mut [Option<(Key, Key::KeyValue)>] {
let ptr = self.ptr.as_ptr() as *mut Option<(Key, Key::KeyValue)>;
// SAFETY: The pointer is valid and points to a valid slice of the correct size.
// This function is &mut self so it can take a mutable *mut pointer.
unsafe { std::slice::from_raw_parts_mut(ptr, self.capacity) }
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::store::backend::{
implementation::custom_slice::tests::{TestKey, TestKeyValue},
StoreBackend as _,
};

#[test]
fn test_resize() {
let mut store = LinuxMemfdSecretBackend::<TestKey>::with_capacity(1).unwrap();

for (idx, key) in [
TestKey::A,
TestKey::B(10),
TestKey::C,
TestKey::B(7),
TestKey::A,
TestKey::C,
]
.into_iter()
.enumerate()
{
store.upsert(key, TestKeyValue::new(idx));
}

assert_eq!(store.get(TestKey::A), Some(&TestKeyValue::new(4)));
assert_eq!(store.get(TestKey::B(10)), Some(&TestKeyValue::new(1)));
assert_eq!(store.get(TestKey::C), Some(&TestKeyValue::new(5)));
assert_eq!(store.get(TestKey::B(7)), Some(&TestKeyValue::new(3)));
assert_eq!(store.get(TestKey::B(20)), None);
}
}
Loading
Loading