1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
#[cfg(test)]
mod tests {
use sec_mem::{SecMem, zeroize::Zeroize};
use rand::{rngs::StdRng, Rng, SeedableRng};
#[test]
fn test_basic_usage() {
let secret = 42u32;
let mut secret_box = SecMem::new(secret);
// Test read access
let value = secret_box.access(|s| *s);
assert_eq!(value, 42);
// Test write access
secret_box.access_mut(|s| *s = 100);
let value = secret_box.access(|s| *s);
assert_eq!(value, 100);
}
#[test]
fn test_clone() {
let secret = "my secret".to_string();
let secret_box = SecMem::new(secret.clone());
let cloned_box = secret_box.clone();
let value = cloned_box.access(|s| s.clone());
assert_eq!(value, secret);
}
#[test]
fn test_debug_redaction() {
let secret = "password123".to_string();
let secret_box = SecMem::new(secret);
let debug_output = format!("{:?}", secret_box);
assert!(debug_output.contains("SecMem<alloc::string::String>([REDACTED])"));
assert!(!debug_output.contains("password123"));
}
#[test]
fn test_default() {
let default_box = SecMem::<u32>::default();
let value = default_box.access(|&s| s);
assert_eq!(value, 0);
}
// Constant-time comparison tests
#[test]
fn test_constant_time_comparison_equal() {
let secret1 = [1u8, 2, 3, 4, 5];
let secret2 = [1u8, 2, 3, 4, 5];
let box1 = SecMem::new(secret1);
let box2 = SecMem::new(secret2);
let res: bool = box1.constant_time_eq(&box2).into();
assert!(res);
}
#[test]
fn test_constant_time_comparison_different() {
let secret1 = [1u8, 2, 3, 4, 5];
let secret2 = [1u8, 2, 3, 4, 6];
let box1 = SecMem::new(secret1);
let box2 = SecMem::new(secret2);
let res: bool = box1.constant_time_eq(&box2).into();
assert!(!res);
}
// Zeroization tests
#[test]
fn test_zeroize_method() {
let mut rng = StdRng::seed_from_u64(42);
let mut secret = [0u8; 32];
rng.fill_bytes(&mut secret);
let original_secret = secret;
let mut protected = SecMem::new(secret);
// Zeroize and verify the secret is cleared
protected.zeroize();
protected.access(|s| {
assert!(s.iter().all(|&x| x == 0), "Secret was not properly zeroized");
});
// Original secret should remain unchanged (it was copied)
assert!(!original_secret.iter().all(|&x| x == 0));
}
#[test]
fn test_zeroize_on_drop_effectiveness() {
// Force initialization
let _ = SecMem::new([0u8; 1]);
let secret = [0xDDu8; 32];
let protected = SecMem::new(secret);
let raw_ptr = protected.access(|s| s.as_ptr());
// Drop the SecMem, which triggers zeroization and hardware munmap
drop(protected);
// Assert that the memory is truly physically gone from the process.
// Reading it should immediately cause a hardware SIGSEGV.
assert_segfaults(|| {
unsafe {
let _val = std::ptr::read_volatile(raw_ptr);
}
});
}
// Helper function to assert a closure causes a SIGSEGV
fn assert_segfaults<F: FnOnce()>(f: F) {
// Force initialization of global OnceLocks before forking to prevent deadlocks in the child!
let _ = SecMem::new([0u8; 1]);
unsafe {
// Flush output buffers before forking
libc::fflush(std::ptr::null_mut());
let pid = libc::fork();
if pid == 0 {
// Child process
f();
libc::exit(0); // If we survive, it's a failure
} else if pid > 0 {
// Parent process
let mut status: libc::c_int = 0;
libc::waitpid(pid, &mut status, 0);
if libc::WIFSIGNALED(status) {
assert_eq!(libc::WTERMSIG(status), libc::SIGSEGV, "Expected SIGSEGV but got different signal");
} else if libc::WIFEXITED(status) {
panic!("Process exited normally instead of segfaulting. Memory protection failed!");
} else {
panic!("Unexpected process status");
}
} else {
panic!("fork() failed");
}
}
}
#[test]
fn attack_unlocked_memory_access() {
assert_segfaults(|| {
let secret = [0u8; 32];
let protected = SecMem::new(secret);
// Extract the raw pointer while legitimately accessing
let raw_ptr = protected.access(|s| s.as_ptr());
// Now OUTSIDE the access() closure, the memory automatically gated itself back to PROT_NONE.
// Attempting to read it directly mimics an attacker or memory vulnerability trying to read the pointer.
// This MUST cause a segmentation fault.
unsafe {
let _val = std::ptr::read_volatile(raw_ptr);
}
});
}
#[test]
fn attack_buffer_underflow_guard_page() {
assert_segfaults(|| {
let secret = [0u8; 32];
let protected = SecMem::new(secret);
protected.access(|s| {
// We are inside the access block, so the secret region itself is PROT_READ.
// An attacker tries to trigger a buffer underflow to read preceding memory.
// The guard page directly before the secret should remain PROT_NONE and cause a SIGSEGV.
unsafe {
let ptr = s.as_ptr() as *const u8;
let underflow_ptr = ptr.sub(1); // 1 byte before allocation
let _val = std::ptr::read_volatile(underflow_ptr);
}
});
});
}
#[test]
fn attack_buffer_overflow_guard_page() {
assert_segfaults(|| {
let secret = [0u8; 32];
let protected = SecMem::new(secret);
protected.access(|s| {
// An attacker tries to trigger a buffer overflow.
// Since memory is mapped to page boundaries, we calculate the next page.
let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) } as usize;
unsafe {
let ptr = s.as_ptr() as *const u8;
// Read exactly at the start of the next page (the trailing guard page)
let overflow_ptr = ptr.add(page_size);
let _val = std::ptr::read_volatile(overflow_ptr);
}
});
});
}
#[test]
fn attack_fork_inheritance() {
// Force initialization of global OnceLocks before forking
let _ = SecMem::new([0u8; 1]);
let secret = [0u8; 32];
let protected = SecMem::new(secret);
let raw_ptr = protected.access(|s| s.as_ptr());
unsafe {
libc::fflush(std::ptr::null_mut());
let pid = libc::fork();
if pid == 0 {
// Child process attempts to access the memory
// Due to MADV_DONTFORK, this memory is fully unmapped in the child.
// Reading the pointer directly should immediately cause a SIGSEGV
let _val = std::ptr::read_volatile(raw_ptr);
// If it survives, it's a failure
libc::exit(0);
} else if pid > 0 {
let mut status: libc::c_int = 0;
libc::waitpid(pid, &mut status, 0);
assert!(libc::WIFSIGNALED(status), "Child did not receive a signal - inheritance prevention failed!");
assert_eq!(libc::WTERMSIG(status), libc::SIGSEGV, "Child should have segfaulted due to MADV_DONTFORK");
} else {
panic!("fork failed");
}
}
}
#[test]
#[cfg(feature = "encryption")]
fn test_encryption_at_rest() {
let plaintext = [0xAAu8; 32];
let protected = SecMem::new(plaintext);
let raw_ptr = protected.access(|s| s.as_ptr());
// At this point, the memory is PROT_NONE and encrypted.
// We simulate a kernel-level memory dumper or advanced attacker by
// forcefully bypassing PROT_NONE using mprotect directly to read the raw RAM
// WITHOUT going through the .access() decryption layer.
unsafe {
// Find the page-aligned start of the secret region
let page_size = libc::sysconf(libc::_SC_PAGESIZE) as usize;
let addr = raw_ptr as usize;
let aligned_addr = addr & !(page_size - 1);
let size_to_protect = (addr + 32) - aligned_addr;
// Forcefully unlock the RAM
if libc::mprotect(aligned_addr as *mut libc::c_void, size_to_protect, libc::PROT_READ) != 0 {
panic!("Failed to forcefully unlock memory for raw dump");
}
// Dump the raw RAM bytes
let mut raw_dump = [0u8; 32];
std::ptr::copy_nonoverlapping(raw_ptr, raw_dump.as_mut_ptr(), 32);
// Verify that the data sitting in RAM is NOT the plaintext
assert_ne!(raw_dump, plaintext, "CRITICAL VULNERABILITY: Plaintext found in raw RAM dump!");
// It should look like random noise (we check that it's not mostly zeros either)
let zeros = raw_dump.iter().filter(|&&x| x == 0).count();
assert!(zeros < 16, "Ciphertext doesn't look random enough");
// Restore protection
libc::mprotect(aligned_addr as *mut libc::c_void, size_to_protect, libc::PROT_NONE);
}
// Verify that the legitimate accessor still gets the plaintext
protected.access(|s| {
assert_eq!(*s, plaintext, "Decryption layer failed to return correct plaintext");
});
}
#[test]
fn attack_panic_during_access_restores_protection() {
// Force initialization
let _ = SecMem::new([0u8; 1]);
let protected = SecMem::new([0xCCu8; 32]);
let raw_ptr = protected.access(|s| s.as_ptr());
// Simulate a thread panicking while it holds the access guard
let result = std::panic::catch_unwind(|| {
protected.access(|_s| {
panic!("Simulated panic during access!");
});
});
assert!(result.is_err(), "Panic was not caught properly");
// Assert that the RAII drop handler STILL locked the memory back to PROT_NONE
// despite the stack unwinding from the panic.
assert_segfaults(|| {
unsafe {
let _val = std::ptr::read_volatile(raw_ptr);
}
});
}
#[test]
#[cfg(target_os = "linux")]
fn attack_mseal_guard_page_bypass() {
// Force initialization
let _ = SecMem::new([0u8; 1]);
let protected = SecMem::new([0u8; 32]);
let raw_ptr = protected.access(|s| s.as_ptr());
let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) } as usize;
let addr = raw_ptr as usize;
let aligned_addr = addr & !(page_size - 1);
let front_guard_page = (aligned_addr - page_size) as *mut libc::c_void;
// Attacker attempts to explicitly unprotect the guard page using mprotect
// to bypass the overflow/underflow trap.
unsafe {
let res = libc::mprotect(front_guard_page, page_size, libc::PROT_READ | libc::PROT_WRITE);
if res != 0 {
// If mprotect failed, check if it's because mseal blocked it (EPERM)
let err = *libc::__errno_location();
// We don't strictly assert EPERM because older kernels without mseal
// might fail for other reasons, or might even succeed.
println!("mprotect on guard page failed with errno: {}", err);
} else {
// If the kernel is older (< 6.10) and mseal isn't supported, mprotect might succeed.
// However, we just ensure the test doesn't crash.
println!("mseal not active or bypassed on this kernel version");
}
}
}
#[test]
#[cfg(feature = "encryption")]
fn attack_proc_self_mem_read() {
// Attackers often bypass PROT_NONE entirely by using ptrace or reading /proc/self/mem
use std::os::unix::fs::FileExt;
// Force initialization
let _ = SecMem::new([0u8; 1]);
let plaintext = [0xBB; 32];
let protected = SecMem::new(plaintext);
let raw_ptr = protected.access(|s| s.as_ptr());
if let Ok(file) = std::fs::File::open("/proc/self/mem") {
let mut buf = [0u8; 32];
// Try to read the memory region bypassing the MMU PTE protections
let bytes_read = file.read_at(&mut buf, raw_ptr as u64).unwrap_or(0);
if bytes_read == 32 {
// The kernel allowed the read!
// Because of Encrypt-at-Rest, the attacker MUST NOT see the plaintext.
assert_ne!(buf, plaintext, "CRITICAL VULNERABILITY: /proc/self/mem exposed the plaintext!");
let zeros = buf.iter().filter(|&&x| x == 0).count();
assert!(zeros < 16, "Ciphertext doesn't look random enough");
}
}
}
}