Memory Allocations
C language requires explicitly allocating and freeing memory. The main two problems with this are:
A lot of allocations and frees cause memory fragmentation. The longer a process runs, the more it could have leaked memory because there are tiny unused free spots all around in heap.
Freeing memory is easy to forget, causing memory leaks. Sometimes it can be accidentally done multiple times, causing a potential security hole. A lot of
free()
calls all around in the code also makes the code more difficult to read and write.
The second problem could be solved with Boehm garbage collector, which Dovecot used to support, but it wasn't very efficient. It also doesn't help with the first problem.
To reduce the problems caused by these issues, Dovecot has several ways to do memory management.
Common Design Decisions
All memory allocations (with some exceptions in data stack) return memory filled with NULs. This is also true for new memory when growing an allocated memory with realloc. The zeroing reduces accidental use of uninitialized memory and makes the code simpler since there is no need to explicitly set all fields in allocated structs to zero/NULL. Note the C standard doesn't require that NULL is zero, but this is practically true everywhere and appears to be a (future) requirement for POSIX as well.
In out-of-memory situations memory allocation functions die internally by calling i_fatal_status(FATAL_OUTOFMEM, ..)
. There are several reasons for this:
Trying to handle malloc() failures explicitly would add a lot of error handling code paths and make the code much more complex than necessary.
In most systems malloc() rarely actually fails because the system has run out of memory. Instead the kernel will just start killing processes.
Typically when malloc() does fail, it's because the process's address space limit is reached. Dovecot enforces these limits by default. Reaching it could mean that the process was leaking memory and it should be killed. It could also mean that the process is doing more work than anticipated and that the limit should probably be increased.
Even with perfect out-of-memory handling, the result isn't much better anyway than the process dying. User isn't any happier by seeing "out of memory" error than "server disconnected".
When freeing memory, most functions usually also change the pointer to NULL. This is also the reason why most APIs' deinit functions take pointer-to-pointer parameter, so that when they're done they can change the original pointer to NULL.
malloc() Replacements
lib/imem.h
has replacements for all the common memory allocation functions:
malloc
,calloc
->i_malloc()
realloc()
->i_realloc()
strdup()
->i_strdup()
free()
->i_free
All memory allocation functions that begin with i_
prefix require that the memory is later freed with i_free()
. This is a macro that is guaranteed to set the freed pointer to NULL afterwards.
Memory Pools
lib/mempool.h
defines API for allocating memory through memory pools. All memory allocations actually go through memory pools. Even the i_*()
functions get called through default_pool
, which by default is system_pool
but can be changed to another pool if wanted. All memory allocation functions that begin with p_
prefix have a memory pool parameter, which it uses to allocate the memory.
Dovecot has many APIs that require you to specify a memory pool. Usually (but not always) they don't bother freeing any memory from the pool. Instead, they assume that more memory can be just allocated from the pool and the whole pool is freed later. These pools are usually alloconly-pools, but can also be data stack pools. See below.
Alloc-only Pools
pool_alloconly_create()
creates an allocate-only memory pool with a given initial size.
As the name says, alloconly-pools only support allocating more memory. As a special case its last allocation can be freed. p_realloc()
also tries to grow the existing allocation only if it's the last allocation, otherwise it'll just allocates new memory area and copies the data there.
Initial memory pool sizes are often optimized in Dovecot to be set large enough that in most situations the pool doesn't need to be grown. To make this easier, when Dovecot is configured with --enable-devel-checks
, it logs a warning each time a memory pool is grown. The initial pool size shouldn't of course be made too large, so usually it's best to just pick some small initial guessed size and if there are too many "growing memory pool" warnings start growing the pool sizes. Sometimes there's just no good way to set the initial pool size and avoid the warnings, in that situation you can prefix the pool's name with MEMPOOL_GROWING
which prevents logging warnings about the pool.
Alloconly-pools are commonly used for an object that builds its state from many memory allocations, but doesn't modify (much of) its state. It's a lot easier when you can do a lot of small memory allocations and in object destroy you simply free the memory pool.
Data Stack
lib/data-stack.h
describes the low-level data stack functions. Data stack works a bit like C's control stack. alloca()
is quite near to what it does, but there's one major difference: In data stack the stack frames are explicitly defined, so functions can return values allocated from data stack. t_strdup_printf()
call is an excellent example of why this is useful. Rather than creating some arbitrary sized buffer and using snprintf()
, which might truncate the value, you can just use t_strdup_printf()
without worrying about buffer sizes being large enough.
Try to keep the allocations from data stack small, since the data stack's highest memory usage size is kept for the rest of the process's lifetime. The initial data stack size is 32kB, which should be enough in normal use. If Dovecot is configured with --enable-devel-checks
, it logs a warning each time the data stack needs to be grown.
Stack frames are preferably created using a T_BEGIN
/T_END
block, for example:
T_BEGIN {
string_t *str1 = t_str_new(256);
string_t *str2 = t_str_new(256);
/* ... */
} T_END;
In the above example the two strings are allocated from data stack. They get freed once the code goes past T_END
. That's why the variables are preferably declared inside the T_BEGIN
/T_END
block so they won't accidentally be used after they're freed.
T_BEGIN
and T_END
expand to t_push()
and t_pop()
calls and they must be synchronized. Returning from the block without going past T_END
is going to cause Dovecot to panic in next T_END
call with "Leaked t_pop() call" error.
Data stack allocations have similar disadvantages to alloc-only memory pools. Allocations can't be grown, so with the above example if str1 grows past 256 characters, it needs to be reallocated, which will cause it to forget about the original 256 bytes and allocate 512 bytes more. However, as with alloc-only pools, the last allocation can be grown.
Memory allocations from data stack often begin with t_
prefix, meaning "temporary". There are however many other functions that allocate memory from data stack without mentioning it. Memory allocated from data stack is usually returned as a const pointer, so that the caller doesn't try to free it (which would cause a compiler warning).
When should T_BEGIN
/T_END
used and when not? This is kind of black magic. In general they shouldn't be used unless it's really necessary, because they make the code more complex. But if the code is going through loops with many iterations, where each iteration is allocating memory from data stack, running each iteration inside its own stack frame would be a good idea to avoid excessive memory usage. It's also difficult to guess how public APIs are being used, so it's often good for such API functions use their own private stack frames. Dovecot's ioloop code also wraps all I/O callbacks and timeout callbacks into their own stack frames, so you don't need to worry about them. It's actually a good idea for any callback to be called with its own data stack frame.
You can create memory pools from data stack too. Usually you should be calling pool_datastack_create()
to generate a new pool, which also tries to track that it's not being used unsafely across different stack frames. Some low-level functions can also use the slightly more efficient unsafe_data_stack_pool
as the pool, which doesn't do such tracking.
Data stack's advantages over malloc():
FAST, most of the time allocating memory means only updating a couple of pointers and integers. Freeing memory all at once also is a fast operation.
No need to
free()
each allocation resulting in prettier codeNo memory leaks
No memory fragmentation
It also has some disadvantages:
Allocating memory inside loops can accidentally allocate a lot of memory
Memory allocated from data stack can be accidentally stored into a permanent location and accessed after it's already been freed.
Debugging invalid memory usage may be difficult using existing tools