secmem_alloc/
zeroizing_alloc.rs

1//! An allocator zeroizing memory on deallocation.
2//!
3//! This module contains a wrapper for any memory allocator to zeroize memory
4//! before deallocation. This allows use both as a [`GlobalAlloc`] and as
5//! [`Allocator`].
6//!
7//! This is safer than zeroizing your secret objects on drop because the
8//! allocator approach also zeroizes old memory when the object is only moved
9//! in memory but not dropped. This can happen for example when resizing
10//! [`Vec`]s.
11
12use crate::macros::{
13    debug_handleallocerror_precondition, debug_handleallocerror_precondition_valid_layout,
14    precondition_memory_range,
15};
16use crate::zeroize::zeroize_mem;
17use alloc::alloc::handle_alloc_error;
18use allocator_api2::alloc::{AllocError, Allocator};
19use core::alloc::{GlobalAlloc, Layout};
20use core::ptr::NonNull;
21
22/// Wrapper around an allocator which zeroizes memory on deallocation. See the
23/// module level documentation.
24///
25/// If debug assertions are enabled, *some* of the safety requirement for using
26/// an allocator are checked.
27#[derive(Debug, Default)]
28pub struct ZeroizeAlloc<A> {
29    /// Allocator used for the actual allocations.
30    backend_alloc: A,
31}
32
33impl<A> ZeroizeAlloc<A> {
34    /// Create a zeroizing allocator using `backend_alloc` for allocations and
35    /// `zeroizer` to zeroize memory upon deallocation.
36    pub const fn new(backend_alloc: A) -> Self {
37        Self { backend_alloc }
38    }
39}
40
41unsafe impl<A: GlobalAlloc> GlobalAlloc for ZeroizeAlloc<A> {
42    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
43        // debug assertions
44        // SAFETY: the allocator is not allowed to unwind (panic!)
45        // check that `layout` is a valid layout
46        debug_handleallocerror_precondition_valid_layout!(layout);
47        // zero sized allocations are not allowed
48        debug_handleallocerror_precondition!(layout.size() != 0, layout);
49
50        // SAFETY: caller must uphold the safety contract of `GlobalAlloc::alloc`.
51        unsafe { self.backend_alloc.alloc(layout) }
52    }
53
54    unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
55        // debug assertions
56        // SAFETY: the allocator is not allowed to unwind (panic!)
57        // null pointers are never allowed
58        debug_handleallocerror_precondition!(!ptr.is_null(), layout);
59        // check that `layout` is a valid layout
60        debug_handleallocerror_precondition_valid_layout!(layout);
61        // zero sized allocations are not allowed
62        debug_handleallocerror_precondition!(layout.size() != 0, layout);
63        // you can't wrap around the address space
64        precondition_memory_range!(ptr, layout.size());
65
66        if cfg!(debug_assertions) {
67            // you can't wrap around the address space
68            if ptr.addr().checked_add(layout.size()).is_none() {
69                handle_alloc_error(layout);
70            }
71        }
72
73        // securely wipe the deallocated memory
74        // SAFETY: `ptr` is valid for writes of `layout.size()` bytes since it was
75        // previously successfully allocated (by the safety assumption on this function)
76        // and not yet deallocated SAFETY: `ptr` is at least `layout.align()`
77        // byte aligned and this is a power of two
78        unsafe {
79            zeroize_mem(ptr, layout.size());
80        }
81        // SAFETY: caller must uphold the safety contract of `GlobalAlloc::dealloc`.
82        unsafe { self.backend_alloc.dealloc(ptr, layout) }
83    }
84
85    unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
86        // debug assertions
87        // SAFETY: the allocator is not allowed to unwind (panic!)
88        // check that `layout` is a valid layout
89        debug_handleallocerror_precondition_valid_layout!(layout);
90        // zero sized allocations are not allowed
91        debug_handleallocerror_precondition!(layout.size() != 0, layout);
92
93        // SAFETY: caller must uphold the safety contract of
94        // `GlobalAlloc::alloc_zeroed`.
95        unsafe { self.backend_alloc.alloc_zeroed(layout) }
96    }
97
98    // We do not use `backend_alloc.realloc` but instead use the default
99    // implementation from `std` (actually `core`), so our zeroizing `dealloc`
100    // is used. This can degrade performance for 'smart' allocators that would
101    // try to reuse the same allocation in realloc.
102    // This is the only safe and secure behaviour we can when using an
103    // arbitrary backend allocator.
104}
105
106unsafe impl<A: Allocator> Allocator for ZeroizeAlloc<A> {
107    fn allocate(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
108        // debug assertions
109        // check that `layout` is a valid layout
110        debug_handleallocerror_precondition_valid_layout!(layout);
111
112        self.backend_alloc.allocate(layout)
113    }
114
115    fn allocate_zeroed(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
116        // debug assertions
117        // check that `layout` is a valid layout
118        debug_handleallocerror_precondition_valid_layout!(layout);
119
120        self.backend_alloc.allocate_zeroed(layout)
121    }
122
123    unsafe fn deallocate(&self, ptr: NonNull<u8>, layout: Layout) {
124        // debug assertions
125        // check that `layout` is a valid layout
126        debug_handleallocerror_precondition_valid_layout!(layout);
127
128        // securely wipe the deallocated memory
129        // SAFETY: `ptr` is valid for writes of `layout.size()` bytes since it was
130        // previously successfully allocated and not yet deallocated
131        // SAFETY: `ptr` is at least `layout.align()` byte aligned and this is a power
132        // of two
133        unsafe {
134            zeroize_mem(ptr.as_ptr(), layout.size());
135        }
136        // SAFETY: caller must uphold the safety contract of `Allocator::deallocate`
137        unsafe { self.backend_alloc.deallocate(ptr, layout) }
138    }
139
140    // We do not use `backend_alloc.grow[_zeroed]/shrink` but instead use the
141    // default implementation from `std` (actually `core`), so our zeroizing
142    // `deallocate` is used. This can degrade performance for 'smart' allocators
143    // that would try to reuse the same allocation for such reallocations.
144    // This is the only safe and secure behaviour we can when using an
145    // arbitrary backend allocator.
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151    use crate::allocator_api::{Box, Vec};
152    use std::alloc::System;
153
154    #[test]
155    fn box_allocation_8b() {
156        let allocator = ZeroizeAlloc::new(System);
157        let _heap_mem = Box::new_in([1u8; 8], &allocator);
158        // drop `_heap_mem`
159        // drop `allocator`
160    }
161
162    #[test]
163    fn box_allocation_9b() {
164        let allocator = ZeroizeAlloc::new(System);
165        let _heap_mem = Box::new_in([1u8; 9], &allocator);
166        // drop `_heap_mem`
167        // drop `allocator`
168    }
169
170    #[test]
171    fn box_allocation_zst() {
172        let allocator = ZeroizeAlloc::new(System);
173        let _heap_mem = Box::new_in([(); 8], &allocator);
174        // drop `_heap_mem`
175        // drop `allocator`
176    }
177
178    #[test]
179    fn vec_allocation_9b() {
180        let allocator = ZeroizeAlloc::new(System);
181        let _heap_mem = Vec::<u8, _>::with_capacity_in(9, &allocator);
182        // drop `_heap_mem`
183        // drop `allocator`
184    }
185
186    #[test]
187    fn vec_allocation_grow_repeated() {
188        let allocator = ZeroizeAlloc::new(System);
189
190        let mut heap_mem = Vec::<u8, _>::with_capacity_in(9, &allocator);
191        heap_mem.reserve(1);
192        heap_mem.reserve(7);
193        // drop `heap_mem`
194        // drop `allocator`
195    }
196
197    #[test]
198    fn vec_allocation_shrink() {
199        let allocator = ZeroizeAlloc::new(System);
200
201        let mut heap_mem = Vec::<u8, _>::with_capacity_in(9, &allocator);
202        heap_mem.push(255);
203        heap_mem.shrink_to_fit();
204        // drop `heap_mem`
205        // drop `allocator`
206    }
207
208    #[test]
209    fn allocate_zeroed() {
210        let allocator = ZeroizeAlloc::new(System);
211
212        let layout = Layout::new::<[u8; 16]>();
213        let ptr = allocator
214            .allocate_zeroed(layout)
215            .expect("allocation failed");
216        for i in 0..16 {
217            let val: u8 = unsafe { (ptr.as_ptr() as *const u8).add(i).read() };
218            assert_eq!(val, 0_u8);
219        }
220        unsafe {
221            allocator.deallocate(ptr.cast(), layout);
222        }
223    }
224}