Botan 3.9.0
Crypto and TLS for C&
mem_pool.cpp
Go to the documentation of this file.
1/*
2* (C) 2018,2019 Jack Lloyd
3*
4* Botan is released under the Simplified BSD License (see license.txt)
5*/
6
7#include <botan/internal/mem_pool.h>
8
9#include <botan/mem_ops.h>
10#include <botan/internal/target_info.h>
11#include <algorithm>
12#include <optional>
13
14#if defined(BOTAN_HAS_VALGRIND) || defined(BOTAN_ENABLE_DEBUG_ASSERTS)
15 /**
16 * @brief Prohibits access to unused memory pages in Botan's memory pool
17 *
18 * If BOTAN_MEM_POOL_USE_MMU_PROTECTIONS is defined, the Memory_Pool
19 * class used for mlock'ed memory will use OS calls to set page
20 * permissions so as to prohibit access to pages on the free list, then
21 * enable read/write access when the page is set to be used. This will
22 * turn (some) use after free bugs into a crash.
23 *
24 * The additional syscalls have a substantial performance impact, which
25 * is why this option is not enabled by default. It is used when built for
26 * running in valgrind or debug assertions are enabled.
27 */
28 #define BOTAN_MEM_POOL_USE_MMU_PROTECTIONS
29#endif
30
31#if defined(BOTAN_MEM_POOL_USE_MMU_PROTECTIONS) && defined(BOTAN_HAS_OS_UTILS)
32 #include <botan/internal/os_utils.h>
33#endif
34
35namespace Botan {
36
37/*
38* Memory pool theory of operation
39*
40* This allocator is not useful for general purpose but works well within the
41* context of allocating cryptographic keys. It makes several assumptions which
42* don't work for implementing malloc but simplify and speed up the implementation:
43*
44* - There is some set of pages, which cannot be expanded later. These are pages
45* which were allocated, mlocked and passed to the Memory_Pool constructor.
46*
47* - The allocator is allowed to return null anytime it feels like not servicing
48* a request, in which case the request will be sent to calloc instead. In
49* particular, requests which are too small or too large are rejected.
50*
51* - Most allocations are powers of 2, the remainder are usually a multiple of 8
52*
53* - Free requests include the size of the allocation, so there is no need to
54* track this within the pool.
55*
56* - Alignment is important to the caller. For this allocator, any allocation of
57* size N is aligned evenly at N bytes.
58*
59* Initially each page is in the free page list. Each page is used for just one
60* size of allocation, with requests bucketed into a small number of common
61* sizes. If the allocation would be too big or too small it is rejected by the pool.
62*
63* The free list is maintained by a bitmap, one per page/Bucket. Since each
64* Bucket only maintains objects of a single size, each bit set or clear
65* indicates the status of one object.
66*
67* An allocation walks the list of buckets and asks each in turn if there is
68* space. If a Bucket does not have any space, it sets a boolean flag m_is_full
69* so that it does not need to rescan when asked again. The flag is cleared on
70* first free from that bucket. If no bucket has space, but there are some free
71* pages left, a free page is claimed as a new Bucket for that size. In this case
72* it is pushed to the front of the list so it is first in line to service new
73* requests.
74*
75* A deallocation also walks the list of buckets for the size and asks each
76* Bucket in turn if it recognizes the pointer. When a Bucket becomes empty as a
77* result of a deallocation, it is recycled back into the free pool. When this
78* happens, the Buckets page goes to the end of the free list. All pages on the
79* free list are marked in the MMU as noaccess, so anything touching them will
80* immediately crash. They are only marked R/W once placed into a new bucket.
81* Making the free list FIFO maximizes the time between the last free of a bucket
82* and that page being writable again, maximizing chances of crashing after a
83* use-after-free.
84*
85* Future work
86* -------------
87*
88* The allocator is protected by a global lock. It would be good to break this
89* up, since almost all of the work can actually be done in parallel especially
90* when allocating objects of different sizes (which can't possibly share a
91* bucket).
92*
93* It may be worthwhile to optimize deallocation by storing the Buckets in order
94* (by pointer value) which would allow binary search to find the owning bucket.
95*
96* A useful addition would be to randomize the allocations. Memory_Pool would be
97* changed to receive also a RandomNumberGenerator& object (presumably the system
98* RNG, or maybe a ChaCha_RNG seeded with system RNG). Then the bucket to use and
99* the offset within the bucket would be chosen randomly, instead of using first fit.
100*
101* Right now we don't make any provision for threading, so if two threads both
102* allocate 32 byte values one after the other, the two allocations will likely
103* share a cache line. Ensuring that distinct threads will (tend to) use distinct
104* buckets would reduce this.
105*
106* Supporting a realloc-style API may be useful.
107*/
108
109namespace {
110
111size_t choose_bucket(size_t n) {
112 constexpr size_t MINIMUM_ALLOCATION = 16;
113 constexpr size_t MAXIMUM_ALLOCATION = 256;
114
115 if(n < MINIMUM_ALLOCATION || n > MAXIMUM_ALLOCATION) {
116 return 0;
117 }
118
119 // Need to tune these
120
121 constexpr size_t buckets[] = {
122 16,
123 24,
124 32,
125 48,
126 64,
127 80,
128 96,
129 112,
130 128,
131 160,
132 192,
133 256,
134 0,
135 };
136
137 for(size_t i = 0; buckets[i] != 0; ++i) {
138 if(n <= buckets[i]) {
139 return buckets[i];
140 }
141 }
142
143 return 0;
144}
145
146inline bool ptr_in_pool(const void* pool_ptr, size_t poolsize, const void* buf_ptr, size_t bufsize) {
147 const uintptr_t pool = reinterpret_cast<uintptr_t>(pool_ptr);
148 const uintptr_t buf = reinterpret_cast<uintptr_t>(buf_ptr);
149 return (buf >= pool) && (buf + bufsize <= pool + poolsize);
150}
151
152// return index of first set bit
153template <typename T>
154size_t find_set_bit(T b) {
155 size_t s = 8 * sizeof(T) / 2;
156 size_t bit = 0;
157
158 // In this context we don't need to be const-time
159 while(s > 0) {
160 const T mask = (static_cast<T>(1) << s) - 1;
161 if((b & mask) == 0) {
162 bit += s;
163 b >>= s;
164 }
165 s /= 2;
166 }
167
168 return bit;
169}
170
171class BitMap final {
172 public:
173 explicit BitMap(size_t bits) : m_len(bits) {
174 m_bits.resize((bits + BITMASK_BITS - 1) / BITMASK_BITS);
175
176 if(bits % BITMASK_BITS != 0) {
177 m_last_mask = (static_cast<bitmask_type>(1) << (bits % BITMASK_BITS)) - 1;
178 }
179 }
180
181 std::optional<size_t> find_free();
182
183 void free(size_t bit) {
184 BOTAN_ASSERT_NOMSG(bit <= m_len);
185 const size_t w = bit / BITMASK_BITS;
186 BOTAN_ASSERT_NOMSG(w < m_bits.size());
187 const bitmask_type mask = static_cast<bitmask_type>(1) << (bit % BITMASK_BITS);
188 m_bits[w] = m_bits[w] & (~mask);
189 }
190
191 bool empty() const {
192 for(auto bitset : m_bits) {
193 if(bitset != 0) {
194 return false;
195 }
196 }
197
198 return true;
199 }
200
201 private:
202#if defined(BOTAN_ENABLE_DEBUG_ASSERTS)
203 using bitmask_type = uint8_t;
204#else
205 using bitmask_type = word;
206#endif
207
208 static const size_t BITMASK_BITS = sizeof(bitmask_type) * 8;
209
210 size_t m_len;
211 // MSVC warns if the cast isn't there, clang-tidy warns that the cast is pointless
212 bitmask_type m_main_mask = static_cast<bitmask_type>(~0); // NOLINT(bugprone-misplaced-widening-cast)
213 bitmask_type m_last_mask = static_cast<bitmask_type>(~0); // NOLINT(bugprone-misplaced-widening-cast)
214 std::vector<bitmask_type> m_bits;
215};
216
217std::optional<size_t> BitMap::find_free() {
218 for(size_t i = 0; i != m_bits.size(); ++i) {
219 const bitmask_type mask = (i == m_bits.size() - 1) ? m_last_mask : m_main_mask;
220 if((m_bits[i] & mask) != mask) {
221 const size_t free_bit = find_set_bit(~m_bits[i]);
222 const bitmask_type bmask = static_cast<bitmask_type>(1) << (free_bit % BITMASK_BITS);
223 BOTAN_ASSERT_NOMSG((m_bits[i] & bmask) == 0);
224 m_bits[i] |= bmask;
225 return BITMASK_BITS * i + free_bit;
226 }
227 }
228
229 return {};
230}
231
232} // namespace
233
234class Bucket final {
235 public:
236 Bucket(uint8_t* mem, size_t mem_size, size_t item_size) :
237 m_item_size(item_size),
238 m_page_size(mem_size),
239 m_range(mem),
240 m_bitmap(mem_size / item_size),
241 m_is_full(false) {}
242
243 uint8_t* alloc() {
244 if(m_is_full) {
245 // I know I am full
246 return nullptr;
247 }
248
249 auto offset = m_bitmap.find_free();
250 if(!offset) {
251 // I just found out I am full
252 m_is_full = true;
253 return nullptr;
254 } else {
255 BOTAN_ASSERT(*offset * m_item_size < m_page_size, "Offset is in range");
256 return m_range + m_item_size * (*offset);
257 }
258 }
259
260 bool free(void* p) {
261 if(!in_this_bucket(p)) {
262 return false;
263 }
264
265 /*
266 Zero also any trailing bytes, which should not have been written to,
267 but maybe the user was bad and wrote past the end.
268 */
269 std::memset(p, 0, m_item_size);
270
271 const size_t offset = (reinterpret_cast<uintptr_t>(p) - reinterpret_cast<uintptr_t>(m_range)) / m_item_size;
272
273 m_bitmap.free(offset);
274 m_is_full = false;
275
276 return true;
277 }
278
279 bool in_this_bucket(void* p) const { return ptr_in_pool(m_range, m_page_size, p, m_item_size); }
280
281 bool empty() const { return m_bitmap.empty(); }
282
283 uint8_t* ptr() const { return m_range; }
284
285 private:
286 size_t m_item_size;
287 size_t m_page_size;
288 uint8_t* m_range;
289 BitMap m_bitmap;
290 bool m_is_full;
291};
292
293Memory_Pool::Memory_Pool(const std::vector<void*>& pages, size_t page_size) noexcept : m_page_size(page_size) {
294 for(auto* page : pages) {
295 const uintptr_t p = reinterpret_cast<uintptr_t>(page);
296
297 m_min_page_ptr = std::min(p, m_min_page_ptr);
298 m_max_page_ptr = std::max(p, m_max_page_ptr);
299
300 clear_bytes(page, m_page_size);
301#if defined(BOTAN_MEM_POOL_USE_MMU_PROTECTIONS)
303#endif
304 m_free_pages.push_back(static_cast<uint8_t*>(page));
305 }
306
307 /*
308 Right now this points to the start of the last page, adjust it to instead
309 point to the first byte of the following page
310 */
311 m_max_page_ptr += page_size;
312}
313
314Memory_Pool::~Memory_Pool() noexcept // NOLINT(*-use-equals-default)
315{
316#if defined(BOTAN_MEM_POOL_USE_MMU_PROTECTIONS)
317 for(size_t i = 0; i != m_free_pages.size(); ++i) {
318 OS::page_allow_access(m_free_pages[i]);
319 }
320#endif
321}
322
323void* Memory_Pool::allocate(size_t n) {
324 if(n > m_page_size) {
325 return nullptr;
326 }
327
328 const size_t n_bucket = choose_bucket(n);
329
330 if(n_bucket > 0) {
332
333 std::deque<Bucket>& buckets = m_buckets_for[n_bucket];
334
335 /*
336 It would be optimal to pick the bucket with the most usage,
337 since a bucket with say 1 item allocated out of it has a high
338 chance of becoming later freed and then the whole page can be
339 recycled.
340 */
341 for(auto& bucket : buckets) {
342 if(uint8_t* p = bucket.alloc()) {
343 return p;
344 }
345
346 // If the bucket is full, maybe move it to the end of the list?
347 // Otoh bucket search should be very fast
348 }
349
350 if(!m_free_pages.empty()) {
351 uint8_t* ptr = m_free_pages[0];
352 m_free_pages.pop_front();
353#if defined(BOTAN_MEM_POOL_USE_MMU_PROTECTIONS)
355#endif
356 buckets.push_front(Bucket(ptr, m_page_size, n_bucket));
357 void* p = buckets[0].alloc();
358 BOTAN_ASSERT_NOMSG(p != nullptr);
359 return p;
360 }
361 }
362
363 // out of room
364 return nullptr;
365}
366
367bool Memory_Pool::deallocate(void* p, size_t len) noexcept {
368 // Do a fast range check first, before taking the lock
369 const uintptr_t p_val = reinterpret_cast<uintptr_t>(p);
370 if(p_val < m_min_page_ptr || p_val > m_max_page_ptr) {
371 return false;
372 }
373
374 const size_t n_bucket = choose_bucket(len);
375
376 if(n_bucket != 0) {
377 try {
379
380 std::deque<Bucket>& buckets = m_buckets_for[n_bucket];
381
382 for(size_t i = 0; i != buckets.size(); ++i) {
383 Bucket& bucket = buckets[i];
384 if(bucket.free(p)) {
385 if(bucket.empty()) {
386#if defined(BOTAN_MEM_POOL_USE_MMU_PROTECTIONS)
387 OS::page_prohibit_access(bucket.ptr());
388#endif
389 m_free_pages.push_back(bucket.ptr());
390
391 if(i != buckets.size() - 1) {
392 std::swap(buckets.back(), buckets[i]);
393 }
394 buckets.pop_back();
395 }
396 return true;
397 }
398 }
399 } catch(...) {
400 /*
401 * The only exception throws that can occur in the above code are from
402 * either the STL or BOTAN_ASSERT failures. In either case, such an
403 * error indicates a logic error or data corruption in the memory
404 * allocator such that it is no longer safe to continue executing.
405 *
406 * Since this function is noexcept, simply letting the exception escape
407 * is sufficient for terminate to be called. However in this scenario
408 * it is implementation defined if any stack unwinding is performed.
409 * Since stack unwinding could cause further memory deallocations this
410 * could result in further corruption in this allocator state. To prevent
411 * this, call terminate directly.
412 */
413 std::terminate();
414 }
415 }
416
417 return false;
418}
419
420} // namespace Botan
#define BOTAN_ASSERT_NOMSG(expr)
Definition assert.h:75
#define BOTAN_ASSERT(expr, assertion_made)
Definition assert.h:62
~Memory_Pool() noexcept
Definition mem_pool.cpp:314
bool deallocate(void *p, size_t size) noexcept
Definition mem_pool.cpp:367
void * allocate(size_t size)
Definition mem_pool.cpp:323
Memory_Pool(const std::vector< void * > &pages, size_t page_size) noexcept
Definition mem_pool.cpp:293
void page_allow_access(void *page)
Definition os_utils.cpp:609
void page_prohibit_access(void *page)
Definition os_utils.cpp:623
secure_vector< T > lock(const std::vector< T > &in)
Definition secmem.h:81
constexpr void clear_bytes(void *ptr, size_t bytes)
Definition mem_ops.h:102
lock_guard< T > lock_guard_type
Definition mutex.h:55
std::conditional_t< HasNative64BitRegisters, std::uint64_t, uint32_t > word
Definition types.h:119