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}