-
Notifications
You must be signed in to change notification settings - Fork 12
[PM-18100] Add mlock
and memfd_secret
implementations
#125
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
base: main
Are you sure you want to change the base?
Changes from all commits
c52afa4
bf22b91
3bf0b24
2c41a85
c2e8bce
56f1e03
d1901f4
dfc2465
560b6c6
75870c4
f79bdb2
3bb64d8
4892247
1ab42c5
96d82c1
a3dd159
15cf875
01925f7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
use std::{alloc::Layout, ptr::NonNull, sync::LazyLock}; | ||
|
||
use allocator_api2::alloc::{AllocError, Allocator}; | ||
|
||
pub(crate) struct LinuxMemfdSecretAlloc; | ||
|
||
impl LinuxMemfdSecretAlloc { | ||
pub fn new() -> Option<Self> { | ||
// To test if memfd_secret is supported, we try to allocate a 1 byte and see if that | ||
// succeeds. | ||
static IS_SUPPORTED: LazyLock<bool> = LazyLock::new(|| { | ||
let Some(ptr): Option<NonNull<[u8]>> = (unsafe { memsec::memfd_secret_sized(1) }) | ||
else { | ||
return false; | ||
Check warning on line 14 in crates/bitwarden-crypto/src/store/backend/implementation/custom_alloc/linux_memfd_secret.rs
|
||
}; | ||
|
||
// Check that the pointer is readable and writable | ||
let result = unsafe { | ||
let ptr = ptr.as_ptr() as *mut u8; | ||
*ptr = 30; | ||
*ptr += 107; | ||
*ptr == 137 | ||
}; | ||
|
||
unsafe { memsec::free_memfd_secret(ptr) }; | ||
result | ||
}); | ||
|
||
(*IS_SUPPORTED).then_some(Self) | ||
} | ||
} | ||
|
||
unsafe impl Allocator for LinuxMemfdSecretAlloc { | ||
fn allocate(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> { | ||
// Note: The allocator_api2 Allocator traits requires us to handle zero-sized allocations. | ||
// We return an invalid pointer as you cannot allocate a zero-sized slice in most | ||
// allocators. This is what allocator_api2::Global does as well: | ||
// https://github.com/zakarumych/allocator-api2/blob/2dde97af85f3559619689cef152e90e6d8a0cee3/src/alloc/global.rs#L24-L29 | ||
if layout.size() == 0 { | ||
return Ok(unsafe { | ||
NonNull::new_unchecked(core::ptr::slice_from_raw_parts_mut( | ||
layout.align() as *mut u8, | ||
0, | ||
)) | ||
}); | ||
Check warning on line 45 in crates/bitwarden-crypto/src/store/backend/implementation/custom_alloc/linux_memfd_secret.rs
|
||
} | ||
|
||
// Ensure the size we want to allocate is a multiple of the alignment, | ||
// so that both the start and end of the allocation are aligned. | ||
let layout = layout.pad_to_align(); | ||
|
||
let Some(ptr): Option<NonNull<[u8]>> = | ||
(unsafe { memsec::memfd_secret_sized(layout.size()) }) | ||
else { | ||
return Err(AllocError); | ||
Check warning on line 55 in crates/bitwarden-crypto/src/store/backend/implementation/custom_alloc/linux_memfd_secret.rs
|
||
}; | ||
|
||
// The pointer that we return needs to be aligned to the requested alignment. | ||
// If that's not the case, we should free the memory and return an allocation error. | ||
// While we check for this error condition, this should never happen in practice, as the | ||
// pointer returned by `memfd_secret_sized` should be page-aligned (typically 4KB) | ||
// which should be larger than any possible alignment value. | ||
if (ptr.as_ptr() as *mut u8).align_offset(layout.align()) != 0 { | ||
unsafe { memsec::free_memfd_secret(ptr) }; | ||
return Err(AllocError); | ||
Check warning on line 65 in crates/bitwarden-crypto/src/store/backend/implementation/custom_alloc/linux_memfd_secret.rs
|
||
} | ||
|
||
Ok(ptr) | ||
} | ||
|
||
unsafe fn deallocate(&self, ptr: NonNull<u8>, layout: Layout) { | ||
if layout.size() == 0 { | ||
return; | ||
Check warning on line 73 in crates/bitwarden-crypto/src/store/backend/implementation/custom_alloc/linux_memfd_secret.rs
|
||
} | ||
|
||
memsec::free_memfd_secret(ptr); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
use std::{ | ||
alloc::{GlobalAlloc, Layout}, | ||
ptr::NonNull, | ||
}; | ||
|
||
use allocator_api2::alloc::{AllocError, Allocator}; | ||
|
||
pub(crate) struct MlockAlloc(crate::ZeroizingAllocator<std::alloc::System>); | ||
|
||
impl MlockAlloc { | ||
pub fn new() -> Self { | ||
Self(crate::ZeroizingAllocator(std::alloc::System)) | ||
} | ||
} | ||
|
||
unsafe impl Allocator for MlockAlloc { | ||
fn allocate(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> { | ||
// Note: The allocator_api2 Allocator traits requires us to handle zero-sized allocations. | ||
// We return an invalid pointer as you cannot allocate a zero-sized slice in most | ||
// allocators. This is what allocator_api2::Global does as well: | ||
// https://github.com/zakarumych/allocator-api2/blob/2dde97af85f3559619689cef152e90e6d8a0cee3/src/alloc/global.rs#L24-L29 | ||
if layout.size() == 0 { | ||
return Ok(unsafe { | ||
NonNull::new_unchecked(core::ptr::slice_from_raw_parts_mut( | ||
layout.align() as *mut u8, | ||
0, | ||
)) | ||
}); | ||
Check warning on line 28 in crates/bitwarden-crypto/src/store/backend/implementation/custom_alloc/malloc.rs
|
||
} | ||
|
||
let ptr = unsafe { self.0.alloc(layout) }; | ||
|
||
if ptr.is_null() { | ||
return Err(AllocError); | ||
} | ||
unsafe { memsec::mlock(ptr, layout.size()) }; | ||
Ok(unsafe { | ||
let slice = std::slice::from_raw_parts_mut(ptr, layout.size()); | ||
NonNull::new(slice).expect("slice is never null") | ||
}) | ||
} | ||
|
||
unsafe fn deallocate(&self, ptr: NonNull<u8>, layout: Layout) { | ||
if layout.size() == 0 { | ||
return; | ||
} | ||
|
||
memsec::munlock(ptr.as_ptr(), layout.size()); | ||
self.0.dealloc(ptr.as_ptr(), layout); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
use allocator_api2::alloc::Allocator; | ||
use zeroize::ZeroizeOnDrop; | ||
|
||
use crate::{store::backend::StoreBackend, KeyId}; | ||
|
||
#[cfg(all(not(target_arch = "wasm32"), not(windows)))] | ||
pub(super) mod malloc; | ||
|
||
#[cfg(target_os = "linux")] | ||
pub(super) mod linux_memfd_secret; | ||
|
||
pub(super) struct CustomAllocBackend<Key: KeyId, Alloc: Allocator + Send + Sync> { | ||
map: hashbrown::HashMap<Key, Key::KeyValue, hashbrown::DefaultHashBuilder, Alloc>, | ||
} | ||
|
||
impl<Key: KeyId, Alloc: Allocator + Send + Sync> CustomAllocBackend<Key, Alloc> { | ||
pub(super) fn new(alloc: Alloc) -> Self { | ||
Self { | ||
map: hashbrown::HashMap::new_in(alloc), | ||
} | ||
} | ||
} | ||
|
||
impl<Key: KeyId, Alloc: Allocator + Send + Sync> ZeroizeOnDrop for CustomAllocBackend<Key, Alloc> {} | ||
|
||
impl<Key: KeyId, Alloc: Allocator + Send + Sync> StoreBackend<Key> | ||
for CustomAllocBackend<Key, Alloc> | ||
{ | ||
fn upsert(&mut self, key_id: Key, key: <Key as KeyId>::KeyValue) { | ||
self.map.insert(key_id, key); | ||
} | ||
|
||
fn get(&self, key_id: Key) -> Option<&<Key as KeyId>::KeyValue> { | ||
self.map.get(&key_id) | ||
} | ||
|
||
fn remove(&mut self, key_id: Key) { | ||
self.map.remove(&key_id); | ||
} | ||
|
||
fn clear(&mut self) { | ||
self.map.clear(); | ||
} | ||
|
||
fn retain(&mut self, f: fn(Key) -> bool) { | ||
self.map.retain(|key, _| f(*key)); | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
impl<Key: KeyId, Alloc: Allocator + Send + Sync> super::super::StoreBackendDebug<Key> | ||
for CustomAllocBackend<Key, Alloc> | ||
{ | ||
fn elements(&self) -> Vec<(Key, &Key::KeyValue)> { | ||
self.map.iter().map(|(k, v)| (*k, v)).collect() | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question: what happens if it isn't? What is returned as
ptr
ifmemfd_secret
isn't supported? Trying to write to an invalid pointer/pointer to memory outside of the process should cause a segmentation fault, right?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In theory it shouldn't happen, the
memfd_secret_sized
function should returnNone
if it can't allocate or the feature is not supported, and if it returnsSome
, then the file descripto is valid and open for read/write according to the API: https://man7.org/linux/man-pages/man2/memfd_secret.2.html#DESCRIPTION.This was added more as a sanity check during development, so it can probably be removed.