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