#![cfg(feature = "alloc")]
use std::alloc::{
GlobalAlloc,
Layout,
System,
};
use std::sync::Mutex;
use std::sync::atomic::Ordering::SeqCst;
use std::sync::atomic::{
AtomicBool,
AtomicUsize,
};
use lib_q_core::{
AeadKey,
KemKeypair,
KemSecretKey,
SigSecretKey,
};
struct WatchAlloc;
static WATCH_PTR: AtomicUsize = AtomicUsize::new(0);
static WATCH_ARMED: AtomicBool = AtomicBool::new(false);
static DEALLOC_SEEN: AtomicBool = AtomicBool::new(false);
static NONZERO_AT_DEALLOC: AtomicUsize = AtomicUsize::new(usize::MAX);
static SIZE_AT_DEALLOC: AtomicUsize = AtomicUsize::new(0);
unsafe impl GlobalAlloc for WatchAlloc {
unsafe fn alloc(&self, l: Layout) -> *mut u8 {
unsafe { System.alloc(l) }
}
unsafe fn dealloc(&self, ptr: *mut u8, l: Layout) {
if WATCH_ARMED.load(SeqCst) &&
ptr as usize == WATCH_PTR.load(SeqCst) &&
WATCH_ARMED.swap(false, SeqCst)
{
let mut nonzero = 0usize;
for i in 0..l.size() {
if unsafe { *ptr.add(i) } != 0 {
nonzero += 1;
}
}
NONZERO_AT_DEALLOC.store(nonzero, SeqCst);
SIZE_AT_DEALLOC.store(l.size(), SeqCst);
DEALLOC_SEEN.store(true, SeqCst);
}
unsafe { System.dealloc(ptr, l) }
}
}
#[global_allocator]
static WATCHED_ALLOC: WatchAlloc = WatchAlloc;
static WATCH_LOCK: Mutex<()> = Mutex::new(());
fn observe_dealloc(buf_addr: usize, consume: impl FnOnce()) -> (bool, usize, usize) {
DEALLOC_SEEN.store(false, SeqCst);
NONZERO_AT_DEALLOC.store(usize::MAX, SeqCst);
SIZE_AT_DEALLOC.store(0, SeqCst);
WATCH_PTR.store(buf_addr, SeqCst);
WATCH_ARMED.store(true, SeqCst);
consume();
WATCH_ARMED.store(false, SeqCst);
(
DEALLOC_SEEN.load(SeqCst),
NONZERO_AT_DEALLOC.load(SeqCst),
SIZE_AT_DEALLOC.load(SeqCst),
)
}
macro_rules! assert_wiped_on_drop {
($key:expr) => {{
let _guard = WATCH_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let key = $key;
let addr = key.data.as_ptr() as usize;
let (seen, nonzero, size) = observe_dealloc(addr, move || drop(key));
assert!(seen, "watched buffer was never deallocated");
assert_eq!(
nonzero, 0,
"secret buffer held {nonzero} nonzero of {size} bytes at dealloc — NOT wiped on drop"
);
}};
}
#[test]
fn kem_secret_key_is_wiped_on_drop() {
assert_wiped_on_drop!(KemSecretKey::new(vec![0xAB; 64]));
}
#[test]
fn sig_secret_key_is_wiped_on_drop() {
assert_wiped_on_drop!(SigSecretKey::new(vec![0xAB; 64]));
}
#[test]
fn aead_key_is_wiped_on_drop() {
assert_wiped_on_drop!(AeadKey::new(vec![0xAB; 64]));
}
#[test]
fn kem_keypair_secret_is_wiped_on_drop() {
let _guard = WATCH_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let keypair = KemKeypair::new(vec![0xCD; 8], vec![0xAB; 64]);
let addr = keypair.secret_key.data.as_ptr() as usize;
let (seen, nonzero, size) = observe_dealloc(addr, move || drop(keypair));
assert!(seen, "watched secret-key buffer was never deallocated");
assert_eq!(
nonzero, 0,
"keypair secret buffer held {nonzero} nonzero of {size} bytes at dealloc — NOT wiped on drop"
);
}
#[test]
fn mem_take_transfers_wipe_responsibility_away_from_the_key() {
let _guard = WATCH_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let mut key = KemSecretKey::new(vec![0xAB; 64]);
let stolen = std::mem::take(&mut key.data);
let stolen_addr = stolen.as_ptr() as usize;
drop(key); let (seen, nonzero, _size) = observe_dealloc(stolen_addr, move || drop(stolen));
assert!(seen, "stolen buffer was never deallocated");
assert_eq!(
nonzero, 64,
"mem::take transfers wipe responsibility to the caller — update this test \
(and the crate docs) if that changed"
);
}