Skip to main content

cortex_airlock/
secure_mem.rs

1use zeroize::Zeroize;
2
3/// A memory buffer that is:
4/// - mlock'd on Unix (pinned in RAM, never swapped to disk)
5/// - Zeroized on drop (all bytes set to 0)
6///
7/// This ensures plaintext data never touches disk, even under memory pressure.
8pub struct LockedBuffer {
9    data: Vec<u8>,
10    locked: bool,
11}
12
13impl LockedBuffer {
14    /// Allocate a new locked buffer. Memory is immediately mlock'd.
15    pub fn new(capacity: usize) -> Self {
16        let data = vec![0u8; capacity];
17        let locked = Self::mlock_region(data.as_ptr(), data.len());
18
19        if !locked {
20            tracing::warn!(
21                "Failed to mlock {} bytes — data may be swapped to disk",
22                capacity
23            );
24        }
25
26        Self { data, locked }
27    }
28
29    /// Write data into the locked buffer
30    pub fn write(&mut self, src: &[u8]) -> usize {
31        let len = src.len().min(self.data.len());
32        self.data[..len].copy_from_slice(&src[..len]);
33        len
34    }
35
36    /// Read the contents of the locked buffer
37    pub fn as_bytes(&self) -> &[u8] {
38        &self.data
39    }
40
41    /// Get a mutable reference to the buffer contents
42    pub fn as_bytes_mut(&mut self) -> &mut [u8] {
43        &mut self.data
44    }
45
46    pub fn len(&self) -> usize {
47        self.data.len()
48    }
49
50    pub fn is_empty(&self) -> bool {
51        self.data.is_empty()
52    }
53
54    pub fn is_locked(&self) -> bool {
55        self.locked
56    }
57
58    /// Explicitly wipe and unlock. Also called on drop.
59    pub fn wipe(&mut self) {
60        self.data.zeroize();
61        if self.locked {
62            Self::munlock_region(self.data.as_ptr(), self.data.len());
63            self.locked = false;
64        }
65    }
66
67    #[cfg(unix)]
68    fn mlock_region(ptr: *const u8, len: usize) -> bool {
69        unsafe { libc::mlock(ptr as *const libc::c_void, len) == 0 }
70    }
71
72    #[cfg(unix)]
73    fn munlock_region(ptr: *const u8, len: usize) {
74        unsafe {
75            libc::munlock(ptr as *const libc::c_void, len);
76        }
77    }
78
79    #[cfg(not(unix))]
80    fn mlock_region(_ptr: *const u8, _len: usize) -> bool {
81        false // mlock not available, warn but continue
82    }
83
84    #[cfg(not(unix))]
85    fn munlock_region(_ptr: *const u8, _len: usize) {}
86}
87
88impl Drop for LockedBuffer {
89    fn drop(&mut self) {
90        self.wipe();
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn test_locked_buffer_write_read() {
100        let mut buf = LockedBuffer::new(32);
101        let data = b"secret content";
102        buf.write(data);
103        assert_eq!(&buf.as_bytes()[..data.len()], data);
104    }
105
106    #[test]
107    fn test_locked_buffer_wipe() {
108        let mut buf = LockedBuffer::new(16);
109        buf.write(b"secret");
110        buf.wipe();
111        assert!(buf.as_bytes().iter().all(|&b| b == 0));
112    }
113
114    #[test]
115    fn test_locked_buffer_drop_zeroizes() {
116        let ptr: *const u8;
117        let len: usize;
118        {
119            let mut buf = LockedBuffer::new(8);
120            buf.write(b"12345678");
121            ptr = buf.as_bytes().as_ptr();
122            len = buf.len();
123            // buf dropped here — should zeroize
124        }
125        // Note: we can't safely read ptr after drop, but the
126        // zeroize implementation is tested via wipe() above
127        let _ = (ptr, len);
128    }
129}