odhcpd: cherry pick patches from main

CVE-2026-53918: Use-after-free (dangling first lease pointer)
CVE-2026-53920: Stack memory disclosure via truncated IA_NA/IA_PD option (GHSA-p769-5v73-pc4f)
CVE-2026-53921: Out-of-bounds write in build_ia (stack overflow in IA reply serialization)
CVE-2026-53922: Integer/size_t underflow DoS in dhcpv6_ia_handle_IAs
GHSA-hhmc-92hw-535f: odhcpd/LuCI stored XSS — unauth DHCPv6 client injects lease-file lines via crafted FQDN
TOB-OWRT-5: Trail of Bits audit finding (snprintf return handling)

There's also an unassigned NDP hop-limit spoofing fix, CVSS 5.4,
that IS included: ndp: enforce RFC4861 §7.1.1 hop-limit.

Link: https://github.com/openwrt/openwrt/pull/24184
Signed-off-by: Hauke Mehrtens <hauke@hauke-m.de>
(cherry picked from commit 67c5b71d77e5277dc7f364238588d8e37e9a3ffa)
This commit is contained in:
Hauke Mehrtens 2026-07-11 22:58:17 +02:00 committed by ahmed sassi
parent f75c726430
commit 029fa04f50
35 changed files with 1634 additions and 11 deletions

View File

@ -8,7 +8,7 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=odhcpd
PKG_RELEASE:=2
PKG_RELEASE:=3
PKG_SOURCE_PROTO:=git
PKG_SOURCE_URL=$(PROJECT_GIT)/project/odhcpd.git

View File

@ -1,7 +1,7 @@
From f0d855358b86a36efbfeb5a9de5a1d2a4d9d80fe Mon Sep 17 00:00:00 2001
From 95a69c4cafc3a064bf4e0e0c9d06cea3411a4ba4 Mon Sep 17 00:00:00 2001
From: Haoyi Ci <cihaoyi@outlook.com>
Date: Thu, 2 Oct 2025 16:14:05 +0800
Subject: [PATCH] ndp: Allow NS loopback for master iface
Subject: ndp: Allow NS loopback for master iface
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

View File

@ -1,7 +1,7 @@
From 5eac9c56ff3b0a013c5241f449ca144f70bf4c02 Mon Sep 17 00:00:00 2001
From b655d759c4bc87ae729c86fe85617f48d74c9e40 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= <noltari@gmail.com>
Date: Tue, 7 Oct 2025 10:30:15 +0200
Subject: [PATCH] router: fix SLAAC on subnets > 64
Subject: router: fix SLAAC on subnets > 64
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

View File

@ -1,7 +1,7 @@
From d402cdae431668f55f9d82b7072b0afa3b8090df Mon Sep 17 00:00:00 2001
From bf4dbad4ba8867cdb570426de252f3d994e2db2d Mon Sep 17 00:00:00 2001
From: Stephen Groat <stephen.groat@datadoghq.com>
Date: Wed, 8 Oct 2025 11:54:51 -0700
Subject: [PATCH] ndp: fix macOS IPv6 compatibility by using link-local source
Subject: ndp: fix macOS IPv6 compatibility by using link-local source
addresses
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8

View File

@ -1,7 +1,7 @@
From 30780debd691aee7567784daf1fdfd8db500a485 Mon Sep 17 00:00:00 2001
From d9a202773d18ab9f88fc55f79b11b5f61a200085 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20H=C3=A4rdeman?= <david@hardeman.nu>
Date: Thu, 9 Oct 2025 11:08:14 +0200
Subject: [PATCH] odhcpd: fix a compilation error
Subject: odhcpd: fix a compilation error
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

View File

@ -1,7 +1,7 @@
From be7ca7c0792b185263ad86b961ea61129494a7f9 Mon Sep 17 00:00:00 2001
From 7b2d0bd30306dcc014f11b15817b0c5010554e7c Mon Sep 17 00:00:00 2001
From: Fei Lv <feilv@asrmicro.com>
Date: Fri, 14 Nov 2025 15:31:22 +0800
Subject: [PATCH] config: fix memleak during odhcpd reload
Subject: config: fix memleak during odhcpd reload
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

View File

@ -0,0 +1,103 @@
From 8ea9f568dfd8164b0483b5508830f51d488a1c4b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20H=C3=A4rdeman?= <david@hardeman.nu>
Date: Fri, 17 Oct 2025 15:32:08 +0200
Subject: ndp: correctness fix for BPF filter
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Note that AF_PACKET sockets start receiving packets as soon as they are
created. Thus, a packet can arrive between the creation of the socket
and the time the real filter is installed. Fix this using the same
technique as used in libpcap, i.e. by installing a drop-all filter,
removing any packets from the socket, then installing the real filter
later (an atomic operation which replaces the drop filter).
Signed-off-by: David Härdeman <david@hardeman.nu>
Link: https://github.com/openwrt/odhcpd/pull/319
Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com>
(cherry picked from commit 650a5df88ebae82d956831c2f87c7c6eedd8c283)
---
src/ndp.c | 56 +++++++++++++++++++++++++++++++++++++++++++------------
1 file changed, 44 insertions(+), 12 deletions(-)
--- a/src/ndp.c
+++ b/src/ndp.c
@@ -39,17 +39,6 @@ static void setup_addr_for_relaying(stru
static void handle_solicit(void *addr, void *data, size_t len,
struct interface *iface, void *dest);
-/* Filter ICMPv6 messages of type neighbor solicitation */
-static struct sock_filter bpf[] = {
- BPF_STMT(BPF_LD | BPF_B | BPF_ABS, offsetof(struct ip6_hdr, ip6_nxt)),
- BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, IPPROTO_ICMPV6, 0, 3),
- BPF_STMT(BPF_LD | BPF_B | BPF_ABS, sizeof(struct ip6_hdr) +
- offsetof(struct icmp6_hdr, icmp6_type)),
- BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, ND_NEIGHBOR_SOLICIT, 0, 1),
- BPF_STMT(BPF_RET | BPF_K, 0xffffffff),
- BPF_STMT(BPF_RET | BPF_K, 0),
-};
-static const struct sock_fprog bpf_prog = {sizeof(bpf) / sizeof(*bpf), bpf};
static struct netevent_handler ndp_netevent_handler = { .cb = ndp_netevent_cb, };
/* Initialize NDP-proxy */
@@ -67,6 +56,30 @@ int ndp_init(void)
int ndp_setup_interface(struct interface *iface, bool enable)
{
+ /* Drop everything */
+ static const struct sock_filter bpf_drop_filter[] = {
+ BPF_STMT(BPF_RET | BPF_K, 0),
+ };
+ static const struct sock_fprog bpf_drop = {
+ .len = ARRAY_SIZE(bpf_drop_filter),
+ .filter = (struct sock_filter *)bpf_drop_filter,
+ };
+
+ /* Filter ICMPv6 messages of type neighbor solicitation */
+ static const struct sock_filter bpf[] = {
+ BPF_STMT(BPF_LD | BPF_B | BPF_ABS, offsetof(struct ip6_hdr, ip6_nxt)),
+ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, IPPROTO_ICMPV6, 0, 3),
+ BPF_STMT(BPF_LD | BPF_B | BPF_ABS, sizeof(struct ip6_hdr) +
+ offsetof(struct icmp6_hdr, icmp6_type)),
+ BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, ND_NEIGHBOR_SOLICIT, 0, 1),
+ BPF_STMT(BPF_RET | BPF_K, 0xffffffff),
+ BPF_STMT(BPF_RET | BPF_K, 0),
+ };
+ static const struct sock_fprog bpf_prog = {
+ .len = ARRAY_SIZE(bpf),
+ .filter = (struct sock_filter *)bpf,
+ };
+
int ret = 0, procfd;
bool dump_neigh = false;
char procbuf[64];
@@ -170,8 +183,27 @@ int ndp_setup_interface(struct interface
}
#endif
+ /*
+ * AF_PACKET sockets can receive packets as soon as they are
+ * created, so make sure we don't accept anything...
+ */
+ if (setsockopt(iface->ndp_event.uloop.fd, SOL_SOCKET, SO_ATTACH_FILTER,
+ &bpf_drop, sizeof(bpf_drop))) {
+ syslog(LOG_ERR, "setsockopt(SO_ATTACH_FILTER): %m");
+ ret = -1;
+ goto out;
+ }
+
+ /* ...and remove stray packets... */
+ while (true) {
+ char null[1];
+ if (recv(iface->ndp_event.uloop.fd, null, sizeof(null), MSG_DONTWAIT | MSG_TRUNC) < 0)
+ break;
+ }
+
+ /* ...until the real filter is installed */
if (setsockopt(iface->ndp_event.uloop.fd, SOL_SOCKET, SO_ATTACH_FILTER,
- &bpf_prog, sizeof(bpf_prog))) {
+ &bpf_prog, sizeof(bpf_prog))) {
syslog(LOG_ERR, "setsockopt(SO_ATTACH_FILTER): %m");
ret = -1;
goto out;

View File

@ -0,0 +1,97 @@
From a7100a04a7da0936fd84e0808eded4e30e90696b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20H=C3=A4rdeman?= <david@hardeman.nu>
Date: Sun, 23 Nov 2025 14:46:49 +0100
Subject: config: fix (S)NTP realloc handling
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Make sure that realloc errors are dealt with properly in the (S)NTP
option parsing.
Signed-off-by: David Härdeman <david@hardeman.nu>
Link: https://github.com/openwrt/odhcpd/pull/320
Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com>
(cherry picked from commit b4ab371c541ab481c7dff001964afd621b5a968d)
---
src/config.c | 30 ++++++++++++++++--------------
1 file changed, 16 insertions(+), 14 deletions(-)
--- a/src/config.c
+++ b/src/config.c
@@ -564,10 +564,10 @@ static int parse_ntp_options(uint16_t *d
{
uint16_t sub_opt = 0, sub_len = htons(IPV6_ADDR_LEN);
uint16_t ntp_len = IPV6_ADDR_LEN + 4;
- uint8_t *ntp = *dhcpv6_ntp;
+ uint8_t *ntp;
size_t pos = *dhcpv6_ntp_len;
- ntp = realloc(ntp, pos + ntp_len);
+ ntp = realloc(*dhcpv6_ntp, pos + ntp_len);
if (!ntp)
return -1;
@@ -594,7 +594,7 @@ static int parse_ntp_fqdn(uint16_t *dhcp
{
size_t fqdn_len = strlen(fqdn);
uint16_t sub_opt = 0, sub_len = 0, ntp_len = 0;
- uint8_t *ntp = *dhcpv6_ntp;
+ uint8_t *ntp;
size_t pos = *dhcpv6_ntp_len;
uint8_t buf[256] = {0};
@@ -607,7 +607,7 @@ static int parse_ntp_fqdn(uint16_t *dhcp
ntp_len = len + 4;
- ntp = realloc(ntp, pos + ntp_len);
+ ntp = realloc(*dhcpv6_ntp, pos + ntp_len);
if (!ntp)
return -1;
@@ -1472,32 +1472,34 @@ int config_parse_interface(void *data, s
continue;
char *str = blobmsg_get_string(cur);
- struct in_addr addr4;
- struct in6_addr addr6;
+ struct in_addr addr4, *tmp4;
+ struct in6_addr addr6, *tmp6;
if (inet_pton(AF_INET, str, &addr4) == 1) {
if (addr4.s_addr == INADDR_ANY)
goto err;
- iface->dhcpv4_ntp = realloc(iface->dhcpv4_ntp,
- (++iface->dhcpv4_ntp_cnt) * sizeof(*iface->dhcpv4_ntp));
- if (!iface->dhcpv4_ntp)
+ tmp4 = realloc(iface->dhcpv4_ntp, (iface->dhcpv4_ntp_cnt + 1) * sizeof(*iface->dhcpv4_ntp));
+ if (!tmp4)
goto err;
- iface->dhcpv4_ntp[iface->dhcpv4_ntp_cnt - 1] = addr4;
+ iface->dhcpv4_ntp = tmp4;
+ iface->dhcpv4_ntp[iface->dhcpv4_ntp_cnt++] = addr4;
+
} else if (inet_pton(AF_INET6, str, &addr6) == 1) {
if (IN6_IS_ADDR_UNSPECIFIED(&addr6))
goto err;
- iface->dhcpv6_sntp = realloc(iface->dhcpv6_sntp,
- (++iface->dhcpv6_sntp_cnt) * sizeof(*iface->dhcpv6_sntp));
- if (!iface->dhcpv6_sntp)
+ tmp6 = realloc(iface->dhcpv6_sntp, (iface->dhcpv6_sntp_cnt + 1) * sizeof(*iface->dhcpv6_sntp));
+ if (!tmp6)
goto err;
- iface->dhcpv6_sntp[iface->dhcpv6_sntp_cnt - 1] = addr6;
+ iface->dhcpv6_sntp = tmp6;
+ iface->dhcpv6_sntp[iface->dhcpv6_sntp_cnt++] = addr6;
if (!parse_ntp_options(&iface->dhcpv6_ntp_len, addr6, &iface->dhcpv6_ntp))
iface->dhcpv6_ntp_cnt++;
+
} else {
if (!parse_ntp_fqdn(&iface->dhcpv6_ntp_len, str, &iface->dhcpv6_ntp))
iface->dhcpv6_ntp_cnt++;

View File

@ -0,0 +1,41 @@
From 061253933a52a394df7e79eae312212b1911d7fa Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20H=C3=A4rdeman?= <david@hardeman.nu>
Date: Sun, 23 Nov 2025 14:50:00 +0100
Subject: config: fix realloc() handling for the "upstream" option
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Deal properly with realloc() failure.
Signed-off-by: David Härdeman <david@hardeman.nu>
Link: https://github.com/openwrt/odhcpd/pull/320
Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com>
(cherry picked from commit adb2b8a1afffd805753dcc2e52c4764028116676)
---
src/config.c | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
--- a/src/config.c
+++ b/src/config.c
@@ -1062,16 +1062,17 @@ int config_parse_interface(void *data, s
if (overwrite && (c = tb[IFACE_ATTR_UPSTREAM])) {
struct blob_attr *cur;
unsigned rem;
+ char *tmp;
blobmsg_for_each_attr(cur, c, rem) {
if (blobmsg_type(cur) != BLOBMSG_TYPE_STRING || !blobmsg_check_attr(cur, false))
continue;
- iface->upstream = realloc(iface->upstream,
- iface->upstream_len + blobmsg_data_len(cur));
- if (!iface->upstream)
+ tmp = realloc(iface->upstream, iface->upstream_len + blobmsg_data_len(cur));
+ if (!tmp)
goto err;
+ iface->upstream = tmp;
memcpy(iface->upstream + iface->upstream_len, blobmsg_get_string(cur), blobmsg_data_len(cur));
iface->upstream_len += blobmsg_data_len(cur);
}

View File

@ -0,0 +1,38 @@
From 7474f1b5fb05705d9f04ccd45eeeea995ee9f09a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20H=C3=A4rdeman?= <david@hardeman.nu>
Date: Sun, 23 Nov 2025 14:53:35 +0100
Subject: config: fix realloc() error handling for "dhcpv6_raw" option
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Check the return value from realloc() to avoid leaking memory.
Signed-off-by: David Härdeman <david@hardeman.nu>
Link: https://github.com/openwrt/odhcpd/pull/320
Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com>
(cherry picked from commit 38b0920e376b236ea0b56686d08a4ef49b4bfcbe)
---
src/config.c | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
--- a/src/config.c
+++ b/src/config.c
@@ -1241,8 +1241,15 @@ int config_parse_interface(void *data, s
iface->dhcpv4_forcereconf = blobmsg_get_bool(c);
if ((c = tb[IFACE_ATTR_DHCPV6_RAW])) {
- iface->dhcpv6_raw_len = blobmsg_data_len(c) / 2;
- iface->dhcpv6_raw = realloc(iface->dhcpv6_raw, iface->dhcpv6_raw_len);
+ void *tmp;
+ size_t opt_len = blobmsg_data_len(c) / 2;
+
+ tmp = realloc(iface->dhcpv6_raw, opt_len);
+ if (!tmp)
+ goto err;
+
+ iface->dhcpv6_raw = tmp;
+ iface->dhcpv6_raw_len = opt_len;
odhcpd_unhexlify(iface->dhcpv6_raw, iface->dhcpv6_raw_len, blobmsg_get_string(c));
}

View File

@ -0,0 +1,33 @@
From 4e3aeeb71ba64f5fafff779e08e902510a4baff9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20H=C3=A4rdeman?= <david@hardeman.nu>
Date: Sun, 23 Nov 2025 15:00:45 +0100
Subject: config: handle realloc failure in piofolder parsing
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Another realloc that goes unchecked.
Signed-off-by: David Härdeman <david@hardeman.nu>
Link: https://github.com/openwrt/odhcpd/pull/320
Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com>
(cherry picked from commit 2e1042c6c07faad22bc300d1c526b24089b1ced9)
---
src/config.c | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
--- a/src/config.c
+++ b/src/config.c
@@ -1893,7 +1893,11 @@ void config_load_ra_pio(struct interface
free(iface->pios);
iface->pios = NULL;
} else if (iface->pio_cnt != pio_cnt) {
- iface->pios = realloc(iface->pios, sizeof(struct ra_pio) * iface->pio_cnt);
+ struct ra_pio *tmp;
+
+ tmp = realloc(iface->pios, sizeof(struct ra_pio) * iface->pio_cnt);
+ if (tmp)
+ iface->pios = tmp;
}
}

View File

@ -0,0 +1,32 @@
From 6bfc8547642eb61c755b36bbcc6616f742832aff Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20H=C3=A4rdeman?= <david@hardeman.nu>
Date: Sun, 23 Nov 2025 15:12:01 +0100
Subject: dhcpv6: handle realloc failure
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Another realloc failure case.
Signed-off-by: David Härdeman <david@hardeman.nu>
Link: https://github.com/openwrt/odhcpd/pull/320
Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com>
(cherry picked from commit b9db4d7061a08bf82a25222074065cce71973d0c)
---
src/dhcpv6-ia.c | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
--- a/src/dhcpv6-ia.c
+++ b/src/dhcpv6-ia.c
@@ -1636,8 +1636,9 @@ ssize_t dhcpv6_ia_handle_IAs(uint8_t *bu
hdr->msg_type == DHCPV6_MSG_REQUEST ||
hdr->msg_type == DHCPV6_MSG_REBIND)) {
if ((!(a->flags & OAF_STATIC) || !a->hostname) && hostname_len > 0) {
- a->hostname = realloc(a->hostname, hostname_len + 1);
- if (a->hostname) {
+ char *hostname = realloc(a->hostname, hostname_len + 1);
+ if (hostname) {
+ a->hostname = hostname;
memcpy(a->hostname, hostname, hostname_len);
a->hostname[hostname_len] = 0;

View File

@ -0,0 +1,37 @@
From 4ff35533494ea5b531540a69e90ede1124eaede7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?David=20H=C3=A4rdeman?= <david@hardeman.nu>
Date: Thu, 27 Nov 2025 11:35:09 +0100
Subject: dhcpv6-ia: fix realloc bug
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Commit b9db4d7061a08bf82a25222074065cce71973d0c introduced a bug, the
"hostname" variable used for the realloc would shadow the real hostname
defined at the beginning of the function. Fix this by using a different
variable name.
Fixes: b9db4d7061a0 ("dhcpv6: handle realloc failure")
Signed-off-by: David Härdeman <david@hardeman.nu>
Link: https://github.com/openwrt/odhcpd/pull/326
Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com>
(cherry picked from commit d21e504b38ab4c880c43b7f1649104bb2f0d2d8b)
---
src/dhcpv6-ia.c | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
--- a/src/dhcpv6-ia.c
+++ b/src/dhcpv6-ia.c
@@ -1636,9 +1636,9 @@ ssize_t dhcpv6_ia_handle_IAs(uint8_t *bu
hdr->msg_type == DHCPV6_MSG_REQUEST ||
hdr->msg_type == DHCPV6_MSG_REBIND)) {
if ((!(a->flags & OAF_STATIC) || !a->hostname) && hostname_len > 0) {
- char *hostname = realloc(a->hostname, hostname_len + 1);
- if (hostname) {
- a->hostname = hostname;
+ char *tmp = realloc(a->hostname, hostname_len + 1);
+ if (tmp) {
+ a->hostname = tmp;
memcpy(a->hostname, hostname, hostname_len);
a->hostname[hostname_len] = 0;

View File

@ -0,0 +1,51 @@
From 67ef12f1fbd69915ed72fbd030afe70ed08c96f8 Mon Sep 17 00:00:00 2001
From: Alexander Couzens <lynxis@fe80.eu>
Date: Fri, 6 Feb 2026 14:15:03 +0100
Subject: dhcpv6-ia: dhcpv6_log_ia_addr(): parse return of snprintf
snprintf() returns also in the failure case how many characters would have been required
to print the full string.
Check return of snprintf() and ensure the log_ctxt->buf_idx never points
outside of the given log_ctxt->buf.
Fixes: TOB-OWRT-5
Reported-by: Trail of Bits
Signed-off-by: Alexander Couzens <lynxis@fe80.eu>
(cherry picked from commit 13cec018c0d9bd452f4cb5e583c52fdf206cdc31)
---
src/dhcpv6-ia.c | 15 +++++++++++++--
1 file changed, 13 insertions(+), 2 deletions(-)
--- a/src/dhcpv6-ia.c
+++ b/src/dhcpv6-ia.c
@@ -1266,6 +1266,7 @@ static size_t build_ia(uint8_t *buf, siz
struct log_ctxt {
char *buf;
int buf_len;
+ /* if full, buf_idx will point to the last valid memory in buf */
int buf_idx;
};
@@ -1274,10 +1275,20 @@ static void dhcpv6_log_ia_addr(struct in
{
struct log_ctxt *ctxt = (struct log_ctxt *)arg;
char addrbuf[INET6_ADDRSTRLEN];
+ int ret;
+
+ /* Log buffer full */
+ if (ctxt->buf_idx >= ctxt->buf_len - 1)
+ return;
inet_ntop(AF_INET6, addr, addrbuf, sizeof(addrbuf));
- ctxt->buf_idx += snprintf(ctxt->buf + ctxt->buf_idx, ctxt->buf_len - ctxt->buf_idx,
- "%s/%d ", addrbuf, prefix);
+ ret = snprintf(ctxt->buf + ctxt->buf_idx, ctxt->buf_len - ctxt->buf_idx,
+ "%s/%d ", addrbuf, prefix);
+
+ if (ret + ctxt->buf_idx < ctxt->buf_len - 1)
+ ctxt->buf_idx += ret;
+ else
+ ctxt->buf_idx = ctxt->buf_len - 1;
}
static void dhcpv6_log(uint8_t msgtype, struct interface *iface, time_t now,

View File

@ -0,0 +1,120 @@
From 3c3223c0aadb8a3949f7cd4050a911a65c0abd70 Mon Sep 17 00:00:00 2001
From: Hauke Mehrtens <hauke@hauke-m.de>
Date: Sat, 23 May 2026 14:06:55 +0200
Subject: dhcpv6: bound nested-relay recursion to HOP_COUNT_LIMIT
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
handle_nested_message() and update_nested_message() recurse once for
every OPTION_RELAY_MSG inside a Relay-Forward message, with no depth
limit. A crafted UDP datagram packed full of nested Relay-Forward
options can drive recursion ~1700 deep on a ~64KB packet and exhaust
the daemon's stack.
Each nested Relay-Forward layer is exactly one relay hop: a relay agent
wraps the message it received in a new Relay-Forward and increments
hop_count. So the recursion depth here is just the relay-chain length
seen structurally. RFC8415 bounds that chain via HOP_COUNT_LIMIT
(defined in §7.6; a relay discards an over-limit Relay-forward per
§19.1.2), so a conforming message can never nest deeper than
HOP_COUNT_LIMIT layers.
Cap the descent at DHCPV6_HOP_COUNT_LIMIT accordingly. We count the
actual recursion depth instead of trusting each header's on-wire
hop_count, because an attacker controls the nesting and every per-layer
hop_count independently (e.g. nest deeply while stamping hop_count=0
everywhere). This is the same limit relay_client_request() already
enforces on the outer header of a forwarded message.
Note: DHCPV6_HOP_COUNT_LIMIT is 32, the value from the obsoleted
RFC3315 §5.5; RFC8415 §7.6 lowered the default to 8. Either way 32 is a
safe structural ceiling that drops only pathologically nested messages,
never a legitimate relay chain.
Assisted-by: Claude:claude-opus-4-7
Link: https://github.com/openwrt/odhcpd/pull/401
Signed-off-by: Hauke Mehrtens <hauke@hauke-m.de>
(cherry picked from commit ee0a11f40fa48302482fd6e85458e26d48de6e3a)
---
src/dhcpv6.c | 23 +++++++++++++++++------
1 file changed, 17 insertions(+), 6 deletions(-)
--- a/src/dhcpv6.c
+++ b/src/dhcpv6.c
@@ -188,7 +188,7 @@ enum {
IOV_TOTAL
};
-static void handle_nested_message(uint8_t *data, size_t len,
+static void handle_nested_message(uint8_t *data, size_t len, unsigned depth,
struct dhcpv6_client_header **c_hdr, uint8_t **opts,
uint8_t **end, struct iovec iov[IOV_TOTAL])
{
@@ -212,19 +212,26 @@ static void handle_nested_message(uint8_
return;
}
+ /* Each nested Relay-Forward is one relay hop; RFC8415 bounds a relay
+ * chain via HOP_COUNT_LIMIT (defined in §7.6, enforced in §19.1.2),
+ * so refuse to recurse deeper and avoid stack exhaustion on crafted
+ * relay loops. */
+ if (depth >= DHCPV6_HOP_COUNT_LIMIT)
+ return;
+
dhcpv6_for_each_option(r_hdr->options, data + len, otype, olen, odata) {
if (otype == DHCPV6_OPT_RELAY_MSG) {
iov[IOV_RELAY_MSG].iov_base = odata + olen;
iov[IOV_RELAY_MSG].iov_len = (((uint8_t *)iov[IOV_NESTED].iov_base) +
iov[IOV_NESTED].iov_len) - (odata + olen);
- handle_nested_message(odata, olen, c_hdr, opts, end, iov);
+ handle_nested_message(odata, olen, depth + 1, c_hdr, opts, end, iov);
return;
}
}
}
-static void update_nested_message(uint8_t *data, size_t len, ssize_t pdiff)
+static void update_nested_message(uint8_t *data, size_t len, unsigned depth, ssize_t pdiff)
{
struct dhcpv6_relay_header *hdr = (struct dhcpv6_relay_header*)data;
if (hdr->msg_type != DHCPV6_MSG_RELAY_FORW)
@@ -232,6 +239,10 @@ static void update_nested_message(uint8_
hdr->msg_type = DHCPV6_MSG_RELAY_REPL;
+ /* Bound recursion to mirror handle_nested_message(). */
+ if (depth >= DHCPV6_HOP_COUNT_LIMIT)
+ return;
+
uint16_t otype, olen;
uint8_t *odata;
dhcpv6_for_each_option(hdr->options, data + len, otype, olen, odata) {
@@ -239,7 +250,7 @@ static void update_nested_message(uint8_
olen += pdiff;
odata[-2] = (olen >> 8) & 0xff;
odata[-1] = olen & 0xff;
- update_nested_message(odata, olen - pdiff, pdiff);
+ update_nested_message(odata, olen - pdiff, depth + 1, pdiff);
return;
}
}
@@ -564,7 +575,7 @@ static void handle_client_request(void *
};
if (hdr->msg_type == DHCPV6_MSG_RELAY_FORW)
- handle_nested_message(data, len, &hdr, &opts, &opts_end, iov);
+ handle_nested_message(data, len, 0, &hdr, &opts, &opts_end, iov);
switch (hdr->msg_type) {
case DHCPV6_MSG_SOLICIT:
@@ -730,7 +741,7 @@ static void handle_client_request(void *
}
if (iov[IOV_NESTED].iov_len > 0) /* Update length */
- update_nested_message(data, len, iov[IOV_DEST].iov_len + iov[IOV_MAXRT].iov_len +
+ update_nested_message(data, len, 0, iov[IOV_DEST].iov_len + iov[IOV_MAXRT].iov_len +
iov[IOV_RAPID_COMMIT].iov_len + iov[IOV_DNS].iov_len +
iov[IOV_DNS_ADDR].iov_len + iov[IOV_SEARCH].iov_len +
iov[IOV_SEARCH_DOMAIN].iov_len + iov[IOV_PDBUF].iov_len +

View File

@ -0,0 +1,52 @@
From d1e66d0afb8ed749bee242c5ad9f322a0a9130e0 Mon Sep 17 00:00:00 2001
From: Hauke Mehrtens <hauke@hauke-m.de>
Date: Sat, 23 May 2026 14:09:09 +0200
Subject: dhcpv4: honor Pad/End option encoding when iterating options
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The dhcpv4_for_each_option() macro always read a length byte at offset 1
and advanced by len+2, which is wrong for the Pad (0) and End (255)
options. Per RFC2132 §3.1/§3.2 / RFC1497 those two options are a single octet
with no length byte, so the parser was effectively interpreting the
byte after a Pad as a length and skipping a variable, attacker-chosen
number of bytes — letting a crafted request hide later options (e.g.
DHCP message type, client-id) from the server. End was likewise
treated as a long-with-length option instead of terminating the scan.
Rewrite the macro to advance by 1 on Pad, stop on End, and otherwise
validate that code+len+data all fit in the buffer.
Assisted-by: Claude:claude-opus-4-7
Link: https://github.com/openwrt/odhcpd/pull/401
Signed-off-by: Hauke Mehrtens <hauke@hauke-m.de>
(cherry picked from commit db034cfd5eab2d0f3315cfb3cd74002fefa15828)
---
src/dhcpv4.h | 17 +++++++++++++----
1 file changed, 13 insertions(+), 4 deletions(-)
--- a/src/dhcpv4.h
+++ b/src/dhcpv4.h
@@ -98,8 +98,17 @@ struct dhcpv4_option {
};
+/* RFC2132 §3.1/§3.2 (orig. RFC1497): the Pad (0) and End (255) options are
+ * 1 octet long and have no length byte. Every other DHCPv4 option is
+ * { code, len, data[len] }. Treat Pad as a 1-byte no-op, End as loop
+ * termination, and reject any other option whose declared length runs past
+ * the buffer.
+ */
#define dhcpv4_for_each_option(start, end, opt)\
- for (opt = (struct dhcpv4_option*)(start); \
- &opt[1] <= (struct dhcpv4_option*)(end) && \
- &opt->data[opt->len] <= (end); \
- opt = (struct dhcpv4_option*)&opt->data[opt->len])
+ for (uint8_t *_o = (uint8_t *)(start); \
+ _o < (uint8_t *)(end) && \
+ (opt = (struct dhcpv4_option *)_o)->type != DHCPV4_OPT_END && \
+ (opt->type == DHCPV4_OPT_PAD || \
+ (_o + 2 <= (uint8_t *)(end) && \
+ _o + 2 + opt->len <= (uint8_t *)(end))); \
+ _o += (opt->type == DHCPV4_OPT_PAD) ? 1 : 2 + opt->len)

View File

@ -0,0 +1,62 @@
From 4100329ec73d4767602ca2c507ec1e1c1264752f Mon Sep 17 00:00:00 2001
From: Hauke Mehrtens <hauke@hauke-m.de>
Date: Sat, 23 May 2026 14:17:55 +0200
Subject: dhcpv6-ia: avoid undefined shifts in assign_pd()
assign_pd() computes (1 << (64 - length)) where the literal is a plain
int. When the prefix length is <= 32 the shift count is >= 32 bits,
which is undefined behaviour, and even at length == 33 the result
(1 << 31) is signed-overflow UB.
In normal operation the delegated prefix length is bounded by
dhcpv6_pd_min_len whose default keeps the shift in safe territory, but
that knob is user-configurable down to 1 in UCI, so a misconfigured
interface trips UB on every PD assignment. Compute the shift in
uint64_t and truncate explicitly to uint32_t so the arithmetic is
well-defined; for sane prefix lengths (>=33) the value is unchanged.
Assisted-by: Claude:claude-opus-4-7
Link: https://github.com/openwrt/odhcpd/pull/401
Signed-off-by: Hauke Mehrtens <hauke@hauke-m.de>
(cherry picked from commit 19bf3b73580b50030490c04e44adb6a14fe8c31c)
---
src/dhcpv6-ia.c | 14 ++++++++++----
1 file changed, 10 insertions(+), 4 deletions(-)
--- a/src/dhcpv6-ia.c
+++ b/src/dhcpv6-ia.c
@@ -769,8 +769,14 @@ static bool assign_pd(struct interface *
} else if (iface->addr6_len < 1)
return false;
- /* Try honoring the hint first */
- uint32_t current = 1, asize = (1 << (64 - assign->length)) - 1;
+ /* Try honoring the hint first.
+ *
+ * Subnet-id slots per delegated prefix = 2^(64 - length); the literal
+ * 1 is plain int, so shifting it by 32-63 bits is undefined behaviour
+ * for short prefix lengths (the user can drive this via
+ * dhcpv6_pd_min_len). Compute the shift in uint64_t and truncate.
+ */
+ uint32_t current = 1, asize = (uint32_t)((1ULL << (64 - assign->length)) - 1);
if (assign->assigned_subnet_id) {
list_for_each_entry(c, &iface->ia_assignments, head) {
if (c->flags & OAF_DHCPV6_NA)
@@ -785,7 +791,7 @@ static bool assign_pd(struct interface *
return true;
}
- current = (c->assigned_subnet_id + (1 << (64 - c->length)));
+ current = (uint32_t)(c->assigned_subnet_id + (1ULL << (64 - c->length)));
}
}
@@ -807,7 +813,7 @@ static bool assign_pd(struct interface *
return true;
}
- current = (c->assigned_subnet_id + (1 << (64 - c->length)));
+ current = (uint32_t)(c->assigned_subnet_id + (1ULL << (64 - c->length)));
}
return false;

View File

@ -0,0 +1,48 @@
From e587d403b689c04141cfeac638c68e898c5b565e Mon Sep 17 00:00:00 2001
From: Hauke Mehrtens <hauke@hauke-m.de>
Date: Sat, 23 May 2026 14:21:10 +0200
Subject: =?UTF-8?q?ndp:=20enforce=20RFC4861=20=C2=A77.1.1=20hop-limit=20an?=
=?UTF-8?q?d=20ICMP-code=20checks=20on=20NS?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
handle_solicit() previously trusted whatever the AF_PACKET socket
delivered, so a Neighbor Solicitation with Hop Limit < 255 or a
non-zero ICMP Code was forwarded to peer interfaces (triggering proxy
NDP lookups and an NA on the requesting interface). RFC4861 §7.1.1
requires NS messages with those properties to be silently discarded —
the hop-limit rule in particular is what prevents off-link attackers
from forging NS / DAD packets onto our link.
The router.c equivalent (router_icmpv6_valid + the IPV6_RECVHOPLIMIT
cmsg check in odhcpd_receive_packets) already does this for RA/RS;
the NDP path needs to enforce it itself because AF_PACKET sees raw
frames with no kernel-side L3 validation.
Assisted-by: Claude:claude-opus-4-7
Link: https://github.com/openwrt/odhcpd/pull/401
Signed-off-by: Hauke Mehrtens <hauke@hauke-m.de>
(cherry picked from commit 0eade3d4b7a37106f6498f4ce952ecd6ec1b932d)
---
src/ndp.c | 9 +++++++++
1 file changed, 9 insertions(+)
--- a/src/ndp.c
+++ b/src/ndp.c
@@ -382,6 +382,15 @@ static void handle_solicit(void *addr, v
if (len < sizeof(*ip6) + sizeof(*req))
return; // Invalid total length
+ /* RFC4861 §7.1.1: a Neighbor Solicitation MUST be silently discarded
+ * if the IP Hop Limit field is not 255 or the ICMP Code is non-zero.
+ * The hop-limit check is what protects against off-link attackers
+ * forging NS / DAD messages, so it must not be skipped. Because the
+ * NDP socket is AF_PACKET (not a kernel-managed ICMPv6 socket), we
+ * have to enforce it ourselves. */
+ if (ip6->ip6_hlim != 255 || req->nd_ns_hdr.icmp6_code != 0)
+ return;
+
if (IN6_IS_ADDR_LINKLOCAL(&req->nd_ns_target) ||
IN6_IS_ADDR_LOOPBACK(&req->nd_ns_target) ||
IN6_IS_ADDR_MULTICAST(&req->nd_ns_target))

View File

@ -0,0 +1,46 @@
From 82dca288b46f1cb5c6f5eaffb2080ebca4a3d651 Mon Sep 17 00:00:00 2001
From: Hauke Mehrtens <hauke@hauke-m.de>
Date: Sat, 30 May 2026 01:29:46 +0200
Subject: dhcpv6: validate minimum length in relay_client_request()
relay_server_response() refuses packets shorter than the relay header,
but the client-side relay path skipped any length check. With UDP
permitting zero/short payloads, a 0- or 1-byte datagram would let the
switch read h->msg_type from data[0] and (if it happened to equal
DHCPV6_MSG_RELAY_FORW = 12) h->hop_count from data[1] out of bounds.
The bytes come from the kernel receive buffer so it is a contained UB
read, but it is reachable.
relay_client_request() casts data to a struct dhcpv6_relay_header and
reads only h->msg_type (offset 0) and, for a RELAY_FORW, h->hop_count
(offset 1); the payload is forwarded verbatim and never parsed here.
Bound len on offsetof(struct dhcpv6_relay_header, link_address) so the
guard names the struct the function actually uses and the exact extent
it reads, mirroring how relay_server_response() bounds on its own
header.
Assisted-by: Claude:claude-opus-4-7
Link: https://github.com/openwrt/odhcpd/pull/401
Signed-off-by: Hauke Mehrtens <hauke@hauke-m.de>
(cherry picked from commit cc04882b3fac7643921a194ab65650274a69b2d6)
---
src/dhcpv6.c | 8 ++++++++
1 file changed, 8 insertions(+)
--- a/src/dhcpv6.c
+++ b/src/dhcpv6.c
@@ -902,6 +902,14 @@ static void relay_client_request(struct
struct odhcpd_ipaddr *ip;
struct sockaddr_in6 s;
+ /* A bare UDP socket can deliver a zero/short payload; the relay-reply
+ * path (relay_server_response) checks this but the client-side relay
+ * did not. relay_client_request() reads h->msg_type, plus h->hop_count
+ * for a RELAY_FORW, out of the relay header and forwards the rest
+ * verbatim, so require at least those two leading header bytes. */
+ if (len < offsetof(struct dhcpv6_relay_header, link_address))
+ return;
+
if (h->msg_type == DHCPV6_MSG_RELAY_REPL ||
h->msg_type == DHCPV6_MSG_RECONFIGURE ||
h->msg_type == DHCPV6_MSG_REPLY ||

View File

@ -0,0 +1,42 @@
From 60185a154c404004730c3136cc4cb63c8d61c978 Mon Sep 17 00:00:00 2001
From: Hauke Mehrtens <hauke@hauke-m.de>
Date: Sat, 11 Jul 2026 22:15:32 +0200
Subject: dhcpv4: copy ifname into arpreq without reading past the source
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
dhcpv4_set_dest_addr() filled arp.arp_dev (a fixed 16-byte field) via
memcpy(..., iface->ifname, sizeof(arp.arp_dev)). iface->ifname is a
strdup'd string whose allocation is just strlen(ifname)+1 bytes — for
typical names like "eth0" or "br-lan" that is 5–7 bytes, so the memcpy
reads ~10 bytes past the heap allocation. The bytes happen to be
inside the same malloc chunk on glibc so it has never blown up in
practice, but it is undefined behaviour and would trip valgrind / ASan.
Switch to the strncpy form already used for ifr_name elsewhere; the
struct is zero-initialised so the trailing byte stays NUL.
Assisted-by: Claude:claude-opus-4-7
Link: https://github.com/openwrt/odhcpd/pull/401
Signed-off-by: Hauke Mehrtens <hauke@hauke-m.de>
(cherry picked from commit 7c834a54ae6da732edaf2e8021fa3681e69951a3)
---
src/dhcpv4.c | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
--- a/src/dhcpv4.c
+++ b/src/dhcpv4.c
@@ -971,7 +971,11 @@ void dhcpv4_handle_msg(void *addr, void
if (!(iface->ifflags & IFF_NOARP)) {
memcpy(arp.arp_ha.sa_data, req->chaddr, 6);
memcpy(&arp.arp_pa, &dest, sizeof(arp.arp_pa));
- memcpy(arp.arp_dev, iface->ifname, sizeof(arp.arp_dev));
+ /* arp_dev is a 16-byte fixed buffer; strdup'd ifname is
+ * typically shorter than that, so memcpy()ing the full
+ * field length reads past the allocation. Match the
+ * strncpy pattern used elsewhere for ifr_name. */
+ strncpy(arp.arp_dev, iface->ifname, sizeof(arp.arp_dev) - 1);
if (ioctl(sock, SIOCSARP, &arp) < 0)
syslog(LOG_ERR, "ioctl(SIOCSARP): %m");

View File

@ -0,0 +1,75 @@
From 5669df2f672f093933dc67de549b765206ae6523 Mon Sep 17 00:00:00 2001
From: Hauke Mehrtens <hauke@hauke-m.de>
Date: Sat, 23 May 2026 14:39:03 +0200
Subject: dhcpv6: avoid unaligned uint16_t reads in ORO option parsing
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The Option Request Option payload is an array of 16-bit codes, but the
buffer (odata) is at an arbitrary offset inside the wire packet — on
strict-alignment platforms (ARM v5/v6, MIPS, SPARC) reading it through
a uint16_t * pointer triggers a misaligned access, which the kernel
either fixes up at high cost or signals as SIGBUS. The same pattern
appeared in the top-level ORO scan and again in the inner per-option
loop. Use byte-pointer access plus memcpy() to extract each code
portably.
Assisted-by: Claude:claude-opus-4-7
Link: https://github.com/openwrt/odhcpd/pull/401
Signed-off-by: Hauke Mehrtens <hauke@hauke-m.de>
(cherry picked from commit fa270221504c32861a9457dfa815b4215eaabbb0)
---
src/dhcpv6.c | 21 ++++++++++++++++-----
1 file changed, 16 insertions(+), 5 deletions(-)
--- a/src/dhcpv6.c
+++ b/src/dhcpv6.c
@@ -424,7 +424,11 @@ static void handle_client_request(void *
uint16_t otype, olen;
uint8_t *odata;
- uint16_t *reqopts = NULL;
+ /* OPTION_ORO payload is an array of uint16_t but the underlying buffer
+ * isn't guaranteed to be 2-byte aligned (it's at an arbitrary offset
+ * inside the packed wire packet), so keep it as a byte pointer and
+ * memcpy each value out rather than casting to uint16_t *. */
+ uint8_t *reqopts = NULL;
size_t reqopts_cnt = 0;
/* FIXME: this should be merged with the second loop further down */
@@ -432,14 +436,17 @@ static void handle_client_request(void *
/* Requested options, array of uint16_t, RFC 8415 §21.7 */
if (otype == DHCPV6_OPT_ORO) {
reqopts_cnt = olen / sizeof(uint16_t);
- reqopts = (uint16_t *)odata;
+ reqopts = odata;
break;
}
}
/* Requested options */
for (size_t i = 0; i < reqopts_cnt; i++) {
- uint16_t opt = ntohs(reqopts[i]);
+ uint16_t opt;
+
+ memcpy(&opt, &reqopts[i * sizeof(uint16_t)], sizeof(opt));
+ opt = ntohs(opt);
switch (opt) {
case DHCPV6_OPT_SNTP_SERVERS:
@@ -647,8 +654,12 @@ static void handle_client_request(void *
iov[IOV_RAPID_COMMIT].iov_len = sizeof(rapid_commit);
o_rapid_commit = true;
} else if (otype == DHCPV6_OPT_ORO) {
- for (int i=0; i < olen/2; i++) {
- uint16_t option = ntohs(((uint16_t *)odata)[i]);
+ for (int i = 0; i < olen / 2; i++) {
+ uint16_t option;
+
+ /* odata is not guaranteed to be uint16-aligned. */
+ memcpy(&option, &odata[i * 2], sizeof(option));
+ option = ntohs(option);
switch (option) {
#ifdef DHCPV4_SUPPORT

View File

@ -0,0 +1,39 @@
From 17ce0e6e745009bc4dd8f9d44862bf91c5627cfa Mon Sep 17 00:00:00 2001
From: Hauke Mehrtens <hauke@hauke-m.de>
Date: Sat, 23 May 2026 14:41:19 +0200
Subject: dhcpv6-pxe: free previous default entry on replacement
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
ipv6_pxe_entry_new() stores arch == 0xFFFFFFFF (the "no-arch / default"
case) in the standalone ipv6_pxe_default pointer instead of on the
list. When the function is called with that arch more than once
between ipv6_pxe_clear() runs — e.g. a UCI config with two boot6
sections that omit the arch field — the previous default was simply
overwritten and leaked.
Free the prior default before replacing it.
Assisted-by: Claude:claude-opus-4-7
Link: https://github.com/openwrt/odhcpd/pull/401
Signed-off-by: Hauke Mehrtens <hauke@hauke-m.de>
(cherry picked from commit 3e1dd3b16c9a9f83a85bc1a889ac636f2528d043)
---
src/dhcpv6-pxe.c | 5 +++++
1 file changed, 5 insertions(+)
--- a/src/dhcpv6-pxe.c
+++ b/src/dhcpv6-pxe.c
@@ -32,6 +32,11 @@ const struct ipv6_pxe_entry* ipv6_pxe_en
ipe->bootfile_url.type = htons(DHCPV6_OPT_BOOTFILE_URL);
if (arch == 0xFFFFFFFF) {
+ /* The "default" entry lives outside ipv6_pxe_list; if config
+ * already supplied one (e.g. two boot6 sections without an
+ * arch), free the previous one before replacing it, otherwise
+ * it would leak until ipv6_pxe_clear() runs. */
+ free(ipv6_pxe_default);
ipv6_pxe_default = ipe;
}
else {

View File

@ -0,0 +1,45 @@
From df87c38468a5fbd49988255f7dfaa7b3b13deb86 Mon Sep 17 00:00:00 2001
From: Hauke Mehrtens <hauke@hauke-m.de>
Date: Sat, 23 May 2026 14:49:22 +0200
Subject: dhcpv6: validate length and alignment when reading CLIENT_ARCH option
The Client System Architecture Type option (RFC5970) carries one or
more 16-bit architecture codes. The handler read the first code via
((uint16_t *)odata)[0], which has two problems:
- if a client sends OPTION_CLIENT_ARCH with olen < 2 (a malformed
but otherwise validly framed option), this reads two bytes from
odata where only olen are guaranteed to be in bounds, drifting
into the next option header,
- odata is at an arbitrary offset within the wire packet, so the
uint16_t * cast crashes with SIGBUS on strict-alignment targets.
Require olen to cover the first entry and copy it out via memcpy().
Assisted-by: Claude:claude-opus-4-7
Link: https://github.com/openwrt/odhcpd/pull/401
Signed-off-by: Hauke Mehrtens <hauke@hauke-m.de>
(cherry picked from commit f3fdcf4d3fe2ad4dc6f906aaef0aee485e1e9c2c)
---
src/dhcpv6.c | 10 ++++++++--
1 file changed, 8 insertions(+), 2 deletions(-)
--- a/src/dhcpv6.c
+++ b/src/dhcpv6.c
@@ -688,8 +688,14 @@ static void handle_client_request(void *
break;
}
}
- } else if (otype == DHCPV6_OPT_CLIENT_ARCH) {
- uint16_t arch_code = ntohs(((uint16_t*)odata)[0]);
+ } else if (otype == DHCPV6_OPT_CLIENT_ARCH && olen >= sizeof(uint16_t)) {
+ uint16_t arch_code;
+
+ /* odata is not guaranteed to be uint16-aligned, and
+ * RFC5970 §3.3 mandates olen be a multiple of 2 with
+ * at least one architecture entry — read defensively. */
+ memcpy(&arch_code, odata, sizeof(arch_code));
+ arch_code = ntohs(arch_code);
ipv6_pxe_serve_boot_url(arch_code, &iov[IOV_BOOTFILE_URL]);
}
}

View File

@ -0,0 +1,45 @@
From 071f6525dc6b47861d15820587635a0587ecc650 Mon Sep 17 00:00:00 2001
From: Hauke Mehrtens <hauke@hauke-m.de>
Date: Sat, 23 May 2026 14:50:42 +0200
Subject: dhcpv6-ia: validate IA_NA/IA_PD option size before reading header
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
dhcpv6_ia_handle_IAs() casts the option payload to dhcpv6_ia_hdr and
reads ia->iaid (followed later by t1/t2 via build_ia()) without ever
checking that the option is long enough. The dhcpv6_for_each_option
macro only guarantees odata + olen <= end, so a client sending an
IA_NA or IA_PD option with olen < 12 lets us read the iaid/t1/t2
fields out of the next adjacent option (still inside the recv buffer,
so it's not exploitable for code execution, but it pollutes lookups
and breaks lease matching).
RFC8415 §21.4 / §21.21 fix the IA header at exactly 12 bytes plus
sub-options, so reject anything shorter.
Assisted-by: Claude:claude-opus-4-7
Link: https://github.com/openwrt/odhcpd/pull/401
Signed-off-by: Hauke Mehrtens <hauke@hauke-m.de>
(cherry picked from commit f2275efc72a87a95f971f159babcbadc5d241d42)
---
src/dhcpv6-ia.c | 8 ++++++++
1 file changed, 8 insertions(+)
--- a/src/dhcpv6-ia.c
+++ b/src/dhcpv6-ia.c
@@ -1456,6 +1456,14 @@ ssize_t dhcpv6_ia_handle_IAs(uint8_t *bu
if (!is_pd && !is_na)
continue;
+ /* RFC8415 §21.4 / §21.21: an IA_NA/IA_PD option carries at
+ * least 12 bytes of fixed payload (iaid + t1 + t2) before any
+ * sub-options. Without this guard, a crafted option with
+ * olen < 12 lets us read ia->iaid/t1/t2 out of bounds (the
+ * iterator only enforces odata + olen <= end). */
+ if (olen < sizeof(struct dhcpv6_ia_hdr) - 4)
+ continue;
+
struct dhcpv6_ia_hdr *ia = (struct dhcpv6_ia_hdr*)&odata[-4];
size_t ia_response_len = 0;
uint8_t reqlen = (is_pd) ? 62 : 128;

View File

@ -0,0 +1,37 @@
From c3791f5855cd92609df710f60b5b99ab21196b7f Mon Sep 17 00:00:00 2001
From: Hauke Mehrtens <hauke@hauke-m.de>
Date: Sat, 30 May 2026 23:29:16 +0200
Subject: config: bound DNR 'mandatory' SvcParam key count
The mandatory SvcParam parser collects the referenced SvcParamKeys into a
fixed-size stack array mkeys[DNR_SVC_MAX] but only rejected unknown keys and
keys not otherwise present in the entry -- it never bounded the number of
written entries. RFC 9460 requires each key to appear at most once in the
mandatory list, but the parser accepted duplicates, so a config string such
as "mandatory=alpn,alpn,alpn,..." (with a present key repeated more than
DNR_SVC_MAX times) writes past the end of mkeys, smashing the stack.
Reject the entry once DNR_SVC_MAX keys have been recorded.
Assisted-by: Claude:claude-opus-4-8
Link: https://github.com/openwrt/odhcpd/pull/401
Signed-off-by: Hauke Mehrtens <hauke@hauke-m.de>
(cherry picked from commit 461043731085761bc067e875a31da569c321f9cd)
---
src/config.c | 5 +++++
1 file changed, 5 insertions(+)
--- a/src/config.c
+++ b/src/config.c
@@ -772,6 +772,11 @@ static int parse_dnr_str(char *str, stru
goto err;
}
+ if (svc_val_len >= DNR_SVC_MAX) {
+ syslog(LOG_ERR, "Too many keys for SvcParam 'mandatory'");
+ goto err;
+ }
+
mkeys[svc_val_len++] = ntohs(mkey);
}

View File

@ -0,0 +1,63 @@
From e2c01cf55d1cc97c48234b0e70ed66657631d5f1 Mon Sep 17 00:00:00 2001
From: Hauke Mehrtens <hauke@hauke-m.de>
Date: Sat, 30 May 2026 23:43:21 +0200
Subject: router: restore upstream DNS between relayed RAs
forward_router_advertisement() forwards a single, shared packet buffer to
every slave interface in turn. The source MAC is reset to its upstream state
at the top of each iteration, but the RDNSS addresses -- rewritten in place
when a slave has always_rewrite_dns -- were not. Once one slave rewrote them,
every subsequent slave in the same loop was sent the previous slave's DNS
servers instead of the original upstream ones, so a slave without
always_rewrite_dns could leak another interface's DNS servers to its clients.
Snapshot the upstream DNS addresses before the loop and restore them at the
start of each iteration.
Backport note: upstream additionally restores the MTU option value and the
PIO/RA flags, which are only rewritten per-slave in later versions. 24.10
rewrites just the DNS addresses, so only that half of the fix applies here.
Assisted-by: Claude:claude-opus-4-8
Link: https://github.com/openwrt/odhcpd/pull/401
Signed-off-by: Hauke Mehrtens <hauke@hauke-m.de>
(cherry picked from commit d119e1abc35f61a5fe96a03bbb85811ceabca2e9)
---
src/router.c | 15 ++++++++++++++-
1 file changed, 14 insertions(+), 1 deletion(-)
--- a/src/router.c
+++ b/src/router.c
@@ -1105,7 +1105,7 @@ static void forward_router_advertisement
/* Rewrite options */
uint8_t *end = data + len;
uint8_t *mac_ptr = NULL;
- struct in6_addr *dns_ptr = NULL;
+ struct in6_addr *dns_ptr = NULL, *dns_ptr_orig = NULL;
size_t dns_count = 0;
icmpv6_for_each_option(opt, &adv[1], end) {
@@ -1129,10 +1129,23 @@ static void forward_router_advertisement
all_nodes.sin6_family = AF_INET6;
inet_pton(AF_INET6, ALL_IPV6_NODES, &all_nodes.sin6_addr);
+ /* Preserve the upstream DNS addresses: they are rewritten in place in the
+ * single shared packet buffer, so without restoring them a later slave
+ * would inherit a previous slave's rewritten values. */
+ if (dns_ptr && dns_count > 0) {
+ dns_ptr_orig = alloca(dns_count * sizeof(*dns_ptr_orig));
+ memcpy(dns_ptr_orig, dns_ptr, dns_count * sizeof(*dns_ptr_orig));
+ }
+
avl_for_each_element(&interfaces, c, avl) {
if (c->ra != MODE_RELAY || c->master)
continue;
+ /* Restore the upstream DNS addresses that a previous slave may
+ * have rewritten in the shared buffer. */
+ if (dns_ptr_orig)
+ memcpy(dns_ptr, dns_ptr_orig, dns_count * sizeof(*dns_ptr));
+
/* Fixup source hardware address option */
if (mac_ptr)
odhcpd_get_mac(c, mac_ptr);

View File

@ -0,0 +1,56 @@
From 8c00cc8447a55beb9f2dd9115de7cd09fa1f6126 Mon Sep 17 00:00:00 2001
From: Hauke Mehrtens <hauke@hauke-m.de>
Date: Sat, 30 May 2026 23:43:36 +0200
Subject: router: keep RA DNS option lengths within the uint8 length field
The RDNSS and DNSSL option length fields (RFC 8106) are uint8_t values
counting 8-byte units, but send_router_advert() computed them from the
configured DNS server count / search-list size without any bound. With 128 or
more configured DNS servers, "1 + 2*dns_addrs6_cnt" exceeds 255 and the len
field wrapped; likewise a search list larger than UINT8_MAX*8 bytes wrapped
search_sz/8. In both cases the actual bytes written (sized via dns_sz /
search_sz and the iovec length) used the full untruncated size, so the
emitted option's Length no longer matched its real size and clients would
mis-parse or drop the whole RA.
Cap the RDNSS server count at 127 (1 + 2*127 == 255) and skip the DNSSL
option entirely when it would not fit in a valid uint8 length, matching the
existing UINT8_MAX guard on the DHCPv4 search-list path.
Assisted-by: Claude:claude-opus-4-8
Link: https://github.com/openwrt/odhcpd/pull/401
Signed-off-by: Hauke Mehrtens <hauke@hauke-m.de>
(cherry picked from commit 60f488192d5d32023455d51e37ea9f11819e417a)
---
src/router.c | 13 ++++++++++++-
1 file changed, 12 insertions(+), 1 deletion(-)
--- a/src/router.c
+++ b/src/router.c
@@ -828,6 +828,12 @@ static int send_router_advert(struct int
dns_cnt = 1;
}
+ /* The RDNSS len field is a uint8_t counting 8-byte units, so it
+ * holds at most 127 addresses (1 + 2*127 == 255); drop any extra
+ * so the on-wire length stays consistent with the bytes sent. */
+ if (dns_cnt > 127)
+ dns_cnt = 127;
+
if (dns_cnt) {
dns_sz = sizeof(*dns) + sizeof(struct in6_addr)*dns_cnt;
@@ -849,7 +855,12 @@ static int send_router_advert(struct int
}
}
- if (search_len > 0) {
+ /* The DNSSL len field is a uint8_t counting 8-byte units, so the
+ * whole option must fit in UINT8_MAX*8 bytes; skip it otherwise
+ * rather than emit an option whose length field does not match
+ * its real size. */
+ if (search_len > 0 &&
+ sizeof(*search) + ((search_len + 7) & (~7)) + 8 <= UINT8_MAX * 8) {
size_t search_padded = ((search_len + 7) & (~7)) + 8;
search_sz = sizeof(*search) + search_padded;

View File

@ -0,0 +1,62 @@
From 72ee1787466736a109cda44ea36c60ce2b8fc168 Mon Sep 17 00:00:00 2001
From: Hauke Mehrtens <hauke@hauke-m.de>
Date: Sun, 31 May 2026 00:12:10 +0200
Subject: dhcpv6: validate rewritten RELAY_MSG length in
update_nested_message()
When building a relay-reply, update_nested_message() walks the nested
RELAY-FORW envelope of the (untrusted) request and adjusts every
OPTION_RELAY_MSG length by pdiff, the signed difference between the assembled
reply options and the original request options. It did this with
olen += pdiff; /* olen is uint16_t */
odata[-2] = olen >> 8; odata[-1] = olen;
update_nested_message(odata, olen - pdiff, ...);
If "olen + pdiff" leaves the 0..65535 range, the uint16_t wraps: the wrapped
value is written into the on-wire RELAY_MSG length field, and "olen - pdiff"
no longer recovers the original (iterator-validated) inner length. The bogus
length is then passed as the recursion's buffer size, so the inner option
walk uses "odata + bogus_len" as its end bound and reads option headers far
past the end of the receive buffer (an out-of-bounds read, plus a 2-byte
out-of-bounds write if a RELAY_MSG pattern is hit) driven by a crafted
multi-level Relay-Forward packet.
Compute the new length in a signed wide type, bail out if it would not fit in
the 16-bit field, and pass the already-validated original olen to the
recursion instead of reconstructing it from the mutated value.
Assisted-by: Claude:claude-opus-4-8
Link: https://github.com/openwrt/odhcpd/pull/401
Signed-off-by: Hauke Mehrtens <hauke@hauke-m.de>
(cherry picked from commit 5b1e3befb0b23f7b7506fb6209aed3aa9e04a6f7)
---
src/dhcpv6.c | 17 +++++++++++++----
1 file changed, 13 insertions(+), 4 deletions(-)
--- a/src/dhcpv6.c
+++ b/src/dhcpv6.c
@@ -247,10 +247,19 @@ static void update_nested_message(uint8_
uint8_t *odata;
dhcpv6_for_each_option(hdr->options, data + len, otype, olen, odata) {
if (otype == DHCPV6_OPT_RELAY_MSG) {
- olen += pdiff;
- odata[-2] = (olen >> 8) & 0xff;
- odata[-1] = olen & 0xff;
- update_nested_message(odata, olen - pdiff, depth + 1, pdiff);
+ ssize_t newlen = (ssize_t)olen + pdiff;
+
+ /* olen was validated to lie within the buffer by the option
+ * iterator. Reject a pdiff that would make the rewritten
+ * RELAY_MSG length wrap the 16-bit field, which would also feed
+ * a bogus length into the recursion below and walk options past
+ * the end of the (untrusted) packet buffer. */
+ if (newlen < 0 || newlen > UINT16_MAX)
+ return;
+
+ odata[-2] = (newlen >> 8) & 0xff;
+ odata[-1] = newlen & 0xff;
+ update_nested_message(odata, olen, depth + 1, pdiff);
return;
}
}

View File

@ -0,0 +1,33 @@
From 6b0d954c94f71ef2ad93bcd220196b5facf06f8a Mon Sep 17 00:00:00 2001
From: Hauke Mehrtens <hauke@hauke-m.de>
Date: Sun, 31 May 2026 00:25:16 +0200
Subject: dhcpv6-ia: signal buffer-full from build_ia() IA_ADDR branch
In the CONFIRM/RELEASE/DECLINE echo path of build_ia(), the IA_PREFIX branch
returns 0 (the buffer-full failure signal) when the next IA_PREFIX option will
not fit, but the sibling IA_ADDR branch used "continue" instead. That kept the
option loop running and then fell through to writing the IA header length and
returning a non-zero ia_len, so the caller treated the truncated IA -- now
missing the IA_ADDR(s) that did not fit -- as a successfully built option and
put it on the wire. Return 0 from the IA_ADDR branch as well so a full buffer
is reported consistently and a malformed IA is never emitted.
Assisted-by: Claude:claude-opus-4-8
Link: https://github.com/openwrt/odhcpd/pull/401
Signed-off-by: Hauke Mehrtens <hauke@hauke-m.de>
(cherry picked from commit 1782f3f2aad21cd7971798c898355a12a9611304)
---
src/dhcpv6-ia.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
--- a/src/dhcpv6-ia.c
+++ b/src/dhcpv6-ia.c
@@ -1255,7 +1255,7 @@ static size_t build_ia(uint8_t *buf, siz
};
if (buflen < ia_len + sizeof(o_ia_a))
- continue;
+ return 0;
memcpy(buf + ia_len, &o_ia_a, sizeof(o_ia_a));
ia_len += sizeof(o_ia_a);

View File

@ -0,0 +1,41 @@
From 85105c6eb4e9c34fbbddebee7641ca68e2aa1cd3 Mon Sep 17 00:00:00 2001
From: David Bauer <mail@david-bauer.net>
Date: Tue, 9 Jun 2026 21:42:17 +0200
Subject: odhcpd: fix integer underflow in dhcpv6_ia_handle_IAs
With buflen underflowed to ~SIZE_MAX, every subsequent check inside
build_ia passes, and the next matched IA serializes its IA_ADDR sub-options
into the caller's pdbuf[512] with no effective bound, smashing the
parent stack frame.
Cap the subtraction to avoid underflowing buflen.
Fixes: CVE-2026-53922
Reported-by: Trevor Lawrence <_SiCk@afflicted.sh>
Signed-off-by: David Bauer <mail@david-bauer.net>
(cherry picked from commit c6792bac3905d4bf726d914e9994f6d4b98c5b57)
---
src/dhcpv6-ia.c | 5 +++++
1 file changed, 5 insertions(+)
--- a/src/dhcpv6-ia.c
+++ b/src/dhcpv6-ia.c
@@ -1637,6 +1637,8 @@ ssize_t dhcpv6_ia_handle_IAs(uint8_t *bu
handshake_len += sizeof(auth);
}
+ if (handshake_len > buflen)
+ handshake_len = buflen;
buf += handshake_len;
buflen -= handshake_len;
@@ -1719,6 +1721,9 @@ ssize_t dhcpv6_ia_handle_IAs(uint8_t *bu
}
}
+ if (ia_response_len > buflen)
+ ia_response_len = buflen;
+
buf += ia_response_len;
buflen -= ia_response_len;
response_len += ia_response_len;

View File

@ -0,0 +1,33 @@
From 5119be91f5d9508ce9f3c86b0bb6d965186a336e Mon Sep 17 00:00:00 2001
From: David Bauer <mail@david-bauer.net>
Date: Tue, 9 Jun 2026 20:16:40 +0200
Subject: odhcpd: fix out-of-bounds write in build_ia
If a error status shall be indicated, odhcpd did not validate the size
of the available buffer correctly.
Account for the additionally required space and exit early in case the
available buffer is not sufficient.
Fixes: CVE-2026-53921
Fixes: CVE-2026-53922
Reported-by: Ying Dong <dyingc@gmail.com>
Reported-by: Trevor Lawrence <_SiCk@afflicted.sh>
Signed-off-by: David Bauer <mail@david-bauer.net>
(cherry picked from commit d329a15413387027453644cd363dca06ade28328)
---
src/dhcpv6-ia.c | 3 +++
1 file changed, 3 insertions(+)
--- a/src/dhcpv6-ia.c
+++ b/src/dhcpv6-ia.c
@@ -1035,6 +1035,9 @@ static size_t build_ia(uint8_t *buf, siz
.val = htons(status),
};
+ if (buflen < ia_len + sizeof(o_status))
+ return 0;
+
memcpy(buf + ia_len, &o_status, sizeof(o_status));
ia_len += sizeof(o_status);

View File

@ -0,0 +1,42 @@
From 9fd7ee00717f1a9a96297ca6ebb30956f6b695e1 Mon Sep 17 00:00:00 2001
From: David Bauer <mail@david-bauer.net>
Date: Tue, 9 Jun 2026 20:29:28 +0200
Subject: odhcpd: fix out of bounds write in dhcpv6_ia_handle_IAs
Check the buffer size before writing the DHCOv6 Reconf accept element to
the buffer. This was previously not the case, allowing an out of bounds
write.
Fixes: CVE-2026-53921
Reported-by: Ying Dong <dyingc@gmail.com>
Signed-off-by: David Bauer <mail@david-bauer.net>
(cherry picked from commit 0320032ae313452f3c8cb8725c621dd06c36dc03)
---
src/dhcpv6-ia.c | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
--- a/src/dhcpv6-ia.c
+++ b/src/dhcpv6-ia.c
@@ -1611,6 +1611,11 @@ ssize_t dhcpv6_ia_handle_IAs(uint8_t *bu
}
}
+ /* Reconfigure Accept */
+ size_t handshake_len = 4;
+ if (hdr->msg_type == DHCPV6_MSG_REQUEST)
+ handshake_len += sizeof(struct dhcpv6_auth_reconfigure);
+
if (!assigned || iface->addr6_len == 0)
/* Set error status */
status = (is_pd) ? DHCPV6_STATUS_NOPREFIXAVAIL : DHCPV6_STATUS_NOADDRSAVAIL;
@@ -1619,8 +1624,8 @@ ssize_t dhcpv6_ia_handle_IAs(uint8_t *bu
status = DHCPV6_STATUS_NOTONLINK;
assigned = false;
} else if (accept_reconf && assigned && !first &&
- hdr->msg_type != DHCPV6_MSG_REBIND) {
- size_t handshake_len = 4;
+ hdr->msg_type != DHCPV6_MSG_REBIND &&
+ buflen >= handshake_len) {
buf[0] = 0;
buf[1] = DHCPV6_OPT_RECONF_ACCEPT;
buf[2] = 0;

View File

@ -0,0 +1,73 @@
From 401fabdc7f525e7881dbcb016f1427bade57bd77 Mon Sep 17 00:00:00 2001
From: Caesurus <caesurus@openai.com>
Date: Thu, 14 May 2026 16:42:26 -0400
Subject: dhcpv6-ia: avoid dangling first lease pointer
odhcpd's DHCPv6 IA handler keeps a per-message `first`
lease pointer so multiple IA options in one message reuse
the same Reconfigure authentication key.
A DHCPv6 REQUEST from an adjacent-network client can make the
first IA set `first`, then make a later same-IAID IA fail the
on-link check and free that same lease, leaving `first` dangling.
A following IA in the same packet allocates a new lease and copies
`first->key`, producing a 16-byte heap-use-after-free read.
ASan confirms the UAF; native tracing shows the packet shape can
deterministically reclaim the freed lease as the destination
allocation, producing a zeroed Reconfigure key copy.
The validated impact is unauthenticated adjacent-network denial of
service and DHCPv6 Reconfigure/ForceRenew key confusion.
Arbitrary PC control is not proven for this UAF alone.
Fixes: CVE-2026-53918
Reported-by: OpenAI Security Team <OutboundDisclosures@openai.com>
Signed-off-by: David Bauer <mail@david-bauer.net>
(cherry picked from commit 26b122007030ebf192376aff8f98d428b25e2a93)
---
src/dhcpv6-ia.c | 12 +++++++-----
1 file changed, 7 insertions(+), 5 deletions(-)
--- a/src/dhcpv6-ia.c
+++ b/src/dhcpv6-ia.c
@@ -1410,7 +1410,8 @@ ssize_t dhcpv6_ia_handle_IAs(uint8_t *bu
const struct sockaddr_in6 *addr, const void *data, const uint8_t *end)
{
struct lease *l;
- struct dhcp_assignment *first = NULL;
+ uint8_t first_key[16];
+ bool first_key_valid = false;
const struct dhcpv6_client_header *hdr = data;
time_t now = odhcpd_time();
uint16_t otype, olen, clid_len = 0;
@@ -1581,8 +1582,8 @@ ssize_t dhcpv6_ia_handle_IAs(uint8_t *bu
a->iface = iface;
a->flags = (is_pd ? OAF_DHCPV6_PD : OAF_DHCPV6_NA);
- if (first)
- memcpy(a->key, first->key, sizeof(a->key));
+ if (first_key_valid)
+ memcpy(a->key, first_key, sizeof(a->key));
else
odhcpd_urandom(a->key, sizeof(a->key));
@@ -1623,7 +1624,7 @@ ssize_t dhcpv6_ia_handle_IAs(uint8_t *bu
/* Send NOTONLINK status for the IA */
status = DHCPV6_STATUS_NOTONLINK;
assigned = false;
- } else if (accept_reconf && assigned && !first &&
+ } else if (accept_reconf && assigned && !first_key_valid &&
hdr->msg_type != DHCPV6_MSG_REBIND &&
buflen >= handshake_len) {
buf[0] = 0;
@@ -1652,7 +1653,8 @@ ssize_t dhcpv6_ia_handle_IAs(uint8_t *bu
buflen -= handshake_len;
response_len += handshake_len;
- first = a;
+ memcpy(first_key, a->key, sizeof(first_key));
+ first_key_valid = true;
}
ia_response_len = build_ia(buf, buflen, status, ia, a, iface,

View File

@ -0,0 +1,50 @@
From e37e0500d8690ea2270e98718ec2886124e72828 Mon Sep 17 00:00:00 2001
From: Hauke Mehrtens <hauke@hauke-m.de>
Date: Thu, 18 Jun 2026 00:57:46 +0200
Subject: dhcpv6-ia: fix Reconfigure Accept Auth option write offset
Commit 0320032ae313 hoisted handshake_len above the Reconfigure Accept
block and pre-added sizeof(struct dhcpv6_auth_reconfigure) so the new
"buflen >= handshake_len" guard would reserve the full 36-byte handshake.
It left the inner write as "memcpy(buf + handshake_len, ...)" and kept
the now-redundant "handshake_len += sizeof(auth)", so handshake_len was
36 (not 4) at the copy and ended at 68.
As a result the 32-byte AUTH option was written at buf + 36 instead of
buf + 4. The guard only ensures buflen >= 36, so for buflen in [36, 67]
the copy ran past the buffer end - a reachable stack out-of-bounds write
(a REQUEST with 21 failing IA_PD options leaves buflen == 50 and
overflows the 512-byte pdbuf by 18 bytes). Even when the copy fit,
buf[4..35] was left uninitialised but counted in the response, leaking
32 bytes of stack and producing a malformed option stream.
Write the AUTH option at buf + 4 and drop the double-count so
handshake_len stays the validated full length. The "handshake_len >
buflen" cap is now unreachable (handshake_len no longer changes inside
the block) and is removed.
Fixes: 0320032ae313 ("odhcpd: fix out of bounds write in dhcpv6_ia_handle_IAs")
Assisted-by: Claude:claude-opus-4-8
Signed-off-by: Hauke Mehrtens <hauke@hauke-m.de>
(cherry picked from commit 03dacc23356b5789842b9d1bba0200fd005e35f3)
---
src/dhcpv6-ia.c | 6 +-----
1 file changed, 1 insertion(+), 5 deletions(-)
--- a/src/dhcpv6-ia.c
+++ b/src/dhcpv6-ia.c
@@ -1642,13 +1642,9 @@ ssize_t dhcpv6_ia_handle_IAs(uint8_t *bu
{0}
};
memcpy(auth.key, a->key, sizeof(a->key));
- memcpy(buf + handshake_len, &auth, sizeof(auth));
- handshake_len += sizeof(auth);
+ memcpy(buf + 4, &auth, sizeof(auth));
}
- if (handshake_len > buflen)
- handshake_len = buflen;
-
buf += handshake_len;
buflen -= handshake_len;
response_len += handshake_len;

View File

@ -0,0 +1,127 @@
From b3c111d5e2e28721d828ff968b1c31184d5d0629 Mon Sep 17 00:00:00 2001
From: Hauke Mehrtens <hauke@hauke-m.de>
Date: Sat, 11 Jul 2026 22:35:17 +0200
Subject: statefiles: escape client hostnames in the lease state file
A DHCP client fully controls its requested hostname (DHCPv4 option 12,
DHCPv6 FQDN option 39). The hostname is decoded with dn_expand(), and
since DNS labels may carry any octet (RFC 2181 section 11) while a valid
hostname is restricted to the LDH set (RFC 1035 section 2.3.1), the
decoded label can contain arbitrary bytes - spaces, control characters
and newlines included.
dhcpv6_ia_write_statefile() wrote c->hostname into the space-delimited,
line-based state file via snprintf("%s") without escaping, for both the
DHCPv6 and the DHCPv4 lease records, regardless of OAF_BROKEN_HOSTNAME;
the "broken\x20" marker was only cosmetic. A newline in the hostname
therefore injected an additional '#'-prefixed line into the state file,
forging an arbitrary extra lease record, and a space forged additional
fields on the same line. The forged record is served verbatim by
downstream consumers such as LuCI's rpcd-mod-luci getDHCPLeases, turning
an unauthenticated, pre-association DHCPv6/DHCPv4 client into a
lease-record spoofer and - combined with a missing HTML-escape in LuCI -
a stored XSS in the admin status page.
The dnsmasq hosts-file writers already bail on OAF_BROKEN_HOSTNAME, so
only the state file was affected.
Escape every byte that is not a bare LDH character as \xNN before writing
the hostname field. Valid hostnames are pure LDH and are emitted
unchanged, so only hostnames already flagged as broken are altered.
Backport note: 24.10 has no separate statefiles.c - both lease records are
written by dhcpv6_ia_write_statefile() in dhcpv6-ia.c, so the escape helper
lives there and is applied to both call sites.
Reported-by: puru1761 (GHSA-hhmc-92hw-535f)
Assisted-by: Claude:claude-opus-4-8
Link: https://github.com/openwrt/odhcpd/pull/404
Signed-off-by: Hauke Mehrtens <hauke@hauke-m.de>
(cherry picked from commit 68f382690bfaec56d5b1f31c3c31c48bcb642e3a)
---
src/dhcpv6-ia.c | 47 +++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 45 insertions(+), 2 deletions(-)
--- a/src/dhcpv6-ia.c
+++ b/src/dhcpv6-ia.c
@@ -288,6 +288,45 @@ struct write_ctxt {
int buf_idx;
};
+/*
+ * Escape a client-supplied hostname so it is safe to write as a single field
+ * into the space-delimited, line-based state file: every byte that is not a
+ * bare LDH character (the RFC 1035 section 2.3.1 "preferred name syntax" set
+ * accepted by odhcpd_valid_hostname()) is encoded as \xNN. A DHCP client
+ * controls its requested hostname (DHCPv4 option 12, DHCPv6 FQDN option) and,
+ * since DNS labels may carry any octet (RFC 2181 section 11), dn_expand()/the
+ * option parser copy the bytes verbatim - so without escaping a hostname could
+ * embed a newline (forging an extra '#'-prefixed lease record) or a space
+ * (forging additional fields). Valid hostnames are pure LDH and are copied
+ * unchanged. The result is written into dst (always NUL-terminated, truncated
+ * if it would not fit) and returned.
+ */
+static const char *escape_hostname(char *dst, size_t dstlen, const char *src)
+{
+ size_t pos = 0;
+
+ if (dstlen == 0)
+ return dst;
+
+ for (const unsigned char *c = (const unsigned char *)src; *c; c++) {
+ if ((*c >= '0' && *c <= '9') ||
+ (*c >= 'A' && *c <= 'Z') ||
+ (*c >= 'a' && *c <= 'z') ||
+ *c == '-' || *c == '_' || *c == '.') {
+ if (pos + 1 >= dstlen)
+ break;
+ dst[pos++] = *c;
+ } else {
+ if (pos + 4 >= dstlen)
+ break;
+ pos += sprintf(&dst[pos], "\\x%02x", *c);
+ }
+ }
+
+ dst[pos] = '\0';
+ return dst;
+}
+
static void dhcpv6_write_ia_addrhosts(struct in6_addr *addr, int prefix, _unused uint32_t pref_lt,
_unused uint32_t valid_lt, void *arg)
{
@@ -489,6 +528,7 @@ void dhcpv6_ia_write_statefile(void)
continue;
char duidbuf[264];
+ char hostbuf[256 * 4];
odhcpd_hexlify(duidbuf, ctxt.c->clid_data, ctxt.c->clid_len);
@@ -496,7 +536,8 @@ void dhcpv6_ia_write_statefile(void)
ctxt.buf_idx = snprintf(ctxt.buf, ctxt.buf_len, "# %s %s %x %s%s %"PRId64" ",
ctxt.iface->ifname, duidbuf, ntohl(ctxt.c->iaid),
(ctxt.c->flags & OAF_BROKEN_HOSTNAME) ? "broken\\x20" : "",
- (ctxt.c->hostname ? ctxt.c->hostname : "-"),
+ escape_hostname(hostbuf, sizeof(hostbuf),
+ ctxt.c->hostname ? ctxt.c->hostname : "-"),
(ctxt.c->valid_until > now ?
(int64_t)(ctxt.c->valid_until - now + wall_time) :
(INFINITE_VALID(ctxt.c->valid_until) ? -1 : 0)));
@@ -526,13 +567,15 @@ void dhcpv6_ia_write_statefile(void)
char ipbuf[INET6_ADDRSTRLEN];
char duidbuf[16];
+ char hostbuf[256 * 4];
odhcpd_hexlify(duidbuf, c->hwaddr, sizeof(c->hwaddr));
/* iface DUID iaid hostname lifetime assigned length [addrs...] */
ctxt.buf_idx = snprintf(ctxt.buf, ctxt.buf_len, "# %s %s ipv4 %s%s %"PRId64" %x 32 ",
ctxt.iface->ifname, duidbuf,
(c->flags & OAF_BROKEN_HOSTNAME) ? "broken\\x20" : "",
- (c->hostname ? c->hostname : "-"),
+ escape_hostname(hostbuf, sizeof(hostbuf),
+ c->hostname ? c->hostname : "-"),
(c->valid_until > now ?
(int64_t)(c->valid_until - now + wall_time) :
(INFINITE_VALID(c->valid_until) ? -1 : 0)),