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 ) );
	}
}