Malloc & Free in coreboot
This document provides a guide to the implementation and usage of
malloc and free within the coreboot project.
Historical Context
Because coreboot has a very short lived execution environment, it does
not employ a traditional operating system memory manager or a complex
heap allocator with capabilities like fragmentation reduction, multiple
bins, or coalescing freed blocks. The complex heap management simply
isn’t needed for coreboot. Look at the
libpayload section below for a full heap
allocator with complete malloc & free implementations.
For the vast majority of coreboot’s history, the free() function
literally did nothing but return. When it was initially checked into the
codebase in 2003, it was a dummy function with an empty body. In 2012,
it was removed entirely from malloc.c and replaced with an inline
stub, as the execution time of coreboot is so brief that memory never
actually needed to be freed. It wasn’t until late 2019 (commit
a347ea3787) that free() was given its current behavior.
Implementation
The current allocator (implemented in src/lib/malloc.c) uses a trivial
bump allocator (also known as a linear allocator). Three static pointers
track the allocator state:
free_mem_ptr— points to the next free byte in the heap.free_mem_end_ptr— points to the end of the heap.free_last_alloc_ptr— points to the start of the most recent allocation, used byfree()for its rewind feature.
The following diagram illustrates how these pointers move through a sequence of allocations and a free:
After malloc(A), malloc(B):
_heap _eheap
| |
v v
+----------+----------+---------------------------+
| A | B | (free space) |
+----------+----------+---------------------------+
^ ^
free_last_alloc_ptr free_mem_ptr
After free(B) — rewinds because B was the last allocation:
_heap _eheap
| |
v v
+----------+--------------------------------------+
| A | (free space) |
+----------+--------------------------------------+
^
free_mem_ptr
(free_last_alloc_ptr = NULL, no further rewind possible)
Because this is a bump allocator, memory from non-last allocations cannot be reclaimed. This is by design — coreboot’s brief execution lifetime means the entire environment is torn down before handing control to the OS payload, so long-term memory leaks don’t exist in the normal sense.
API Reference
void *malloc(size_t size)
A thin wrapper that calls memalign(sizeof(u64), size), aligning each
allocation to an 8-byte boundary. Returns NULL on failure.
void *calloc(size_t nitems, size_t size)
Allocates nitems * size bytes via malloc() and zeroes the memory
with memset(). Returns NULL on failure. Note that the implementation
does not check for integer overflow on nitems * size.
void *memalign(size_t boundary, size_t size)
The core allocation function. All other allocation functions delegate to
memalign(). It aligns free_mem_ptr up to boundary, records the
aligned position as free_last_alloc_ptr, then advances free_mem_ptr
by size. Returns a pointer to the allocated region, or NULL if the
heap is exhausted (printing error messages at BIOS_ERR level).
Note that memalign() does not enforce any restrictions on the boundary
value — as the source code comment states, “This is firmware, you are
supposed to know what you are doing.”
void free(void *ptr)
Frees memory allocated by malloc(), calloc(), or memalign(). The
behavior is as follows:
If
ptrisNULL, returns immediately (consistent with standard C).If
ptrfalls outside the valid heap range (_heaptofree_mem_end_ptr), prints aBIOS_WARNINGand returns.If
ptrmatchesfree_last_alloc_ptr(i.e., it was the most recent allocation), rewindsfree_mem_ptrback to that location and setsfree_last_alloc_ptrtoNULL, effectively reclaiming the memory.Otherwise, does nothing — the memory cannot be reclaimed by the bump allocator.
Because free_last_alloc_ptr is set to NULL after a successful
rewind, only a single rewind is possible. Calling free() again on an
earlier allocation will have no effect.
Heap Initialization & Size
The heap in coreboot is not a complex dynamic structure initialized at
runtime. Instead, the start (_heap) and end (_eheap) symbols are
defined directly in the linker scripts (e.g., src/lib/program.ld).
The total size of the heap is statically determined at build time by the
CONFIG_HEAP_SIZE Kconfig option. The general default size is
0x100000 (1MB), as set in src/Kconfig. However, specific platforms
may override this default. For example, several Intel SoCs (Alder Lake,
Meteor Lake, Panther Lake, Lunar Lake) default to 0x200000 (2MB),
Intel Xeon SP GNR defaults to 0x80000 (512KB), and the SiFive HiFive
Unmatched board defaults to 0x10000 (64KB).
Because the allocator’s internal pointers (like free_mem_ptr) are
statically initialized to &_heap, the allocator is ready to use
immediately as soon as the C environment is set up for a stage. The heap
memory region itself is generally not zeroed out during stage loading
because the .heap section is marked as (NOLOAD) in the linker
script. If you require zero-initialized memory, you must use calloc(),
which explicitly zeroes the requested block.
Usage Recommendations
Given this implementation, there can be questions from developers about
whether to bother writing free() at all.
The general guidance for malloc and free usage is as follows:
Use
free()when it matters: If you are allocating a large, temporary buffer and you know you can free it before any other allocations are made, you should definitely callfree(). This takes advantage of the bump allocator’s rewind feature and saves space on the heap. Reviewers may specifically ask for afree()if they notice a non-trivial, temporary allocation that fits this criteria.You are not forced to
free()everywhere: If an allocation obviously cannot be freed usefully under the current implementation (e.g., other allocations have occurred since), you are not strictly required to include afree()call just to satisfy standard C conventions or static analyzers.Author’s Discretion: In general situations, adding a
free()call is left up to the discretion of the author. It does no harm, and can be useful in specific cases.
In short, developers can ignore free() unless the last allocated space
can be freed and it’s large enough to bother with. If you want to add it
for completeness, that is fine, but it is not a strict requirement for
all memory allocations in the coreboot source tree.
Debugging the coreboot Heap
If you need to trace memory allocations, you can enable the
DEBUG_MALLOC Kconfig option. When enabled, the allocator will print
debugging information (such as the boundaries and pointers of each
allocation or free event) to the console at the BIOS_SPEW log level.
Heap Poisoning and ASAN
Because the coreboot bump allocator is extremely rudimentary, it does
not support per-allocation heap poisoning or redzones natively. There
are no per-object redzones, and free() does not poison freed memory,
so use-after-free bugs within the heap will not be detected at the
allocator level.
However, coreboot does support AddressSanitizer (ASAN) during the
ramstage via the CONFIG_ASAN_IN_RAMSTAGE option. When enabled, ASAN
uses a shadow memory region to track which bytes are valid across the
entire ramstage memory map. It can detect out-of-bounds reads and writes
(e.g., accesses past _eheap), stack buffer overflows, and
use-after-scope errors. However, because ASAN does not hook individual
malloc() or free() calls in coreboot’s bump allocator, it cannot
detect heap-specific errors such as intra-heap buffer overruns between
adjacent allocations or use-after-free within the heap region.
Malloc & Free in libpayload
It is important to differentiate between the coreboot execution environment (which is brief and uses the bump allocator described above) and the payload execution environment. Payloads can run for the entire duration of the system’s uptime and can have significantly more complex memory management needs.
The libpayload library (located at payloads/libpayload/) provides
its own separate implementation of malloc and free in
payloads/libpayload/libc/malloc.c.
Unlike the main coreboot heap, libpayload implements a fully
functional (albeit simple, O(N)) dynamic heap allocator:
Functional
free(): Callingfree()inlibpayloadactually works. It marks blocks as free and consolidates adjacent free blocks (coalescing) to reduce fragmentation.Header Tracking: Each allocated block has a header that tracks its size, whether it is free, and includes a “magic” byte to detect basic buffer overruns or heap poisoning.
Extended Functions: It fully supports
realloc(),calloc(), andmemalign(), as well as specialized DMA memory allocators (dma_malloc(),dma_memalign()).Debugging:
libpayloadhas its own debug configuration,CONFIG_LP_DEBUG_MALLOC, which when enabled tracks the maximum memory consumption, warns about poisoned memory blocks, and provides aprint_malloc_map()function to dump the entire state of the heap to the console.
In summary, while you can largely ignore free() in coreboot proper,
you must treat memory management with normal care when writing or
modifying libpayload and any payloads using it, ensuring you properly
free() memory to avoid leaks.