redoubt_zero_core/
collections.rs

1// Copyright (c) 2025-2026 Federico Hoerth <memparanoid@gmail.com>
2// SPDX-License-Identifier: GPL-3.0-only
3// See LICENSE in the repository root for full license text.
4
5//! Trait implementations and helpers for collections (slices, arrays, `Vec<T>`).
6use alloc::string::String;
7use alloc::vec::Vec;
8
9use core::sync::atomic::{Ordering, compiler_fence};
10
11use super::traits::{FastZeroizable, ZeroizationProbe, ZeroizeMetadata};
12
13/// Converts a mutable reference to a trait object (`&mut dyn FastZeroizable`).
14///
15/// Helper for working with heterogeneous collections where elements implement
16/// `FastZeroizable` but may have different concrete types.
17#[inline(always)]
18pub fn to_fast_zeroizable_dyn_mut<'a, T: FastZeroizable>(
19    x: &'a mut T,
20) -> &'a mut (dyn FastZeroizable + 'a) {
21    x
22}
23
24/// Converts a reference to a trait object (`&dyn ZeroizationProbe`).
25///
26/// Helper for working with heterogeneous collections where elements implement
27/// `ZeroizationProbe` but may have different concrete types.
28#[inline(always)]
29pub fn to_zeroization_probe_dyn_ref<'a, T: ZeroizationProbe>(
30    x: &'a T,
31) -> &'a (dyn ZeroizationProbe + 'a) {
32    x
33}
34
35/// Zeroizes all elements in a collection via an iterator.
36///
37/// Iterates over `&mut dyn FastZeroizable` and calls `.fast_zeroize()` on each element.
38pub fn zeroize_collection(collection_iter: &mut dyn Iterator<Item = &mut dyn FastZeroizable>) {
39    for z in collection_iter {
40        z.fast_zeroize();
41        compiler_fence(Ordering::SeqCst);
42    }
43}
44
45/// Checks if all elements in a collection are zeroized.
46///
47/// Returns `true` if all elements return `true` for `.is_zeroized()`, `false` otherwise.
48pub fn collection_zeroed(collection_iter: &mut dyn Iterator<Item = &dyn ZeroizationProbe>) -> bool {
49    for z in collection_iter {
50        if !z.is_zeroized() {
51            return false;
52        }
53    }
54
55    true
56}
57
58// === === === === === === === === === ===
59// [T] - slices
60// === === === === === === === === === ===
61/// Zeroizes an array using either bulk memset or recursive element zeroization.
62///
63/// When `fast=true`, forces bulk memset regardless of `T::CAN_BE_BULK_ZEROIZED`.
64/// When `fast=false`, recursively zeroizes each element.
65///
66/// This function is exposed for testing both code paths independently.
67#[inline(always)]
68pub(crate) fn slice_fast_zeroize<T: FastZeroizable + ZeroizeMetadata>(slice: &mut [T], fast: bool) {
69    if fast {
70        // Fast path: bulk zeroize the entire array
71        redoubt_util::fast_zeroize_slice(slice);
72        compiler_fence(Ordering::SeqCst);
73    } else {
74        // Slow path: recursively zeroize each element
75        for elem in slice.iter_mut() {
76            elem.fast_zeroize();
77            compiler_fence(Ordering::SeqCst);
78        }
79    }
80}
81
82impl<T> ZeroizeMetadata for [T]
83where
84    T: FastZeroizable + ZeroizeMetadata,
85{
86    const CAN_BE_BULK_ZEROIZED: bool = T::CAN_BE_BULK_ZEROIZED;
87}
88
89impl<T> FastZeroizable for [T]
90where
91    T: FastZeroizable + ZeroizeMetadata,
92{
93    fn fast_zeroize(&mut self) {
94        slice_fast_zeroize(self, T::CAN_BE_BULK_ZEROIZED);
95    }
96}
97
98impl<T> ZeroizationProbe for [T]
99where
100    T: ZeroizeMetadata + FastZeroizable + ZeroizationProbe,
101{
102    fn is_zeroized(&self) -> bool {
103        collection_zeroed(&mut self.iter().map(to_zeroization_probe_dyn_ref))
104    }
105}
106
107// === === === === === === === === === ===
108// [T; N] - arrays
109// === === === === === === === === === ===
110
111/// Zeroizes an array using either bulk memset or recursive element zeroization.
112///
113/// When `fast=true`, forces bulk memset regardless of `T::CAN_BE_BULK_ZEROIZED`.
114/// When `fast=false`, recursively zeroizes each element.
115///
116/// This function is exposed for testing both code paths independently.
117impl<T: ZeroizeMetadata, const N: usize> ZeroizeMetadata for [T; N] {
118    // Arrays inherit bulk-zeroize capability from their element type
119    const CAN_BE_BULK_ZEROIZED: bool = T::CAN_BE_BULK_ZEROIZED;
120}
121
122impl<T: ZeroizeMetadata + FastZeroizable, const N: usize> FastZeroizable for [T; N] {
123    #[inline(always)]
124    fn fast_zeroize(&mut self) {
125        slice_fast_zeroize(self, T::CAN_BE_BULK_ZEROIZED);
126    }
127}
128
129impl<T, const N: usize> ZeroizationProbe for [T; N]
130where
131    T: ZeroizationProbe,
132{
133    fn is_zeroized(&self) -> bool {
134        collection_zeroed(&mut self.iter().map(to_zeroization_probe_dyn_ref))
135    }
136}
137
138// === === === === === === === === === ===
139// Vec<T>
140// === === === === === === === === === ===
141
142/// Zeroizes a Vec using either bulk memset or recursive element zeroization.
143///
144/// When `fast=true`, forces bulk memset of entire allocation (contents + spare capacity).
145/// When `fast=false`, recursively zeroizes each element, then spare capacity.
146///
147/// This function is exposed for testing both code paths independently.
148#[inline(always)]
149pub(crate) fn vec_fast_zeroize<T: FastZeroizable + ZeroizeMetadata>(vec: &mut Vec<T>, fast: bool) {
150    if fast {
151        // T is primitive: fast zeroize entire allocation (contents + spare capacity)
152        redoubt_util::fast_zeroize_vec(vec);
153        compiler_fence(Ordering::SeqCst);
154    } else {
155        // T is complex: recursively zeroize each element, then spare capacity
156        for elem in vec.iter_mut() {
157            elem.fast_zeroize();
158            compiler_fence(Ordering::SeqCst);
159        }
160        redoubt_util::zeroize_spare_capacity(vec);
161        compiler_fence(Ordering::SeqCst);
162    }
163}
164
165impl<T: ZeroizeMetadata> ZeroizeMetadata for Vec<T> {
166    // Vec can NEVER be bulk-zeroized from outside (has ptr/len/capacity)
167    const CAN_BE_BULK_ZEROIZED: bool = false;
168}
169
170impl<T: ZeroizeMetadata + FastZeroizable> FastZeroizable for Vec<T> {
171    #[inline(always)]
172    fn fast_zeroize(&mut self) {
173        vec_fast_zeroize(self, T::CAN_BE_BULK_ZEROIZED);
174    }
175}
176
177impl<T> ZeroizationProbe for Vec<T>
178where
179    T: ZeroizationProbe,
180{
181    /// Returns true if all elements AND spare capacity are zeroed.
182    ///
183    /// # Important
184    /// This is only meaningful after calling `fast_zeroize()`.
185    /// A freshly allocated Vec may have uninitialized memory in
186    /// spare capacity, causing this to return false even if no
187    /// sensitive data was ever stored.
188    fn is_zeroized(&self) -> bool {
189        // Short-circuit: check elements first (cheaper for complex types)
190        collection_zeroed(&mut self.iter().map(to_zeroization_probe_dyn_ref))
191            && redoubt_util::is_spare_capacity_zeroized(self)
192    }
193}
194
195// === === === === === === === === === ===
196// String
197// === === === === === === === === === ===
198impl ZeroizeMetadata for String {
199    // String can NEVER be bulk-zeroized from outside (has ptr/len/capacity)
200    const CAN_BE_BULK_ZEROIZED: bool = false;
201}
202
203impl FastZeroizable for String {
204    #[inline(always)]
205    fn fast_zeroize(&mut self) {
206        // Safety: String is Vec<u8> internally, and u8::CAN_BE_BULK_ZEROIZED = true
207        // SAFETY: This is sound because we're treating String as Vec<u8>
208        unsafe {
209            let vec_bytes = self.as_mut_vec();
210            redoubt_util::fast_zeroize_vec(vec_bytes);
211        }
212    }
213}
214
215impl ZeroizationProbe for String {
216    fn is_zeroized(&self) -> bool {
217        redoubt_util::is_slice_zeroized(self.as_bytes())
218    }
219}
220
221// Blanket impls for Box<T>
222impl<T: ZeroizeMetadata + FastZeroizable> ZeroizeMetadata for alloc::boxed::Box<T> {
223    const CAN_BE_BULK_ZEROIZED: bool = T::CAN_BE_BULK_ZEROIZED;
224}
225
226impl<T: FastZeroizable> FastZeroizable for alloc::boxed::Box<T> {
227    #[inline(always)]
228    fn fast_zeroize(&mut self) {
229        (**self).fast_zeroize();
230    }
231}
232
233impl<T: ZeroizationProbe> ZeroizationProbe for alloc::boxed::Box<T> {
234    fn is_zeroized(&self) -> bool {
235        (**self).is_zeroized()
236    }
237}
238
239// Blanket impls for Option<T>
240// Option has discriminant/tag that requires proper handling, cannot bulk zeroize
241impl<T: ZeroizeMetadata + FastZeroizable> ZeroizeMetadata for Option<T> {
242    const CAN_BE_BULK_ZEROIZED: bool = false;
243}
244
245impl<T: FastZeroizable> FastZeroizable for Option<T> {
246    #[inline(always)]
247    fn fast_zeroize(&mut self) {
248        if let Some(val) = self {
249            val.fast_zeroize();
250        }
251        // Zeroize the discriminant by setting to None
252        *self = None;
253    }
254}
255
256impl<T: ZeroizationProbe> ZeroizationProbe for Option<T> {
257    fn is_zeroized(&self) -> bool {
258        match self {
259            Some(val) => val.is_zeroized(),
260            None => true, // None is considered zeroized
261        }
262    }
263}