sec-mem 0.1.1

High-assurance, attack-resistant cryptographic memory allocator and hardware-enforced secret container
Documentation
#[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");
            }
        }
    }
}