27 min readUpdated

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: one unchecked TCP header length that gives two reads past the packet, and an NFS path overflow past a 2048-byte buffer.

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, both reached through a single header-length field that is never checked against the segment carrying it, 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 — Out-of-bounds read in tcp_parse_options(), reached from tcp_rx_state_machine() when a packet declares a TCP header longer than the segment that carried it.
  • CVE-2026-29008 — Integer underflow in tcp_rx_state_machine(). The same unchecked header length makes the payload length negative, and the TCP_SYN_SENT handler passes that negative value on where the other states do not.
  • 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 require the target to be performing a network boot, and all three are reached by whatever the device is talking to during that narrow window. There are two ways to be that party. Spoofing or racing the legitimate server needs the attacker on the same local network. Operating the boot server, or having compromised it, does not — that server can sit anywhere the device can route to, which is why all three CVE records score the attack vector as Network rather than Adjacent. Everything demonstrated in Part 4 is the local case. The TCP bugs (CVE-2026-29007, CVE-2026-29008) produce out-of-bounds reads: crash or information disclosure, not code execution. The first of those reads past the packet on any build; whether the second reaches a read depends on how the target was configured, and Part 2 is exact about when. The NFS bug (CVE-2026-29009) is the serious one — a write overflow with fully attacker-controlled content, which by the declaration order in net/nfs-common.c lands on nfs_server_ip and would redirect all subsequent boot-image fetches to the attacker’s server. Part 3 sets out that chain. Part 4 demonstrates the two overflows in standalone harnesses rather than in U-Boot itself, and says plainly under each recording what that does and does not establish — the memory layout of a real U-Boot image is the step I did not measure. CVE-2026-29009 is fixed upstream; the two TCP bugs are not. Part 5 has the versions.

Bootloader vulnerabilities are unusually dangerous, regardless of how narrow the exploitation window is. U-Boot runs before any operating system protections exist — 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. None of the usual mitigations are present to limit the impact of a bug. In environments where network boot is the primary deployment mechanism — factory provisioning lines, data center PXE boot, embedded device fleets — this network stack is exposed to every device on the local network.


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. For embedded Linux devices, U-Boot performs the same role that BIOS or UEFI firmware performs on 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. Newly manufactured devices 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, written 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. Both TCP bugs start from one field in the incoming packet: the declared TCP header length, which is never checked against the size of the segment that carried it. That single unchecked value produces two separate defects, and only one of the two depends on the connection state at all.

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;
    u32 tcp_seq_num, tcp_ack_num, tcp_win_size;
    int tcp_hdr_len, payload_len;
    u8  tcp_flags, action;

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

    if (tcp_hdr_len > TCP_HDR_SIZE)
        tcp_parse_options(tcp,
                          (uchar *)b + IP_TCP_HDR_SIZE,
                          tcp_hdr_len - TCP_HDR_SIZE);

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, the declared length can reach 60 bytes. This value comes from the packet itself and is attacker-controlled.

Line 976: payload_len = tcp_len - tcp_hdr_len — This is the bug. 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’s declared header length is larger than the entire segment, this subtraction produces a negative value.

Lines 978 to 980: The declared header length is used a second time, and this use does not involve payload_len at all. TCP_HDR_SIZE is 20 — the size of a TCP header carrying no options — so tcp_hdr_len - TCP_HDR_SIZE is the number of option bytes the packet claims. The parser is pointed at the first byte after the fixed IP and TCP headers and told to walk that many bytes, and nothing compares that count against the bytes that actually arrived. This second use is CVE-2026-29007; the section on the two CVEs below returns to it.

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’s declared TCP header length is 60 bytes, but the entire TCP segment is only 24 bytes. The subtraction yields -36. Nothing on that line rejects a negative result, and the code goes on to use the value in two 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(). Two handlers contain that call — the TCP_SYN_SENT one at lines 1061-1063, and the one shared by TCP_ESTABLISHED and six other states at lines 1104-1106. Both write it the same way:

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

Only the first of the two is reachable with a negative length. The shared handler runs a window check first, on line 1073, and returns if it fails:

if (!tcp_seg_in_wnd(tcp, tcp_seq_num, payload_len)) {
    if (tcp_flags & TCP_RST)
        return;
    action = tcp_stream_fin_needed(tcp, tcp->snd_una)
             | TCP_ACK;
    tcp_send_packet(tcp, action, tcp->snd_una,
                    tcp->rcv_nxt, 0);
    return;
}

tcp_seg_in_wnd() has three branches that can return 1, and every one of them requires payload_len to be either exactly zero or greater than zero. A negative value matches none of them, so the function falls through to return 0 and the handler returns before reaching the call. The TCP_SYN_SENT handler has no such check in front of it. That difference is why CVE-2026-29008 names TCP_SYN_SENT alone and not the seven states that share the other call site, and it is why the attack has to catch the connection during the handshake rather than during a transfer.

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().

Where that negative value becomes a huge length depends on which consumer is registered on the stream, and it is worth following exactly. tcp_rx_user_data() hands len to tcp->rx, whose prototype also takes an int. For an HTTP transfer that callback is tcp_stream_rx() in net/wget.c, which passes the value straight on to store_block() — and store_block() declares its length parameter unsigned int. So the negative int is converted to a 32-bit unsigned value at that boundary. Computers store negative integers using two’s complement, a representation where the most significant bit indicates the sign, so -36 sits in memory as 0xFFFFFFDC; read as unsigned that is 4,294,967,260, or roughly 4 GiB. store_block() then carries that length to a memcpy(). The CVE record for CVE-2026-29008 describes the same step and gives 0xFFFFFFD8 as its example, which is the same arithmetic with a slightly different packet.

Two checks sit between the widened length and that copy, and neither is in the listing above, so it is worth being exact about when the copy actually runs. The first is skipped unless the caller supplied a buffer size, and U-Boot’s own default for the wget command does not supply one. The second runs when two things hold at once: CONFIG_LMB is compiled in, which is the default on every architecture U-Boot builds for, and the caller asked for a boot device to be set, which U-Boot’s own default does. It then asks the memory-map bookkeeping whether a read of that length at that address is permitted. A request of roughly 4 GiB is not, so on a stock build store_block() returns an error and the copy never happens. The CVE record draws the same line: it notes that memory corruption follows only when CONFIG_LMB is disabled. What the underflow reliably produces on a stock build is the corrupted pointer, a failed transfer and a connection torn down — not the out-of-bounds copy.

The width of that conversion is not a detail. A negative int handed directly to memcpy() would be sign-extended to a 64-bit size_t instead, giving 0xFFFFFFFFFFFFFFDC — about 18 exabytes — and AddressSanitizer reports that case as negative-size-param rather than as an oversized read. That is what the demonstration in Part 4 produces, because the harness there passes the int on without an intermediate unsigned int parameter. Either way the length reaching the copy is enormous; only the number in the report changes. Nothing gates the copy in that harness, which is why it completes there and the stock wget path described above stops short of it.

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

One unchecked header length, two defects Crafted Packet tcp_hlen = 0xF0 Missing Check hdr_len <= tcp_len Declared header length is never bounded tcp_hdr_len = 60 in a 24-byte segment CVE-2026-29007 tcp_parse_options walks 40 option bytes CVE-2026-29008 TCP_SYN_SENT gets payload_len = -36 Reads 40 bytes past the packet Negative length reaches memcpy

Figure 1 — One field the code never bounds, two different reads past the packet. The declared header length is used twice: once as the number of option bytes to walk, and once in the subtraction that yields the payload length. The first read runs before the state switch; the second reaches memory only in TCP_SYN_SENT.

Why Two CVEs

The two share one unchecked field — the declared header length read on line 975 — but they are two different defects, reached by two different lines, and the CVE records describe them that way:

  • CVE-2026-29007, the options over-read. Lines 978 to 980 hand tcp_hdr_len - TCP_HDR_SIZE to tcp_parse_options() as a count of option bytes to walk, and nothing compares that count against what arrived. The CVE record’s example is a packet whose IP total length is 40 bytes and whose data offset claims a 60-byte header, which makes the parser read 40 bytes past the end of the segment. Those bytes are not discarded: tcp_parse_options() assigns tcp->rmt_win_scale and tcp->rmt_timestamp from whatever it finds, so adjacent memory ends up inside the connection’s own state. This one runs before the state switch, so no particular connection state is required.

  • CVE-2026-29008, the negative payload length. Line 976 makes payload_len negative, and the TCP_SYN_SENT handler passes it to tcp_rx_user_data() with no window check in front of it, for the reason set out above. The attacker responds to U-Boot’s outbound SYN with a malformed SYN-ACK whose tcp_hlen declares a header length larger than the segment. Any device on the local network can race the legitimate server to do this.

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;

That closes CVE-2026-29008 and nothing else, because the options parser works from tcp_hdr_len alone and never reads payload_len. Validating tcp_hdr_len against tcp_len before the subtraction closes both:

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

With that check in place the subtraction cannot go negative, and the options parser cannot be asked for more bytes than the packet carried.

Impact

The practical impact of both 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). CVE-2026-29007 adds one consequence beyond the read itself: the bytes it walks are stored as the remote window scale and the remote timestamp, so whatever sat next to the packet in memory is folded into the connection’s window arithmetic.

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 the function abridged to the lines that matter (the NFSv3 form adds an attributes offset to each data[] index, and the absolute-path branch is left out):

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 has not declared 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 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. One more byte goes with them: the line after the copy is nfs_path[pathlen + rlen] = 0, which writes a terminating zero at index 2101, so the write reaches 54 bytes past the end in total. The CVE record lists the variables that span that distance — nfs_server_ip, nfs_server_mount_port, nfs_server_port, nfs_our_port, nfs_state and rpc_id.

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 is located 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 is located 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.

Here is the full attack chain, step by step:

  1. The target device powers on and begins NFS boot. U-Boot sends NFS requests to the legitimate server.
  2. The attacker, on the same local network, responds to U-Boot’s requests before the legitimate server does (or replaces the legitimate server entirely). The attacker now controls the NFS server.
  3. U-Boot requests a file. The attacker’s server returns a symlink (a redirect to another path). U-Boot resolves the symlink by sending a READLINK request. The attacker’s server replies with a long target path — say, 500 bytes. This target is appended to nfs_path_buff.
  4. U-Boot follows the resolved path and encounters another symlink. The attacker returns another long target. This repeats several times, each resolution adding hundreds of bytes to nfs_path_buff. After four rounds, the buffer contains approximately 1900 bytes.
  5. On the final symlink resolution, the attacker returns a 200-byte target. The total (1901 + 200 = 2101) exceeds the 2048-byte buffer. The last 53 bytes write past the end of the buffer directly into nfs_server_ip.
  6. The attacker places their own IP address at exactly the right offset within the 200-byte symlink target. After the overflow, nfs_server_ip contains the attacker’s IP.
  7. From this point forward, every NFS request U-Boot makes — including the request to fetch the kernel image and initrd — goes to the attacker’s server. The legitimate server is no longer contacted.
  8. The attacker serves a malicious kernel. U-Boot loads and executes it. The attacker now has code execution on the target device.

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

What the two demonstrations below are, stated before anything else. They are not U-Boot. Each one is a small standalone C program I wrote that reimplements the vulnerable function and the values around it, built with gcc and driven by a Python script over a local socket. The TCP harness is tcp_server.c, built with -fsanitize=address so AddressSanitizer (ASAN — a compiler tool that detects memory errors at runtime by placing inaccessible “red zones” around every allocation) catches the read. The NFS harness is nfs_server.c, built with plain gcc -g and no sanitizer, so that the write is allowed to land on the neighbouring variables and the program can print them. Both commands are visible in the first frame of each recording. What each harness demonstrates and what it cannot is stated under the recording it belongs to.

TCP Integer Underflow (CVE-2026-29008)

The PoC sends a TCP packet with tcp_hlen = 0xF0 (declaring 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. This covers the underflow only. The harness does not include tcp_parse_options(), so nothing in the recording demonstrates CVE-2026-29007.

Live capture — the left terminal sends a 44-byte packet whose tcp_hlen field claims a 60-byte header inside a 24-byte segment, so payload_len computes to -36. The right terminal shows AddressSanitizer stopping the TCP state machine with a negative-size-param report.

The harness prints its own arithmetic first — pkt_len=44 tcp_len=24 hdr_len=60, then payload_len = 24 - 60 = -36 — and then ASAN stops it with negative-size-param: (size=-36) inside memcpy, called from tcp_rx_user_data. ASAN also reports the destination: 80 bytes inside of the 1500-byte packet region, which is the buf + 80 computed above. That is the pointer arithmetic and the negative length, both observed. What it does not show is U-Boot: the reimplemented function reproduces lines 970 to 976 of net/tcp.c and the call site that consumes the result, and nothing else about the bootloader.

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 writes past the end of the buffer. This harness is deliberately built without a sanitizer, for the reason given after the recording.

Live capture — the left terminal sends a READLINK reply whose 100-byte symlink target takes the accumulated path from 2001 to 2101 bytes against a 2048-byte buffer. The right terminal is the target: it shows the RPC packet bounds check passing, the copy running, and 53 bytes written past the end of the buffer.

There is no sanitizer report to read here. The harness prints the three neighbouring variables before the copy and again after it, and the copy itself is allowed to complete. Before: nfs_server_ip = 192.168.1.1 (legitimate), mount_port = 20048, server_port = 2049. It then reports rlen = 100, the packet bounds check passing, pathlen after '/' = 2001, total = 2101 vs buffer = 2048, and 53 bytes past the end. After: nfs_server_ip = 192.168.1.69, mount_port = 31337, server_port = 31338, each marked as overwritten.

Be precise about what that proves, because it is easy to claim too much here. It proves that this ordering of variables produces this outcome: 53 bytes of attacker-chosen content written past a 2048-byte buffer land on the IP address and the two port numbers declared after it, and the program then holds the attacker’s values. What it does not prove is that a real U-Boot binary lays those variables out the same way. The harness declares them in the order net/nfs-common.c declares them, which is where the adjacency claim in Part 3 comes from, but a compiler is free to pad between globals and a linker is free to reorder them, and neither was measured against a real build. Confirming that step would take nm --numeric-sort on an actual U-Boot image for the target board, checking that nfs_server_ip really is the next symbol after nfs_path_buff and at what offset. I did not run that, so the layout half of the redirect step stays reasoning rather than measurement. A sanitizer build cannot answer it either: ASAN inserts a redzone between globals and stops the copy at the first byte past the buffer, so under ASAN the write never reaches a neighbour at all — which is exactly why this harness was built without it.


Part 5: Fix Status

The three bugs are not in the same place, and this is the part a reader most needs.

CVE-2026-29009 (NFS) is fixed. Commit d669401 — “net: nfs: fix buffer overflow in nfs_readlink_reply()” — was authored on 9 April 2026, merged on 6 May, and shipped in v2026.07-rc2. Upgrade to v2026.07-rc2 or later. The check upstream added to the relative-path branch is the one proposed above, byte for byte:

if (pathlen + rlen >= sizeof(nfs_path_buff))
        return -NFS_RPC_DROP;

The absolute-path branch of the same function got a matching one. That branch copies from index 0 rather than appending, so it has no accumulated prefix to add and tests rlen on its own:

if (rlen >= sizeof(nfs_path_buff))
        return -NFS_RPC_DROP;

Its commit message adds two things worth knowing. It carries Fixes: cf3a4f1e86 ("net: nfs: Fix CVE-2019-14195"), which is the 2019 fix that added the packet-length check this bug slipped past — so the insufficient check and the missing one have a direct lineage. And it records that an earlier commit, fd6e3d34097f, had already fixed the same overflow class in net/lwip/nfs.c while leaving the legacy path in net/nfs-common.c alone. The same defect existed in two copies of the NFS client and only one of them was repaired.

CVE-2026-29007 and CVE-2026-29008 (TCP) are not fixed. payload_len = tcp_len - tcp_hdr_len is still on line 976 of net/tcp.c with no bounds check, and the tcp_parse_options() call two lines below it still takes its length from the same unvalidated field. Both are unchanged from v2026.04-rc3 through the current development branch at the time of writing. There is nothing to upgrade to for these two; the mitigation is the one in the next part — if the device does not network-boot, or if CONFIG_PROT_TCP is not enabled, the code is never reached.


Part 6: 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 three codebases (strongSwan, BusyBox, U-Boot) 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.

If You Are Auditing Network-Boot Code

Here is a concrete checklist, derived from the bugs in this post and the barebox findings:

  1. Find every subtraction involving a packet-derived value. Search for expressions like len - hdr_len, total - offset, size - header_size. For each one, check whether the second operand can exceed the first. If it can, and the result is used as a length or pointer offset, there is an integer underflow.

  2. Find every memcpy, strcat, or memmove where the destination is a fixed-size buffer. Check whether the length argument is validated against the remaining capacity of the destination buffer — not just against the source packet size. These are two different constraints, and validating only the source (as in the NFS bug) is not sufficient.

  3. Check the types. A subtraction that produces a negative int is dangerous. A subtraction that produces a negative unsigned int wraps to a large positive number — which is even more dangerous, because it silently passes if (!len) guards and produces massive memcpy lengths.

  4. Map the globals adjacent to every buffer. For global buffer overflows, the impact depends entirely on what variables are stored next in memory. Compile the binary with debug symbols (-g), then use nm --numeric-sort binary | grep -A5 buffer_name to see what follows each buffer in the .bss section.

  5. If the project supports sandbox mode, use it. Compile with -fsanitize=address (ASAN). ASAN detects overflows at the exact instruction that causes them, with a stack trace. Without ASAN, many of these bugs produce silent corruption that only manifests much later — making the root cause extremely difficult to trace.


References

Kazuma Matsumoto. “U-Boot TCP/NFS Vulnerabilities: Integer Underflow and Buffer Overflow in the World’s Most Popular Bootloader”. 637th Research Lab, 2026-07-12. https://y637f9qq2x.com/posts/u-boot-tcp-nfs-vulns/