// SPDX-License-Identifier: LGPL-2.1-only // Copyright (C) 2026 Liberux Labs, S. L. //! 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 ) ); } }