A Three-Phase PXE: Cleaning Up the Boot Process

The PXE/extlinux boot mechanism is a cornerstone of U-Boot’s standard boot flow. It parses a configuration file (like extlinux.conf), presents a menu, and loads the kernel, initrd, and device tree for the chosen option. A typical configuration file looks like this:

menu title Boot Menu
timeout 50
default linux

label linux
    menu label Ubuntu Linux
    kernel /vmlinuz
    initrd /initrd.img
    fdt /dtb/board.dtb
    fdtoverlays /dtb/overlay1.dtbo /dtb/overlay2.dtbo
    append root=/dev/sda1 quiet

While functional, the traditional implementation relied heavily on callbacks: as the parser encountered a file path (like /vmlinuz above), it would immediately call a function to load it.

We have submitted a series of 26 patches that introduces a new, cleaner API for this process.

The Problem with Callbacks

The callback-based approach mixes parsing logic with file loading logic. This makes it difficult for a caller to know what files need to be loaded without actually loading them. It also complicates environments where file access requires special handling or where the caller wants precise control over memory layout.

Furthermore, analysing the existing parser revealed several memory leaks where token strings and menu structures were not being freed properly.

The New Three-Phase API

The new API separates the boot process into three distinct phases:

  1. Parse (pxe_parse): The configuration file is parsed into a menu structure. Instead of loading files immediately, the parser populates a list of required files (kernel, initrd, FDT, overlays) for each label.
  2. Load (pxe_load): The caller iterates over the file list and loads the files manually to their desired addresses. The pxe_load() helper is then called to record the size and location of each loaded file.
  3. Boot (pxe_boot): Once everything is loaded, this function performs the final boot sequence using the pre-loaded file information.

This structure gives callers complete visibility and control. For example, a caller can now inspect the menu to see which files are required for a specific boot option before deciding where to load them.

Dealing with Includes

Configuration files often use the include directive to pull in other configs. In the new model, includes are not processed automatically. Instead, parsing populates an includes list. The caller is responsible for loading these included files and calling pxe_parse_include() to merge them into the menu. This explicit handling mirrors the control provided for boot files.

Improvements and Testing

Beyond the new API, this series includes several other improvements:

  • Memory Leaks: Fixed leaks in token parsing, menu labels, and FDT overlay paths. These were trivial to find with the recent malloc additions.
  • Explicit Sizing: The parser now accepts a file size parameter, removing the requirement for configuration files to be null-terminated strings.
  • Documentation: Comprehensive documentation for the new API has been added to doc/develop/bootstd/pxe_api.rst.

Some recently added tests, such as pxe_test_sysboot_norun(), continue to verify the traditional end-to-end functionality via the sysboot command. We added a new test, pxe_test_files_api_norun(), to verify the new callback-free workflow. It parses a configuration file, manually loads the kernel, initrd, and device tree overlays, records their positions via pxe_load(), and then simulates a boot. This new test also explicitly checks for memory leaks, ensuring that pxe_cleanup() properly frees all allocated memory, including the new file lists.

While these changes increase code size slightly (around 300 bytes on Thumb2 mostly fixing memory leaks and avoid access outside the file bounds), the result is a more robust, flexible, and leak-free PXE subsystem.