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 include DEBUG_PROPERTY_DEBUG_POOL_ENABLED forces the core allocator to clear memory upon allocation and deallocation with known patterns (e.g., 0xAF for allocated, 0xDF for 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):

  1. Function Interception: The engineer intercepts gBS->AllocatePool, gBS->AllocatePages, gBS->FreePool, and gBS->FreePages during driver initialization.
  2. 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.
  3. Audit on Unload: When the driver unloads or the system reaches the EFI_EVENT_GROUP_READY_TO_BOOT signaling phase, the tracking layer traverses the registry. Any active allocation not paired with an equivalent FreePool is logged over the serial debug port (DEBUG macro) 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 .map files and .pdb (or DWARF) debug symbols for the target .efi driver 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:

  • memmap Command: Running memmap before and after repeatedly loading and unloading a driver via load and unload displays overall system memory allocation. An unexplained net increase in BS_Data or BS_Code pages indicates persistent leaks.
  • Protocol Tracking (dh command): Drivers that install protocols often allocate private context structures via CR (Containment Record) macros. Inspecting handle databases using dh -d verifies 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.