Merge pull request #614 from vishy11/master

Add zeroization support for heapless data structures
This commit is contained in:
Soso 2025-09-24 17:26:58 +00:00 committed by GitHub
commit bbe988d87f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
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
}