diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7eede1ce..dbb73106 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 }} diff --git a/CHANGELOG.md b/CHANGELOG.md index ddb1d69b..14bb3511 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Cargo.toml b/Cargo.toml index 76e2e0be..7390c86b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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] diff --git a/src/binary_heap.rs b/src/binary_heap.rs index 22b882a1..452b84a1 100644 --- a/src/binary_heap.rs +++ b/src/binary_heap.rs @@ -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 + ?Sized> { pub(crate) _kind: PhantomData, pub(crate) data: VecInner, @@ -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::::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 } diff --git a/src/c_string.rs b/src/c_string.rs index 25df69b7..cf4ce301 100644 --- a/src/c_string.rs +++ b/src/c_string.rs @@ -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 { inner: Vec, } +#[cfg(feature = "zeroize")] +impl Zeroize for CString { + 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 CString { /// Creates a new C-compatible string with a terminating nul byte. /// @@ -483,6 +500,35 @@ mod tests { assert_eq!(Borrow::::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::*; diff --git a/src/deque.rs b/src/deque.rs index f318859e..ec12bbf8 100644 --- a/src/deque.rs +++ b/src/deque.rs @@ -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 + ?Sized> { // This phantomdata is required because otherwise rustc thinks that `T` is not used phantom: PhantomData, @@ -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::::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()); + } } diff --git a/src/history_buf.rs b/src/history_buf.rs index b06e4648..67df4cd7 100644 --- a/src/history_buf.rs +++ b/src/history_buf.rs @@ -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 { 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 + ?Sized> { // This phantomdata is required because otherwise rustc thinks that `T` is not used phantom: PhantomData, @@ -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::::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 } diff --git a/src/index_map.rs b/src/index_map.rs index 34846240..e990c1e0 100644 --- a/src/index_map.rs +++ b/src/index_map.rs @@ -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 = IndexMap, 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 { hash: HashValue, key: K, @@ -88,6 +93,7 @@ pub struct Bucket { #[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` @@ -138,6 +144,11 @@ macro_rules! probe_loop { } } +#[cfg_attr( + feature = "zeroize", + derive(Zeroize), + zeroize(bound = "K: Zeroize, V: Zeroize") +)] struct CoreMap { entries: Vec, N, usize>, indices: [Option; N], @@ -722,8 +733,14 @@ where /// println!("{}: \"{}\"", book, review); /// } /// ``` +#[cfg_attr( + feature = "zeroize", + derive(Zeroize), + zeroize(bound = "K: Zeroize, V: Zeroize") +)] pub struct IndexMap { core: CoreMap, + #[cfg_attr(feature = "zeroize", zeroize(skip))] build_hasher: S, } @@ -1988,4 +2005,28 @@ mod tests { let map: FnvIndexMap = Default::default(); assert_eq!(map, map); } + + #[test] + #[cfg(feature = "zeroize")] + fn test_index_map_zeroize() { + use zeroize::Zeroize; + + let mut map: FnvIndexMap = 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)); + } } diff --git a/src/index_set.rs b/src/index_set.rs index 53fd0ea3..803fa291 100644 --- a/src/index_set.rs +++ b/src/index_set.rs @@ -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 = IndexSet { map: IndexMap, } @@ -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, 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)); + } } diff --git a/src/len_type.rs b/src/len_type.rs index d42a659d..0eb6ff6a 100644 --- a/src/len_type.rs +++ b/src/len_type.rs @@ -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!( diff --git a/src/lib.rs b/src/lib.rs index 656999ee..fd506a92 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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 diff --git a/src/linear_map.rs b/src/linear_map.rs index b7051737..c7f34278 100644 --- a/src/linear_map.rs +++ b/src/linear_map.rs @@ -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 = OwnedVecStorage<(K, V), N>; pub type ViewStorage = 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 + ?Sized> { pub(crate) buffer: VecInner<(K, V), usize, S>, } @@ -752,4 +760,24 @@ mod test { let map: LinearMap = Default::default(); assert_eq!(map, map); } + + #[test] + #[cfg(feature = "zeroize")] + fn test_linear_map_zeroize() { + use zeroize::Zeroize; + + let mut map: LinearMap = 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()); + } } diff --git a/src/sorted_linked_list.rs b/src/sorted_linked_list.rs index d319fe0f..0b40b454 100644 --- a/src/sorted_linked_list.rs +++ b/src/sorted_linked_list.rs @@ -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 { val: MaybeUninit, next: Idx, @@ -845,6 +849,26 @@ where } } +#[cfg(feature = "zeroize")] +impl Zeroize for SortedLinkedListInner +where + T: Ord + Zeroize, + Idx: LenType + Zeroize, + K: Kind, + S: SortedLinkedListStorage + ?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 = 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> { diff --git a/src/string/mod.rs b/src/string/mod.rs index b7f07712..207d35c6 100644 --- a/src/string/mod.rs +++ b/src/string/mod.rs @@ -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; /// /// 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 { vec: VecInner, } @@ -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()); + } } diff --git a/src/vec/mod.rs b/src/vec/mod.rs index 52a5f830..5e916dfb 100644 --- a/src/vec/mod.rs +++ b/src/vec/mod.rs @@ -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; } + #[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 { 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 + ?Sized> { phantom: PhantomData, len: LenT, @@ -2247,6 +2255,69 @@ mod tests { as TryInto>>::try_into(av.clone()).unwrap_err(); } + #[test] + #[cfg(feature = "zeroize")] + fn test_vec_zeroize() { + use zeroize::Zeroize; + + let mut v: Vec = 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 = 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 }