Analyzing UEFI Memory Leaks in Driver Development
Analyzing memory leaks during UEFI driver development requires a combination of firmware configuration flags, debug wrapper hooks, static analysis, and runtime inspection tools. Because UEFI operates in a pre-boot environment without the safety nets of modern operating systems, unreleased pool allocations and page reservations can exhaust limited platform resources or corrupt reserved memory. This article explains how firmware engineers detect, trace, and resolve memory leaks within EDK II and custom UEFI environments.
Enabling EDK II Pool Tracking and Debug PCDs
The standard TianoCore EDK II framework includes built-in debugging
mechanisms configured via Platform Configuration Databases (PCDs).
Engineers start by enabling pool allocation tracking in their platform
description file (.dsc):
gEfiMdeModulePkgTokenSpaceGuid.PcdPoolAllocationQuarks: Adds signature tags (quarks) and head/tail guards around allocations. This helps the firmware detect buffer overflows, double frees, and orphaned allocations upon driver exit.gEfiMdePkgTokenSpaceGuid.PcdDebugPropertyMask: Setting this mask to includeDEBUG_PROPERTY_DEBUG_POOL_ENABLEDforces the core allocator to clear memory upon allocation and deallocation with known patterns (e.g.,0xAFfor allocated,0xDFfor freed), immediately exposing stale pointer references and unreleased buffers.
Hooking Boot Services Allocators
When built-in PCDs are insufficient for pinpointing the exact driver
or function responsible for a leak, developers implement wrapper hooks
around the core Boot Services table (gBS):
- Function Interception: The engineer intercepts
gBS->AllocatePool,gBS->AllocatePages,gBS->FreePool, andgBS->FreePagesduring driver initialization. - Tracking Metadata: Each allocation intercepts the
caller's return address using compiler built-ins (such as
_ReturnAddress()). The wrapper stores the allocation address, size, memory type (e.g.,EfiBootServicesData), and return pointer in a private linked list or hash table. - Audit on Unload: When the driver unloads or the
system reaches the
EFI_EVENT_GROUP_READY_TO_BOOTsignaling phase, the tracking layer traverses the registry. Any active allocation not paired with an equivalentFreePoolis logged over the serial debug port (DEBUGmacro) alongside its call-site address.
Resolving Call Sites via PE/COFF Symbol Tables
Because memory tracking wrappers yield raw instruction pointer (IP) addresses, engineers must map these addresses back to source code lines:
- During compilation, the build system generates
.mapfiles and.pdb(or DWARF) debug symbols for the target.efidriver image. - By subtracting the driver's runtime base address—retrieved from the
EFI_LOADED_IMAGE_PROTOCOL—from the logged return address, engineers calculate the Relative Virtual Address (RVA). - Cross-referencing the RVA with the driver map file identifies the specific C function and line where memory was requested but never returned.
Runtime Inspection with the UEFI Shell
Engineers validate driver behavior under runtime conditions using interactive tools available in the UEFI Shell:
memmapCommand: Runningmemmapbefore and after repeatedly loading and unloading a driver vialoadandunloaddisplays overall system memory allocation. An unexplained net increase inBS_DataorBS_Codepages indicates persistent leaks.- Protocol Tracking (
dhcommand): Drivers that install protocols often allocate private context structures viaCR(Containment Record) macros. Inspecting handle databases usingdh -dverifies whether handles and their associated context memory are completely freed when protocols are removed.
Emulation and Static Analysis
To catch leaks before flashing physical SPI ROMs, engineers use QEMU
paired with GDB. Emulating firmware execution allows developers to set
memory breakpoints on pool boundaries and track driver execution paths.
Additionally, static analysis tools like Clang Static Analyzer or
Coverity are integrated into continuous integration pipelines to
identify non-deterministic leaks, such as allocations abandoned along
early EFI_ERROR exit paths (goto ErrorExit;)
where clean-up routines are inadvertently bypassed.