diff options
| author | Hauke Mehrtens | 2026-07-11 20:58:17 +0000 |
|---|---|---|
| committer | Hauke Mehrtens | 2026-07-21 00:35:30 +0000 |
| commit | 67c5b71d77e5277dc7f364238588d8e37e9a3ffa (patch) | |
| tree | 533190353c43c062e2a10b855b69147b2e5229c9 | |
| parent | 262c09368fda76c85c519b7554f33bb98fb3263f (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>
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)), |