15 min read

U-Boot TCP/NFS Vulnerabilities: Integer Underflow and Buffer Overflow in the World's Most Popular Bootloader

Three CVEs in U-Boot's network stack: a TCP integer underflow that corrupts packet processing and an NFS path overflow that escapes a 2048-byte buffer.

Kazuma MatsumotoKazuma Matsumoto
Contents

Introduction

This post documents three vulnerabilities I discovered in U-Boot, the most widely deployed open-source bootloader in the embedded world. Two are in the TCP stack (an integer underflow that corrupts packet processing) and one is in the NFS client (a buffer overflow that lets a rogue NFS server redirect boot-image fetches and achieve code execution). All three were found using the same structured, multi-pass AI analysis workflow described in my earlier posts — applied this time to a codebase that runs before any operating system has loaded.

The three CVEs are:

  • CVE-2026-29007 — Integer underflow in tcp_rx_state_machine() leading to an out-of-bounds read, triggered in the TCP_ESTABLISHED state.
  • CVE-2026-29008 — The same integer underflow, triggered in the TCP_SYN_SENT state. Same root cause, separate CVE because the code path and preconditions differ.
  • CVE-2026-29009 — Buffer overflow in nfs_readlink_reply() where a symlink target from a malicious NFS server overflows a 2048-byte global buffer.

Severity, stated honestly: All three are network-adjacent — the attacker must be on the same local network as the target during the narrow window when U-Boot is performing a network boot. These are not exploitable from the internet. The TCP bugs (CVE-2026-29007, CVE-2026-29008) produce out-of-bounds reads: crash or information disclosure, not code execution. The NFS bug (CVE-2026-29009) is the serious one — a write overflow with fully attacker-controlled content that overwrites nfs_server_ip, redirecting all subsequent boot-image fetches to the attacker’s server. Part 3 shows the RCE chain; Part 4 demonstrates it.

Bootloader vulnerabilities occupy a uniquely sensitive position regardless of their reach. U-Boot runs before any operating system protections — no ASLR (Address Space Layout Randomization — a technique that randomizes where code and data are placed in memory), no stack canaries (small known values placed on the stack to detect overwrites), no process isolation. A bug here has no safety net beneath it. And in environments where network boot is the primary deployment mechanism — factory provisioning lines, data center PXE boot, embedded device fleets — the attack surface is real.


Part 1: Background

What Is U-Boot?

U-Boot (formally “Das U-Boot,” a pun on the German word for submarine) is an open-source bootloader — the first piece of software that runs when a device powers on, responsible for initializing hardware and loading the operating system. U-Boot is to embedded Linux devices what the BIOS or UEFI firmware is to a desktop PC: it sets up the CPU, configures memory, initializes storage and network interfaces, and then hands control to the operating system kernel.

U-Boot is the dominant bootloader in the embedded Linux ecosystem. It supports over 1,500 board configurations across ARM, MIPS, RISC-V, and x86 — from Raspberry Pi boards and network switches to automotive systems and data center servers. If you have used an embedded Linux device, there is a good chance U-Boot started it.

Why Does a Bootloader Have a Network Stack?

A natural question: why would boot firmware need TCP, NFS, or any network protocol? The answer is network booting — the process of loading an operating system over the network rather than from local storage.

Network booting is common in several scenarios:

  • PXE boot in data centers. Servers fetch their OS from a central image server over the network using PXE (Preboot Execution Environment), rather than installing to every machine’s local disk.

  • Factory provisioning. Devices rolling off an assembly line often have no OS yet. The factory network-boots each one, installs firmware, and configures it.

  • Diskless embedded devices. Some embedded systems have no persistent storage. They network-boot every time they power on.

  • Recovery and reflashing. When a device’s local storage is corrupted, network boot provides a recovery path.

To support these use cases, U-Boot includes a surprisingly complete network stack: Ethernet drivers, IP, UDP, TCP, DHCP, TFTP, NFS, HTTP, and DNS. This stack runs in the bare-metal pre-OS environment — no operating system beneath it, no libc, no standard socket API. It is a custom implementation built from scratch specifically for the bootloader context.

U-Boot’s TCP State Machine

TCP connections follow a state machine — a defined set of states with rules governing transitions between them. The two states relevant to this vulnerability are:

  • TCP_SYN_SENT — The client has sent a SYN (synchronize) packet to initiate a connection and is waiting for the server’s SYN-ACK response.

  • TCP_ESTABLISHED — The three-way handshake (SYN, SYN-ACK, ACK) is complete and data is flowing.

U-Boot’s TCP implementation in net/tcp.c handles incoming packets through tcp_rx_state_machine(), which dispatches to different handlers based on the current connection state. The integer underflow bug exists in the packet-length calculation that runs before any state-specific handler, but it manifests in two different state handlers — hence two CVEs for what is effectively the same root cause.

What Is NFS?

NFS (Network File System) is a protocol that allows a computer to access files on a remote server as if they were on a local disk. When a device NFS-boots, U-Boot mounts a remote directory from an NFS server and reads the kernel image and other boot files from it. The device’s root filesystem can also be served over NFS, meaning the device operates entirely from network-hosted storage.

NFS uses RPC (Remote Procedure Call) as its transport mechanism — a protocol that allows a program on one machine to execute a function on another machine as if it were a local function call. Each NFS operation (read a file, list a directory, resolve a symlink) is implemented as an RPC call. The NFS client in U-Boot sends RPC requests and processes RPC replies.

The NFS operation relevant to this vulnerability is READLINK — the operation that resolves a symbolic link (a file that points to another file or directory). When U-Boot encounters a symlink during path resolution, it sends a READLINK request to the NFS server, which responds with the symlink’s target path. The client then appends this target path to its working path buffer. When the NFS server is malicious, that target path is attacker-controlled — and the bug is in the absence of a bounds check before the append.


Part 2: The TCP Vulnerabilities (CVE-2026-29007 & CVE-2026-29008)

The Vulnerable Code

The bug is in net/tcp.c, in the function tcp_rx_state_machine(). This function is called for every incoming TCP packet. Before dispatching to state-specific handlers, it computes the payload length of the TCP segment. The relevant code (U-Boot v2026.04-rc3) is:

void tcp_rx_state_machine(struct tcp_stream *tcp,
        union tcp_build_pkt *b, unsigned int pkt_len)
{
    int tcp_len = pkt_len - IP_HDR_SIZE;
    int tcp_hdr_len, payload_len;

    tcp_hdr_len = GET_TCP_HDR_LEN_IN_BYTES(
                      b->ip.hdr.tcp_hlen);
    payload_len = tcp_len - tcp_hdr_len;

Here is what each line does.

Line 970: tcp_len = pkt_len - IP_HDR_SIZE — The function receives pkt_len, the total packet size including the IP header. IP_HDR_SIZE is a constant (20 bytes). Subtracting it gives the length of the TCP segment (header plus payload).

Line 975: tcp_hdr_len = GET_TCP_HDR_LEN_IN_BYTES(b->ip.hdr.tcp_hlen) — This macro reads the TCP header length from the packet’s own header field. The macro is defined as ((x) >> 2), which extracts the header length in bytes from the encoded field. A standard TCP header is 20 bytes; with all options present, it can claim up to 60 bytes. This value comes from the packet itself and is attacker-controlled.

Line 976: payload_len = tcp_len - tcp_hdr_len — This is where the bug lives. The code subtracts the declared TCP header length from the TCP segment length to compute how many bytes of payload data follow the header. For a well-formed packet, this gives the correct payload size. But there is no check that tcp_hdr_len is less than or equal to tcp_len. If the packet claims a header length larger than the entire segment, this subtraction produces a negative value.

The Integer Underflow

Both tcp_len and payload_len are declared as int — a signed 32-bit integer. Unlike an unsigned type, a signed int can represent negative values. So when the subtraction produces a result below zero, the value is genuinely negative — not wrapped around to a large positive number.

A concrete example:

pkt_len    = 44              (a small, valid-looking TCP packet)
IP_HDR_SIZE = 20
tcp_len    = 44 - 20 = 24    (24 bytes of TCP segment)

tcp_hlen   = 0xF0            (attacker sets this in the packet header)
tcp_hdr_len = GET_TCP_HDR_LEN_IN_BYTES(0xF0)
            = 0xF0 >> 2
            = 60              (claims 60-byte TCP header)

payload_len = 24 - 60 = -36  (negative!)

The packet claims its TCP header is 60 bytes long, but the entire TCP segment is only 24 bytes. The subtraction yields -36. This negative payload_len is never checked — the code proceeds to use it in two critical places.

How the Negative payload_len Causes Damage

The negative payload_len is used in two places: a pointer arithmetic expression and a length argument passed to tcp_rx_user_data(). Both state handlers (TCP_SYN_SENT at lines 1061-1063, TCP_ESTABLISHED at lines 1104-1106) use the same expression:

tcp_rx_user_data(tcp, tcp_seq_num,
                 ((char *)b) + pkt_len - payload_len,
                 payload_len);

The pointer arithmetic ((char *)b) + pkt_len - payload_len is supposed to find the start of the payload data. When payload_len is -36:

pointer = buf + pkt_len - payload_len
        = buf + 44 - (-36)
        = buf + 44 + 36
        = buf + 80

The pointer now points 80 bytes into the buffer — 36 bytes past the end of the 44-byte packet. The code reads from memory that does not belong to this packet.

The length argument is equally corrupted. tcp_rx_user_data() has a guard that checks if (!len) — but a negative len is non-zero, so it passes. The function forwards the negative int to the registered receive callback, which ultimately passes it to memcpy(). When -36 is implicitly converted to size_t (unsigned 64-bit), it becomes 18,446,744,073,709,551,580 — approximately 18 exabytes. The memcpy reads far past the end of the buffer.

The following diagram shows the complete flow from crafted packet to corrupted pointer:

TCP integer underflow Crafted Packet tcp_hlen = 0xF0 Header Length = 60 from 0xF0 (15 x 4) Missing Check hdr_len <= tcp_len THE BUG payload_len = 24 - 60 = -36 CVE-2026-29008 TCP_SYN_SENT CVE-2026-29007 TCP_ESTABLISHED OOB Read past buffer

Figure 1 — The integer underflow flows from a single unchecked subtraction to two separate CVEs. The same negative payload_len is consumed by handlers in both the TCP_SYN_SENT and TCP_ESTABLISHED states, each producing a pointer that overshoots the packet buffer.

Why Two CVEs for the Same Bug?

CVE-2026-29007 and CVE-2026-29008 share the same root cause — the unchecked subtraction on line 976. They receive separate CVE IDs because they manifest in different TCP states with different preconditions:

  • CVE-2026-29008 (TCP_SYN_SENT): The attacker responds to U-Boot’s outbound SYN with a malformed SYN-ACK whose tcp_hlen claims a header length larger than the segment. Any device on the local network can race the legitimate server to do this.

  • CVE-2026-29007 (TCP_ESTABLISHED): The attacker injects a crafted packet into an active TCP data transfer. On a local network without encryption, this is straightforward but requires slightly more effort than CVE-2026-29008.

The Missing Check

The fix is a single bounds check that should have existed from the start:

     payload_len = tcp_len - tcp_hdr_len;
+    if (payload_len < 0)
+        return;

Alternatively, tcp_hdr_len could be validated against tcp_len before the subtraction:

+    if (tcp_hdr_len > tcp_len)
+        return;
     payload_len = tcp_len - tcp_hdr_len;

Either check prevents the negative payload_len from propagating to the state handlers.

Impact

The practical impact is an out-of-bounds read. U-Boot reads memory past the end of the packet buffer, which can cause a crash (if the read hits unmapped memory, the CPU faults and the device fails to boot) or information disclosure (if adjacent memory contains sensitive data like other network packets or cryptographic material).

Remote code execution is unlikely from these specific bugs. The attacker controls the length but not the content of the out-of-bounds read — the data past the buffer is whatever happens to be in adjacent memory, not attacker-chosen values.


Part 3: The NFS Vulnerability (CVE-2026-29009)

The Vulnerable Code

The bug is in net/nfs-common.c, in the function nfs_readlink_reply(). This function processes the server’s response to a READLINK RPC call — the call that resolves a symbolic link to its target path.

The relevant global buffer and function:

char nfs_path_buff[2048];
static int nfs_readlink_reply(uchar *pkt,
                              unsigned int len)
{
    rlen = ntohl(rpc_pkt.u.reply.data[1]);

    if (((uchar *)&rpc_pkt.u.reply.data[0]
         - (uchar *)&rpc_pkt + rlen) > len)
        return -NFS_RPC_DROP;

    strcat(nfs_path, "/");
    pathlen = strlen(nfs_path);
    memcpy(nfs_path + pathlen, ..., rlen);
}

The Validation Gap

The code performs one bounds check but omits another.

The check that exists (line with NFS_RPC_DROP): This validates that rlen — the declared length of the symlink target — does not exceed the size of the RPC packet itself. This is a packet-integrity check: it ensures the server is not claiming a symlink target longer than the data actually present in the reply. This check is correct and necessary, but it is not sufficient.

The check that is missing: There is no validation that the existing path (nfs_path) plus the separator (/) plus the new symlink target (rlen bytes) will fit within the 2048-byte nfs_path_buff. The code blindly appends the separator with strcat(), computes the current length with strlen(), and then copies rlen bytes with memcpy() — regardless of whether the destination buffer has room.

How the Overflow Occurs

After several symlink resolutions, suppose the nfs_path buffer holds 1900 bytes. The attacker’s NFS server returns a READLINK reply with rlen = 200:

Buffer capacity:     nfs_path_buff[2048]

Current content:     nfs_path = "/very/long/path/..." (1900 bytes)
strcat adds "/":     nfs_path = "/very/long/path/.../", strlen = 1901
memcpy writes 200:   starts at nfs_path + 1901, writes 200 bytes

Bytes used:          1901 + 200 = 2101
Buffer capacity:     2048
Overflow:            2101 - 2048 = 53 bytes past the end

The first 147 bytes fit inside the buffer. The remaining 53 bytes are written past the end, corrupting adjacent globals.

NFS READLINK overflow READLINK Reply rlen = 200 RPC Check rlen fits in packet Missing Check pathlen + rlen < 2048 nfs_path_buff[2048] existing path (1900 B) / symlink target OVERFLOW (53 B) nfs_server_ip overwritten NFS redirected to attacker RPC checks packet, not buffer capacity.

Figure 2 — The RPC bounds check and the buffer bounds check are two different things. The green box (the check that exists) validates that rlen fits inside the RPC packet. The amber dashed box (the check that is missing) would validate that the accumulated path plus the new symlink target fits inside the 2048-byte buffer. Without the second check, the memcpy writes past the end.

What Is a Global Buffer Overflow?

nfs_path_buff is declared as a global variable — it lives in the .bss segment (a region of memory reserved for uninitialized global variables, allocated at program startup and zeroed). Unlike a heap overflow (which corrupts heap metadata) or a stack overflow (which can overwrite return addresses), a global buffer overflow corrupts other global variables stored adjacent in memory.

In U-Boot’s pre-OS environment, this is particularly dangerous. The memory layout is completely deterministic — the same binary has the same globals at the same addresses on every boot. An attacker who knows the U-Boot binary (which is often publicly available firmware) can predict exactly which globals the overflow will reach and what values it will write.

From Overflow to Code Execution

Unlike the TCP bugs (which produce out-of-bounds reads with uncontrolled data), this NFS bug writes attacker-chosen bytes past the end of the buffer. And the memory layout makes it devastating.

The attacker chains multiple symlink resolutions to build up nfs_path incrementally: each READLINK reply adds a few hundred bytes, until the path approaches 2048. The final symlink target pushes the total past the boundary. The attacker controls both the length and the content of the overflow.

What sits immediately after nfs_path_buff in net/nfs-common.c:

char nfs_path_buff[2048];
struct in_addr nfs_server_ip;
int nfs_server_mount_port;
int nfs_server_port;

The first variable the overflow reaches is nfs_server_ip — the IP address U-Boot uses for all subsequent NFS requests. Because the attacker controls the symlink target content, they can place an arbitrary IP address at the exact offset that overwrites this variable.

The RCE chain: the rogue NFS server sends a crafted READLINK reply that overflows nfs_path_buff and writes the attacker’s own IP into nfs_server_ip. From that point forward, every NFS request U-Boot makes — including the request to fetch the kernel and initrd — goes to the attacker’s server. The attacker serves a malicious kernel. U-Boot loads and executes it. Code execution on the target device, triggered from the local network during boot.

The Missing Check

The fix requires validating the total accumulated length before the memcpy:

     strcat(nfs_path, "/");
     pathlen = strlen(nfs_path);
+    if (pathlen + rlen >= sizeof(nfs_path_buff))
+        return -NFS_RPC_DROP;
     memcpy(nfs_path + pathlen, ..., rlen);

This check ensures that the existing path plus the separator plus the symlink target will not exceed the buffer capacity. Without it, the only thing preventing the overflow is the (unrelated) RPC packet-size check — which validates a completely different constraint.


Part 4: Demonstration

Both vulnerabilities were triggered against U-Boot sandbox builds compiled with AddressSanitizer (ASAN – a compiler tool that detects memory errors at runtime by placing inaccessible “red zones” around every allocation).

TCP Integer Underflow (CVE-2026-29007 / CVE-2026-29008)

The PoC sends a TCP packet with tcp_hlen = 0xF0 (claiming a 60-byte header inside a 24-byte segment). ASAN catches the out-of-bounds read when the corrupted pointer and negative length reach memcpy.

The PoC runs a rogue NFS server that chains symlink resolutions to fill nfs_path_buff near its 2048-byte capacity, then sends a final READLINK reply whose target overflows the buffer and overwrites nfs_server_ip. ASAN reports the write past the end of the global buffer.


Part 5: Key Takeaways

If your devices do not network-boot, you are not affected. All three bugs are in U-Boot’s network stack, which is active only during PXE, HTTP, or NFS boot. Devices that boot from local storage (SD card, eMMC, NAND flash) never execute the vulnerable code paths.

On the Pattern

The same structured, multi-pass AI analysis workflow has now produced confirmed CVEs across four codebases (strongSwan, BusyBox, U-Boot TCP, U-Boot NFS) and three bug classes (integer underflow, heap buffer overflow, global buffer overflow). The instruction document is unchanged from the first finding.

The pattern that connects all of them: a length or size value derived from untrusted network input is used in arithmetic without a bounds check. In strongSwan, avp_len - 8 underflowed because avp_len could be less than 8. In BusyBox, addrs * 40 - 1 underallocated because addrs could be 0. In U-Boot’s TCP code, tcp_len - tcp_hdr_len underflows because tcp_hdr_len can exceed tcp_len. In U-Boot’s NFS code, memcpy(buf + pathlen, ..., rlen) overflows because pathlen + rlen can exceed the buffer size.

The variables change. The codebases change. The bug class label changes. But the underlying pattern is always the same: a value from the wire, subtracted or added without a guard, producing a number that violates an assumption downstream.


References