Skip to main content

nodedb_wal/
secure_mem.rs

1// SPDX-License-Identifier: BUSL-1.1
2
3//! Secure memory utilities for key material.
4//!
5//! Wraps `libc::mlock`/`munlock` to prevent key bytes from being swapped
6//! to disk. mlock is best-effort: if the OS refuses (e.g. RLIMIT_MEMLOCK
7//! exceeded on some container configurations), a warning is logged and
8//! startup continues. Failing to mlock does not expose the key — it only
9//! means the key could be paged out under extreme memory pressure.
10//!
11//! On platforms where mlock is not available (e.g. some WASM targets) the
12//! calls are no-ops.
13
14#[cfg(all(unix, not(target_arch = "wasm32")))]
15use tracing::warn;
16
17/// A 32-byte key held in memory, mlocked against swap.
18///
19/// On `Drop`, the memory is explicitly zeroed and then munlocked.
20pub struct SecureKey {
21    bytes: Box<[u8; 32]>,
22}
23
24impl SecureKey {
25    /// Wrap a 32-byte key, attempting to mlock it.
26    ///
27    /// If mlock fails, logs a warning and continues — startup is not aborted.
28    pub fn new(bytes: [u8; 32]) -> Self {
29        let mut boxed = Box::new(bytes);
30        #[cfg(all(unix, not(target_arch = "wasm32")))]
31        mlock_best_effort(boxed.as_mut_ptr() as *mut libc::c_void, 32);
32        Self { bytes: boxed }
33    }
34
35    /// Access the key bytes.
36    pub fn as_bytes(&self) -> &[u8; 32] {
37        &self.bytes
38    }
39}
40
41impl Drop for SecureKey {
42    fn drop(&mut self) {
43        // Zero the key before releasing.
44        // Use volatile writes so the compiler cannot optimize them away.
45        for byte in self.bytes.iter_mut() {
46            unsafe { std::ptr::write_volatile(byte, 0u8) };
47        }
48        #[cfg(all(unix, not(target_arch = "wasm32")))]
49        munlock_best_effort(self.bytes.as_mut_ptr() as *mut libc::c_void, 32);
50    }
51}
52
53/// Public convenience wrapper for mlocking raw key bytes from `crypto.rs`.
54///
55/// Locks `len` bytes starting at `ptr`. Best-effort: logs a warning on failure.
56/// No-op on non-Unix targets.
57pub fn mlock_key_bytes(ptr: *mut u8, len: usize) {
58    #[cfg(all(unix, not(target_arch = "wasm32")))]
59    mlock_best_effort(ptr as *mut libc::c_void, len);
60    #[cfg(not(all(unix, not(target_arch = "wasm32"))))]
61    let _ = (ptr, len);
62}
63
64/// Attempt to mlock `len` bytes starting at `ptr`.
65///
66/// Logs a warning if mlock fails.
67#[cfg(all(unix, not(target_arch = "wasm32")))]
68fn mlock_best_effort(ptr: *mut libc::c_void, len: usize) {
69    let rc = unsafe { libc::mlock(ptr, len) };
70    if rc != 0 {
71        warn!(
72            "mlock failed for {} bytes (errno {}): key may be swapped to disk \
73             under extreme memory pressure. Increase RLIMIT_MEMLOCK if this \
74             is a concern.",
75            len,
76            std::io::Error::last_os_error()
77        );
78    }
79}
80
81/// Attempt to munlock `len` bytes starting at `ptr`. Best-effort, no error.
82#[cfg(all(unix, not(target_arch = "wasm32")))]
83fn munlock_best_effort(ptr: *mut libc::c_void, len: usize) {
84    unsafe {
85        libc::munlock(ptr, len);
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    #[test]
94    fn secure_key_stores_bytes() {
95        let key = SecureKey::new([0x42u8; 32]);
96        assert_eq!(*key.as_bytes(), [0x42u8; 32]);
97    }
98
99    #[test]
100    fn secure_key_zeros_on_drop() {
101        // We can't observe zeroing from outside since the bytes move on drop,
102        // but this at least exercises the path without panic.
103        let key = SecureKey::new([0xABu8; 32]);
104        drop(key);
105        // If we get here without panic or memory error, mlock/munlock worked.
106    }
107
108    #[test]
109    #[cfg(all(unix, not(target_arch = "wasm32")))]
110    fn mlock_graceful_on_linux() {
111        // mlock with a stack pointer that may or may not succeed depending on
112        // RLIMIT_MEMLOCK. Either way we must not panic.
113        let mut buf = [0u8; 32];
114        mlock_best_effort(buf.as_mut_ptr() as *mut libc::c_void, 32);
115        munlock_best_effort(buf.as_mut_ptr() as *mut libc::c_void, 32);
116        // Success = no panic.
117    }
118}