Skip to content

Commit bbe988d

Browse files
authored
Merge pull request #614 from vishy11/master
Add zeroization support for heapless data structures
2 parents d29f95c + 0b027b1 commit bbe988d

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)