summaryrefslogtreecommitdiffstats
path: root/src/lib/mempool-alloconly.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/mempool-alloconly.c')
-rw-r--r--src/lib/mempool-alloconly.c546
1 files changed, 546 insertions, 0 deletions
diff --git a/src/lib/mempool-alloconly.c b/src/lib/mempool-alloconly.c
new file mode 100644
index 0000000..26f3360
--- /dev/null
+++ b/src/lib/mempool-alloconly.c
@@ -0,0 +1,546 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+/* @UNSAFE: whole file */
+#include "lib.h"
+#include "safe-memset.h"
+#include "mempool.h"
+
+/*
+ * As the name implies, alloconly pools support only allocating memory.
+ * Memory freeing is not supported, except as a special case - the pool's
+ * last allocation can be freed. Additionally, p_realloc() also tries to
+ * grow an existing allocation if and only if it is the last allocation,
+ * otherwise it just allocates a new memory area and copies the data there.
+ *
+ * Alloconly pools are commonly used for an object that builds its state
+ * from many memory allocations, but doesn't change (much of) its state.
+ * It is simpler to free such an object by destroying the entire memory
+ * pool.
+ *
+ * Implementation
+ * ==============
+ *
+ * Each alloconly pool contains a pool structure (struct alloconly_pool) to
+ * keep track of alloconly-specific pool information and one or more blocks
+ * (struct pool_block) that keep track of ranges of memory used to back the
+ * allocations. The blocks are kept in a linked list implementing a stack.
+ * The block size decreases the further down the stack one goes.
+ *
+ * +-----------+
+ * | alloconly |
+ * | pool |
+ * +-----+-----+
+ * |
+ * | block +------------+ next +------------+ next
+ * \------->| pool block |------>| pool block |------>...
+ * +------------+ +------------+
+ * | <data> | | <data> |
+ * . .
+ * . .
+ * . | <data> |
+ * . +------------+
+ * | <data> |
+ * +------------+
+ *
+ * Creation
+ * --------
+ *
+ * When an alloconly pool is created, one block is allocated. This block is
+ * large enough to hold the necessary internal structures (struct
+ * alloconly_pool and struct pool_block) and still have enough space to
+ * satisfy allocations for at least the amount of space requested by the
+ * consumer via the size argument to pool_alloconly_create().
+ *
+ * Allocation
+ * ----------
+ *
+ * Each allocation (via p_malloc()) checks the top-most block to see whether
+ * or not it has enough space to satisfy the allocation. If there is not
+ * enough space, it allocates a new block (via block_alloc()) to serve as
+ * the new top-most block. This newly-allocated block is guaranteed to have
+ * enough space for the allocation. Then, regardless of whether or not a
+ * new block was allocated, the allocation code reserves enough space in the
+ * top-most block for the allocation and returns a pointer to it to the
+ * caller.
+ *
+ * The free space tracking within each block is very simple. In addition to
+ * keeping track of the size of the block, the block header contains a
+ * "pointer" to the beginning of free space. A new allocation simply moves
+ * this pointer by the number of bytes allocated.
+ *
+ * Reallocation
+ * ------------
+ *
+ * If the passed in allocation is the last allocation in a block and there
+ * is enough space after it, the allocation is resized. Otherwise, a new
+ * buffer is allocated (see Allocation above) and the contents are copied
+ * over.
+ *
+ * Freeing
+ * -------
+ *
+ * Freeing of the last allocation moves the "pointer" to free space back by
+ * the size of the last allocation.
+ *
+ * Freeing of any other allocation is a no-op.
+ *
+ * Clearing
+ * --------
+ *
+ * Clearing the pool is supposed to return the pool to the same state it was
+ * in when it was first created. To that end, the alloconly pool frees all
+ * the blocks allocated since the pool's creation. The remaining block
+ * (allocated during creation) is reset to consider all the space for
+ * allocations as available.
+ *
+ * In other words, the per-block free space tracking variables are set to
+ * indicate that the full block is available and that there have been no
+ * allocations.
+ *
+ * Finally, if the pool was created via pool_alloconly_create_clean(), all
+ * blocks are safe_memset()/memset() to zero before being free()d.
+ *
+ * Destruction
+ * -----------
+ *
+ * Destroying a pool first clears it (see above). The clearing leaves the
+ * pool in a minimal state with only one block allocated. This remaining
+ * block may be safe_memset() to zero if the pool was created with
+ * pool_alloconly_create_clean().
+ *
+ * Since the pool structure itself is allocated from the first block, this
+ * final call to free() will release the memory allocated for struct
+ * alloconly_pool and struct pool.
+ */
+
+#ifndef DEBUG
+# define POOL_ALLOCONLY_MAX_EXTRA MEM_ALIGN(1)
+#else
+# define POOL_ALLOCONLY_MAX_EXTRA \
+ (MEM_ALIGN(sizeof(size_t)) + MEM_ALIGN(1) + MEM_ALIGN(SENTRY_COUNT))
+#endif
+
+struct alloconly_pool {
+ struct pool pool;
+ int refcount;
+
+ struct pool_block *block;
+#ifdef DEBUG
+ const char *name;
+ size_t base_size;
+ bool disable_warning;
+#endif
+ bool clean_frees;
+};
+
+struct pool_block {
+ struct pool_block *prev;
+
+ size_t size;
+ size_t left;
+ size_t last_alloc_size;
+
+ /* unsigned char data[]; */
+};
+#define SIZEOF_POOLBLOCK (MEM_ALIGN(sizeof(struct pool_block)))
+
+#define POOL_BLOCK_DATA(block) \
+ ((unsigned char *) (block) + SIZEOF_POOLBLOCK)
+
+#define DEFAULT_BASE_SIZE MEM_ALIGN(sizeof(struct alloconly_pool))
+
+#ifdef DEBUG
+# define CLEAR_CHR 0xde
+# define SENTRY_COUNT 8
+#else
+# define SENTRY_COUNT 0
+# define CLEAR_CHR 0
+#endif
+
+static const char *pool_alloconly_get_name(pool_t pool);
+static void pool_alloconly_ref(pool_t pool);
+static void pool_alloconly_unref(pool_t *pool);
+static void *pool_alloconly_malloc(pool_t pool, size_t size);
+static void pool_alloconly_free(pool_t pool, void *mem);
+static void *pool_alloconly_realloc(pool_t pool, void *mem,
+ size_t old_size, size_t new_size);
+static void pool_alloconly_clear(pool_t pool);
+static size_t pool_alloconly_get_max_easy_alloc_size(pool_t pool);
+
+static void block_alloc(struct alloconly_pool *pool, size_t size);
+
+static const struct pool_vfuncs static_alloconly_pool_vfuncs = {
+ pool_alloconly_get_name,
+
+ pool_alloconly_ref,
+ pool_alloconly_unref,
+
+ pool_alloconly_malloc,
+ pool_alloconly_free,
+
+ pool_alloconly_realloc,
+
+ pool_alloconly_clear,
+ pool_alloconly_get_max_easy_alloc_size
+};
+
+static const struct pool static_alloconly_pool = {
+ .v = &static_alloconly_pool_vfuncs,
+
+ .alloconly_pool = TRUE,
+ .datastack_pool = FALSE
+};
+
+#ifdef DEBUG
+static void check_sentries(struct pool_block *block)
+{
+ const unsigned char *data = POOL_BLOCK_DATA(block);
+ size_t i, max_pos, alloc_size, used_size;
+
+ used_size = block->size - block->left;
+ for (i = 0; i < used_size; ) {
+ alloc_size = *(size_t *)(data + i);
+ if (alloc_size == 0 || used_size - i < alloc_size)
+ i_panic("mempool-alloconly: saved alloc size broken");
+ i += MEM_ALIGN(sizeof(alloc_size));
+ max_pos = i + MEM_ALIGN(alloc_size + SENTRY_COUNT);
+ i += alloc_size;
+
+ for (; i < max_pos; i++) {
+ if (data[i] != CLEAR_CHR)
+ i_panic("mempool-alloconly: buffer overflow");
+ }
+ }
+
+ if (i != used_size)
+ i_panic("mempool-alloconly: used_size wrong");
+
+ /* The unused data must be NULs */
+ for (; i < block->size; i++) {
+ if (data[i] != '\0')
+ i_unreached();
+ }
+ if (block->prev != NULL)
+ check_sentries(block->prev);
+}
+#endif
+
+pool_t pool_alloconly_create(const char *name ATTR_UNUSED, size_t size)
+{
+ struct alloconly_pool apool, *new_apool;
+ size_t min_alloc = SIZEOF_POOLBLOCK +
+ MEM_ALIGN(sizeof(struct alloconly_pool) + SENTRY_COUNT);
+
+ if (POOL_ALLOCONLY_MAX_EXTRA > (SSIZE_T_MAX - POOL_MAX_ALLOC_SIZE))
+ i_panic("POOL_MAX_ALLOC_SIZE is too large");
+
+#ifdef DEBUG
+ min_alloc += MEM_ALIGN(strlen(name) + 1 + SENTRY_COUNT) +
+ sizeof(size_t)*2;
+#endif
+
+ /* create a fake alloconly_pool so we can call block_alloc() */
+ i_zero(&apool);
+ apool.pool = static_alloconly_pool;
+ apool.refcount = 1;
+
+ if (size < min_alloc)
+ size = nearest_power(size + min_alloc);
+ block_alloc(&apool, size);
+
+ /* now allocate the actual alloconly_pool from the created block */
+ new_apool = p_new(&apool.pool, struct alloconly_pool, 1);
+ *new_apool = apool;
+#ifdef DEBUG
+ if (str_begins(name, MEMPOOL_GROWING) ||
+ getenv("DEBUG_SILENT") != NULL) {
+ name += strlen(MEMPOOL_GROWING);
+ new_apool->disable_warning = TRUE;
+ }
+ new_apool->name = p_strdup(&new_apool->pool, name);
+
+ /* set base_size so p_clear() doesn't trash alloconly_pool structure. */
+ new_apool->base_size = new_apool->block->size - new_apool->block->left;
+ new_apool->block->last_alloc_size = 0;
+#endif
+ /* the first pool allocations must be from the first block */
+ i_assert(new_apool->block->prev == NULL);
+
+ return &new_apool->pool;
+}
+
+pool_t pool_alloconly_create_clean(const char *name, size_t size)
+{
+ struct alloconly_pool *apool;
+ pool_t pool;
+
+ pool = pool_alloconly_create(name, size);
+ apool = container_of(pool, struct alloconly_pool, pool);
+ apool->clean_frees = TRUE;
+ return pool;
+}
+
+static void pool_alloconly_free_block(struct alloconly_pool *apool ATTR_UNUSED,
+ struct pool_block *block)
+{
+#ifdef DEBUG
+ safe_memset(block, CLEAR_CHR, SIZEOF_POOLBLOCK + block->size);
+#else
+ if (apool->clean_frees) {
+ safe_memset(block, CLEAR_CHR,
+ SIZEOF_POOLBLOCK + block->size);
+ }
+#endif
+ free(block);
+}
+
+static void
+pool_alloconly_free_blocks_until_last(struct alloconly_pool *apool)
+{
+ struct pool_block *block;
+
+ /* destroy all blocks but the oldest, which contains the
+ struct alloconly_pool allocation. */
+ while (apool->block->prev != NULL) {
+ block = apool->block;
+ apool->block = block->prev;
+
+ pool_alloconly_free_block(apool, block);
+ }
+}
+
+static void pool_alloconly_destroy(struct alloconly_pool *apool)
+{
+ /* destroy all but the last block */
+ pool_alloconly_free_blocks_until_last(apool);
+
+ /* destroy the last block */
+ pool_alloconly_free_block(apool, apool->block);
+}
+
+static const char *pool_alloconly_get_name(pool_t pool ATTR_UNUSED)
+{
+#ifdef DEBUG
+ struct alloconly_pool *apool =
+ container_of(pool, struct alloconly_pool, pool);
+
+ return apool->name;
+#else
+ return "alloconly";
+#endif
+}
+
+static void pool_alloconly_ref(pool_t pool)
+{
+ struct alloconly_pool *apool =
+ container_of(pool, struct alloconly_pool, pool);
+
+ apool->refcount++;
+}
+
+static void pool_alloconly_unref(pool_t *pool)
+{
+ struct alloconly_pool *apool =
+ container_of(*pool, struct alloconly_pool, pool);
+
+ /* erase the pointer before freeing anything, as the pointer may
+ exist inside the pool's memory area */
+ *pool = NULL;
+
+ if (--apool->refcount > 0)
+ return;
+
+ pool_alloconly_destroy(apool);
+}
+
+static void block_alloc(struct alloconly_pool *apool, size_t size)
+{
+ struct pool_block *block;
+
+ i_assert(size > SIZEOF_POOLBLOCK);
+ i_assert(size <= SSIZE_T_MAX);
+
+ if (apool->block != NULL) {
+ /* each block is at least twice the size of the previous one */
+ if (size <= apool->block->size)
+ size += apool->block->size;
+
+ /* avoid crashing in nearest_power() if size is too large */
+ size = I_MIN(size, SSIZE_T_MAX);
+ size = nearest_power(size);
+ /* nearest_power() could have grown size to SSIZE_T_MAX+1 */
+ size = I_MIN(size, SSIZE_T_MAX);
+#ifdef DEBUG
+ if (!apool->disable_warning) {
+ /* i_debug() overwrites unallocated data in data
+ stack, so make sure everything is allocated before
+ calling it. */
+ t_buffer_alloc_last_full();
+ i_debug("Growing pool '%s' with: %zu",
+ apool->name, size);
+ }
+#endif
+ }
+
+ block = calloc(size, 1);
+ if (unlikely(block == NULL)) {
+ i_fatal_status(FATAL_OUTOFMEM, "block_alloc(%zu"
+ "): Out of memory", size);
+ }
+ block->prev = apool->block;
+ apool->block = block;
+
+ block->size = size - SIZEOF_POOLBLOCK;
+ block->left = block->size;
+}
+
+static void *pool_alloconly_malloc(pool_t pool, size_t size)
+{
+ struct alloconly_pool *apool =
+ container_of(pool, struct alloconly_pool, pool);
+ void *mem;
+ size_t alloc_size;
+
+#ifndef DEBUG
+ alloc_size = MEM_ALIGN(size);
+#else
+ alloc_size = MEM_ALIGN(sizeof(size)) + MEM_ALIGN(size + SENTRY_COUNT);
+#endif
+
+ if (apool->block->left < alloc_size) {
+ /* we need a new block */
+ block_alloc(apool, alloc_size + SIZEOF_POOLBLOCK);
+ }
+
+ mem = POOL_BLOCK_DATA(apool->block) +
+ (apool->block->size - apool->block->left);
+
+ apool->block->left -= alloc_size;
+ apool->block->last_alloc_size = alloc_size;
+#ifdef DEBUG
+ memcpy(mem, &size, sizeof(size));
+ mem = PTR_OFFSET(mem, MEM_ALIGN(sizeof(size)));
+ /* write CLEAR_CHRs to sentry */
+ memset(PTR_OFFSET(mem, size), CLEAR_CHR,
+ MEM_ALIGN(size + SENTRY_COUNT) - size);
+#endif
+ return mem;
+}
+
+static void pool_alloconly_free(pool_t pool, void *mem)
+{
+ struct alloconly_pool *apool =
+ container_of(pool, struct alloconly_pool, pool);
+
+ /* we can free only the last allocation */
+ if (POOL_BLOCK_DATA(apool->block) +
+ (apool->block->size - apool->block->left -
+ apool->block->last_alloc_size) == mem) {
+ memset(mem, 0, apool->block->last_alloc_size);
+ apool->block->left += apool->block->last_alloc_size;
+ apool->block->last_alloc_size = 0;
+ }
+}
+
+static bool pool_alloconly_try_grow(struct alloconly_pool *apool, void *mem, size_t size)
+{
+ /* see if we want to grow the memory we allocated last */
+ if (POOL_BLOCK_DATA(apool->block) +
+ (apool->block->size - apool->block->left -
+ apool->block->last_alloc_size) == mem) {
+ /* yeah, see if we can grow */
+ if (apool->block->left >= size-apool->block->last_alloc_size) {
+ /* just shrink the available size */
+ apool->block->left -=
+ size - apool->block->last_alloc_size;
+ apool->block->last_alloc_size = size;
+ return TRUE;
+ }
+ }
+
+ return FALSE;
+}
+
+static void *pool_alloconly_realloc(pool_t pool, void *mem,
+ size_t old_size, size_t new_size)
+{
+ struct alloconly_pool *apool =
+ container_of(pool, struct alloconly_pool, pool);
+ unsigned char *new_mem;
+
+ if (new_size <= old_size)
+ return mem;
+
+ new_size = MEM_ALIGN(new_size);
+
+ /* see if we can directly grow it */
+ if (!pool_alloconly_try_grow(apool, mem, new_size)) {
+ /* slow way - allocate + copy */
+ new_mem = pool_alloconly_malloc(pool, new_size);
+ memcpy(new_mem, mem, old_size);
+ mem = new_mem;
+ }
+
+ return mem;
+}
+
+static void pool_alloconly_clear(pool_t pool)
+{
+ struct alloconly_pool *apool =
+ container_of(pool, struct alloconly_pool, pool);
+ size_t base_size, avail_size;
+
+#ifdef DEBUG
+ check_sentries(apool->block);
+#endif
+
+ pool_alloconly_free_blocks_until_last(apool);
+
+ /* clear the first block */
+#ifdef DEBUG
+ base_size = apool->base_size;
+#else
+ base_size = DEFAULT_BASE_SIZE;
+#endif
+ avail_size = apool->block->size - base_size;
+ memset(PTR_OFFSET(POOL_BLOCK_DATA(apool->block), base_size), 0,
+ avail_size - apool->block->left);
+ apool->block->left = avail_size;
+ apool->block->last_alloc_size = 0;
+}
+
+static size_t pool_alloconly_get_max_easy_alloc_size(pool_t pool)
+{
+ struct alloconly_pool *apool =
+ container_of(pool, struct alloconly_pool, pool);
+
+ return apool->block->left;
+}
+
+size_t pool_alloconly_get_total_used_size(pool_t pool)
+{
+ struct alloconly_pool *apool =
+ container_of(pool, struct alloconly_pool, pool);
+ struct pool_block *block;
+ size_t size = 0;
+
+ i_assert(pool->v == &static_alloconly_pool_vfuncs);
+
+ for (block = apool->block; block != NULL; block = block->prev)
+ size += block->size - block->left;
+ return size;
+}
+
+size_t pool_alloconly_get_total_alloc_size(pool_t pool)
+{
+ struct alloconly_pool *apool =
+ container_of(pool, struct alloconly_pool, pool);
+ struct pool_block *block;
+ size_t size = 0;
+
+ i_assert(pool->v == &static_alloconly_pool_vfuncs);
+
+ for (block = apool->block; block != NULL; block = block->prev)
+ size += block->size + SIZEOF_POOLBLOCK;
+ return size;
+}