summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorHauke Mehrtens2026-07-11 20:58:17 +0000
committerHauke Mehrtens2026-07-21 00:35:30 +0000
commit67c5b71d77e5277dc7f364238588d8e37e9a3ffa (patch)
tree533190353c43c062e2a10b855b69147b2e5229c9
parent262c09368fda76c85c519b7554f33bb98fb3263f (diff)
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>
-rw-r--r--package/network/services/odhcpd/Makefile2
-rw-r--r--package/network/services/odhcpd/patches/0001-ndp-Allow-NS-loopback-for-master-iface.patch4
-rw-r--r--package/network/services/odhcpd/patches/0002-router-fix-SLAAC-on-subnets-64.patch4
-rw-r--r--package/network/services/odhcpd/patches/0003-ndp-fix-macOS-IPv6-compatibility-by-using-link-local.patch (renamed from package/network/services/odhcpd/patches/0003-ndp-fix-macOS-IPv6-compatibility-by-using-link-local-source-addresses.patch)4
-rw-r--r--package/network/services/odhcpd/patches/0004-odhcpd-fix-a-compilation-error.patch4
-rw-r--r--package/network/services/odhcpd/patches/0005-config-fix-memleak-during-odhcpd-reload.patch4
-rw-r--r--package/network/services/odhcpd/patches/0006-ndp-correctness-fix-for-BPF-filter.patch103
-rw-r--r--package/network/services/odhcpd/patches/0007-config-fix-S-NTP-realloc-handling.patch97
-rw-r--r--package/network/services/odhcpd/patches/0008-config-fix-realloc-handling-for-the-upstream-option.patch41
-rw-r--r--package/network/services/odhcpd/patches/0009-config-fix-realloc-error-handling-for-dhcpv6_raw-opt.patch38
-rw-r--r--package/network/services/odhcpd/patches/0010-config-handle-realloc-failure-in-piofolder-parsing.patch33
-rw-r--r--package/network/services/odhcpd/patches/0011-dhcpv6-handle-realloc-failure.patch32
-rw-r--r--package/network/services/odhcpd/patches/0012-dhcpv6-ia-fix-realloc-bug.patch37
-rw-r--r--package/network/services/odhcpd/patches/0013-dhcpv6-ia-dhcpv6_log_ia_addr-parse-return-of-snprint.patch51
-rw-r--r--package/network/services/odhcpd/patches/0014-dhcpv6-bound-nested-relay-recursion-to-HOP_COUNT_LIM.patch120
-rw-r--r--package/network/services/odhcpd/patches/0015-dhcpv4-honor-Pad-End-option-encoding-when-iterating-.patch52
-rw-r--r--package/network/services/odhcpd/patches/0016-dhcpv6-ia-avoid-undefined-shifts-in-assign_pd.patch62
-rw-r--r--package/network/services/odhcpd/patches/0017-ndp-enforce-RFC4861-7.1.1-hop-limit-and-ICMP-code-ch.patch48
-rw-r--r--package/network/services/odhcpd/patches/0018-dhcpv6-validate-minimum-length-in-relay_client_reque.patch46
-rw-r--r--package/network/services/odhcpd/patches/0019-dhcpv4-copy-ifname-into-arpreq-without-reading-past-.patch42
-rw-r--r--package/network/services/odhcpd/patches/0020-dhcpv6-avoid-unaligned-uint16_t-reads-in-ORO-option-.patch75
-rw-r--r--package/network/services/odhcpd/patches/0021-dhcpv6-pxe-free-previous-default-entry-on-replacemen.patch39
-rw-r--r--package/network/services/odhcpd/patches/0022-dhcpv6-validate-length-and-alignment-when-reading-CL.patch45
-rw-r--r--package/network/services/odhcpd/patches/0023-dhcpv6-ia-validate-IA_NA-IA_PD-option-size-before-re.patch45
-rw-r--r--package/network/services/odhcpd/patches/0024-config-bound-DNR-mandatory-SvcParam-key-count.patch37
-rw-r--r--package/network/services/odhcpd/patches/0025-router-restore-upstream-DNS-between-relayed-RAs.patch63
-rw-r--r--package/network/services/odhcpd/patches/0026-router-keep-RA-DNS-option-lengths-within-the-uint8-l.patch56
-rw-r--r--package/network/services/odhcpd/patches/0027-dhcpv6-validate-rewritten-RELAY_MSG-length-in-update.patch62
-rw-r--r--package/network/services/odhcpd/patches/0028-dhcpv6-ia-signal-buffer-full-from-build_ia-IA_ADDR-b.patch33
-rw-r--r--package/network/services/odhcpd/patches/0029-odhcpd-fix-integer-underflow-in-dhcpv6_ia_handle_IAs.patch41
-rw-r--r--package/network/services/odhcpd/patches/0030-odhcpd-fix-out-of-bounds-write-in-build_ia.patch33
-rw-r--r--package/network/services/odhcpd/patches/0031-odhcpd-fix-out-of-bounds-write-in-dhcpv6_ia_handle_I.patch42
-rw-r--r--package/network/services/odhcpd/patches/0032-dhcpv6-ia-avoid-dangling-first-lease-pointer.patch73
-rw-r--r--package/network/services/odhcpd/patches/0033-dhcpv6-ia-fix-Reconfigure-Accept-Auth-option-write-o.patch50
-rw-r--r--package/network/services/odhcpd/patches/0034-statefiles-escape-client-hostnames-in-the-lease-stat.patch127
35 files changed, 1634 insertions, 11 deletions
diff --git a/package/network/services/odhcpd/Makefile b/package/network/services/odhcpd/Makefile
index 707e85be7f..03d012bcda 100644
--- a/package/network/services/odhcpd/Makefile
+++ b/package/network/services/odhcpd/Makefile
@@ -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
diff --git a/package/network/services/odhcpd/patches/0001-ndp-Allow-NS-loopback-for-master-iface.patch b/package/network/services/odhcpd/patches/0001-ndp-Allow-NS-loopback-for-master-iface.patch
index 1251dc3958..e54fba7da0 100644
--- a/package/network/services/odhcpd/patches/0001-ndp-Allow-NS-loopback-for-master-iface.patch
+++ b/package/network/services/odhcpd/patches/0001-ndp-Allow-NS-loopback-for-master-iface.patch
@@ -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
diff --git a/package/network/services/odhcpd/patches/0002-router-fix-SLAAC-on-subnets-64.patch b/package/network/services/odhcpd/patches/0002-router-fix-SLAAC-on-subnets-64.patch
index 67f1486bc4..95bf767c34 100644
--- a/package/network/services/odhcpd/patches/0002-router-fix-SLAAC-on-subnets-64.patch
+++ b/package/network/services/odhcpd/patches/0002-router-fix-SLAAC-on-subnets-64.patch
@@ -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
diff --git a/package/network/services/odhcpd/patches/0003-ndp-fix-macOS-IPv6-compatibility-by-using-link-local-source-addresses.patch b/package/network/services/odhcpd/patches/0003-ndp-fix-macOS-IPv6-compatibility-by-using-link-local.patch
index b3c240afe6..7f6a84ab14 100644
--- a/package/network/services/odhcpd/patches/0003-ndp-fix-macOS-IPv6-compatibility-by-using-link-local-source-addresses.patch
+++ b/package/network/services/odhcpd/patches/0003-ndp-fix-macOS-IPv6-compatibility-by-using-link-local.patch
@@ -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
diff --git a/package/network/services/odhcpd/patches/0004-odhcpd-fix-a-compilation-error.patch b/package/network/services/odhcpd/patches/0004-odhcpd-fix-a-compilation-error.patch
index 00f108e11e..d1f9c81052 100644
--- a/package/network/services/odhcpd/patches/0004-odhcpd-fix-a-compilation-error.patch
+++ b/package/network/services/odhcpd/patches/0004-odhcpd-fix-a-compilation-error.patch
@@ -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
diff --git a/package/network/services/odhcpd/patches/0005-config-fix-memleak-during-odhcpd-reload.patch b/package/network/services/odhcpd/patches/0005-config-fix-memleak-during-odhcpd-reload.patch
index 1d61133284..e2296b4035 100644
--- a/package/network/services/odhcpd/patches/0005-config-fix-memleak-during-odhcpd-reload.patch
+++ b/package/network/services/odhcpd/patches/0005-config-fix-memleak-during-odhcpd-reload.patch
@@ -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
diff --git a/package/network/services/odhcpd/patches/0006-ndp-correctness-fix-for-BPF-filter.patch b/package/network/services/odhcpd/patches/0006-ndp-correctness-fix-for-BPF-filter.patch
new file mode 100644
index 0000000000..03d68318ca
--- /dev/null
+++ b/package/network/services/odhcpd/patches/0006-ndp-correctness-fix-for-BPF-filter.patch
@@ -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;
diff --git a/package/network/services/odhcpd/patches/0007-config-fix-S-NTP-realloc-handling.patch b/package/network/services/odhcpd/patches/0007-config-fix-S-NTP-realloc-handling.patch
new file mode 100644
index 0000000000..31e020da02
--- /dev/null
+++ b/package/network/services/odhcpd/patches/0007-config-fix-S-NTP-realloc-handling.patch
@@ -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++;
diff --git a/package/network/services/odhcpd/patches/0008-config-fix-realloc-handling-for-the-upstream-option.patch b/package/network/services/odhcpd/patches/0008-config-fix-realloc-handling-for-the-upstream-option.patch
new file mode 100644
index 0000000000..c8fcfa7726
--- /dev/null
+++ b/package/network/services/odhcpd/patches/0008-config-fix-realloc-handling-for-the-upstream-option.patch
@@ -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);
+ }
diff --git a/package/network/services/odhcpd/patches/0009-config-fix-realloc-error-handling-for-dhcpv6_raw-opt.patch b/package/network/services/odhcpd/patches/0009-config-fix-realloc-error-handling-for-dhcpv6_raw-opt.patch
new file mode 100644
index 0000000000..4ecbcca53c
--- /dev/null
+++ b/package/network/services/odhcpd/patches/0009-config-fix-realloc-error-handling-for-dhcpv6_raw-opt.patch
@@ -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));
+ }
+
diff --git a/package/network/services/odhcpd/patches/0010-config-handle-realloc-failure-in-piofolder-parsing.patch b/package/network/services/odhcpd/patches/0010-config-handle-realloc-failure-in-piofolder-parsing.patch
new file mode 100644
index 0000000000..b2b6159517
--- /dev/null
+++ b/package/network/services/odhcpd/patches/0010-config-handle-realloc-failure-in-piofolder-parsing.patch
@@ -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;
+ }
+ }
+
diff --git a/package/network/services/odhcpd/patches/0011-dhcpv6-handle-realloc-failure.patch b/package/network/services/odhcpd/patches/0011-dhcpv6-handle-realloc-failure.patch
new file mode 100644
index 0000000000..b7cfb4471a
--- /dev/null
+++ b/package/network/services/odhcpd/patches/0011-dhcpv6-handle-realloc-failure.patch
@@ -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;
+
diff --git a/package/network/services/odhcpd/patches/0012-dhcpv6-ia-fix-realloc-bug.patch b/package/network/services/odhcpd/patches/0012-dhcpv6-ia-fix-realloc-bug.patch
new file mode 100644
index 0000000000..d4f7b152bf
--- /dev/null
+++ b/package/network/services/odhcpd/patches/0012-dhcpv6-ia-fix-realloc-bug.patch
@@ -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;
+
diff --git a/package/network/services/odhcpd/patches/0013-dhcpv6-ia-dhcpv6_log_ia_addr-parse-return-of-snprint.patch b/package/network/services/odhcpd/patches/0013-dhcpv6-ia-dhcpv6_log_ia_addr-parse-return-of-snprint.patch
new file mode 100644
index 0000000000..13c463ee0f
--- /dev/null
+++ b/package/network/services/odhcpd/patches/0013-dhcpv6-ia-dhcpv6_log_ia_addr-parse-return-of-snprint.patch
@@ -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,
diff --git a/package/network/services/odhcpd/patches/0014-dhcpv6-bound-nested-relay-recursion-to-HOP_COUNT_LIM.patch b/package/network/services/odhcpd/patches/0014-dhcpv6-bound-nested-relay-recursion-to-HOP_COUNT_LIM.patch
new file mode 100644
index 0000000000..26e35bc9c5
--- /dev/null
+++ b/package/network/services/odhcpd/patches/0014-dhcpv6-bound-nested-relay-recursion-to-HOP_COUNT_LIM.patch
@@ -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 +
diff --git a/package/network/services/odhcpd/patches/0015-dhcpv4-honor-Pad-End-option-encoding-when-iterating-.patch b/package/network/services/odhcpd/patches/0015-dhcpv4-honor-Pad-End-option-encoding-when-iterating-.patch
new file mode 100644
index 0000000000..ef5c578972
--- /dev/null
+++ b/package/network/services/odhcpd/patches/0015-dhcpv4-honor-Pad-End-option-encoding-when-iterating-.patch
@@ -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)
diff --git a/package/network/services/odhcpd/patches/0016-dhcpv6-ia-avoid-undefined-shifts-in-assign_pd.patch b/package/network/services/odhcpd/patches/0016-dhcpv6-ia-avoid-undefined-shifts-in-assign_pd.patch
new file mode 100644
index 0000000000..c9501cbf01
--- /dev/null
+++ b/package/network/services/odhcpd/patches/0016-dhcpv6-ia-avoid-undefined-shifts-in-assign_pd.patch
@@ -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;
diff --git a/package/network/services/odhcpd/patches/0017-ndp-enforce-RFC4861-7.1.1-hop-limit-and-ICMP-code-ch.patch b/package/network/services/odhcpd/patches/0017-ndp-enforce-RFC4861-7.1.1-hop-limit-and-ICMP-code-ch.patch
new file mode 100644
index 0000000000..0adf748f35
--- /dev/null
+++ b/package/network/services/odhcpd/patches/0017-ndp-enforce-RFC4861-7.1.1-hop-limit-and-ICMP-code-ch.patch
@@ -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))
diff --git a/package/network/services/odhcpd/patches/0018-dhcpv6-validate-minimum-length-in-relay_client_reque.patch b/package/network/services/odhcpd/patches/0018-dhcpv6-validate-minimum-length-in-relay_client_reque.patch
new file mode 100644
index 0000000000..47e0ce877b
--- /dev/null
+++ b/package/network/services/odhcpd/patches/0018-dhcpv6-validate-minimum-length-in-relay_client_reque.patch
@@ -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 ||
diff --git a/package/network/services/odhcpd/patches/0019-dhcpv4-copy-ifname-into-arpreq-without-reading-past-.patch b/package/network/services/odhcpd/patches/0019-dhcpv4-copy-ifname-into-arpreq-without-reading-past-.patch
new file mode 100644
index 0000000000..42b64683bc
--- /dev/null
+++ b/package/network/services/odhcpd/patches/0019-dhcpv4-copy-ifname-into-arpreq-without-reading-past-.patch
@@ -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");
diff --git a/package/network/services/odhcpd/patches/0020-dhcpv6-avoid-unaligned-uint16_t-reads-in-ORO-option-.patch b/package/network/services/odhcpd/patches/0020-dhcpv6-avoid-unaligned-uint16_t-reads-in-ORO-option-.patch
new file mode 100644
index 0000000000..6b2a3c2bb6
--- /dev/null
+++ b/package/network/services/odhcpd/patches/0020-dhcpv6-avoid-unaligned-uint16_t-reads-in-ORO-option-.patch
@@ -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
diff --git a/package/network/services/odhcpd/patches/0021-dhcpv6-pxe-free-previous-default-entry-on-replacemen.patch b/package/network/services/odhcpd/patches/0021-dhcpv6-pxe-free-previous-default-entry-on-replacemen.patch
new file mode 100644
index 0000000000..466b2e78da
--- /dev/null
+++ b/package/network/services/odhcpd/patches/0021-dhcpv6-pxe-free-previous-default-entry-on-replacemen.patch
@@ -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 {
diff --git a/package/network/services/odhcpd/patches/0022-dhcpv6-validate-length-and-alignment-when-reading-CL.patch b/package/network/services/odhcpd/patches/0022-dhcpv6-validate-length-and-alignment-when-reading-CL.patch
new file mode 100644
index 0000000000..c7e0992e0d
--- /dev/null
+++ b/package/network/services/odhcpd/patches/0022-dhcpv6-validate-length-and-alignment-when-reading-CL.patch
@@ -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]);
+ }
+ }
diff --git a/package/network/services/odhcpd/patches/0023-dhcpv6-ia-validate-IA_NA-IA_PD-option-size-before-re.patch b/package/network/services/odhcpd/patches/0023-dhcpv6-ia-validate-IA_NA-IA_PD-option-size-before-re.patch
new file mode 100644
index 0000000000..9c7f165d75
--- /dev/null
+++ b/package/network/services/odhcpd/patches/0023-dhcpv6-ia-validate-IA_NA-IA_PD-option-size-before-re.patch
@@ -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;
diff --git a/package/network/services/odhcpd/patches/0024-config-bound-DNR-mandatory-SvcParam-key-count.patch b/package/network/services/odhcpd/patches/0024-config-bound-DNR-mandatory-SvcParam-key-count.patch
new file mode 100644
index 0000000000..0c3f6a3434
--- /dev/null
+++ b/package/network/services/odhcpd/patches/0024-config-bound-DNR-mandatory-SvcParam-key-count.patch
@@ -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);
+ }
+
diff --git a/package/network/services/odhcpd/patches/0025-router-restore-upstream-DNS-between-relayed-RAs.patch b/package/network/services/odhcpd/patches/0025-router-restore-upstream-DNS-between-relayed-RAs.patch
new file mode 100644
index 0000000000..5a05793df9
--- /dev/null
+++ b/package/network/services/odhcpd/patches/0025-router-restore-upstream-DNS-between-relayed-RAs.patch
@@ -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);
diff --git a/package/network/services/odhcpd/patches/0026-router-keep-RA-DNS-option-lengths-within-the-uint8-l.patch b/package/network/services/odhcpd/patches/0026-router-keep-RA-DNS-option-lengths-within-the-uint8-l.patch
new file mode 100644
index 0000000000..3ec7989998
--- /dev/null
+++ b/package/network/services/odhcpd/patches/0026-router-keep-RA-DNS-option-lengths-within-the-uint8-l.patch
@@ -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;
diff --git a/package/network/services/odhcpd/patches/0027-dhcpv6-validate-rewritten-RELAY_MSG-length-in-update.patch b/package/network/services/odhcpd/patches/0027-dhcpv6-validate-rewritten-RELAY_MSG-length-in-update.patch
new file mode 100644
index 0000000000..02d8c00bb0
--- /dev/null
+++ b/package/network/services/odhcpd/patches/0027-dhcpv6-validate-rewritten-RELAY_MSG-length-in-update.patch
@@ -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;
+ }
+ }
diff --git a/package/network/services/odhcpd/patches/0028-dhcpv6-ia-signal-buffer-full-from-build_ia-IA_ADDR-b.patch b/package/network/services/odhcpd/patches/0028-dhcpv6-ia-signal-buffer-full-from-build_ia-IA_ADDR-b.patch
new file mode 100644
index 0000000000..765b469a30
--- /dev/null
+++ b/package/network/services/odhcpd/patches/0028-dhcpv6-ia-signal-buffer-full-from-build_ia-IA_ADDR-b.patch
@@ -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);
diff --git a/package/network/services/odhcpd/patches/0029-odhcpd-fix-integer-underflow-in-dhcpv6_ia_handle_IAs.patch b/package/network/services/odhcpd/patches/0029-odhcpd-fix-integer-underflow-in-dhcpv6_ia_handle_IAs.patch
new file mode 100644
index 0000000000..6456cd50f3
--- /dev/null
+++ b/package/network/services/odhcpd/patches/0029-odhcpd-fix-integer-underflow-in-dhcpv6_ia_handle_IAs.patch
@@ -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;
diff --git a/package/network/services/odhcpd/patches/0030-odhcpd-fix-out-of-bounds-write-in-build_ia.patch b/package/network/services/odhcpd/patches/0030-odhcpd-fix-out-of-bounds-write-in-build_ia.patch
new file mode 100644
index 0000000000..23541a3e31
--- /dev/null
+++ b/package/network/services/odhcpd/patches/0030-odhcpd-fix-out-of-bounds-write-in-build_ia.patch
@@ -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);
+
diff --git a/package/network/services/odhcpd/patches/0031-odhcpd-fix-out-of-bounds-write-in-dhcpv6_ia_handle_I.patch b/package/network/services/odhcpd/patches/0031-odhcpd-fix-out-of-bounds-write-in-dhcpv6_ia_handle_I.patch
new file mode 100644
index 0000000000..f8d679c5f0
--- /dev/null
+++ b/package/network/services/odhcpd/patches/0031-odhcpd-fix-out-of-bounds-write-in-dhcpv6_ia_handle_I.patch
@@ -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;
diff --git a/package/network/services/odhcpd/patches/0032-dhcpv6-ia-avoid-dangling-first-lease-pointer.patch b/package/network/services/odhcpd/patches/0032-dhcpv6-ia-avoid-dangling-first-lease-pointer.patch
new file mode 100644
index 0000000000..519b8e4f0a
--- /dev/null
+++ b/package/network/services/odhcpd/patches/0032-dhcpv6-ia-avoid-dangling-first-lease-pointer.patch
@@ -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,
diff --git a/package/network/services/odhcpd/patches/0033-dhcpv6-ia-fix-Reconfigure-Accept-Auth-option-write-o.patch b/package/network/services/odhcpd/patches/0033-dhcpv6-ia-fix-Reconfigure-Accept-Auth-option-write-o.patch
new file mode 100644
index 0000000000..37186a88f3
--- /dev/null
+++ b/package/network/services/odhcpd/patches/0033-dhcpv6-ia-fix-Reconfigure-Accept-Auth-option-write-o.patch
@@ -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;
diff --git a/package/network/services/odhcpd/patches/0034-statefiles-escape-client-hostnames-in-the-lease-stat.patch b/package/network/services/odhcpd/patches/0034-statefiles-escape-client-hostnames-in-the-lease-stat.patch
new file mode 100644
index 0000000000..9fdcf51220
--- /dev/null
+++ b/package/network/services/odhcpd/patches/0034-statefiles-escape-client-hostnames-in-the-lease-stat.patch
@@ -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)),