Coverage Report

Created: 2026-10-28 07:10

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/dovecot/src/lib/mempool-alloconly.c
Line
Count
Source
1
/* Copyright (c) Dovecot authors, see top-level COPYING file */
2
3
/* @UNSAFE: whole file */
4
#include "lib.h"
5
#include "safe-memset.h"
6
#include "mempool.h"
7
8
/*
9
 * As the name implies, alloconly pools support only allocating memory.
10
 * Memory freeing is not supported, except as a special case - the pool's
11
 * last allocation can be freed.  Additionally, p_realloc() also tries to
12
 * grow an existing allocation if and only if it is the last allocation,
13
 * otherwise it just allocates a new memory area and copies the data there.
14
 *
15
 * Alloconly pools are commonly used for an object that builds its state
16
 * from many memory allocations, but doesn't change (much of) its state.
17
 * It is simpler to free such an object by destroying the entire memory
18
 * pool.
19
 *
20
 * Implementation
21
 * ==============
22
 *
23
 * Each alloconly pool contains a pool structure (struct alloconly_pool) to
24
 * keep track of alloconly-specific pool information and one or more blocks
25
 * (struct pool_block) that keep track of ranges of memory used to back the
26
 * allocations.  The blocks are kept in a linked list implementing a stack.
27
 * The block size decreases the further down the stack one goes.
28
 *
29
 * +-----------+
30
 * | alloconly |
31
 * |    pool   |
32
 * +-----+-----+
33
 *       |
34
 *       | block  +------------+ next  +------------+ next
35
 *       \------->| pool block |------>| pool block |------>...
36
 *                +------------+       +------------+
37
 *                |   <data>   |       |   <data>   |
38
 *                      .                    .
39
 *                      .                    .
40
 *                      .              |   <data>   |
41
 *                      .              +------------+
42
 *                |   <data>   |
43
 *                +------------+
44
 *
45
 * Creation
46
 * --------
47
 *
48
 * When an alloconly pool is created, one block is allocated.  This block is
49
 * large enough to hold the necessary internal structures (struct
50
 * alloconly_pool and struct pool_block) and still have enough space to
51
 * satisfy allocations for at least the amount of space requested by the
52
 * consumer via the size argument to pool_alloconly_create().
53
 *
54
 * Allocation
55
 * ----------
56
 *
57
 * Each allocation (via p_malloc()) checks the top-most block to see whether
58
 * or not it has enough space to satisfy the allocation.  If there is not
59
 * enough space, it allocates a new block (via block_alloc()) to serve as
60
 * the new top-most block.  This newly-allocated block is guaranteed to have
61
 * enough space for the allocation.  Then, regardless of whether or not a
62
 * new block was allocated, the allocation code reserves enough space in the
63
 * top-most block for the allocation and returns a pointer to it to the
64
 * caller.
65
 *
66
 * The free space tracking within each block is very simple.  In addition to
67
 * keeping track of the size of the block, the block header contains a
68
 * "pointer" to the beginning of free space.  A new allocation simply moves
69
 * this pointer by the number of bytes allocated.
70
 *
71
 * Reallocation
72
 * ------------
73
 *
74
 * If the passed in allocation is the last allocation in a block and there
75
 * is enough space after it, the allocation is resized.  Otherwise, a new
76
 * buffer is allocated (see Allocation above) and the contents are copied
77
 * over.
78
 *
79
 * Freeing
80
 * -------
81
 *
82
 * Freeing of the last allocation moves the "pointer" to free space back by
83
 * the size of the last allocation.
84
 *
85
 * Freeing of any other allocation is a no-op.
86
 *
87
 * Clearing
88
 * --------
89
 *
90
 * Clearing the pool is supposed to return the pool to the same state it was
91
 * in when it was first created.  To that end, the alloconly pool frees all
92
 * the blocks allocated since the pool's creation.  The remaining block
93
 * (allocated during creation) is reset to consider all the space for
94
 * allocations as available.
95
 *
96
 * In other words, the per-block free space tracking variables are set to
97
 * indicate that the full block is available and that there have been no
98
 * allocations.
99
 *
100
 * Finally, if the pool was created via pool_alloconly_create_clean(), all
101
 * blocks are safe_memset()/memset() to zero before being free()d.
102
 *
103
 * Destruction
104
 * -----------
105
 *
106
 * Destroying a pool first clears it (see above).  The clearing leaves the
107
 * pool in a minimal state with only one block allocated.  This remaining
108
 * block may be safe_memset() to zero if the pool was created with
109
 * pool_alloconly_create_clean().
110
 *
111
 * Since the pool structure itself is allocated from the first block, this
112
 * final call to free() will release the memory allocated for struct
113
 * alloconly_pool and struct pool.
114
 */
115
116
#ifndef DEBUG
117
#  define POOL_ALLOCONLY_MAX_EXTRA MEM_ALIGN(1)
118
#else
119
#  define POOL_ALLOCONLY_MAX_EXTRA \
120
  (MEM_ALIGN(sizeof(size_t)) + MEM_ALIGN(1) + MEM_ALIGN(SENTRY_COUNT))
121
#endif
122
123
struct alloconly_pool {
124
  struct pool pool;
125
  int refcount;
126
127
  struct pool_block *block;
128
#ifdef DEBUG
129
  const char *name;
130
  size_t base_size;
131
  bool disable_warning;
132
#endif
133
  bool clean_frees;
134
};
135
136
struct pool_block {
137
  struct pool_block *prev;
138
139
  size_t size;
140
  size_t left;
141
  size_t last_alloc_size;
142
143
  /* unsigned char data[]; */
144
};
145
1.38M
#define SIZEOF_POOLBLOCK (MEM_ALIGN(sizeof(struct pool_block)))
146
147
#define POOL_BLOCK_DATA(block) \
148
1.04M
  ((unsigned char *) (block) + SIZEOF_POOLBLOCK)
149
150
0
#define DEFAULT_BASE_SIZE MEM_ALIGN(sizeof(struct alloconly_pool))
151
152
#ifdef DEBUG
153
#  define CLEAR_CHR 0xde
154
#  define SENTRY_COUNT 8
155
#else
156
#  define SENTRY_COUNT 0
157
0
#  define CLEAR_CHR 0
158
#endif
159
160
static const char *pool_alloconly_get_name(pool_t pool);
161
static void pool_alloconly_ref(pool_t pool);
162
static void pool_alloconly_unref(pool_t *pool);
163
static void *pool_alloconly_malloc(pool_t pool, size_t size);
164
static void pool_alloconly_free(pool_t pool, void *mem);
165
static void *pool_alloconly_realloc(pool_t pool, void *mem,
166
            size_t old_size, size_t new_size);
167
static void pool_alloconly_clear(pool_t pool);
168
static size_t pool_alloconly_get_max_easy_alloc_size(pool_t pool);
169
170
static void block_alloc(struct alloconly_pool *pool, size_t size);
171
172
static const struct pool_vfuncs static_alloconly_pool_vfuncs = {
173
  pool_alloconly_get_name,
174
175
  pool_alloconly_ref,
176
  pool_alloconly_unref,
177
178
  pool_alloconly_malloc,
179
  pool_alloconly_free,
180
181
  pool_alloconly_realloc,
182
183
  pool_alloconly_clear,
184
  pool_alloconly_get_max_easy_alloc_size
185
};
186
187
static const struct pool static_alloconly_pool = {
188
  .v = &static_alloconly_pool_vfuncs,
189
190
  .alloconly_pool = TRUE,
191
  .datastack_pool = FALSE
192
};
193
194
#ifdef DEBUG
195
static void check_sentries(struct pool_block *block)
196
{
197
  const unsigned char *data = POOL_BLOCK_DATA(block);
198
  size_t i, max_pos, alloc_size, used_size;
199
200
  used_size = block->size - block->left;
201
  for (i = 0; i < used_size; ) {
202
    alloc_size = *(size_t *)(data + i);
203
    if (alloc_size == 0 || used_size - i < alloc_size)
204
      i_panic("mempool-alloconly: saved alloc size broken");
205
    i += MEM_ALIGN(sizeof(alloc_size));
206
    max_pos = i + MEM_ALIGN(alloc_size + SENTRY_COUNT);
207
    i += alloc_size;
208
209
    for (; i < max_pos; i++) {
210
      if (data[i] != CLEAR_CHR)
211
        i_panic("mempool-alloconly: buffer overflow");
212
    }
213
  }
214
215
  if (i != used_size)
216
    i_panic("mempool-alloconly: used_size wrong");
217
218
  /* The unused data must be NULs */
219
  for (; i < block->size; i++) {
220
    if (data[i] != '\0')
221
      i_unreached();
222
  }
223
  if (block->prev != NULL)
224
    check_sentries(block->prev);
225
}
226
#endif
227
228
pool_t pool_alloconly_create(const char *name ATTR_UNUSED, size_t size)
229
163k
{
230
163k
  struct alloconly_pool apool, *new_apool;
231
163k
  size_t min_alloc = SIZEOF_POOLBLOCK +
232
163k
    MEM_ALIGN(sizeof(struct alloconly_pool) + SENTRY_COUNT);
233
234
163k
  (void) COMPILE_ERROR_IF_TRUE(POOL_ALLOCONLY_MAX_EXTRA >
235
163k
             (SSIZE_T_MAX - POOL_MAX_ALLOC_SIZE));
236
237
#ifdef DEBUG
238
  min_alloc += MEM_ALIGN(strlen(name) + 1 + SENTRY_COUNT) +
239
    sizeof(size_t)*2;
240
#endif
241
242
  /* create a fake alloconly_pool so we can call block_alloc() */
243
163k
  i_zero(&apool);
244
163k
  apool.pool = static_alloconly_pool;
245
163k
  apool.refcount = 1;
246
247
163k
  if (size < min_alloc)
248
0
    size = nearest_power(size + min_alloc);
249
163k
  block_alloc(&apool, size);
250
251
  /* now allocate the actual alloconly_pool from the created block */
252
163k
  new_apool = p_new(&apool.pool, struct alloconly_pool, 1);
253
163k
  *new_apool = apool;
254
#ifdef DEBUG
255
  if (str_begins(name, MEMPOOL_GROWING, &name) ||
256
      getenv("DEBUG_SILENT") != NULL)
257
    new_apool->disable_warning = TRUE;
258
  new_apool->name = p_strdup(&new_apool->pool, name);
259
260
  /* set base_size so p_clear() doesn't trash alloconly_pool structure. */
261
  new_apool->base_size = new_apool->block->size - new_apool->block->left;
262
  new_apool->block->last_alloc_size = 0;
263
#endif
264
  /* the first pool allocations must be from the first block */
265
163k
  i_assert(new_apool->block->prev == NULL);
266
267
163k
  return &new_apool->pool;
268
163k
}
269
270
pool_t pool_alloconly_create_clean(const char *name, size_t size)
271
0
{
272
0
  struct alloconly_pool *apool;
273
0
  pool_t pool;
274
275
0
  pool = pool_alloconly_create(name, size);
276
0
  apool = container_of(pool, struct alloconly_pool, pool);
277
0
  apool->clean_frees = TRUE;
278
0
  return pool;
279
0
}
280
281
static void pool_alloconly_free_block(struct alloconly_pool *apool ATTR_UNUSED,
282
              struct pool_block *block)
283
167k
{
284
167k
  int old_errno = errno;
285
286
#ifdef DEBUG
287
  safe_memset(block, CLEAR_CHR, SIZEOF_POOLBLOCK + block->size);
288
#else
289
167k
  if (apool->clean_frees) {
290
0
    safe_memset(block, CLEAR_CHR,
291
0
          SIZEOF_POOLBLOCK + block->size);
292
0
  }
293
167k
#endif
294
167k
  free(block);
295
167k
  errno = old_errno;
296
167k
}
297
298
static void
299
pool_alloconly_free_blocks_until_last(struct alloconly_pool *apool)
300
163k
{
301
163k
  struct pool_block *block;
302
303
  /* destroy all blocks but the oldest, which contains the
304
     struct alloconly_pool allocation. */
305
167k
  while (apool->block->prev != NULL) {
306
4.96k
    block = apool->block;
307
4.96k
    apool->block = apool->block->prev;
308
4.96k
    pool_alloconly_free_block(apool, block);
309
4.96k
  }
310
163k
}
311
312
static void pool_alloconly_destroy(struct alloconly_pool *apool)
313
163k
{
314
  /* destroy all but the last block */
315
163k
  pool_alloconly_free_blocks_until_last(apool);
316
317
  /* destroy the last block */
318
163k
  pool_alloconly_free_block(apool, apool->block);
319
163k
}
320
321
static const char *pool_alloconly_get_name(pool_t pool ATTR_UNUSED)
322
0
{
323
#ifdef DEBUG
324
  struct alloconly_pool *apool =
325
    container_of(pool, struct alloconly_pool, pool);
326
327
  return apool->name;
328
#else
329
0
  return "alloconly";
330
0
#endif
331
0
}
332
333
static void pool_alloconly_ref(pool_t pool)
334
8.85k
{
335
8.85k
  struct alloconly_pool *apool =
336
8.85k
    container_of(pool, struct alloconly_pool, pool);
337
338
8.85k
  apool->refcount++;
339
8.85k
}
340
341
static void pool_alloconly_unref(pool_t *pool)
342
171k
{
343
171k
  struct alloconly_pool *apool =
344
171k
    container_of(*pool, struct alloconly_pool, pool);
345
346
  /* erase the pointer before freeing anything, as the pointer may
347
     exist inside the pool's memory area */
348
171k
  *pool = NULL;
349
350
171k
  if (--apool->refcount > 0)
351
8.85k
    return;
352
353
163k
  pool_external_refs_unref(&apool->pool);
354
163k
  pool_alloconly_destroy(apool);
355
163k
}
356
357
static void block_alloc(struct alloconly_pool *apool, size_t size)
358
167k
{
359
167k
  struct pool_block *block;
360
361
167k
  i_assert(size > SIZEOF_POOLBLOCK);
362
167k
  i_assert(size <= SSIZE_T_MAX);
363
364
167k
  if (apool->block != NULL) {
365
    /* each block is at least twice the size of the previous one */
366
4.96k
    if (size <= apool->block->size)
367
2.76k
      size += apool->block->size;
368
369
    /* avoid crashing in nearest_power() if size is too large */
370
4.96k
    size = I_MIN(size, SSIZE_T_MAX);
371
4.96k
    size = nearest_power(size);
372
    /* nearest_power() could have grown size to SSIZE_T_MAX+1 */
373
4.96k
    size = I_MIN(size, SSIZE_T_MAX);
374
#ifdef DEBUG
375
    if (!apool->disable_warning) {
376
      /* i_debug() overwrites unallocated data in data
377
         stack, so make sure everything is allocated before
378
         calling it. */
379
      t_buffer_alloc_last_full();
380
      i_debug("Growing pool '%s' with: %zu",
381
          apool->name, size);
382
    }
383
#endif
384
4.96k
  }
385
386
167k
  int old_errno = errno;
387
167k
  block = calloc(size, 1);
388
167k
  if (unlikely(block == NULL)) {
389
0
    i_fatal_status(FATAL_OUTOFMEM, "block_alloc(%zu"
390
0
             "): Out of memory", size);
391
0
  }
392
167k
  errno = old_errno;
393
167k
  block->prev = apool->block;
394
167k
  apool->block = block;
395
396
167k
  block->size = size - SIZEOF_POOLBLOCK;
397
167k
  block->left = block->size;
398
167k
}
399
400
static void *pool_alloconly_malloc(pool_t pool, size_t size)
401
1.04M
{
402
1.04M
  struct alloconly_pool *apool =
403
1.04M
    container_of(pool, struct alloconly_pool, pool);
404
1.04M
  void *mem;
405
1.04M
  size_t alloc_size;
406
407
1.04M
#ifndef DEBUG
408
1.04M
  alloc_size = MEM_ALIGN(size);
409
#else
410
  alloc_size = MEM_ALIGN(sizeof(size)) + MEM_ALIGN(size + SENTRY_COUNT);
411
#endif
412
413
1.04M
  if (apool->block->left < alloc_size) {
414
    /* we need a new block */
415
4.96k
    block_alloc(apool, alloc_size + SIZEOF_POOLBLOCK);
416
4.96k
  }
417
418
1.04M
  mem = POOL_BLOCK_DATA(apool->block) +
419
1.04M
    (apool->block->size - apool->block->left);
420
421
1.04M
  apool->block->left -= alloc_size;
422
1.04M
  apool->block->last_alloc_size = alloc_size;
423
#ifdef DEBUG
424
  memcpy(mem, &size, sizeof(size));
425
  mem = PTR_OFFSET(mem, MEM_ALIGN(sizeof(size)));
426
  /* write CLEAR_CHRs to sentry */
427
  memset(PTR_OFFSET(mem, size), CLEAR_CHR,
428
         MEM_ALIGN(size + SENTRY_COUNT) - size);
429
#endif
430
1.04M
  return mem;
431
1.04M
}
432
433
static void pool_alloconly_free(pool_t pool, void *mem)
434
0
{
435
0
  struct alloconly_pool *apool =
436
0
    container_of(pool, struct alloconly_pool, pool);
437
438
  /* we can free only the last allocation */
439
0
  if (POOL_BLOCK_DATA(apool->block) +
440
0
      (apool->block->size - apool->block->left -
441
0
       apool->block->last_alloc_size) == mem) {
442
0
    memset(mem, 0, apool->block->last_alloc_size);
443
0
    apool->block->left += apool->block->last_alloc_size;
444
0
    apool->block->last_alloc_size = 0;
445
0
  }
446
0
}
447
448
static bool pool_alloconly_try_grow(struct alloconly_pool *apool, void *mem, size_t size)
449
2.07k
{
450
  /* see if we want to grow the memory we allocated last */
451
2.07k
  if (POOL_BLOCK_DATA(apool->block) +
452
2.07k
      (apool->block->size - apool->block->left -
453
2.07k
       apool->block->last_alloc_size) == mem) {
454
    /* yeah, see if we can grow */
455
2.07k
    if (apool->block->left >= size-apool->block->last_alloc_size) {
456
      /* just shrink the available size */
457
1.72k
      apool->block->left -=
458
1.72k
        size - apool->block->last_alloc_size;
459
1.72k
      apool->block->last_alloc_size = size;
460
1.72k
      return TRUE;
461
1.72k
    }
462
2.07k
  }
463
464
358
  return FALSE;
465
2.07k
}
466
467
static void *pool_alloconly_realloc(pool_t pool, void *mem,
468
            size_t old_size, size_t new_size)
469
2.07k
{
470
2.07k
  struct alloconly_pool *apool =
471
2.07k
    container_of(pool, struct alloconly_pool, pool);
472
2.07k
  unsigned char *new_mem;
473
474
2.07k
  i_assert(old_size < SIZE_MAX);
475
476
2.07k
  if (new_size <= old_size)
477
0
    return mem;
478
479
2.07k
  new_size = MEM_ALIGN(new_size);
480
481
  /* see if we can directly grow it */
482
2.07k
  if (!pool_alloconly_try_grow(apool, mem, new_size)) {
483
    /* slow way - allocate + copy */
484
358
    new_mem = pool_alloconly_malloc(pool, new_size);
485
358
    if (old_size > 0)
486
358
      memcpy(new_mem, mem, old_size);
487
358
    mem = new_mem;
488
358
  }
489
490
2.07k
  return mem;
491
2.07k
}
492
493
static void pool_alloconly_clear(pool_t pool)
494
0
{
495
0
  struct alloconly_pool *apool =
496
0
    container_of(pool, struct alloconly_pool, pool);
497
0
  size_t base_size, avail_size;
498
499
#ifdef DEBUG
500
  check_sentries(apool->block);
501
#endif
502
503
  /* the pool's contents are being freed, so anything referring to the
504
     external pools is gone as well */
505
0
  pool_external_refs_unref(&apool->pool);
506
0
  pool_alloconly_free_blocks_until_last(apool);
507
508
  /* clear the first block */
509
#ifdef DEBUG
510
  base_size = apool->base_size;
511
#else
512
0
  base_size = DEFAULT_BASE_SIZE;
513
0
#endif
514
0
  avail_size = apool->block->size - base_size;
515
0
  memset(PTR_OFFSET(POOL_BLOCK_DATA(apool->block), base_size), 0,
516
0
         avail_size - apool->block->left);
517
0
  apool->block->left = avail_size;
518
0
  apool->block->last_alloc_size = 0;
519
0
}
520
521
static size_t pool_alloconly_get_max_easy_alloc_size(pool_t pool)
522
2.07k
{
523
2.07k
  struct alloconly_pool *apool =
524
2.07k
    container_of(pool, struct alloconly_pool, pool);
525
526
2.07k
  return apool->block->left;
527
2.07k
}
528
529
size_t pool_alloconly_get_total_used_size(pool_t pool)
530
0
{
531
0
  struct alloconly_pool *apool =
532
0
    container_of(pool, struct alloconly_pool, pool);
533
0
  struct pool_block *block;
534
0
  size_t size = 0;
535
536
0
  i_assert(pool->v == &static_alloconly_pool_vfuncs);
537
538
0
  for (block = apool->block; block != NULL; block = block->prev)
539
0
    size += block->size - block->left;
540
0
  return size;
541
0
}
542
543
size_t pool_alloconly_get_total_alloc_size(pool_t pool)
544
0
{
545
0
  struct alloconly_pool *apool =
546
0
    container_of(pool, struct alloconly_pool, pool);
547
0
  struct pool_block *block;
548
0
  size_t size = 0;
549
550
0
  i_assert(pool->v == &static_alloconly_pool_vfuncs);
551
552
0
  for (block = apool->block; block != NULL; block = block->prev)
553
0
    size += block->size + SIZEOF_POOLBLOCK;
554
0
  return size;
555
0
}