* [PATCH 1/2] net: dhcp6: bound received DUID option lengths
2026-08-06 18:37 [PATCH 0/2] net: dhcp6: fix out-of-bounds access in the option parser Shahriyar Jalayeri
@ 2026-08-06 18:37 ` Shahriyar Jalayeri
2026-08-06 18:37 ` [PATCH 2/2] test: dm: eth: add DHCPv6 oversized option regression tests Shahriyar Jalayeri
2026-08-14 15:42 ` [PATCH 0/2] net: dhcp6: fix out-of-bounds access in the option parser Jerome Forissier
2 siblings, 0 replies; 5+ messages in thread
From: Shahriyar Jalayeri @ 2026-08-06 18:37 UTC (permalink / raw)
To: Sean Edmond, Ramon Fried, u-boot
Cc: Argus, Jerome Forissier, Tom Rini, Francois Berder,
Shahriyar Jalayeri
dhcp6_parse_options() takes the SERVERID and CLIENTID option lengths
from the received ADVERTISE/REPLY and uses them unchecked:
- the SERVERID length is stored as server_uid_size and later copied into
the fixed net_tx_packet buffer in dhcp6_send_request_packet() with no
capacity check, so a large SERVERID overruns net_tx_packet (an
out-of-bounds write);
- the CLIENTID length is passed straight to memcmp() against the fixed
sm_params.duid buffer, so a length larger than it reads past the end
(an out-of-bounds read).
Both are reachable by any on-link attacker that answers a DHCPv6 SOLICIT
during netboot; neither length is bounded against anything but the
received packet size.
Bound the SERVERID length to the RFC 8415 DUID maximum at parse time,
which also bounds the malloc() and the copy into the REQUEST for every
caller, and only compare a CLIENTID that is exactly the size of the
client DUID.
Fixes: a0245818f7f8 ("net: dhcp6: Add DHCPv6 (DHCP for IPv6)")
Signed-off-by: Shahriyar Jalayeri <shahriyar@byteray.co.uk>
---
net/dhcpv6.c | 12 ++++++++----
net/dhcpv6.h | 3 +++
2 files changed, 11 insertions(+), 4 deletions(-)
diff --git a/net/dhcpv6.c b/net/dhcpv6.c
index 640f089a2e1..a18e358137c 100644
--- a/net/dhcpv6.c
+++ b/net/dhcpv6.c
@@ -322,15 +322,19 @@ static void dhcp6_parse_options(uchar *rx_pkt, unsigned int len)
switch (ntohs(option_hdr->option_id)) {
case DHCP6_OPTION_CLIENTID:
- if (memcmp(option_ptr, sm_params.duid, option_len)
- != 0) {
- debug("CLIENT ID DOESN'T MATCH\n");
- } else {
+ if (option_len == sizeof(sm_params.duid) &&
+ !memcmp(option_ptr, sm_params.duid, option_len)) {
debug("CLIENT ID FOUND and MATCHES\n");
sm_params.rx_status.client_id_match = true;
+ } else {
+ debug("CLIENT ID DOESN'T MATCH\n");
}
break;
case DHCP6_OPTION_SERVERID:
+ if (option_len > DHCP6_DUID_MAX_LEN) {
+ debug("SERVER ID too long\n");
+ break;
+ }
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 +
diff --git a/net/dhcpv6.h b/net/dhcpv6.h
index d41a3c30615..b0207a9f2c9 100644
--- a/net/dhcpv6.h
+++ b/net/dhcpv6.h
@@ -37,6 +37,9 @@
#define DUID_LL_SIZE (sizeof(struct dhcp6_option_duid_ll) + ETH_ALEN)
#define DUID_MAX_SIZE DUID_LL_SIZE /* only supports DUID-LL currently */
+/* RFC 8415 sec 11.1: a DUID is a 2-octet type plus at most 128 octets */
+#define DHCP6_DUID_MAX_LEN 130
+
/* vendor-class-data to send in vendor clas option */
#define DHCP6_VCI_STRING "U-Boot"
--
2.43.0
^ permalink raw reply related [flat|nested] 5+ messages in thread* [PATCH 2/2] test: dm: eth: add DHCPv6 oversized option regression tests
2026-08-06 18:37 [PATCH 0/2] net: dhcp6: fix out-of-bounds access in the option parser Shahriyar Jalayeri
2026-08-06 18:37 ` [PATCH 1/2] net: dhcp6: bound received DUID option lengths Shahriyar Jalayeri
@ 2026-08-06 18:37 ` Shahriyar Jalayeri
2026-08-13 13:21 ` Jerome Forissier
2026-08-14 15:42 ` [PATCH 0/2] net: dhcp6: fix out-of-bounds access in the option parser Jerome Forissier
2 siblings, 1 reply; 5+ messages in thread
From: Shahriyar Jalayeri @ 2026-08-06 18:37 UTC (permalink / raw)
To: Sean Edmond, Ramon Fried, u-boot
Cc: Argus, Jerome Forissier, Tom Rini, Francois Berder,
Shahriyar Jalayeri
Answer a DHCPv6 SOLICIT from the sandbox eth tx handler with an
ADVERTISE that carries an option longer than a valid DUID, and check
the client rejects it rather than sending a REQUEST.
Two cases cover the option parser: an over-long SERVERID, which a client
that trusts the length copies out of bounds while building the REQUEST,
and an over-long CLIENTID, which drives a memcmp past the client DUID
buffer.
Signed-off-by: Shahriyar Jalayeri <shahriyar@byteray.co.uk>
---
test/dm/eth.c | 151 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 151 insertions(+)
diff --git a/test/dm/eth.c b/test/dm/eth.c
index ed0b57d8861..f3a972c5c0a 100644
--- a/test/dm/eth.c
+++ b/test/dm/eth.c
@@ -621,6 +621,157 @@ static int dm_test_eth_async_ping_reply(struct unit_test_state *uts)
return 0;
}
DM_TEST(dm_test_eth_async_ping_reply, UTF_SCAN_FDT);
+
+#if IS_ENABLED(CONFIG_CMD_DHCP6) && IS_ENABLED(CONFIG_IPV6)
+#define DHCP6_DUID_LL_LEN 10 /* type(2) + hw_type(2) + MAC(6) */
+static bool dhcp6_request_seen;
+static bool dhcp6_advertise_sent;
+static int dhcp6_clientid_len;
+static int dhcp6_serverid_len;
+
+/*
+ * Answer a DHCPv6 SOLICIT with an ADVERTISE whose SERVERID option is longer
+ * than any valid DUID. A correct client rejects it and never sends a REQUEST;
+ * a client that trusts the length copies it out of bounds while building the
+ * REQUEST.
+ */
+static int sb_dhcp6_advertise_handler(struct udevice *dev, void *packet,
+ unsigned int len)
+{
+ struct eth_sandbox_priv *priv = dev_get_priv(dev);
+ struct ethernet_hdr *seth = packet;
+ struct ethernet_hdr *eth;
+ struct ip6_hdr *sip6, *ip6;
+ struct udp_hdr *sudp, *udp;
+ uchar *sdhcp6, *d, *opt, *rx;
+ int msglen;
+ u16 udptot;
+
+ if (ntohs(seth->et_protlen) != PROT_IP6)
+ return 0;
+ sip6 = (struct ip6_hdr *)((uchar *)packet + ETHER_HDR_SIZE);
+ if (sip6->nexthdr != IPPROTO_UDP)
+ return 0;
+ sudp = (struct udp_hdr *)((uchar *)sip6 + IP6_HDR_SIZE);
+ if (ntohs(sudp->udp_dst) != 547 || ntohs(sudp->udp_src) != 546)
+ return 0;
+ sdhcp6 = (uchar *)sudp + UDP_HDR_SIZE;
+
+ /* a REQUEST means the client accepted the over-long SERVERID */
+ if (sdhcp6[0] == 3) { /* DHCP6_MSG_REQUEST */
+ dhcp6_request_seen = true;
+ net_set_state(NETLOOP_FAIL);
+ return 0;
+ }
+ if (sdhcp6[0] != 1) /* DHCP6_MSG_SOLICIT */
+ return 0;
+ if (dhcp6_advertise_sent) {
+ /* the client re-solicited, so it rejected the ADVERTISE */
+ net_set_state(NETLOOP_FAIL);
+ return 0;
+ }
+ dhcp6_advertise_sent = true;
+ if (priv->recv_packets >= PKTBUFSRX)
+ return 0;
+
+ rx = priv->recv_packet_buffer[priv->recv_packets];
+ memset(rx, 0, PKTSIZE);
+
+ eth = (struct ethernet_hdr *)rx;
+ memcpy(eth->et_dest, seth->et_src, ARP_HLEN);
+ memcpy(eth->et_src, priv->fake_host_hwaddr, ARP_HLEN);
+ eth->et_protlen = htons(PROT_IP6);
+
+ ip6 = (struct ip6_hdr *)(rx + ETHER_HDR_SIZE);
+ ip6->version = 6;
+ ip6->nexthdr = IPPROTO_UDP;
+ ip6->hop_limit = 255;
+ memcpy(&ip6->saddr, &sip6->daddr, sizeof(struct in6_addr));
+ memcpy(&ip6->daddr, &sip6->saddr, sizeof(struct in6_addr));
+
+ udp = (struct udp_hdr *)((uchar *)ip6 + IP6_HDR_SIZE);
+ udp->udp_src = htons(547);
+ udp->udp_dst = htons(546);
+
+ d = (uchar *)udp + UDP_HDR_SIZE;
+ opt = d;
+ /* dhcp6 header: reuse the SOLICIT trans_id, msg_type = ADVERTISE */
+ memcpy(opt, sdhcp6, 4);
+ opt[0] = 2; /* DHCP6_MSG_ADVERTISE */
+ opt += 4;
+ /* CLIENTID: the client DUID from the SOLICIT, padded to the test size */
+ opt[0] = 0; opt[1] = 1;
+ opt[2] = dhcp6_clientid_len >> 8;
+ opt[3] = dhcp6_clientid_len & 0xff;
+ memcpy(opt + 4, sdhcp6 + 8, DHCP6_DUID_LL_LEN);
+ opt += 4 + dhcp6_clientid_len;
+ /* echo the client's IA_NA (hdr 4 + iaid/t1/t2 12) */
+ memcpy(opt, sdhcp6 + 4 + 14 + 6, 16);
+ opt += 16;
+ /* PREFERENCE = 255 so the client acts on this ADVERTISE at once */
+ opt[0] = 0; opt[1] = 7; opt[2] = 0; opt[3] = 1; opt[4] = 255;
+ opt += 5;
+ /* SERVERID of the test size */
+ opt[0] = 0; opt[1] = 2;
+ opt[2] = dhcp6_serverid_len >> 8;
+ opt[3] = dhcp6_serverid_len & 0xff;
+ memset(opt + 4, 0x41, dhcp6_serverid_len);
+ opt += 4 + dhcp6_serverid_len;
+
+ msglen = opt - d;
+ udptot = UDP_HDR_SIZE + msglen;
+ ip6->payload_len = htons(udptot);
+ udp->udp_len = htons(udptot);
+ udp->udp_xsum = 0;
+ udp->udp_xsum = csum_ipv6_magic(&ip6->saddr, &ip6->daddr, udptot,
+ IPPROTO_UDP,
+ csum_partial((u8 *)udp, udptot, 0));
+
+ priv->recv_packet_length[priv->recv_packets] =
+ ETHER_HDR_SIZE + IP6_HDR_SIZE + udptot;
+ priv->recv_packets++;
+
+ return 0;
+}
+
+static int dhcp6_run_advertise(struct unit_test_state *uts)
+{
+ dhcp6_request_seen = false;
+ dhcp6_advertise_sent = false;
+ sandbox_eth_set_tx_handler(0, sb_dhcp6_advertise_handler);
+ sandbox_eth_skip_timeout();
+
+ env_set("ethact", "eth@10002000");
+ net_loop(DHCP6);
+
+ sandbox_eth_set_tx_handler(0, NULL);
+
+ /* the malformed ADVERTISE must be rejected: no REQUEST is sent */
+ ut_assert(!dhcp6_request_seen);
+
+ return 0;
+}
+
+/* Check the DHCPv6 client rejects an over-long SERVERID option */
+static int dm_test_dhcp6_serverid_reject(struct unit_test_state *uts)
+{
+ dhcp6_clientid_len = DHCP6_DUID_LL_LEN;
+ dhcp6_serverid_len = 200;
+
+ return dhcp6_run_advertise(uts);
+}
+DM_TEST(dm_test_dhcp6_serverid_reject, UTF_SCAN_FDT);
+
+/* Check the DHCPv6 client rejects an over-long CLIENTID option */
+static int dm_test_dhcp6_clientid_reject(struct unit_test_state *uts)
+{
+ dhcp6_clientid_len = 20;
+ dhcp6_serverid_len = DHCP6_DUID_LL_LEN;
+
+ return dhcp6_run_advertise(uts);
+}
+DM_TEST(dm_test_dhcp6_clientid_reject, UTF_SCAN_FDT);
+#endif
#endif
#if IS_ENABLED(CONFIG_IPV6_ROUTER_DISCOVERY)
--
2.43.0
^ permalink raw reply related [flat|nested] 5+ messages in thread* Re: [PATCH 0/2] net: dhcp6: fix out-of-bounds access in the option parser
2026-08-06 18:37 [PATCH 0/2] net: dhcp6: fix out-of-bounds access in the option parser Shahriyar Jalayeri
2026-08-06 18:37 ` [PATCH 1/2] net: dhcp6: bound received DUID option lengths Shahriyar Jalayeri
2026-08-06 18:37 ` [PATCH 2/2] test: dm: eth: add DHCPv6 oversized option regression tests Shahriyar Jalayeri
@ 2026-08-14 15:42 ` Jerome Forissier
2 siblings, 0 replies; 5+ messages in thread
From: Jerome Forissier @ 2026-08-14 15:42 UTC (permalink / raw)
To: Sean Edmond, Ramon Fried, u-boot, Shahriyar Jalayeri
Cc: Argus, Tom Rini, Francois Berder, nd
On Thu, 06 Aug 2026 20:37:49 +0200, Shahriyar Jalayeri wrote:
> The DHCPv6 client takes the SERVERID and CLIENTID option lengths from a
> received ADVERTISE/REPLY without bounding them. The SERVERID length is
> later copied unbounded into the fixed net_tx_packet buffer when the
> REQUEST is built (an out-of-bounds write), and the CLIENTID length
> drives a memcmp against the fixed client-DUID buffer (an out-of-bounds
> read).
> Both are reachable by any on-link attacker that answers a DHCPv6 SOLICIT
> during netboot.
>
> [...]
Applied to u-boot-net branch for-main, thanks!
[1/2] net: dhcp6: bound received DUID option lengths
commit: 806a22de450c945db76a1eb4c96aaa2c0e8b2a21
[2/2] test: dm: eth: add DHCPv6 oversized option regression tests
commit: bee6e6485868342de8ef5de528cf1e2a7b741ce7
Best regards,
--
Jerome Forissier <jerome.forissier@arm.com>
^ permalink raw reply [flat|nested] 5+ messages in thread