[SECURITY] NFS symlink chain buffer overflow → remote code execution (net/nfs-common.c)
Tom Rini
trini at konsulko.com
Fri Apr 3 21:49:55 CEST 2026
On Fri, Apr 03, 2026 at 11:44:49AM -0700, manizzle alexandria wrote:
> Hi Tom,
>
> I'm reporting a remotely exploitable buffer overflow in U-Boot's NFS
> client that I've confirmed leads to arbitrary code execution.
>
> SUMMARY
>
> A rogue NFS server can chain two READLINK (symlink) responses to
> overflow the global nfs_path_buff[2048] in net/nfs-common.c by ~141
> bytes. This corrupts the nfs_path pointer and adjacent globals,
> allowing the attacker to hijack the NFS state machine and deliver
> shellcode to a known memory address. I have a working end-to-end
> exploit with code execution confirmed on QEMU ARM (Cortex-A15).
>
> No authentication is required. The overflow triggers during normal
> NFS boot before any OS is loaded.
>
> AFFECTED
>
> - All U-Boot versions with CONFIG_CMD_NFS=y (NFS boot support)
> - Tested on: 2026.04-rc5, commit c704af3c8b0
> - File: net/nfs-common.c, function nfs_readlink_reply(), lines 667-686
>
> ROOT CAUSE
>
> nfs_readlink_reply() reads the symlink target length (rlen) from
> the RPC reply and validates it against the packet size, but NOT
> against the destination buffer (nfs_path_buff[2048]):
>
> rlen = ntohl(rpc_pkt.u.reply.data[1 + nfsv3_data_offset]);
>
> // Checks rlen fits in packet — CORRECT
> if (((uchar *)&rpc_pkt.u.reply.data[0] - (uchar *)&rpc_pkt + rlen) >
> len)
> return -NFS_RPC_DROP;
>
> // Copies rlen bytes into nfs_path — NO CHECK against buffer size
> memcpy(nfs_path + pathlen, ..., rlen);
>
> A single response can't overflow because rlen maxes at ~1128 (packet
> size limit). But two chained relative symlinks accumulate path length:
>
> 1st READLINK: 1100-byte relative target → nfs_path grows to ~1060B
> 2nd READLINK: 1128-byte relative target → writes to offset 2189
> → overflows nfs_path_buff[2048] by ~141 bytes
>
> EXPLOIT CHAIN
>
> 1. Portmap lookup → attacker returns fake mountd/nfsd ports
> 2. Mount → attacker returns fake file handle
> 3. Lookup → attacker returns file handle
> 4. Read → attacker returns NFSERR_ISDIR (triggers symlink)
> 5. Readlink #1 → 1100-byte relative symlink (grows path)
> 6. Readlink #2 → OVERFLOW — overwrites nfs_path pointer to
> attacker-planted "/x" string, state machine
> survives
> 7. Mount/Lookup → re-enters NFS flow with controlled path
> 8. Read → attacker serves ARM shellcode as file content
> → written to image_load_addr (0x42000000)
> 9. Code execution → shellcode runs, writes "PWNED!" to UART
>
> PROOF
>
> GDB output after exploit:
>
> === SHELLCODE AT 0x42000000 ===
> 0x42000000: mov r0, #9
> 0x42000004: lsl r0, r0, #24 @ r0 = 0x09000000 (UART)
> 0x42000008: mov r1, #0x50 @ 'P'
> 0x4200000c: str r1, [r0] @ write to UART
> ...
>
> === AFTER EXECUTION ===
> pc = 0x42000040 (completed, infinite loop)
> r0 = 0x09000000 (UART base)
> r1 = 0x0a (newline)
>
> === QEMU UART ===
> Loading: *T #PWNED!
>
> SUGGESTED FIX
>
> Add a bounds check before the memcpy in nfs_readlink_reply():
>
> --- a/net/nfs-common.c
> +++ b/net/nfs-common.c
> @@ -670,6 +670,10 @@
> if (((uchar *)&rpc_pkt.u.reply.data[0] - (uchar *)&rpc_pkt + rlen)
> > len)
> return -NFS_RPC_DROP;
>
> + int current_len = strlen(nfs_path) + 1;
> + if (current_len + rlen >= sizeof(nfs_path_buff))
> + return -NFS_RPC_ERR;
> +
> if (*((char *)&rpc_pkt.u.reply.data[2 + nfsv3_data_offset]) !=
> '/') {
>
> I have a full advisory document and working PoC (Python script, ~300
> lines) attached. The PoC runs as a rogue NFS server and was tested
> against QEMU ARM virt with qemu_arm_defconfig.
>
> I'm requesting a CVE for this issue. I plan to follow standard 90-day
> coordinated disclosure. Please let me know if you need anything else
> or would like to discuss the fix.
>
> Thanks,
> Murtaza Munaim
> # Security Advisory: U-Boot NFS Symlink Chain Remote Code Execution
>
> ## Summary
>
> A buffer overflow vulnerability in U-Boot's NFS client allows a rogue NFS
> server to achieve remote code execution on the target device during network
> boot. No authentication is required. The attacker serves crafted NFS
> symlink responses that overflow the global `nfs_path_buff[2048]` buffer,
> corrupting adjacent pointers and hijacking the NFS state machine to deliver
> and execute arbitrary shellcode.
>
> ## Affected Software
>
> - **Product:** Das U-Boot
> - **Component:** `net/nfs-common.c`, function `nfs_readlink_reply()`
> - **Affected versions:** All versions with NFS boot support (at least since NFSv3 support was added; tested on 2026.04-rc5, commit `c704af3c8b0`)
> - **Configurations:** Any build with `CONFIG_CMD_NFS=y` (NFS boot enabled)
>
> ## CVSS Score
>
> **8.1 (High)** — CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H
>
> - **Attack Vector:** Network (rogue NFS server)
> - **Attack Complexity:** High (target must be performing NFS boot)
> - **Privileges Required:** None
> - **User Interaction:** None (beyond initiating NFS boot, which is typically automatic)
> - **Impact:** Complete compromise of bootloader execution context
>
> ## Vulnerability Details
>
> ### Root Cause
>
> In `net/nfs-common.c`, the function `nfs_readlink_reply()` processes NFS
> READLINK responses (symlink target resolution). At line 667, it reads the
> symlink target length (`rlen`) from the RPC reply:
>
> ```c
> rlen = ntohl(rpc_pkt.u.reply.data[1 + nfsv3_data_offset]);
> ```
>
> The bounds check at line 669 validates that `rlen` fits within the **RPC
> packet buffer** (1152 bytes):
>
> ```c
> if (((uchar *)&rpc_pkt.u.reply.data[0] - (uchar *)&rpc_pkt + rlen) > len)
> return -NFS_RPC_DROP;
> ```
>
> However, it does **not** validate that `rlen` fits within the **destination
> buffer** `nfs_path_buff[2048]`. When processing a relative symlink (line
> 672-680), the target is appended to the existing path:
>
> ```c
> strcat(nfs_path, "/");
> pathlen = strlen(nfs_path);
> memcpy(nfs_path + pathlen,
> (uchar *)&rpc_pkt.u.reply.data[2 + nfsv3_data_offset],
> rlen);
> nfs_path[pathlen + rlen] = 0;
> ```
>
> ### Exploitation via Symlink Chaining
>
> A single READLINK response cannot overflow the buffer because `rlen` is
> capped at ~1128 bytes (by the packet size check) and `nfs_path` starts
> short (~10 bytes). However, by chaining **two** symlink resolutions:
>
> 1. **First READLINK:** Server returns a ~1100-byte relative symlink target
> with directory separators (`a/b/c/.../x`). After `nfs_dirname()`,
> `nfs_path` retains ~1060 bytes.
>
> 2. **Second READLINK:** Server returns a ~1128-byte relative symlink
> target. The `memcpy` writes to offset `1061 + 1128 = 2189`, exceeding
> `nfs_path_buff[2048]` by approximately **141 bytes**.
>
> ### What Gets Overwritten
>
> The overflow corrupts global variables adjacent to `nfs_path_buff` in BSS
> (verified via `nm` on QEMU ARM build):
>
> | Offset from buffer end | Variable | Type | Impact |
> |------------------------|----------|------|--------|
> | +0 | `nfs_path` | `char *` | Pointer to current NFS path — controls future memory writes |
> | +4 | `nfs_filename` | `char *` | Pointer to filename component |
> | +8 | `nfs_download_state` | `enum` | Controls boot success/failure |
> | +12 | `filefh3_length` | `uint` | NFS file handle length |
> | +16 | `filefh` | `char[64]` | NFS file handle — controls which file is read |
>
> ### Code Execution Chain
>
> By overwriting `nfs_path` to point to a short valid path string (e.g.,
> `"/x"`) planted within the overflow payload itself, the attacker keeps the
> NFS state machine alive. The state machine then:
>
> 1. Re-mounts the filesystem (MOUNT `/`)
> 2. Looks up the file (LOOKUP `x`)
> 3. Reads file content (READ) — the server serves **arbitrary shellcode**
> 4. `store_block()` writes the shellcode to `image_load_addr` (e.g., `0x42000000`)
>
> The shellcode is now in memory at a known address. In typical embedded boot
> configurations, `bootm` or `go` is called on the load address after NFS
> download, executing the attacker's code.
>
> ## Proof of Concept
>
> A complete working exploit (`nfs_rce.py`) is provided. It was tested
> against U-Boot 2026.04-rc5 running on QEMU ARM (`qemu_arm_defconfig`,
> Cortex-A15, 256MB RAM).
>
> ### Tested Exploit Chain
>
> ```
> [1] PORTMAP → returned fake mountd/nfsd ports
> [2] MOUNT → returned fake file handle
> [3] LOOKUP → returned success with file handle
> [4] READ → returned NFSERR_ISDIR → triggered symlink
> [5] READLINK #1 → 1100-byte relative symlink (grew nfs_path to ~1060B)
> [6] READLINK #2 → 1128-byte overflow payload
> Overwrote nfs_path → valid "/x" string
> State machine survived and continued
> [7] MOUNT/LOOKUP → re-entered NFS flow with attacker-controlled path
> [8] READ → served 68-byte ARM shellcode → written to 0x42000000
> [9] EXECUTION → shellcode wrote "PWNED!" to PL011 UART
> ```
>
> ### GDB Verification
>
> ```
> === SHELLCODE AT 0x42000000 ===
> 0x42000000: mov r0, #9
> 0x42000004: lsl r0, r0, #24 @ r0 = 0x09000000 (UART base)
> 0x42000008: mov r1, #0x50 @ 'P'
> 0x4200000c: str r1, [r0] @ write to UART
>
> === AFTER EXECUTION ===
> pc = 0x42000040 (completed, hit infinite loop)
> r0 = 0x09000000 (UART base)
> r1 = 0x0a (newline — last char written)
>
> === QEMU UART OUTPUT ===
> Loading: *T #PWNED!
> >>> CODE EXECUTION CONFIRMED <<<
> ```
>
> ### Reproduction Steps
>
> ```bash
> # 1. Build U-Boot for QEMU ARM with NFS enabled
> cd u-boot
> make CROSS_COMPILE=arm-none-eabi- qemu_arm_defconfig
> # Enable CONFIG_CMD_NFS=y in .config
> make CROSS_COMPILE=arm-none-eabi- -j$(nproc)
>
> # 2. Start the exploit server
> python3 nfs_rce.py
>
> # 3. Start QEMU
> qemu-system-arm -machine virt -cpu cortex-a15 -m 256M -nographic \
> -bios u-boot.bin \
> -netdev user,id=net0,net=10.0.2.0/24,dhcpstart=10.0.2.15 \
> -device virtio-net-device,netdev=net0
>
> # 4. In U-Boot console:
> setenv ipaddr 10.0.2.15
> setenv serverip 10.0.2.2
> nfs 0x42000000 10.0.2.2:/boot/uImage
>
> # 5. Observe "PWNED!" in UART output (or verify via GDB at 0x42000000)
> ```
>
> ## Suggested Fix
>
> Add a bounds check against `nfs_path_buff` size before the `memcpy` in
> `nfs_readlink_reply()`:
>
> ```c
> --- a/net/nfs-common.c
> +++ b/net/nfs-common.c
> @@ -670,6 +670,11 @@ static int nfs_readlink_reply(uchar *pkt, unsigned int len)
> if (((uchar *)&rpc_pkt.u.reply.data[0] - (uchar *)&rpc_pkt + rlen) > len)
> return -NFS_RPC_DROP;
>
> + /* Validate symlink target fits in nfs_path_buff */
> + int current_len = strlen(nfs_path) + 1; /* +1 for '/' */
> + if (current_len + rlen >= sizeof(nfs_path_buff))
> + return -NFS_RPC_ERR;
> +
> if (*((char *)&rpc_pkt.u.reply.data[2 + nfsv3_data_offset]) != '/') {
> int pathlen;
> ```
>
> Additionally, consider replacing `strcat`/`memcpy` with bounds-checked
> alternatives throughout the NFS path handling code.
Thanks for the report, please see
https://docs.u-boot.org/en/latest/develop/sending_patches.html for how
to properly submit a patch, thanks!
--
Tom
-------------- next part --------------
A non-text attachment was scrubbed...
Name: signature.asc
Type: application/pgp-signature
Size: 228 bytes
Desc: not available
URL: <https://lists.denx.de/pipermail/u-boot/attachments/20260403/1f00d241/attachment.sig>
More information about the U-Boot
mailing list