18 min read

Four Vulnerabilities in barebox: From DHCP Parsing to EFI PE Loading

Four CVEs across barebox's network stack, filesystem layer, and EFI loader: an unbounded DHCP option scan, two ext4 parsing flaws, and a PE virtual-size integer overflow.

Kazuma MatsumotoKazuma Matsumoto
Contents

Introduction

barebox is an open-source bootloader for embedded Linux systems — the first code that runs when an ARM, MIPS, RISC-V, or x86 device powers on. This post documents four vulnerabilities I discovered across three of its subsystems:

CVE Subsystem Bug Class Impact
CVE-2026-34960 DHCP client (net/dhcp.c) Out-of-bounds read Information leak / crash
CVE-2026-34961 ext4 filesystem (fs/ext4/ext4_common.c) Out-of-bounds read Information leak / crash
CVE-2026-34962 ext4 filesystem (fs/ext4/ext_barebox.c) Infinite loop / memory exhaustion Denial of service
CVE-2026-34963 EFI PE loader (efi/loader/pe.c) Integer overflow / heap overflow Crash / potential code execution

The four bugs share a single root cause: barebox uses fields from on-disk and on-wire formats as loop bounds, pointer strides, and arithmetic operands without validating them. A DHCP option scanner that trusts the packet to contain its own terminator. An ext4 loop bounded by an unvalidated entry count read straight from disk. A directory iterator that never checks for zero-length entries. A PE section-size computation that overflows on 32-bit platforms. Each fix is one or two lines of code; each was absent in released builds.

Severity: none of these are remotely exploitable from the internet. The DHCP bug requires network adjacency during PXE boot. The ext4 and PE bugs require a crafted image on the device’s boot media — realistic in supply-chain attacks, compromised firmware updates, or environments where boot media integrity is not verified, but not “scan and exploit” targets.

The vulnerabilities were found using an AI agent following the structured analysis workflow described in my earlier post, and each was verified with AddressSanitizer-instrumented sandbox builds. Methodology is documented there; this post focuses on the bugs themselves.


Part 1: Background

What Is barebox?

barebox is an open-source bootloader maintained by Pengutronix, a German embedded Linux consultancy. A bootloader is the first software that runs when a device powers on — it initializes hardware, locates the operating system image, loads it into RAM, and hands off control. barebox is used on ARM, MIPS, RISC-V, and x86 hardware across industrial, automotive, and consumer embedded devices.

barebox was originally forked from U-Boot (the dominant open-source embedded bootloader) but diverged into a more Linux-kernel-like architecture: a virtual filesystem, a POSIX-like shell, and a driver model mirroring the Linux kernel’s. This architecture means barebox includes real filesystem drivers, network protocol parsers, and binary loaders — not minimal stubs but substantial C code that parses complex on-disk and on-wire formats. That complexity is where the vulnerabilities live.

Sandbox Mode

barebox sandbox mode compiles the entire bootloader as a regular Linux process, using the host’s filesystem, network stack, and terminal in place of real hardware. It was designed for development and testing, but it is ideal for security research: compile with AddressSanitizer (ASAN — a compiler feature that inserts runtime checks around every memory access, detecting out-of-bounds reads and writes the moment they happen), feed crafted inputs, and observe exactly where memory safety violations occur. All four vulnerabilities were confirmed using ASAN-instrumented sandbox builds.


Part 2: DHCP Option Parsing — Out-of-Bounds Read (CVE-2026-34960)

When barebox performs a network boot (PXE boot — loading an OS image over the local network rather than from local storage), it acts as a DHCP client: it broadcasts a request, and a server on the local network responds with configuration (IP address, gateway, DNS servers). The response encodes this configuration as a list of options — tagged TLV (Type-Length-Value) fields, each with a 1-byte type, a 1-byte length, and then the data. A 0xff byte marks the end of the list. DHCP has no built-in authentication: any device on the local network can respond.

The Vulnerable Code

The bug is in net/dhcp.c, in the function dhcp_message_type(). This function scans the DHCP options field looking for option type 53 (the Message Type option, which identifies whether a DHCP packet is a Discover, Offer, Request, or Acknowledge). Here is the vulnerable code:

static int dhcp_message_type(unsigned char *popt)
{
    if (net_read_uint32((uint32_t *)popt) != htonl(BOOTP_VENDOR_MAGIC))
        return -1;

    popt += 4;
    while (*popt != 0xff) {
        if (*popt == 53)
            return *(popt + 2);
        popt += *(popt + 1) + 2;
    }
    return -1;
}

Here is what each part does:

Line 1–2: The function receives popt, a pointer to the start of the DHCP options field within a received packet. It first checks for the BOOTP vendor magic (0x63825363) — a 4-byte signature that must appear at the start of the options field in every valid DHCP packet. htonl() converts the value to network byte order (most-significant byte first, which is the standard for network protocols). If the magic does not match, the function returns -1 (not a valid DHCP packet).

Line 3: popt += 4 advances the pointer past the 4-byte magic to the first option.

Line 4: while (*popt != 0xff) — the loop continues as long as the byte at the current position is not 0xff. In DHCP, 0xff is the end-of-options marker — a special sentinel value that signals “there are no more options after this point.” Every well-formed DHCP packet ends its options field with this byte.

Line 5–6: If the current option’s type byte equals 53, the function returns the value at *(popt + 2) — the first byte of the option’s data, which is the message type. Otherwise, popt += *(popt + 1) + 2 advances the pointer to the next option. The expression *(popt + 1) reads the length byte of the current option, and adding 2 accounts for the type and length bytes themselves. This is standard TLV (Type-Length-Value) parsing: skip forward by length + 2 to reach the next option’s type byte.

The Bug

There is no bounds check against the actual packet length. The loop’s only termination condition is finding a 0xff byte. If the options field does not contain a 0xff end marker — because the packet is truncated, malformed, or deliberately crafted — the loop will read past the end of the packet buffer into whatever memory happens to follow it.

Each iteration reads at least two bytes (*popt for the type and *(popt + 1) for the length), then advances by length + 2 bytes. With no upper bound on popt, every one of these reads can land in memory that does not belong to the packet. The loop continues until it either stumbles upon a 0xff byte in adjacent memory (at which point it returns -1, having read an arbitrary amount of unrelated data), or accesses an unmapped page and crashes with a segmentation fault.

DHCP option parsing Option Scanner while (*popt != 0xff) No Packet-Length Check loop has no bounds limit MAGIC Opt A Opt B no 0xff OOB Read Reads past packet into heap Missing 0xff causes scan past buffer.

Figure 1 — The unbounded option scan. The loop checks only for a 0xff sentinel, not for the end of the buffer. Without the terminator, the pointer advances into unallocated memory (red zone) until it hits a random 0xff or faults.

Impact

This is an out-of-bounds read (CWE-125). The practical impact depends on what memory lies past the packet buffer: if adjacent memory contains sensitive data, the leaked bytes could influence subsequent parsing decisions; if the pointer reaches unmapped memory, the CPU raises a page fault and the bootloader crashes — halting the boot process entirely. On a device with no fallback boot path, this is a denial of service.

The attack requires network adjacency: the attacker must be on the same Layer 2 network segment (the local network — devices that communicate directly without going through a router). A rogue DHCP server that omits the 0xff end marker from its response triggers this bug automatically.

The Fix

The fix adds a bounds check that limits the loop to the actual length of the options field:

-while (*popt != 0xff) {
+while (popt < popt_end && *popt != 0xff) {

Where popt_end is computed from the packet length. The additional condition ensures the pointer never advances past the allocated buffer, regardless of whether a 0xff marker is present.

Demonstration

The DHCP parser trusted the packet to contain its own terminator. The next two vulnerabilities show the same pattern in a different domain: an ext4 filesystem driver that trusts on-disk fields to be self-consistent.


Part 3: ext4 Filesystem Vulnerabilities (CVE-2026-34961 and CVE-2026-34962)

ext4 is the standard Linux filesystem — the format used to organize files and directories on a disk partition. When barebox mounts an ext4 partition to load a kernel image or boot script from local storage, it parses on-disk structures: inodes (file metadata), extent trees (block-mapping indexes), and directory entries. These two vulnerabilities are both in this parsing code, and they share the same root cause as the DHCP bug: trusting input fields without validation. A legitimate tool like mkfs.ext4 produces images where all fields are internally consistent. A crafted image can set those fields to values that exceed the actual buffer boundaries — and barebox uses them as loop bounds and pointer strides without checking.

What Is an Extent Tree?

ext4 uses extent trees to map logical file offsets (byte positions within a file, as a program sees them) to physical disk block numbers (the actual locations on the storage device where the data is written). An extent is a compact record that says “logical blocks N through N+L are stored starting at physical block P” — a range mapping rather than a per-block pointer, which is far more storage-efficient for contiguous files.

The extent tree starts in the inode (the on-disk metadata record for a file, containing its size, permissions, timestamps, and — critically — the root of its block mapping). The first 60 bytes of the inode’s block-mapping area hold an extent header followed by up to four extent entries (or index entries, for deeper trees). The extent header is a 12-byte structure containing:

Field Size Meaning
eh_magic 2 bytes Magic number (0xF30A) identifying this as an extent header
eh_entries 2 bytes Number of valid entries following this header
eh_max 2 bytes Maximum entries this node can hold
eh_depth 2 bytes Tree depth (0 = leaf node containing extents)
eh_generation 4 bytes Generation counter (unused in barebox)

Each extent entry that follows is 12 bytes. In the inode’s inline block-mapping area (60 bytes), after the 12-byte header, there is room for exactly 4 extent entries (4 x 12 = 48 bytes). For larger files, the tree grows deeper: the root node holds index entries that point to additional blocks, each of which holds its own header and extent entries. But the key constraint is that the number of entries in any given node is limited by the size of the buffer holding that node.

CVE-2026-34961: Extent Tree Out-of-Bounds Read

The vulnerable code is in fs/ext4/ext4_common.c. When barebox resolves a file’s logical block number to a physical block number, it walks the extent tree. The iteration loop looks like this:

for (i = 0; i < le16_to_cpu(ext_block->eh_entries); i++) {
    // process extent[i]
}

le16_to_cpu() converts the 2-byte eh_entries field from little-endian on-disk format to the CPU’s native byte order. The problem: eh_entries comes directly from the on-disk image and is used as the loop bound without any validation. A legitimate ext4 image will have eh_entries less than or equal to eh_max, and eh_max will not exceed the buffer’s capacity. But a crafted image can set eh_entries to any value up to 65,535.

If eh_entries is set to 100 but the buffer only holds 4 extent entries, the loop reads 96 entries past the end of the buffer. Each iteration reads 12 bytes (the size of an extent entry), so the out-of-bounds read covers up to 96 x 12 = 1,152 bytes of adjacent heap memory.

CVE-2026-34962: Directory Iteration Infinite Loop and Memory Exhaustion

The second ext4 vulnerability is in fs/ext4/ext_barebox.c, in the code that iterates directory entries. When barebox lists a directory’s contents or resolves a path component, it walks the directory’s data blocks parsing directory entries (on-disk records that associate filenames with inode numbers). Each directory entry has a direntlen field — a 2-byte value that specifies the total length of that entry, including padding. The iterator advances through the directory block by adding direntlen to the current file position:

while (fpos < dir->i_size) {
    const struct ext2_dirent *dirent = buf + fpos;
    // process the directory entry
    fpos += le16_to_cpu(dirent->direntlen);
}

The bug: there is no check that direntlen is non-zero. If a directory entry on a crafted ext4 image has direntlen set to 0, the iterator adds 0 to fpos on every iteration. fpos never changes, the fpos < dir->i_size condition remains true forever, and the loop runs indefinitely.

In barebox’s sandbox mode, this manifests as memory exhaustion rather than a CPU-bound hang. Each iteration processes the same directory entry and triggers internal memory allocations through barebox’s VFS layer (the dir_emit() call that registers each directory entry allocates kernel objects via xzalloc() — a wrapper that allocates zero-initialized memory and aborts if the allocation fails). With direntlen = 0, the loop runs indefinitely without ever advancing, allocating memory on every iteration until barebox’s allocator reports:

EMERG: xfuncs: out of memory
EMERG: xfuncs: Unable to allocate 272 bytes
used: 16295792
free: 336
PANIC: out of memory

The used: 16295792 line shows that nearly 16 MB of memory was consumed by the infinite loop before the allocator gave up — the repeated allocations filled the entire heap. On a real embedded device with limited RAM, this exhaustion would happen even faster.

Two ext4 parsing flaws CVE-2026-34961 Extent Header eh_entries = 100 No Validation buffer holds 4 entries Heap OOB Read CVE-2026-34962 Directory Entry direntlen = 0 No Zero Check fpos never advances PANIC: out of memory Oversized eh_entries Zero-length dirent

Figure 2 — Two ext4 parsing flaws sharing the same root cause. Top: the extent tree loop trusts eh_entries from the disk image and reads past the buffer. Bottom: a directory entry with direntlen=0 creates an infinite loop that exhausts all available memory.

Impact

Both vulnerabilities require the target to mount a crafted ext4 filesystem image — via a compromised firmware update, swapped boot media, or a rogue TFTP/NFS server on the local network. The extent tree OOB read (CVE-2026-34961) can leak adjacent heap contents or crash the bootloader. The directory infinite loop (CVE-2026-34962) is a reliable denial of service — the device will fail to boot, displaying the PANIC: out of memory message.

The Fixes

For CVE-2026-34961, the fix validates eh_entries against the buffer’s actual capacity before using it as a loop bound:

+if (le16_to_cpu(ext_block->eh_entries) > max_entries) {
+    /* eh_entries exceeds buffer capacity */
+    return 0;
+}
 for (i = 0; i < le16_to_cpu(ext_block->eh_entries); i++) {

For CVE-2026-34962, the fix checks for zero-length directory entries and breaks the loop:

 fpos += le16_to_cpu(dirent->direntlen);
+if (le16_to_cpu(dirent->direntlen) == 0)
+    break;

Both fixes follow the same pattern: validate the on-disk field before using it as a control flow parameter.

Demonstrations

Extent OOB Read (CVE-2026-34961):

Directory Infinite Loop (CVE-2026-34962):

The DHCP and ext4 bugs are variations of the same mistake — using an untrusted value as a loop parameter. The final vulnerability extends this pattern to arithmetic: a 32-bit integer overflow in the EFI PE loader.


Part 4: EFI PE Loader Integer Overflow (CVE-2026-34963)

The PE Format and EFI Loading

When barebox loads an EFI application (a UEFI-compatible executable), it must parse a PE (Portable Executable) file — the same binary format used for Windows .exe and .dll files, repurposed by the UEFI specification for firmware-level programs. A PE file contains headers (DOS, COFF, an optional header with the entry point and alignment parameters) followed by section headers — an array of structures describing where each section’s data lives in the file and where it should be loaded in memory.

Each section header contains two fields critical to this vulnerability:

Field Size Meaning
VirtualAddress 4 bytes The offset from the image base where this section should be loaded in memory
VirtualSize 4 bytes The size of the section in memory (may differ from the on-disk size due to padding or uninitialized data)

What Is an Integer Overflow?

An integer overflow occurs when an arithmetic operation produces a result too large to fit in the variable’s data type, causing the value to wrap around. This is the same concept as the unsigned integer underflow described in my strongSwan post — but in the opposite direction. Where an underflow wraps a subtraction result from near-zero to near-maximum, an overflow wraps an addition result from near-maximum back to near-zero.

On a 32-bit system, an unsigned long is 32 bits wide and can hold values from 0 to 4,294,967,295 (0xFFFFFFFF). If you add two values whose sum exceeds this maximum, the result wraps around. For example:

0xFFFFF000 + 0x00002000 = 0x100001000  (33 bits — needs 5 bytes)
                        → truncated to 0x00001000  (32 bits — only 4 bytes kept)

The true sum is 0x100001000 (approximately 4.3 GB), which requires 33 bits to represent. But a 32-bit variable can only hold the bottom 32 bits, so the high bit is silently discarded. The stored result is 0x00001000 (4 KB) — approximately one millionth of the intended value.

The Vulnerable Code

The vulnerable code is in efi/loader/pe.c, in the function efi_load_pe(). When loading a PE file, barebox needs to determine the total virtual image size — the amount of memory to allocate for the loaded binary. It does this by scanning all section headers and finding the maximum extent (the highest VirtualAddress + section_size):

for (i = num_sections - 1; i >= 0; i--) {
    IMAGE_SECTION_HEADER *sec = &sections[i];
    virt_size = max_t(unsigned long, virt_size,
                      sec->VirtualAddress + section_size(sec));
}

max_t is a macro that returns the larger of its two arguments, with a type cast to unsigned long. The expression sec->VirtualAddress + section_size(sec) computes the end address of the section — where it starts plus how big it is. The loop finds the maximum across all sections, which gives the total virtual image size.

The bug: on a 32-bit system (or any platform where unsigned long is 32 bits), the addition sec->VirtualAddress + section_size(sec) can overflow. A crafted PE file with a section at VirtualAddress = 0xFFFFF000 and VirtualSize = 0x2000 produces:

0xFFFFF000 + 0x2000 = 0x100001000 → truncated to 0x1000 on 32-bit

The max_t comparison then sees 0x1000 (4 KB) instead of the true value 0x100001000 (~4 GB). If the .text section already established virt_size = 0x1200, the final virt_size remains 0x1200 (4.5 KB) because max(0x1200, 0x1000) = 0x1200.

barebox then allocates a buffer of only 0x1200 bytes:

efi_reloc = efi_alloc_aligned_pages(virt_size, ...);

Later, when copying sections into the allocated buffer:

memcpy(efi_reloc + sec->VirtualAddress, ...);

The write destination is efi_reloc + 0xFFFFF000 — an address approximately 4 GB past the start of a 4.5 KB buffer. This is a massive out-of-bounds write. The CPU raises a page fault (the memory at that address is not mapped), and the process is killed immediately with a segmentation fault.

PE loader integer overflow PE Sections .text VA=0x1000 .evil VA=0xFFFFF000 32-bit Overflow 0xFFFFF000 + 0x2000 wraps to 0x1000 Allocation Should: ~4 GB Gets: 0x1200 (4 KB) SEGV: write to unmapped memory memcpy at efi_reloc + 0xFFFFF000 Missing: overflow-safe arithmetic Use 64-bit for virt_size

Figure 3 — The integer overflow chain. A crafted PE section with VirtualAddress near the 4 GB boundary causes the 32-bit size computation to wrap to a small value. The loader allocates a tiny buffer, then attempts to write at offset 0xFFFFF000 — approximately 4 GB past the start.

Impact

This is the most severe of the four bugs. An integer overflow leading to an undersized allocation followed by an out-of-bounds write is the classic shape of an exploitable heap overflow (CWE-190).

In this demonstration, the write destination (efi_reloc + 0xFFFFF000) is so far past the allocation that it lands in unmapped memory and crashes immediately. With more careful tuning of VirtualAddress and VirtualSize, an attacker could produce a smaller overflow that lands in mapped heap memory, corrupting adjacent allocations or heap metadata. Whether that leads to code execution depends on the target’s memory layout, heap allocator, and available primitives — but the fundamental bug class (integer overflow to heap overflow) is well-established as exploitable.

The attack requires loading a crafted EFI PE binary — a compromised EFI application on the boot partition, a malicious firmware update, or a supply-chain attack that inserts a crafted binary into the boot process.

The Fix

The fix uses 64-bit arithmetic for the virtual size computation, preventing 32-bit truncation:

-virt_size = max_t(unsigned long, virt_size,
-                  sec->VirtualAddress + section_size(sec));
+uint64_t sec_end = (uint64_t)sec->VirtualAddress + section_size(sec);
+if (sec_end > MAX_VIRT_SIZE) {
+    /* section extends past allowable limit */
+    return NULL;
+}
+virt_size = max_t(unsigned long, virt_size, (unsigned long)sec_end);

By casting VirtualAddress to uint64_t before the addition, the sum 0xFFFFF000 + 0x2000 = 0x100001000 is computed correctly as a 64-bit value. The subsequent range check rejects any section that extends past a reasonable limit, and only then is the value downcast to unsigned long for the allocation.

Demonstration


Part 5: Key Takeaways

The pattern across all four bugs is the same one that produces vulnerabilities in every parser codebase: a format field is read, and the code uses it as a control-flow parameter without checking whether the value makes sense for the buffer at hand. What makes these findings worth documenting is not the pattern itself — it is well known — but where they sit. Bootloader code runs without ASLR (address space layout randomization), without stack canaries, without heap hardening, and without memory protection between components. A bug that causes a contained crash in a modern OS process can corrupt arbitrary memory in a bootloader, where all code runs in the same flat address space with full hardware access.

The attack surfaces are narrow — network adjacency during PXE boot, crafted media on an SD card or eMMC partition, a compromised firmware update — but they are exactly the surfaces that matter in industrial systems, automotive ECUs (electronic control units), and medical devices where boot integrity is critical and firmware update chains are the relevant threat model. For anyone auditing bootloader code, the lesson is concrete: every loop bound, every pointer stride, and every arithmetic operand derived from a parsed format field needs a bounds check against the actual buffer. The fixes here were all one or two lines. The cost of adding them is negligible; the cost of their absence is not.

The barebox sandbox mode deserves a note of its own. Compiling the bootloader as a regular Linux process, instrumenting it with ASAN, and feeding it crafted inputs from the command line reduced verification from “flash a board and observe serial output” to “run a command and read the stack trace.” Sandbox mode is a development tool, but it is also a security research accelerator — and any bootloader project that does not offer one is making vulnerability verification unnecessarily difficult for everyone, including its own maintainers.


References