fix: Migrate away from lazy_static in favor of thread_local

The new secret memory pool was causing CI failures in the fuzzing code,
due to the fuzzer compiling its binaries with memory sanitizer support.

https://doc.rust-lang.org/beta/unstable-book/compiler-flags/sanitizer.html

Using lazy_static was – intentionally – introducing a memory leak, but the
LeakSanitizer detected this and raised an error.

Now by using thread_local we are calling the destructors and so – while still being a
memory leak in practice – the LeakSanitizer no longer detects this behaviour as an error.

Alternatively we could have used a known-leaks list with the leak-sanitizer, but this would have increased the complexity of the build setup.

Finally, this was likely triggered with the migration to memsec, because libsodium circumvents the malloc/free calls,
relying on direct calls to MMAP.
This commit is contained in:
Karolin Varner
2024-01-02 19:57:51 +01:00
committed by wucke13
parent e3b72487db
commit 9824db4f09
4 changed files with 248 additions and 45 deletions

140
Cargo.lock generated
View File

@@ -188,7 +188,7 @@ dependencies = [
"regex", "regex",
"rustc-hash", "rustc-hash",
"shlex", "shlex",
"syn", "syn 2.0.39",
"which", "which",
] ]
@@ -256,6 +256,12 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cfg_aliases"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
[[package]] [[package]]
name = "ciborium" name = "ciborium"
version = "0.2.1" version = "0.2.1"
@@ -337,7 +343,7 @@ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.39",
] ]
[[package]] [[package]]
@@ -471,7 +477,7 @@ checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.39",
] ]
[[package]] [[package]]
@@ -523,7 +529,7 @@ checksum = "d4029edd3e734da6fe05b6cd7bd2960760a616bd2ddd0d59a0124746d6272af0"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
"redox_syscall", "redox_syscall 0.3.5",
"windows-sys 0.48.0", "windows-sys 0.48.0",
] ]
@@ -662,6 +668,15 @@ dependencies = [
"hashbrown 0.14.3", "hashbrown 0.14.3",
] ]
[[package]]
name = "instant"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
dependencies = [
"cfg-if",
]
[[package]] [[package]]
name = "is-terminal" name = "is-terminal"
version = "0.4.9" version = "0.4.9"
@@ -792,6 +807,16 @@ version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456"
[[package]]
name = "lock_api"
version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45"
dependencies = [
"autocfg",
"scopeguard",
]
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.20" version = "0.4.20"
@@ -915,6 +940,31 @@ version = "6.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1"
[[package]]
name = "parking_lot"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
dependencies = [
"instant",
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc"
dependencies = [
"cfg-if",
"instant",
"libc",
"redox_syscall 0.2.16",
"smallvec",
"winapi",
]
[[package]] [[package]]
name = "paste" name = "paste"
version = "1.0.14" version = "1.0.14"
@@ -980,7 +1030,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d" checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"syn", "syn 2.0.39",
] ]
[[package]] [[package]]
@@ -1060,6 +1110,15 @@ dependencies = [
"crossbeam-utils", "crossbeam-utils",
] ]
[[package]]
name = "redox_syscall"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
dependencies = [
"bitflags 1.3.2",
]
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.3.5" version = "0.3.5"
@@ -1213,7 +1272,6 @@ dependencies = [
"allocator-api2", "allocator-api2",
"allocator-api2-tests", "allocator-api2-tests",
"anyhow", "anyhow",
"lazy_static",
"libsodium-sys-stable", "libsodium-sys-stable",
"log", "log",
"memsec", "memsec",
@@ -1221,6 +1279,7 @@ dependencies = [
"rosenpass-sodium", "rosenpass-sodium",
"rosenpass-to", "rosenpass-to",
"rosenpass-util", "rosenpass-util",
"static_init",
"zeroize", "zeroize",
] ]
@@ -1346,7 +1405,7 @@ checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.39",
] ]
[[package]] [[package]]
@@ -1375,6 +1434,12 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380" checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380"
[[package]]
name = "smallvec"
version = "1.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970"
[[package]] [[package]]
name = "spin" name = "spin"
version = "0.9.8" version = "0.9.8"
@@ -1400,12 +1465,51 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "static_init"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a2a1c578e98c1c16fc3b8ec1328f7659a500737d7a0c6d625e73e830ff9c1f6"
dependencies = [
"bitflags 1.3.2",
"cfg_aliases",
"libc",
"parking_lot",
"parking_lot_core",
"static_init_macro",
"winapi",
]
[[package]]
name = "static_init_macro"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70a2595fc3aa78f2d0e45dd425b22282dd863273761cc77780914b2cf3003acf"
dependencies = [
"cfg_aliases",
"memchr",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]] [[package]]
name = "strsim" name = "strsim"
version = "0.10.0" version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.39" version = "2.0.39"
@@ -1466,7 +1570,7 @@ checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.39",
] ]
[[package]] [[package]]
@@ -1636,7 +1740,7 @@ dependencies = [
"once_cell", "once_cell",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.39",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@@ -1658,7 +1762,7 @@ checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.39",
"wasm-bindgen-backend", "wasm-bindgen-backend",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@@ -1961,7 +2065,7 @@ checksum = "c2f140bda219a26ccc0cdb03dba58af72590c53b22642577d88a927bc5c87d6b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 2.0.39",
] ]
[[package]] [[package]]
@@ -1969,6 +2073,20 @@ name = "zeroize"
version = "1.7.0" version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d"
dependencies = [
"zeroize_derive",
]
[[package]]
name = "zeroize_derive"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.39",
]
[[package]] [[package]]
name = "zip" name = "zip"

View File

@@ -42,7 +42,6 @@ doc-comment = "0.3.3"
base64 = "0.21.5" base64 = "0.21.5"
zeroize = "1.7.0" zeroize = "1.7.0"
memoffset = "0.9.0" memoffset = "0.9.0"
lazy_static = "1.4.0"
thiserror = "1.0.50" thiserror = "1.0.50"
paste = "1.0.14" paste = "1.0.14"
env_logger = "0.10.1" env_logger = "0.10.1"

View File

@@ -15,7 +15,6 @@ rosenpass-to = { workspace = true }
rosenpass-sodium = { workspace = true } rosenpass-sodium = { workspace = true }
rosenpass-util = { workspace = true } rosenpass-util = { workspace = true }
libsodium-sys-stable = { workspace = true } libsodium-sys-stable = { workspace = true }
lazy_static = { workspace = true }
zeroize = { workspace = true } zeroize = { workspace = true }
rand = { workspace = true } rand = { workspace = true }
memsec = { workspace = true } memsec = { workspace = true }

View File

@@ -1,15 +1,17 @@
use std::{collections::HashMap, convert::TryInto, fmt, path::Path, sync::Mutex}; use std::cell::RefCell;
use std::collections::HashMap;
use std::convert::TryInto;
use std::fmt;
use std::ops::{Deref, DerefMut};
use std::path::Path;
use anyhow::Context; use anyhow::Context;
use lazy_static::lazy_static;
use rand::{Fill as Randomize, Rng}; use rand::{Fill as Randomize, Rng};
use zeroize::{Zeroize, ZeroizeOnDrop}; use zeroize::{Zeroize, ZeroizeOnDrop};
use rosenpass_util::{ use rosenpass_util::b64::b64_reader;
b64::b64_reader, use rosenpass_util::file::{fopen_r, LoadValue, LoadValueB64, ReadExactToEnd};
file::{fopen_r, LoadValue, LoadValueB64, ReadExactToEnd}, use rosenpass_util::functional::mutating;
functional::mutating,
};
use crate::alloc::{secret_box, SecretBox, SecretVec}; use crate::alloc::{secret_box, SecretBox, SecretVec};
use crate::file::StoreSecret; use crate::file::StoreSecret;
@@ -17,8 +19,78 @@ use crate::file::StoreSecret;
// This might become a problem in library usage; it's effectively a memory // This might become a problem in library usage; it's effectively a memory
// leak which probably isn't a problem right now because most memory will // leak which probably isn't a problem right now because most memory will
// be reused… // be reused…
lazy_static! { thread_local! {
static ref SECRET_CACHE: Mutex<SecretMemoryPool> = Mutex::new(SecretMemoryPool::new()); static SECRET_CACHE: RefCell<SecretMemoryPool> = RefCell::new(SecretMemoryPool::new());
}
fn with_secret_memory_pool<Fn, R>(mut f: Fn) -> R
where
Fn: FnMut(Option<&mut SecretMemoryPool>) -> R,
{
// This acquires the SECRET_CACHE
SECRET_CACHE
.try_with(|cell| {
// And acquires the inner reference
cell.try_borrow_mut()
.as_deref_mut()
// To call the given function
.map(|pool| f(Some(pool)))
.ok()
})
.ok()
.flatten()
// Failing that, the given function is called with None
.unwrap_or_else(|| f(None))
}
// Wrapper around SecretBox that applies automatic zeroization
#[derive(Debug)]
struct ZeroizingSecretBox<T: Zeroize + ?Sized>(Option<SecretBox<T>>);
impl<T: Zeroize> ZeroizingSecretBox<T> {
fn new(boxed: T) -> Self {
ZeroizingSecretBox(Some(secret_box(boxed)))
}
}
impl<T: Zeroize + ?Sized> ZeroizingSecretBox<T> {
fn from_secret_box(inner: SecretBox<T>) -> Self {
Self(Some(inner))
}
fn take(mut self) -> SecretBox<T> {
self.0.take().unwrap()
}
}
impl<T: Zeroize + ?Sized> ZeroizeOnDrop for ZeroizingSecretBox<T> {}
impl<T: Zeroize + ?Sized> Zeroize for ZeroizingSecretBox<T> {
fn zeroize(&mut self) {
if let Some(inner) = &mut self.0 {
let inner: &mut SecretBox<T> = inner; // type annotation
inner.zeroize()
}
}
}
impl<T: Zeroize + ?Sized> Drop for ZeroizingSecretBox<T> {
fn drop(&mut self) {
self.zeroize()
}
}
impl<T: Zeroize + ?Sized> Deref for ZeroizingSecretBox<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0.as_ref().unwrap()
}
}
impl<T: Zeroize + ?Sized> DerefMut for ZeroizingSecretBox<T> {
fn deref_mut(&mut self) -> &mut T {
self.0.as_mut().unwrap()
}
} }
/// Pool that stores secret memory allocations /// Pool that stores secret memory allocations
@@ -31,7 +103,7 @@ lazy_static! {
/// [libsodium documentation](https://libsodium.gitbook.io/doc/memory_management#guarded-heap-allocations) /// [libsodium documentation](https://libsodium.gitbook.io/doc/memory_management#guarded-heap-allocations)
#[derive(Debug)] // TODO check on Debug derive, is that clever #[derive(Debug)] // TODO check on Debug derive, is that clever
struct SecretMemoryPool { struct SecretMemoryPool {
pool: HashMap<usize, Vec<SecretBox<[u8]>>>, pool: HashMap<usize, Vec<ZeroizingSecretBox<[u8]>>>,
} }
impl SecretMemoryPool { impl SecretMemoryPool {
@@ -44,33 +116,37 @@ impl SecretMemoryPool {
} }
/// Return secret back to the pool for future re-use /// Return secret back to the pool for future re-use
pub fn release<const N: usize>(&mut self, mut sec: SecretBox<[u8; N]>) { pub fn release<const N: usize>(&mut self, mut sec: ZeroizingSecretBox<[u8; N]>) {
sec.zeroize(); sec.zeroize();
// This conversion sequence is weird but at least it guarantees // This conversion sequence is weird but at least it guarantees
// that the heap allocation is preserved according to the docs // that the heap allocation is preserved according to the docs
let sec: SecretVec<u8> = sec.into(); let sec: SecretVec<u8> = sec.take().into();
let sec: SecretBox<[u8]> = sec.into(); let sec: SecretBox<[u8]> = sec.into();
self.pool.entry(N).or_default().push(sec); self.pool
.entry(N)
.or_default()
.push(ZeroizingSecretBox::from_secret_box(sec));
} }
/// Take protected memory from the pool, allocating new one if no suitable /// Take protected memory from the pool, allocating new one if no suitable
/// chunk is found in the inventory. /// chunk is found in the inventory.
/// ///
/// The secret is guaranteed to be full of nullbytes /// The secret is guaranteed to be full of nullbytes
pub fn take<const N: usize>(&mut self) -> SecretBox<[u8; N]> { pub fn take<const N: usize>(&mut self) -> ZeroizingSecretBox<[u8; N]> {
let entry = self.pool.entry(N).or_default(); let entry = self.pool.entry(N).or_default();
match entry.pop() { let inner = match entry.pop() {
None => secret_box([0u8; N]), None => secret_box([0u8; N]),
Some(sec) => sec.try_into().unwrap(), Some(sec) => sec.take().try_into().unwrap(),
} };
ZeroizingSecretBox::from_secret_box(inner)
} }
} }
/// Storeage for a secret backed by [rosenpass_sodium::alloc::Alloc] /// Storeage for a secret backed by [rosenpass_sodium::alloc::Alloc]
pub struct Secret<const N: usize> { pub struct Secret<const N: usize> {
storage: Option<SecretBox<[u8; N]>>, storage: Option<ZeroizingSecretBox<[u8; N]>>,
} }
impl<const N: usize> Secret<N> { impl<const N: usize> Secret<N> {
@@ -84,9 +160,12 @@ impl<const N: usize> Secret<N> {
pub fn zero() -> Self { pub fn zero() -> Self {
// Using [SecretMemoryPool] here because this operation is expensive, // Using [SecretMemoryPool] here because this operation is expensive,
// yet it is used in hot loops // yet it is used in hot loops
Self { let buf = with_secret_memory_pool(|pool| {
storage: Some(SECRET_CACHE.lock().unwrap().take()), pool.map(|p| p.take())
} .unwrap_or_else(|| ZeroizingSecretBox::new([0u8; N]))
});
Self { storage: Some(buf) }
} }
/// Returns a new [Secret] that is randomized /// Returns a new [Secret] that is randomized
@@ -101,7 +180,7 @@ impl<const N: usize> Secret<N> {
/// Borrows the data /// Borrows the data
pub fn secret(&self) -> &[u8; N] { pub fn secret(&self) -> &[u8; N] {
self.storage.as_ref().unwrap() &self.storage.as_ref().unwrap()
} }
/// Borrows the data mutably /// Borrows the data mutably
@@ -110,13 +189,6 @@ impl<const N: usize> Secret<N> {
} }
} }
impl<const N: usize> ZeroizeOnDrop for Secret<N> {}
impl<const N: usize> Zeroize for Secret<N> {
fn zeroize(&mut self) {
self.secret_mut().zeroize();
}
}
impl<const N: usize> Randomize for Secret<N> { impl<const N: usize> Randomize for Secret<N> {
fn try_fill<R: Rng + ?Sized>(&mut self, rng: &mut R) -> Result<(), rand::Error> { fn try_fill<R: Rng + ?Sized>(&mut self, rng: &mut R) -> Result<(), rand::Error> {
// Zeroize self first just to make sure the barriers from the zeroize create take // Zeroize self first just to make sure the barriers from the zeroize create take
@@ -127,11 +199,26 @@ impl<const N: usize> Randomize for Secret<N> {
} }
} }
impl<const N: usize> ZeroizeOnDrop for Secret<N> {}
impl<const N: usize> Zeroize for Secret<N> {
fn zeroize(&mut self) {
if let Some(inner) = &mut self.storage {
inner.zeroize()
}
}
}
impl<const N: usize> Drop for Secret<N> { impl<const N: usize> Drop for Secret<N> {
fn drop(&mut self) { fn drop(&mut self) {
self.storage with_secret_memory_pool(|pool| {
.take() if let Some((pool, secret)) = pool.zip(self.storage.take()) {
.map(|sec| SECRET_CACHE.lock().unwrap().release(sec)); pool.release(secret);
}
});
// This should be unnecessary: The pool has one item the inner secret which
// zeroizes itself on drop. Calling it should not do any harm though…
self.zeroize()
} }
} }