ltk/secure_mem.rs
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
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
//! Defensive primitives for credential handling.
//!
//! [`secure_zero`] overwrites a byte slice with zeros using volatile stores
//! so the optimiser cannot elide the wipe even when it can prove the buffer
//! is no longer read. It is the building block the `Drop` impls of secure
//! widgets (currently [`crate::widget::text_edit::TextEdit`] when
//! `secure( true )` is set) use to scrub credential text before the heap is
//! returned to the allocator.
//!
//! This is the minimal stand-in for the well-known `zeroize` crate: ltk is
//! a UI toolkit, the only call site is text-input wiping, and the cost of
//! pulling another dependency is not justified.
use core::ptr;
use core::sync::atomic::{ compiler_fence, Ordering };
/// Overwrite `buf` with zeros. The writes go through `write_volatile` so
/// the compiler treats them as observable side effects — the elision pass
/// cannot drop them even when the buffer is about to be freed.
///
/// A `compiler_fence(SeqCst)` after the loop pins the wipe to "before any
/// later memory operation", so a subsequent `Drop` that hands the
/// underlying allocation back to the allocator cannot be reordered above
/// the zero stores.
pub( crate ) fn secure_zero( buf: &mut [u8] )
{
for b in buf.iter_mut()
{
// SAFETY: writing a primitive byte through a unique mutable
// reference; volatile reflects the intent that the store has an
// observer beyond ordinary Rust semantics (the ex-credential).
unsafe { ptr::write_volatile( b, 0u8 ); }
}
compiler_fence( Ordering::SeqCst );
}
#[ cfg( test ) ]
mod tests
{
use super::*;
#[ test ]
fn empty_slice_is_a_noop()
{
let mut buf: [u8; 0] = [];
secure_zero( &mut buf );
// Nothing to assert beyond "did not panic" — exercised so the
// fence + zero-iter loop compiles for the empty case.
}
#[ test ]
fn fills_every_byte_with_zero()
{
let mut buf = [ 0xAAu8; 64 ];
secure_zero( &mut buf );
assert!( buf.iter().all( |&b| b == 0 ) );
}
#[ test ]
fn wipes_a_credential_string_in_place()
{
let mut password = String::from( "hunter2" );
// SAFETY: as_mut_vec lets us reach the underlying byte buffer.
// We only write zeros, leaving an empty / NUL-filled UTF-8 byte
// sequence which is still valid UTF-8 (NUL is U+0000).
let bytes = unsafe { password.as_mut_vec() };
secure_zero( bytes );
assert!( bytes.iter().all( |&b| b == 0 ) );
// After the wipe the String is technically all NULs, not empty;
// the consumer drops it immediately so the heap allocation is
// returned to the allocator already overwritten.
assert_eq!( password.len(), 7 );
assert!( password.bytes().all( |b| b == 0 ) );
}
}