Implements zeroization support across all heapless data structures to securely clear sensitive data from memory:

- When the zeroize feature is enabled, the LenType sealed trait now has Zeroize as a supertrait
- This simplifies the bound for deriving Zeroize for VecInner and other types
- Added tests to verify VecView also implements Zeroize correctly

This feature is essential for security-sensitive applications needing to prevent data leaks from memory dumps.

Note: Zeroize initially worked on Vector purely via derivation, however was not complete without proper bound checks. Without these checks, the deref implementation of Zeroize was used instead, which led to incomplete zeroization of the Vector's contents.
This commit is contained in:
Zeeshan Ali Khan 2025-09-16 14:19:02 +02:00 committed by Vishal Shenoy
parent d29f95cd68
commit 0b027b1ff8
15 changed files with 422 additions and 4 deletions

View File

@ -48,7 +48,7 @@ jobs:
components: miri
- name: Run miri
run: MIRIFLAGS=-Zmiri-ignore-leaks cargo miri test --features="alloc,defmt,mpmc_large,portable-atomic-critical-section,serde,ufmt,bytes"
run: MIRIFLAGS=-Zmiri-ignore-leaks cargo miri test --features="alloc,defmt,mpmc_large,portable-atomic-critical-section,serde,ufmt,bytes,zeroize"
# Run cargo test
test:
@ -84,7 +84,7 @@ jobs:
toolchain: stable
- name: Run cargo test
run: cargo test --features="alloc,defmt,mpmc_large,portable-atomic-critical-section,serde,ufmt,bytes"
run: cargo test --features="alloc,defmt,mpmc_large,portable-atomic-critical-section,serde,ufmt,bytes,zeroize"
# Run cargo fmt --check
style:
@ -176,6 +176,7 @@ jobs:
cargo check --target="${target}" --features="portable-atomic-critical-section"
cargo check --target="${target}" --features="serde"
cargo check --target="${target}" --features="ufmt"
cargo check --target="${target}" --features="zeroize"
env:
target: ${{ matrix.target }}

View File

@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Implement `defmt::Format` for `CapacityError`.
- Implement `TryFrom` for `Deque` from array.
- Switch from `serde` to `serde_core` for enabling faster compilations.
- Implement `Zeroize` trait for all data structures with the `zeroize` feature to securely clear sensitive data from memory.
## [v0.9.1] - 2025-08-19

View File

@ -47,6 +47,9 @@ ufmt = ["dep:ufmt", "dep:ufmt-write"]
# Implement `defmt::Format`.
defmt = ["dep:defmt"]
# Implement `zeroize::Zeroize` trait.
zeroize = ["dep:zeroize"]
# Enable larger MPMC sizes.
mpmc_large = []
@ -63,6 +66,7 @@ serde_core = { version = "1", optional = true, default-features = false }
ufmt = { version = "0.2", optional = true }
ufmt-write = { version = "0.1", optional = true }
defmt = { version = "1.0.1", optional = true }
zeroize = { version = "1.8", optional = true, default-features = false, features = ["derive"] }
# for the pool module
[target.'cfg(any(target_arch = "arm", target_pointer_width = "32", target_pointer_width = "64"))'.dependencies]

View File

@ -17,6 +17,9 @@ use core::{
ptr, slice,
};
#[cfg(feature = "zeroize")]
use zeroize::Zeroize;
use crate::vec::{OwnedVecStorage, Vec, VecInner, VecStorage, ViewVecStorage};
/// Min-heap
@ -55,6 +58,11 @@ impl private::Sealed for Min {}
///
/// In most cases you should use [`BinaryHeap`] or [`BinaryHeapView`] directly. Only use this
/// struct if you want to write code that's generic over both.
#[cfg_attr(
feature = "zeroize",
derive(Zeroize),
zeroize(bound = "T: Zeroize, S: Zeroize")
)]
pub struct BinaryHeapInner<T, K, S: VecStorage<T> + ?Sized> {
pub(crate) _kind: PhantomData<K>,
pub(crate) data: VecInner<T, usize, S>,
@ -882,6 +890,24 @@ mod tests {
assert_eq!(heap.pop(), None);
}
#[test]
#[cfg(feature = "zeroize")]
fn test_binary_heap_zeroize() {
use zeroize::Zeroize;
let mut heap = BinaryHeap::<u8, Max, 8>::new();
for i in 0..8 {
heap.push(i).unwrap();
}
assert_eq!(heap.len(), 8);
assert_eq!(heap.peek(), Some(&7));
// zeroized using Vec's implementation
heap.zeroize();
assert_eq!(heap.len(), 0);
}
fn _test_variance<'a: 'b, 'b>(x: BinaryHeap<&'a (), Max, 42>) -> BinaryHeap<&'b (), Max, 42> {
x
}

View File

@ -10,6 +10,9 @@ use core::{
ops::Deref,
};
#[cfg(feature = "zeroize")]
use zeroize::Zeroize;
/// A fixed capacity [`CString`](https://doc.rust-lang.org/std/ffi/struct.CString.html).
///
/// It stores up to `N - 1` non-nul characters with a trailing nul terminator.
@ -18,6 +21,20 @@ pub struct CString<const N: usize, LenT: LenType = usize> {
inner: Vec<u8, N, LenT>,
}
#[cfg(feature = "zeroize")]
impl<const N: usize, LenT: LenType> Zeroize for CString<N, LenT> {
fn zeroize(&mut self) {
self.inner.zeroize();
const {
assert!(N > 0);
}
// SAFETY: We just asserted that `N > 0`.
unsafe { self.inner.push_unchecked(b'\0') };
}
}
impl<const N: usize, LenT: LenType> CString<N, LenT> {
/// Creates a new C-compatible string with a terminating nul byte.
///
@ -483,6 +500,35 @@ mod tests {
assert_eq!(Borrow::<CStr>::borrow(&string), c"foo");
}
#[test]
#[cfg(feature = "zeroize")]
fn test_cstring_zeroize() {
use zeroize::Zeroize;
let mut c_string = CString::<32>::from_bytes_with_nul(b"sensitive_password\0").unwrap();
assert_eq!(c_string.to_str(), Ok("sensitive_password"));
assert!(!c_string.to_bytes().is_empty());
let original_length = c_string.to_bytes().len();
assert_eq!(original_length, 18);
let new_string = CString::<32>::from_bytes_with_nul(b"short\0").unwrap();
c_string = new_string;
assert_eq!(c_string.to_str(), Ok("short"));
assert_eq!(c_string.to_bytes().len(), 5);
// zeroized using Vec's implementation
c_string.zeroize();
assert_eq!(c_string.to_bytes().len(), 0);
assert_eq!(c_string.to_bytes_with_nul(), &[0]);
c_string.extend_from_bytes(b"new_data").unwrap();
assert_eq!(c_string.to_str(), Ok("new_data"));
assert_eq!(c_string.to_bytes().len(), 8);
}
mod equality {
use super::*;

View File

@ -42,10 +42,14 @@ use core::marker::PhantomData;
use core::mem::{ManuallyDrop, MaybeUninit};
use core::{ptr, slice};
#[cfg(feature = "zeroize")]
use zeroize::Zeroize;
/// Base struct for [`Deque`] and [`DequeView`], generic over the [`VecStorage`].
///
/// In most cases you should use [`Deque`] or [`DequeView`] directly. Only use this
/// struct if you want to write code that's generic over both.
#[cfg_attr(feature = "zeroize", derive(Zeroize))]
pub struct DequeInner<T, S: VecStorage<T> + ?Sized> {
// This phantomdata is required because otherwise rustc thinks that `T` is not used
phantom: PhantomData<T>,
@ -1674,4 +1678,29 @@ mod tests {
assert_eq!(Droppable::count(), 0);
}
#[test]
#[cfg(feature = "zeroize")]
fn test_deque_zeroize() {
use zeroize::Zeroize;
let mut deque = Deque::<u8, 16>::new();
for i in 1..=8 {
deque.push_back(i).unwrap();
}
for i in 9..=16 {
deque.push_front(i).unwrap();
}
assert_eq!(deque.len(), 16);
assert_eq!(deque.front(), Some(&16));
assert_eq!(deque.back(), Some(&8));
// zeroized using Vec's implementation
deque.zeroize();
assert_eq!(deque.len(), 0);
assert!(deque.is_empty());
}
}

View File

@ -38,10 +38,14 @@ use core::ops::Deref;
use core::ptr;
use core::slice;
mod storage {
use core::mem::MaybeUninit;
#[cfg(feature = "zeroize")]
use zeroize::Zeroize;
mod storage {
use super::{HistoryBufInner, HistoryBufView};
use core::mem::MaybeUninit;
#[cfg(feature = "zeroize")]
use zeroize::Zeroize;
/// Trait defining how data for a container is stored.
///
@ -83,6 +87,7 @@ mod storage {
}
// One sealed layer of indirection to hide the internal details (The MaybeUninit).
#[cfg_attr(feature = "zeroize", derive(Zeroize))]
pub struct HistoryBufStorageInner<T: ?Sized> {
pub(crate) buffer: T,
}
@ -148,6 +153,7 @@ use self::storage::HistoryBufSealedStorage;
///
/// In most cases you should use [`HistoryBuf`] or [`HistoryBufView`] directly. Only use this
/// struct if you want to write code that's generic over both.
#[cfg_attr(feature = "zeroize", derive(Zeroize))]
pub struct HistoryBufInner<T, S: HistoryBufStorage<T> + ?Sized> {
// This phantomdata is required because otherwise rustc thinks that `T` is not used
phantom: PhantomData<T>,
@ -938,6 +944,39 @@ mod tests {
assert_eq!(DROP_COUNT.load(Ordering::SeqCst), 3);
}
#[test]
#[cfg(feature = "zeroize")]
fn test_history_buf_zeroize() {
use zeroize::Zeroize;
let mut buffer = HistoryBuf::<u8, 8>::new();
for i in 0..8 {
buffer.write(i);
}
assert_eq!(buffer.len(), 8);
assert_eq!(buffer.recent(), Some(&7));
// Clear to mark formerly used memory as unused, to make sure that it also gets zeroed
buffer.clear();
buffer.write(20);
assert_eq!(buffer.len(), 1);
assert_eq!(buffer.recent(), Some(&20));
buffer.zeroize();
assert_eq!(buffer.len(), 0);
assert!(buffer.is_empty());
// Check that all underlying memory actually got zeroized
unsafe {
for a in buffer.data.buffer {
assert_eq!(a.assume_init(), 0);
}
}
}
fn _test_variance<'a: 'b, 'b>(x: HistoryBuf<&'a (), 42>) -> HistoryBuf<&'b (), 42> {
x
}

View File

@ -8,6 +8,9 @@ use core::{
ops, slice,
};
#[cfg(feature = "zeroize")]
use zeroize::Zeroize;
use hash32::{BuildHasherDefault, FnvHasher};
use crate::Vec;
@ -66,6 +69,7 @@ use crate::Vec;
pub type FnvIndexMap<K, V, const N: usize> = IndexMap<K, V, BuildHasherDefault<FnvHasher>, N>;
#[derive(Clone, Copy, Eq, PartialEq)]
#[cfg_attr(feature = "zeroize", derive(Zeroize))]
struct HashValue(u16);
impl HashValue {
@ -80,6 +84,7 @@ impl HashValue {
#[doc(hidden)]
#[derive(Clone)]
#[cfg_attr(feature = "zeroize", derive(Zeroize))]
pub struct Bucket<K, V> {
hash: HashValue,
key: K,
@ -88,6 +93,7 @@ pub struct Bucket<K, V> {
#[doc(hidden)]
#[derive(Clone, Copy, PartialEq)]
#[cfg_attr(feature = "zeroize", derive(Zeroize))]
pub struct Pos {
// compact representation of `{ hash_value: u16, index: u16 }`
// To get the most from `NonZero` we store the *value minus 1*. This way `None::Option<Pos>`
@ -138,6 +144,11 @@ macro_rules! probe_loop {
}
}
#[cfg_attr(
feature = "zeroize",
derive(Zeroize),
zeroize(bound = "K: Zeroize, V: Zeroize")
)]
struct CoreMap<K, V, const N: usize> {
entries: Vec<Bucket<K, V>, N, usize>,
indices: [Option<Pos>; N],
@ -722,8 +733,14 @@ where
/// println!("{}: \"{}\"", book, review);
/// }
/// ```
#[cfg_attr(
feature = "zeroize",
derive(Zeroize),
zeroize(bound = "K: Zeroize, V: Zeroize")
)]
pub struct IndexMap<K, V, S, const N: usize> {
core: CoreMap<K, V, N>,
#[cfg_attr(feature = "zeroize", zeroize(skip))]
build_hasher: S,
}
@ -1988,4 +2005,28 @@ mod tests {
let map: FnvIndexMap<usize, f32, 4> = Default::default();
assert_eq!(map, map);
}
#[test]
#[cfg(feature = "zeroize")]
fn test_index_map_zeroize() {
use zeroize::Zeroize;
let mut map: FnvIndexMap<u8, u8, 8> = FnvIndexMap::new();
for i in 1..=8 {
map.insert(i, i * 10).unwrap();
}
assert_eq!(map.len(), 8);
assert!(!map.is_empty());
// zeroized using Vec's implementation
map.zeroize();
assert_eq!(map.len(), 0);
assert!(map.is_empty());
map.insert(1, 10).unwrap();
assert_eq!(map.len(), 1);
assert_eq!(map.get(&1), Some(&10));
}
}

View File

@ -5,6 +5,9 @@ use core::{
hash::{BuildHasher, Hash},
};
#[cfg(feature = "zeroize")]
use zeroize::Zeroize;
use hash32::{BuildHasherDefault, FnvHasher};
use crate::index_map::{self, IndexMap};
@ -85,6 +88,7 @@ pub type FnvIndexSet<T, const N: usize> = IndexSet<T, BuildHasherDefault<FnvHash
/// println!("{}", book);
/// }
/// ```
#[cfg_attr(feature = "zeroize", derive(Zeroize), zeroize(bound = "T: Zeroize"))]
pub struct IndexSet<T, S, const N: usize> {
map: IndexMap<T, (), S, N>,
}
@ -696,4 +700,28 @@ mod tests {
// Ensure a `IndexSet` containing `!Send` values stays `!Send` itself.
assert_not_impl_any!(IndexSet<*const (), BuildHasherDefault<()>, 4>: Send);
#[test]
#[cfg(feature = "zeroize")]
fn test_index_set_zeroize() {
use zeroize::Zeroize;
let mut set: IndexSet<u8, BuildHasherDefault<hash32::FnvHasher>, 8> = IndexSet::new();
for i in 1..=8 {
set.insert(i).unwrap();
}
assert_eq!(set.len(), 8);
assert!(set.contains(&8));
// zeroized using index_map's implementation
set.zeroize();
assert_eq!(set.len(), 0);
assert!(set.is_empty());
set.insert(1).unwrap();
assert_eq!(set.len(), 1);
assert!(set.contains(&1));
}
}

View File

@ -3,6 +3,9 @@ use core::{
ops::{Add, AddAssign, Sub, SubAssign},
};
#[cfg(feature = "zeroize")]
use zeroize::Zeroize;
pub trait Sealed:
Send
+ Sync
@ -75,6 +78,15 @@ macro_rules! impl_lentype {
/// A sealed trait representing a valid type to use as a length for a container.
///
/// This cannot be implemented in user code, and is restricted to `u8`, `u16`, `u32`, and `usize`.
///
/// When the `zeroize` feature is enabled, this trait requires the `Zeroize` trait.
#[cfg(feature = "zeroize")]
pub trait LenType: Sealed + Zeroize {}
/// A sealed trait representing a valid type to use as a length for a container.
///
/// This cannot be implemented in user code, and is restricted to `u8`, `u16`, `u32`, and `usize`.
#[cfg(not(feature = "zeroize"))]
pub trait LenType: Sealed {}
impl_lentype!(

View File

@ -106,6 +106,16 @@
//! - [`mpmc::MpMcQueue`](mpmc): A lock-free multiple-producer, multiple-consumer queue.
//! - [`spsc::Queue`](spsc): A lock-free single-producer, single-consumer queue.
//!
//! # Zeroize Support
//!
//! The `zeroize` feature enables secure memory wiping for the data structures via the [`zeroize`](https://crates.io/crates/zeroize)
//! crate. Sensitive data can be properly erased from memory when no longer needed.
//!
//! When zeroizing a container, all underlying memory (including unused portion of the containers)
//! is overwritten with zeros, length counters are reset, and the container is left in a valid but
//! empty state that can be reused.
//!
//! Check the [documentation of the zeroize crate](https://docs.rs/zeroize/) for more information.
//! # Minimum Supported Rust Version (MSRV)
//!
//! This crate does *not* have a Minimum Supported Rust Version (MSRV) and may make use of language

View File

@ -4,6 +4,9 @@
use core::{borrow::Borrow, fmt, mem, ops, slice};
#[cfg(feature = "zeroize")]
use zeroize::Zeroize;
use crate::vec::{OwnedVecStorage, Vec, VecInner, ViewVecStorage};
mod storage {
@ -88,6 +91,11 @@ pub type OwnedStorage<K, V, const N: usize> = OwnedVecStorage<(K, V), N>;
pub type ViewStorage<K, V> = ViewVecStorage<(K, V)>;
/// Base struct for [`LinearMap`] and [`LinearMapView`]
#[cfg_attr(
feature = "zeroize",
derive(Zeroize),
zeroize(bound = "S: Zeroize, K: Zeroize, V: Zeroize")
)]
pub struct LinearMapInner<K, V, S: LinearMapStorage<K, V> + ?Sized> {
pub(crate) buffer: VecInner<(K, V), usize, S>,
}
@ -752,4 +760,24 @@ mod test {
let map: LinearMap<usize, f32, 4> = Default::default();
assert_eq!(map, map);
}
#[test]
#[cfg(feature = "zeroize")]
fn test_linear_map_zeroize() {
use zeroize::Zeroize;
let mut map: LinearMap<u8, u8, 8> = LinearMap::new();
for i in 1..=8 {
map.insert(i, i * 10).unwrap();
}
assert_eq!(map.len(), 8);
assert_eq!(map.get(&5), Some(&50));
// zeroized using Vec's implementation
map.zeroize();
assert_eq!(map.len(), 0);
assert!(map.is_empty());
}
}

View File

@ -33,6 +33,9 @@ use core::mem::MaybeUninit;
use core::ops::{Deref, DerefMut};
use core::ptr;
#[cfg(feature = "zeroize")]
use zeroize::Zeroize;
mod storage {
use super::{LenType, Node, SortedLinkedListInner, SortedLinkedListView};
@ -191,6 +194,7 @@ impl private::Sealed for Max {}
impl private::Sealed for Min {}
/// A node in the [`SortedLinkedList`].
#[cfg_attr(feature = "zeroize", derive(Zeroize))]
pub struct Node<T, Idx> {
val: MaybeUninit<T>,
next: Idx,
@ -845,6 +849,26 @@ where
}
}
#[cfg(feature = "zeroize")]
impl<T, Idx, K, S> Zeroize for SortedLinkedListInner<T, Idx, K, S>
where
T: Ord + Zeroize,
Idx: LenType + Zeroize,
K: Kind,
S: SortedLinkedListStorage<T, Idx> + ?Sized,
{
fn zeroize(&mut self) {
while let Some(mut item) = self.pop() {
item.zeroize();
}
let buffer = self.list.borrow_mut();
for elem in buffer {
elem.zeroize();
}
}
}
#[cfg(test)]
mod tests {
use static_assertions::assert_not_impl_any;
@ -974,6 +998,38 @@ mod tests {
assert_eq!(ll.peek().unwrap(), &1001);
}
#[test]
#[cfg(feature = "zeroize")]
fn test_sorted_linked_list_zeroize() {
use zeroize::Zeroize;
let mut list: SortedLinkedList<u8, Max, 8, u8> = SortedLinkedList::new_u8();
for i in 1..=8 {
list.push(i).unwrap();
}
assert_eq!(list.is_empty(), false);
assert!(list.is_full());
assert_eq!(list.peek(), Some(&8));
list.pop();
list.pop();
list.push(100).unwrap();
assert_eq!(list.peek(), Some(&100));
list.zeroize();
assert_eq!(list.peek(), None);
assert!(list.is_empty());
unsafe {
for node in &list.list.buffer {
assert_eq!(node.val.assume_init(), 0);
}
}
}
fn _test_variance<'a: 'b, 'b>(
x: SortedLinkedList<&'a (), Max, 42, u8>,
) -> SortedLinkedList<&'b (), Max, 42, u8> {

View File

@ -11,6 +11,9 @@ use core::{
str::{self, Utf8Error},
};
#[cfg(feature = "zeroize")]
use zeroize::Zeroize;
use crate::CapacityError;
use crate::{
len_type::LenType,
@ -142,6 +145,7 @@ pub type ViewStorage = ViewVecStorage<u8>;
///
/// In most cases you should use [`String`] or [`StringView`] directly. Only use this
/// struct if you want to write code that's generic over both.
#[cfg_attr(feature = "zeroize", derive(Zeroize), zeroize(bound = "S: Zeroize"))]
pub struct StringInner<LenT: LenType, S: StringStorage + ?Sized> {
vec: VecInner<u8, LenT, S>,
}
@ -1462,4 +1466,26 @@ mod tests {
let mut s: String<8> = String::try_from("a").unwrap();
_ = s.insert_str(2, "a");
}
#[test]
#[cfg(feature = "zeroize")]
fn test_string_zeroize() {
use zeroize::Zeroize;
let mut s: String<32> = String::try_from("sensitive_password").unwrap();
assert_eq!(s.as_str(), "sensitive_password");
assert!(!s.is_empty());
assert_eq!(s.len(), 18);
s.truncate(9);
assert_eq!(s.as_str(), "sensitive");
assert_eq!(s.len(), 9);
// zeroized using Vec's implementation
s.zeroize();
assert_eq!(s.len(), 0);
assert!(s.is_empty());
}
}

View File

@ -11,6 +11,9 @@ use core::{
slice,
};
#[cfg(feature = "zeroize")]
use zeroize::Zeroize;
use crate::len_type::{check_capacity_fits, LenType};
use crate::CapacityError;
@ -84,7 +87,11 @@ mod storage {
Self: VecStorage<T>;
}
#[cfg(feature = "zeroize")]
use zeroize::Zeroize;
// One sealed layer of indirection to hide the internal details (The MaybeUninit).
#[cfg_attr(feature = "zeroize", derive(Zeroize))]
pub struct VecStorageInner<T: ?Sized> {
pub(crate) buffer: T,
}
@ -208,6 +215,7 @@ pub use drain::Drain;
///
/// In most cases you should use [`Vec`] or [`VecView`] directly. Only use this
/// struct if you want to write code that's generic over both.
#[cfg_attr(feature = "zeroize", derive(Zeroize), zeroize(bound = "S: Zeroize"))]
pub struct VecInner<T, LenT: LenType, S: VecStorage<T> + ?Sized> {
phantom: PhantomData<T>,
len: LenT,
@ -2247,6 +2255,69 @@ mod tests {
<alloc::vec::Vec<u8> as TryInto<Vec<u8, 1>>>::try_into(av.clone()).unwrap_err();
}
#[test]
#[cfg(feature = "zeroize")]
fn test_vec_zeroize() {
use zeroize::Zeroize;
let mut v: Vec<u8, 8> = Vec::new();
for i in 0..8 {
v.push(i).unwrap();
}
for i in 0..8 {
assert_eq!(v[i], i as u8);
}
v.truncate(4);
assert_eq!(v.len(), 4);
for i in 0..4 {
assert_eq!(v[i], i as u8);
}
v.zeroize();
assert_eq!(v.len(), 0);
unsafe {
v.set_len(8);
}
for i in 0..8 {
assert_eq!(v[i], 0);
}
}
#[test]
#[cfg(feature = "zeroize")]
fn test_vecview_zeroize() {
use zeroize::Zeroize;
let mut v: Vec<u8, 8> = Vec::new();
for i in 0..8 {
v.push(i).unwrap();
}
let view = v.as_mut_view();
for i in 0..8 {
assert_eq!(view[i], i as u8);
}
view.zeroize();
assert_eq!(view.len(), 0);
unsafe {
view.set_len(8);
}
for i in 0..8 {
assert_eq!(view[i], 0);
}
}
fn _test_variance<'a: 'b, 'b>(x: Vec<&'a (), 42>) -> Vec<&'b (), 42> {
x
}