Skip to content

Commit 0b027b1

Browse files
zeenixVishal Shenoy
authored andcommitted
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.
1 parent d29f95c commit 0b027b1

File tree

15 files changed

+422
-4
lines changed

15 files changed

+422
-4
lines changed

.github/workflows/build.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ jobs:
4848
components: miri
4949

5050
- name: Run miri
51-
run: MIRIFLAGS=-Zmiri-ignore-leaks cargo miri test --features="alloc,defmt,mpmc_large,portable-atomic-critical-section,serde,ufmt,bytes"
51+
run: MIRIFLAGS=-Zmiri-ignore-leaks cargo miri test --features="alloc,defmt,mpmc_large,portable-atomic-critical-section,serde,ufmt,bytes,zeroize"
5252

5353
# Run cargo test
5454
test:
@@ -84,7 +84,7 @@ jobs:
8484
toolchain: stable
8585

8686
- name: Run cargo test
87-
run: cargo test --features="alloc,defmt,mpmc_large,portable-atomic-critical-section,serde,ufmt,bytes"
87+
run: cargo test --features="alloc,defmt,mpmc_large,portable-atomic-critical-section,serde,ufmt,bytes,zeroize"
8888

8989
# Run cargo fmt --check
9090
style:
@@ -176,6 +176,7 @@ jobs:
176176
cargo check --target="${target}" --features="portable-atomic-critical-section"
177177
cargo check --target="${target}" --features="serde"
178178
cargo check --target="${target}" --features="ufmt"
179+
cargo check --target="${target}" --features="zeroize"
179180
env:
180181
target: ${{ matrix.target }}
181182

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1313
- Implement `defmt::Format` for `CapacityError`.
1414
- Implement `TryFrom` for `Deque` from array.
1515
- Switch from `serde` to `serde_core` for enabling faster compilations.
16+
- Implement `Zeroize` trait for all data structures with the `zeroize` feature to securely clear sensitive data from memory.
1617

1718
## [v0.9.1] - 2025-08-19
1819

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ ufmt = ["dep:ufmt", "dep:ufmt-write"]
4747
# Implement `defmt::Format`.
4848
defmt = ["dep:defmt"]
4949

50+
# Implement `zeroize::Zeroize` trait.
51+
zeroize = ["dep:zeroize"]
52+
5053
# Enable larger MPMC sizes.
5154
mpmc_large = []
5255

@@ -63,6 +66,7 @@ serde_core = { version = "1", optional = true, default-features = false }
6366
ufmt = { version = "0.2", optional = true }
6467
ufmt-write = { version = "0.1", optional = true }
6568
defmt = { version = "1.0.1", optional = true }
69+
zeroize = { version = "1.8", optional = true, default-features = false, features = ["derive"] }
6670

6771
# for the pool module
6872
[target.'cfg(any(target_arch = "arm", target_pointer_width = "32", target_pointer_width = "64"))'.dependencies]

src/binary_heap.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ use core::{
1717
ptr, slice,
1818
};
1919

20+
#[cfg(feature = "zeroize")]
21+
use zeroize::Zeroize;
22+
2023
use crate::vec::{OwnedVecStorage, Vec, VecInner, VecStorage, ViewVecStorage};
2124

2225
/// Min-heap
@@ -55,6 +58,11 @@ impl private::Sealed for Min {}
5558
///
5659
/// In most cases you should use [`BinaryHeap`] or [`BinaryHeapView`] directly. Only use this
5760
/// struct if you want to write code that's generic over both.
61+
#[cfg_attr(
62+
feature = "zeroize",
63+
derive(Zeroize),
64+
zeroize(bound = "T: Zeroize, S: Zeroize")
65+
)]
5866
pub struct BinaryHeapInner<T, K, S: VecStorage<T> + ?Sized> {
5967
pub(crate) _kind: PhantomData<K>,
6068
pub(crate) data: VecInner<T, usize, S>,
@@ -882,6 +890,24 @@ mod tests {
882890
assert_eq!(heap.pop(), None);
883891
}
884892

893+
#[test]
894+
#[cfg(feature = "zeroize")]
895+
fn test_binary_heap_zeroize() {
896+
use zeroize::Zeroize;
897+
898+
let mut heap = BinaryHeap::<u8, Max, 8>::new();
899+
for i in 0..8 {
900+
heap.push(i).unwrap();
901+
}
902+
903+
assert_eq!(heap.len(), 8);
904+
assert_eq!(heap.peek(), Some(&7));
905+
906+
// zeroized using Vec's implementation
907+
heap.zeroize();
908+
assert_eq!(heap.len(), 0);
909+
}
910+
885911
fn _test_variance<'a: 'b, 'b>(x: BinaryHeap<&'a (), Max, 42>) -> BinaryHeap<&'b (), Max, 42> {
886912
x
887913
}

src/c_string.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ use core::{
1010
ops::Deref,
1111
};
1212

13+
#[cfg(feature = "zeroize")]
14+
use zeroize::Zeroize;
15+
1316
/// A fixed capacity [`CString`](https://doc.rust-lang.org/std/ffi/struct.CString.html).
1417
///
1518
/// 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> {
1821
inner: Vec<u8, N, LenT>,
1922
}
2023

24+
#[cfg(feature = "zeroize")]
25+
impl<const N: usize, LenT: LenType> Zeroize for CString<N, LenT> {
26+
fn zeroize(&mut self) {
27+
self.inner.zeroize();
28+
29+
const {
30+
assert!(N > 0);
31+
}
32+
33+
// SAFETY: We just asserted that `N > 0`.
34+
unsafe { self.inner.push_unchecked(b'\0') };
35+
}
36+
}
37+
2138
impl<const N: usize, LenT: LenType> CString<N, LenT> {
2239
/// Creates a new C-compatible string with a terminating nul byte.
2340
///
@@ -483,6 +500,35 @@ mod tests {
483500
assert_eq!(Borrow::<CStr>::borrow(&string), c"foo");
484501
}
485502

503+
#[test]
504+
#[cfg(feature = "zeroize")]
505+
fn test_cstring_zeroize() {
506+
use zeroize::Zeroize;
507+
508+
let mut c_string = CString::<32>::from_bytes_with_nul(b"sensitive_password\0").unwrap();
509+
510+
assert_eq!(c_string.to_str(), Ok("sensitive_password"));
511+
assert!(!c_string.to_bytes().is_empty());
512+
let original_length = c_string.to_bytes().len();
513+
assert_eq!(original_length, 18);
514+
515+
let new_string = CString::<32>::from_bytes_with_nul(b"short\0").unwrap();
516+
c_string = new_string;
517+
518+
assert_eq!(c_string.to_str(), Ok("short"));
519+
assert_eq!(c_string.to_bytes().len(), 5);
520+
521+
// zeroized using Vec's implementation
522+
c_string.zeroize();
523+
524+
assert_eq!(c_string.to_bytes().len(), 0);
525+
assert_eq!(c_string.to_bytes_with_nul(), &[0]);
526+
527+
c_string.extend_from_bytes(b"new_data").unwrap();
528+
assert_eq!(c_string.to_str(), Ok("new_data"));
529+
assert_eq!(c_string.to_bytes().len(), 8);
530+
}
531+
486532
mod equality {
487533
use super::*;
488534

src/deque.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,14 @@ use core::marker::PhantomData;
4242
use core::mem::{ManuallyDrop, MaybeUninit};
4343
use core::{ptr, slice};
4444

45+
#[cfg(feature = "zeroize")]
46+
use zeroize::Zeroize;
47+
4548
/// Base struct for [`Deque`] and [`DequeView`], generic over the [`VecStorage`].
4649
///
4750
/// In most cases you should use [`Deque`] or [`DequeView`] directly. Only use this
4851
/// struct if you want to write code that's generic over both.
52+
#[cfg_attr(feature = "zeroize", derive(Zeroize))]
4953
pub struct DequeInner<T, S: VecStorage<T> + ?Sized> {
5054
// This phantomdata is required because otherwise rustc thinks that `T` is not used
5155
phantom: PhantomData<T>,
@@ -1674,4 +1678,29 @@ mod tests {
16741678

16751679
assert_eq!(Droppable::count(), 0);
16761680
}
1681+
1682+
#[test]
1683+
#[cfg(feature = "zeroize")]
1684+
fn test_deque_zeroize() {
1685+
use zeroize::Zeroize;
1686+
1687+
let mut deque = Deque::<u8, 16>::new();
1688+
1689+
for i in 1..=8 {
1690+
deque.push_back(i).unwrap();
1691+
}
1692+
for i in 9..=16 {
1693+
deque.push_front(i).unwrap();
1694+
}
1695+
1696+
assert_eq!(deque.len(), 16);
1697+
assert_eq!(deque.front(), Some(&16));
1698+
assert_eq!(deque.back(), Some(&8));
1699+
1700+
// zeroized using Vec's implementation
1701+
deque.zeroize();
1702+
1703+
assert_eq!(deque.len(), 0);
1704+
assert!(deque.is_empty());
1705+
}
16771706
}

src/history_buf.rs

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,14 @@ use core::ops::Deref;
3838
use core::ptr;
3939
use core::slice;
4040

41-
mod storage {
42-
use core::mem::MaybeUninit;
41+
#[cfg(feature = "zeroize")]
42+
use zeroize::Zeroize;
4343

44+
mod storage {
4445
use super::{HistoryBufInner, HistoryBufView};
46+
use core::mem::MaybeUninit;
47+
#[cfg(feature = "zeroize")]
48+
use zeroize::Zeroize;
4549

4650
/// Trait defining how data for a container is stored.
4751
///
@@ -83,6 +87,7 @@ mod storage {
8387
}
8488

8589
// One sealed layer of indirection to hide the internal details (The MaybeUninit).
90+
#[cfg_attr(feature = "zeroize", derive(Zeroize))]
8691
pub struct HistoryBufStorageInner<T: ?Sized> {
8792
pub(crate) buffer: T,
8893
}
@@ -148,6 +153,7 @@ use self::storage::HistoryBufSealedStorage;
148153
///
149154
/// In most cases you should use [`HistoryBuf`] or [`HistoryBufView`] directly. Only use this
150155
/// struct if you want to write code that's generic over both.
156+
#[cfg_attr(feature = "zeroize", derive(Zeroize))]
151157
pub struct HistoryBufInner<T, S: HistoryBufStorage<T> + ?Sized> {
152158
// This phantomdata is required because otherwise rustc thinks that `T` is not used
153159
phantom: PhantomData<T>,
@@ -938,6 +944,39 @@ mod tests {
938944
assert_eq!(DROP_COUNT.load(Ordering::SeqCst), 3);
939945
}
940946

947+
#[test]
948+
#[cfg(feature = "zeroize")]
949+
fn test_history_buf_zeroize() {
950+
use zeroize::Zeroize;
951+
952+
let mut buffer = HistoryBuf::<u8, 8>::new();
953+
for i in 0..8 {
954+
buffer.write(i);
955+
}
956+
957+
assert_eq!(buffer.len(), 8);
958+
assert_eq!(buffer.recent(), Some(&7));
959+
960+
// Clear to mark formerly used memory as unused, to make sure that it also gets zeroed
961+
buffer.clear();
962+
963+
buffer.write(20);
964+
assert_eq!(buffer.len(), 1);
965+
assert_eq!(buffer.recent(), Some(&20));
966+
967+
buffer.zeroize();
968+
969+
assert_eq!(buffer.len(), 0);
970+
assert!(buffer.is_empty());
971+
972+
// Check that all underlying memory actually got zeroized
973+
unsafe {
974+
for a in buffer.data.buffer {
975+
assert_eq!(a.assume_init(), 0);
976+
}
977+
}
978+
}
979+
941980
fn _test_variance<'a: 'b, 'b>(x: HistoryBuf<&'a (), 42>) -> HistoryBuf<&'b (), 42> {
942981
x
943982
}

src/index_map.rs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ use core::{
88
ops, slice,
99
};
1010

11+
#[cfg(feature = "zeroize")]
12+
use zeroize::Zeroize;
13+
1114
use hash32::{BuildHasherDefault, FnvHasher};
1215

1316
use crate::Vec;
@@ -66,6 +69,7 @@ use crate::Vec;
6669
pub type FnvIndexMap<K, V, const N: usize> = IndexMap<K, V, BuildHasherDefault<FnvHasher>, N>;
6770

6871
#[derive(Clone, Copy, Eq, PartialEq)]
72+
#[cfg_attr(feature = "zeroize", derive(Zeroize))]
6973
struct HashValue(u16);
7074

7175
impl HashValue {
@@ -80,6 +84,7 @@ impl HashValue {
8084

8185
#[doc(hidden)]
8286
#[derive(Clone)]
87+
#[cfg_attr(feature = "zeroize", derive(Zeroize))]
8388
pub struct Bucket<K, V> {
8489
hash: HashValue,
8590
key: K,
@@ -88,6 +93,7 @@ pub struct Bucket<K, V> {
8893

8994
#[doc(hidden)]
9095
#[derive(Clone, Copy, PartialEq)]
96+
#[cfg_attr(feature = "zeroize", derive(Zeroize))]
9197
pub struct Pos {
9298
// compact representation of `{ hash_value: u16, index: u16 }`
9399
// 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 {
138144
}
139145
}
140146

147+
#[cfg_attr(
148+
feature = "zeroize",
149+
derive(Zeroize),
150+
zeroize(bound = "K: Zeroize, V: Zeroize")
151+
)]
141152
struct CoreMap<K, V, const N: usize> {
142153
entries: Vec<Bucket<K, V>, N, usize>,
143154
indices: [Option<Pos>; N],
@@ -722,8 +733,14 @@ where
722733
/// println!("{}: \"{}\"", book, review);
723734
/// }
724735
/// ```
736+
#[cfg_attr(
737+
feature = "zeroize",
738+
derive(Zeroize),
739+
zeroize(bound = "K: Zeroize, V: Zeroize")
740+
)]
725741
pub struct IndexMap<K, V, S, const N: usize> {
726742
core: CoreMap<K, V, N>,
743+
#[cfg_attr(feature = "zeroize", zeroize(skip))]
727744
build_hasher: S,
728745
}
729746

@@ -1988,4 +2005,28 @@ mod tests {
19882005
let map: FnvIndexMap<usize, f32, 4> = Default::default();
19892006
assert_eq!(map, map);
19902007
}
2008+
2009+
#[test]
2010+
#[cfg(feature = "zeroize")]
2011+
fn test_index_map_zeroize() {
2012+
use zeroize::Zeroize;
2013+
2014+
let mut map: FnvIndexMap<u8, u8, 8> = FnvIndexMap::new();
2015+
for i in 1..=8 {
2016+
map.insert(i, i * 10).unwrap();
2017+
}
2018+
2019+
assert_eq!(map.len(), 8);
2020+
assert!(!map.is_empty());
2021+
2022+
// zeroized using Vec's implementation
2023+
map.zeroize();
2024+
2025+
assert_eq!(map.len(), 0);
2026+
assert!(map.is_empty());
2027+
2028+
map.insert(1, 10).unwrap();
2029+
assert_eq!(map.len(), 1);
2030+
assert_eq!(map.get(&1), Some(&10));
2031+
}
19912032
}

0 commit comments

Comments
 (0)