diff options
| author | Hauke Mehrtens | 2026-07-16 20:09:53 +0000 |
|---|---|---|
| committer | Hauke Mehrtens | 2026-07-21 00:46:29 +0000 |
| commit | b9ad54db42ddf79335f5224a5ad612cb8389028a (patch) | |
| tree | baebbf33ec99c3a080774a516ff7246368024a90 | |
| parent | 67c5b71d77e5277dc7f364238588d8e37e9a3ffa (diff) | |
odhcp6c: cherry pick patches from main
This fixes multiple bugs, some might be security relevant.
Link: https://github.com/openwrt/openwrt/pull/24258
Signed-off-by: Hauke Mehrtens <hauke@hauke-m.de>
14 files changed, 804 insertions, 1 deletions
diff --git a/package/network/ipv6/odhcp6c/Makefile b/package/network/ipv6/odhcp6c/Makefile index c16ecfa083..a2aeddeecb 100644 --- a/package/network/ipv6/odhcp6c/Makefile +++ b/package/network/ipv6/odhcp6c/Makefile @@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=odhcp6c -PKG_RELEASE:=1 +PKG_RELEASE:=2 PKG_SOURCE_PROTO:=git PKG_SOURCE_URL=$(PROJECT_GIT)/project/odhcp6c.git diff --git a/package/network/ipv6/odhcp6c/patches/0001-dhcpv6-fix-out-of-bounds-end-pointer-when-parsing-IA.patch b/package/network/ipv6/odhcp6c/patches/0001-dhcpv6-fix-out-of-bounds-end-pointer-when-parsing-IA.patch new file mode 100644 index 0000000000..125755c1eb --- /dev/null +++ b/package/network/ipv6/odhcp6c/patches/0001-dhcpv6-fix-out-of-bounds-end-pointer-when-parsing-IA.patch @@ -0,0 +1,37 @@ +From 3d812c338eb93d897717627ebb1b7beb01646e6e Mon Sep 17 00:00:00 2001 +From: Hauke Mehrtens <hauke@hauke-m.de> +Date: Sat, 11 Jul 2026 23:29:19 +0200 +Subject: dhcpv6: fix out-of-bounds end pointer when parsing IA in Advertise + +dhcpv6_handle_advert passes 'odata + olen + sizeof(*ia_hdr)' as the end +pointer to dhcpv6_parse_ia. odata points to the start of the IA +option's data (just past the 4-byte option header) and olen is the +data length, so the valid sub-option range ends at 'odata + olen'. +The extra '+ sizeof(*ia_hdr)' (16 bytes) extends the range 16 bytes +past the IA option boundary, allowing dhcpv6_parse_ia's sub-option +iterator to read into adjacent option bytes (or past the receive +buffer when the IA is the last option) and parse them as bogus IA +sub-options, polluting STATE_IA_NA / STATE_IA_PD with garbage. + +The sibling call site in dhcpv6_handle_reply already passes the +correct 'odata + olen'. Match that here. + +Assisted-by: Claude:claude-opus-4-7 +Link: https://github.com/openwrt/odhcp6c/pull/160 +Signed-off-by: Hauke Mehrtens <hauke@hauke-m.de> +(cherry picked from commit b6f0c70f5fc26632d8a6c748efec5aa335aa4fde) +--- + src/dhcpv6.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +--- a/src/dhcpv6.c ++++ b/src/dhcpv6.c +@@ -959,7 +959,7 @@ static int dhcpv6_handle_advert(enum dhc + (otype == DHCPV6_OPT_IA_NA && na_mode != IA_MODE_NONE)) && + olen > -4 + sizeof(struct dhcpv6_ia_hdr)) { + struct dhcpv6_ia_hdr *ia_hdr = (void*)(&odata[-4]); +- dhcpv6_parse_ia(ia_hdr, odata + olen + sizeof(*ia_hdr)); ++ dhcpv6_parse_ia(ia_hdr, odata + olen); + } + + if (otype == DHCPV6_OPT_SERVERID && olen <= 130) { diff --git a/package/network/ipv6/odhcp6c/patches/0002-script-avoid-kill-0-when-SIGCHLD-races-script_call.patch b/package/network/ipv6/odhcp6c/patches/0002-script-avoid-kill-0-when-SIGCHLD-races-script_call.patch new file mode 100644 index 0000000000..28561f5f9f --- /dev/null +++ b/package/network/ipv6/odhcp6c/patches/0002-script-avoid-kill-0-when-SIGCHLD-races-script_call.patch @@ -0,0 +1,49 @@ +From ee3b760cb42935f4b6578c0481f75b793eadd0f7 Mon Sep 17 00:00:00 2001 +From: Hauke Mehrtens <hauke@hauke-m.de> +Date: Sat, 11 Jul 2026 23:31:09 +0200 +Subject: script: avoid kill(0) when SIGCHLD races script_call + +script_call() does + + if (running) { + ... + kill(running, SIGTERM); + } + +where 'running' is a volatile pid_t cleared to 0 by the SIGCHLD +handler when the previous script child exits. The 'if (running)' +gate and the 'kill(running, ...)' that follows it are independent +reads of the volatile variable. If SIGCHLD fires for the previous +child between these two reads, the handler clears 'running' to 0 +and kill() is called with pid == 0, which delivers SIGTERM to every +process in the same process group as odhcp6c, including odhcp6c +itself (and any peer/parent that shares the pgrp). + +Snapshot the pid into a local before checking it and using it, so the +kill() target is the value we actually tested. + +Assisted-by: Claude:claude-opus-4-7 +Link: https://github.com/openwrt/odhcp6c/pull/160 +Signed-off-by: Hauke Mehrtens <hauke@hauke-m.de> + +(cherry picked from commit 2521bf732b9f643ce36c5ac3b314b17c034a0f50) +--- + src/script.c | 5 +++-- + 1 file changed, 3 insertions(+), 2 deletions(-) + +--- a/src/script.c ++++ b/src/script.c +@@ -402,10 +402,11 @@ void script_call(const char *status, int + time_t now = odhcp6c_get_milli_time() / 1000; + bool running_script = false; + +- if (running) { ++ pid_t prev = running; ++ if (prev > 0) { + time_t diff = now - started; + +- kill(running, SIGTERM); ++ kill(prev, SIGTERM); + + if (diff > delay) + delay -= diff; diff --git a/package/network/ipv6/odhcp6c/patches/0003-odhcp6c-propagate-allocation-failure-from-insert_sta.patch b/package/network/ipv6/odhcp6c/patches/0003-odhcp6c-propagate-allocation-failure-from-insert_sta.patch new file mode 100644 index 0000000000..3b1eecdb1b --- /dev/null +++ b/package/network/ipv6/odhcp6c/patches/0003-odhcp6c-propagate-allocation-failure-from-insert_sta.patch @@ -0,0 +1,52 @@ +From 240ca48bb8f783177b86a21425c5b1b5bab2b7ae Mon Sep 17 00:00:00 2001 +From: Hauke Mehrtens <hauke@hauke-m.de> +Date: Sat, 23 May 2026 14:15:36 +0200 +Subject: odhcp6c: propagate allocation failure from insert_state +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +odhcp6c_insert_state always returned 0, even when odhcp6c_resize_state +failed (either because the state would exceed the 1024-byte cap or +because realloc() returned NULL). The callers act on the return value: + + * config_add_requested_options() logs and aborts when nonzero, but + silently accepts the lost option when zero — so a failed insert + becomes "succeeded but not actually added". + * dhcpv6_add_server_cand() frees the candidate's IA_NA / IA_PD only + when nonzero, so an insert failure both loses the candidate and + leaks its IA buffers. + +Return -1 when odhcp6c_resize_state() returns NULL, matching the +documented contract of the function and the behaviour of +odhcp6c_add_state(). + +Assisted-by: Claude:claude-opus-4-7 +Link: https://github.com/openwrt/odhcp6c/pull/160 +Signed-off-by: Hauke Mehrtens <hauke@hauke-m.de> +(cherry picked from commit 49d9c0112bb8cd9760cc13495caa5009b61cf2ad) +--- + src/odhcp6c.c | 11 ++++++----- + 1 file changed, 6 insertions(+), 5 deletions(-) + +--- a/src/odhcp6c.c ++++ b/src/odhcp6c.c +@@ -735,12 +735,13 @@ int odhcp6c_insert_state(enum odhcp6c_st + + uint8_t *n = odhcp6c_resize_state(state, len); + +- if (n) { +- uint8_t *sdata = state_data[state]; ++ if (!n) ++ return -1; + +- memmove(sdata + offset + len, sdata + offset, len_after); +- memcpy(sdata + offset, data, len); +- } ++ uint8_t *sdata = state_data[state]; ++ ++ memmove(sdata + offset + len, sdata + offset, len_after); ++ memcpy(sdata + offset, data, len); + + return 0; + } diff --git a/package/network/ipv6/odhcp6c/patches/0004-dhcpv6-ensure-hostname-buffer-is-NUL-terminated-for-.patch b/package/network/ipv6/odhcp6c/patches/0004-dhcpv6-ensure-hostname-buffer-is-NUL-terminated-for-.patch new file mode 100644 index 0000000000..d36b5d6961 --- /dev/null +++ b/package/network/ipv6/odhcp6c/patches/0004-dhcpv6-ensure-hostname-buffer-is-NUL-terminated-for-.patch @@ -0,0 +1,36 @@ +From 5495a8b3f20df00866946645b01869598d3ddd74 Mon Sep 17 00:00:00 2001 +From: Hauke Mehrtens <hauke@hauke-m.de> +Date: Sat, 11 Jul 2026 23:32:40 +0200 +Subject: dhcpv6: ensure hostname buffer is NUL-terminated for dn_comp + +POSIX permits gethostname() to leave the destination buffer +non-NUL-terminated when the system hostname is at least as long as the +buffer (the trailing NUL is dropped during truncation). Linux glibc +exhibits this behaviour. The buffer is passed directly to dn_comp(), +which scans for the terminator and would otherwise read past +fqdn_buf[]. + +Pass one fewer byte to gethostname() and explicitly NUL-terminate the +last byte so dn_comp() always sees a properly bounded C string. + +Assisted-by: Claude:claude-opus-4-7 +Link: https://github.com/openwrt/odhcp6c/pull/160 +Signed-off-by: Hauke Mehrtens <hauke@hauke-m.de> + +(cherry picked from commit 4bd976fd60c0e2baf00c92991d30507ece2b9075) +--- + src/dhcpv6.c | 3 ++- + 1 file changed, 2 insertions(+), 1 deletion(-) + +--- a/src/dhcpv6.c ++++ b/src/dhcpv6.c +@@ -341,7 +341,8 @@ static void dhcpv6_send(enum dhcpv6_msg + { + // Build FQDN + char fqdn_buf[256]; +- gethostname(fqdn_buf, sizeof(fqdn_buf)); ++ gethostname(fqdn_buf, sizeof(fqdn_buf) - 1); ++ fqdn_buf[sizeof(fqdn_buf) - 1] = '\0'; + struct { + uint16_t type; + uint16_t len; diff --git a/package/network/ipv6/odhcp6c/patches/0005-script-handle-allocation-failures-in-env-helpers.patch b/package/network/ipv6/odhcp6c/patches/0005-script-handle-allocation-failures-in-env-helpers.patch new file mode 100644 index 0000000000..9521d885a7 --- /dev/null +++ b/package/network/ipv6/odhcp6c/patches/0005-script-handle-allocation-failures-in-env-helpers.patch @@ -0,0 +1,121 @@ +From 43f9c5e03c6bf89ef93834c8de57fe6985f88b64 Mon Sep 17 00:00:00 2001 +From: Hauke Mehrtens <hauke@hauke-m.de> +Date: Sat, 23 May 2026 14:19:39 +0200 +Subject: script: handle allocation failures in env helpers + +ipv6_to_env(), fqdn_to_env(), bin_to_env(), entry_to_env(), +search_to_env(), int_to_env() and the inline PASSTHRU=... assembler +all called realloc(NULL, ...) / malloc() without checking the result, +then immediately wrote into the (possibly NULL) buffer with memcpy(), +snprintf() or script_hexlify(). Under memory pressure this dereferences +NULL inside the forked script-helper child and crashes it before +putenv() / execv(), so the state script does not run for that +state-change. + +Check each allocation and skip the corresponding environment variable +when it fails. While at it, replace the cosmetic realloc(NULL, ...) idiom +with plain malloc() to make intent clearer. + +Assisted-by: Claude:claude-opus-4-7 +Link: https://github.com/openwrt/odhcp6c/pull/160 +Signed-off-by: Hauke Mehrtens <hauke@hauke-m.de> +(cherry picked from commit 01130f80338a250f94ae0df786306c11662c88fb) +--- + src/script.c | 42 ++++++++++++++++++++++++++++++++---------- + 1 file changed, 32 insertions(+), 10 deletions(-) + +--- a/src/script.c ++++ b/src/script.c +@@ -98,7 +98,10 @@ static void ipv6_to_env(const char *name + const struct in6_addr *addr, size_t cnt) + { + size_t buf_len = strlen(name); +- char *buf = realloc(NULL, cnt * INET6_ADDRSTRLEN + buf_len + 2); ++ char *buf = malloc(cnt * INET6_ADDRSTRLEN + buf_len + 2); ++ ++ if (!buf) ++ return; + + memcpy(buf, name, buf_len); + buf[buf_len++] = '='; +@@ -121,7 +124,10 @@ static void fqdn_to_env(const char *name + size_t buf_len = strlen(name); + size_t buf_size = len + buf_len + 2; + const uint8_t *fqdn_end = fqdn + len; +- char *buf = realloc(NULL, len + buf_len + 2); ++ char *buf = malloc(buf_size); ++ ++ if (!buf) ++ return; + + memcpy(buf, name, buf_len); + buf[buf_len++] = '='; +@@ -148,9 +154,12 @@ static void bin_to_env(uint8_t *opts, si + uint16_t otype, olen; + + dhcpv6_for_each_option(opts, oend, otype, olen, odata) { +- char *buf = realloc(NULL, 14 + (olen * 2)); ++ char *buf = malloc(14 + (olen * 2)); + size_t buf_len = 0; + ++ if (!buf) ++ continue; ++ + snprintf(buf, 14, "OPTION_%hu=", otype); + buf_len += strlen(buf); + +@@ -173,7 +182,10 @@ static void entry_to_env(const char *nam + // Worst case: ENTRY_PREFIX with iaid != 1 and exclusion + const size_t max_entry_len = (INET6_ADDRSTRLEN-1 + 5 + 22 + 15 + 10 + + INET6_ADDRSTRLEN-1 + 11 + 1); +- char *buf = realloc(NULL, buf_len + 2 + (len / sizeof(*e)) * max_entry_len); ++ char *buf = malloc(buf_len + 2 + (len / sizeof(*e)) * max_entry_len); ++ ++ if (!buf) ++ return; + + memcpy(buf, name, buf_len); + buf[buf_len++] = '='; +@@ -238,8 +250,13 @@ static void entry_to_env(const char *nam + static void search_to_env(const char *name, const uint8_t *start, size_t len) + { + size_t buf_len = strlen(name); +- char *buf = realloc(NULL, buf_len + 2 + len); +- char *c = mempcpy(buf, name, buf_len); ++ char *buf = malloc(buf_len + 2 + len); ++ char *c; ++ ++ if (!buf) ++ return; ++ ++ c = mempcpy(buf, name, buf_len); + *c++ = '='; + + for (struct odhcp6c_entry *e = (struct odhcp6c_entry*)start; +@@ -262,7 +279,10 @@ static void search_to_env(const char *na + static void int_to_env(const char *name, int value) + { + size_t len = 13 + strlen(name); +- char *buf = realloc(NULL, len); ++ char *buf = malloc(len); ++ ++ if (!buf) ++ return; + + snprintf(buf, len, "%s=%d", name, value); + putenv(buf); +@@ -495,9 +515,11 @@ void script_call(const char *status, int + int_to_env("RA_RETRANSMIT", ra_get_retransmit()); + + char *buf = malloc(10 + passthru_len * 2); +- strncpy(buf, "PASSTHRU=", 10); +- script_hexlify(&buf[9], passthru, passthru_len); +- putenv(buf); ++ if (buf) { ++ strncpy(buf, "PASSTHRU=", 10); ++ script_hexlify(&buf[9], passthru, passthru_len); ++ putenv(buf); ++ } + + execv(argv[0], argv); + _exit(128); diff --git a/package/network/ipv6/odhcp6c/patches/0006-odhcp6c-refuse-to-follow-symlinks-when-writing-pidfi.patch b/package/network/ipv6/odhcp6c/patches/0006-odhcp6c-refuse-to-follow-symlinks-when-writing-pidfi.patch new file mode 100644 index 0000000000..b93b95d874 --- /dev/null +++ b/package/network/ipv6/odhcp6c/patches/0006-odhcp6c-refuse-to-follow-symlinks-when-writing-pidfi.patch @@ -0,0 +1,52 @@ +From 2b2aeabbb9550d9617878f32b6ed1ed2a38e33ad Mon Sep 17 00:00:00 2001 +From: Hauke Mehrtens <hauke@hauke-m.de> +Date: Sat, 23 May 2026 14:20:53 +0200 +Subject: odhcp6c: refuse to follow symlinks when writing pidfile + +odhcp6c is generally run as root and writes its pidfile with +fopen(pidfile, "w"), which calls open(O_WRONLY | O_CREAT | O_TRUNC) +underneath. open() follows symlinks by default, so if an attacker (or +a broken init script) can place a symlink at the pidfile path before +odhcp6c starts, the symlink target gets truncated and overwritten with +a single line containing the daemon's PID. With /var/run sometimes +shared between services this gives a primitive for trashing arbitrary +root-owned files, plus the file is created with the symlink target's +ownership instead of root:root. + +Use open() with O_NOFOLLOW | O_CLOEXEC explicitly and fdopen() the +result; the file is still truncated and replaced when it is a regular +file, but a symlink in the path causes open() to fail with ELOOP and +we simply skip pidfile creation rather than clobbering the target. + +Assisted-by: Claude:claude-opus-4-7 +Link: https://github.com/openwrt/odhcp6c/pull/160 +Signed-off-by: Hauke Mehrtens <hauke@hauke-m.de> +(cherry picked from commit 0a19052dc9fb73530adeff9b61edb3a35f15bc2a) +--- + src/odhcp6c.c | 15 +++++++++++---- + 1 file changed, 11 insertions(+), 4 deletions(-) + +--- a/src/odhcp6c.c ++++ b/src/odhcp6c.c +@@ -454,10 +454,17 @@ int main(_unused int argc, char* const a + pidfile = (char*)buf; + } + +- FILE *fp = fopen(pidfile, "w"); +- if (fp) { +- fprintf(fp, "%i\n", getpid()); +- fclose(fp); ++ int pidfd = open(pidfile, ++ O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW | O_CLOEXEC, ++ 0644); ++ if (pidfd >= 0) { ++ FILE *fp = fdopen(pidfd, "w"); ++ if (fp) { ++ fprintf(fp, "%i\n", getpid()); ++ fclose(fp); ++ } else { ++ close(pidfd); ++ } + } + } + diff --git a/package/network/ipv6/odhcp6c/patches/0007-odhcp6c-skip-malformed-proc-net-if_inet6-entries.patch b/package/network/ipv6/odhcp6c/patches/0007-odhcp6c-skip-malformed-proc-net-if_inet6-entries.patch new file mode 100644 index 0000000000..f3c1188fdd --- /dev/null +++ b/package/network/ipv6/odhcp6c/patches/0007-odhcp6c-skip-malformed-proc-net-if_inet6-entries.patch @@ -0,0 +1,54 @@ +From 760b53c7400c2b60cf48723509fc123843ad07f4 Mon Sep 17 00:00:00 2001 +From: Hauke Mehrtens <hauke@hauke-m.de> +Date: Sat, 23 May 2026 14:21:37 +0200 +Subject: odhcp6c: skip malformed /proc/net/if_inet6 entries + +The validation loop in odhcp6c_addr_in_scope() walked addr_buf +checking each character with isxdigit() / !isupper(), but discarded +the result: 'i' was a local that went out of scope at the end of the +loop, and execution fell straight into the decode loop regardless. If +the kernel ever produced unexpected characters (or someone bind-mounts +a non-procfs over /proc/net/if_inet6), index(hex, c) would return NULL +and the subsequent 'NULL - hex' subtraction is undefined behaviour; +the address bytes were then derived from arbitrary pointer arithmetic. + +Track the validation outcome explicitly, also reject inputs whose +length isn't 32 hex characters (one IPv6 address worth), and continue +to the next /proc line on any failure so the decode loop only sees +well-formed inputs. + +Assisted-by: Claude:claude-opus-4-7 +Link: https://github.com/openwrt/odhcp6c/pull/160 +Signed-off-by: Hauke Mehrtens <hauke@hauke-m.de> +(cherry picked from commit d6c2fbdc255c143afc4d2c670f602c9cc9308869) +--- + src/odhcp6c.c | 15 +++++++++++---- + 1 file changed, 11 insertions(+), 4 deletions(-) + +--- a/src/odhcp6c.c ++++ b/src/odhcp6c.c +@@ -940,13 +940,20 @@ bool odhcp6c_addr_in_scope(const struct + (flags & (IFA_F_DADFAILED | IFA_F_TENTATIVE | IFA_F_DEPRECATED))) + continue; + +- for (i = 0; i < strlen(addr_buf); i++) { +- if (!isxdigit(addr_buf[i]) || isupper(addr_buf[i])) +- break; ++ size_t addr_len = strlen(addr_buf); ++ bool valid = (addr_len == 2 * sizeof(inet6_addr.s6_addr)); ++ ++ for (i = 0; valid && i < addr_len; i++) { ++ if (!isxdigit((unsigned char)addr_buf[i]) || ++ isupper((unsigned char)addr_buf[i])) ++ valid = false; + } + ++ if (!valid) ++ continue; ++ + memset(&inet6_addr, 0, sizeof(inet6_addr)); +- for (i = 0; i < (strlen(addr_buf) / 2); i++) { ++ for (i = 0; i < (addr_len / 2); i++) { + unsigned char byte; + static const char hex[] = "0123456789abcdef"; + byte = ((index(hex, addr_buf[i * 2]) - hex) << 4) | diff --git a/package/network/ipv6/odhcp6c/patches/0008-script-handle-fork-failure-when-launching-state-scri.patch b/package/network/ipv6/odhcp6c/patches/0008-script-handle-fork-failure-when-launching-state-scri.patch new file mode 100644 index 0000000000..ba659d92e7 --- /dev/null +++ b/package/network/ipv6/odhcp6c/patches/0008-script-handle-fork-failure-when-launching-state-scri.patch @@ -0,0 +1,50 @@ +From a04d72ac6c809cc9f189b635839ddbb9a48e44a3 Mon Sep 17 00:00:00 2001 +From: Hauke Mehrtens <hauke@hauke-m.de> +Date: Sat, 11 Jul 2026 23:34:04 +0200 +Subject: script: handle fork() failure when launching state script + +If fork() returns -1 (e.g. under EAGAIN / ENOMEM), neither the parent +nor the child branch runs. Control falls through to the bottom of +script_call() while 'running' still holds the previous (just-killed) +child's pid and 'started' still holds the previous start time. The +next call sees running != 0, fires kill() at a process that no longer +exists, and accumulates a misleading 'diff = now - started' against +the older start time. No diagnostic is logged either. + +Log the failure, clear 'running' so subsequent calls do not chase a +ghost pid, and return without going through the (parent-only) +post-fork bookkeeping. + +Assisted-by: Claude:claude-opus-4-7 +Link: https://github.com/openwrt/odhcp6c/pull/160 +Signed-off-by: Hauke Mehrtens <hauke@hauke-m.de> + +(cherry picked from commit c938c168cbef5642cab42389d96c340e40f4f8ff) +[use syslog() instead of the error() log helper, which is not present in 24.10] +--- + src/script.c | 7 +++++++ + 1 file changed, 7 insertions(+) + +--- a/src/script.c ++++ b/src/script.c +@@ -14,6 +14,7 @@ + */ + + #include <stdio.h> ++#include <errno.h> + #include <netdb.h> + #include <resolv.h> + #include <stdlib.h> +@@ -441,6 +442,12 @@ void script_call(const char *status, int + + pid_t pid = fork(); + ++ if (pid < 0) { ++ syslog(LOG_ERR, "Failed to fork script handler: %s", strerror(errno)); ++ running = 0; ++ return; ++ } ++ + if (pid > 0) { + running = pid; + started = now; diff --git a/package/network/ipv6/odhcp6c/patches/0009-odhcp6c-bound-the-address-length-when-parsing-the-P-.patch b/package/network/ipv6/odhcp6c/patches/0009-odhcp6c-bound-the-address-length-when-parsing-the-P-.patch new file mode 100644 index 0000000000..ee62d565c6 --- /dev/null +++ b/package/network/ipv6/odhcp6c/patches/0009-odhcp6c-bound-the-address-length-when-parsing-the-P-.patch @@ -0,0 +1,49 @@ +From 49f251805dd5bce1964d4d6549ff1178ae8df2a5 Mon Sep 17 00:00:00 2001 +From: Hauke Mehrtens <hauke@hauke-m.de> +Date: Sat, 30 May 2026 23:17:47 +0200 +Subject: odhcp6c: bound the address length when parsing the -P argument + +The -P <[pfx/]len> handler copied everything before the '/' into the +fixed 134-byte stack buffer "buf" with + + strncpy((char *)buf, optarg, optpos - optarg); + buf[optpos - optarg] = '\0'; + +without checking that the address portion fits. An argument whose +address part is 134 bytes or longer overflows buf on the stack (the +trailing NUL store is out of bounds as well). + +Reject any address portion that does not fit in buf before copying. +inet_pton() would reject such an over-long string anyway, so no valid +invocation changes behaviour. + +Assisted-by: Claude:claude-opus-4-8 +Link: https://github.com/openwrt/odhcp6c/pull/160 +Signed-off-by: Hauke Mehrtens <hauke@hauke-m.de> +(cherry picked from commit e9a9e9d45f38bee5d761de0421ca52163efa28d7) +--- + src/odhcp6c.c | 13 +++++++++++-- + 1 file changed, 11 insertions(+), 2 deletions(-) + +--- a/src/odhcp6c.c ++++ b/src/odhcp6c.c +@@ -245,8 +245,17 @@ int main(_unused int argc, char* const a + + optpos = strchr(optarg, '/'); + if (optpos) { +- strncpy((char *)buf, optarg, optpos - optarg); +- buf[optpos - optarg] = '\0'; ++ size_t addr_len = optpos - optarg; ++ ++ /* Leave room for the terminating NUL; reject anything ++ * that cannot be a valid IPv6 literal instead of ++ * overflowing buf. */ ++ if (addr_len >= sizeof(buf)) { ++ syslog(LOG_ERR, "invalid argument: '%s'", optarg); ++ return 1; ++ } ++ strncpy((char *)buf, optarg, addr_len); ++ buf[addr_len] = '\0'; + if (inet_pton(AF_INET6, (char *)buf, &prefix.addr) <= 0) { + syslog(LOG_ERR, "invalid argument: '%s'", optarg); + return 1; diff --git a/package/network/ipv6/odhcp6c/patches/0010-dhcpv6-require-known-SERVERID-when-validating-Reconf.patch b/package/network/ipv6/odhcp6c/patches/0010-dhcpv6-require-known-SERVERID-when-validating-Reconf.patch new file mode 100644 index 0000000000..2f1cf44c81 --- /dev/null +++ b/package/network/ipv6/odhcp6c/patches/0010-dhcpv6-require-known-SERVERID-when-validating-Reconf.patch @@ -0,0 +1,43 @@ +From 7d71729e4da366d1c19c205487e4decf6d646dde Mon Sep 17 00:00:00 2001 +From: Hauke Mehrtens <hauke@hauke-m.de> +Date: Sat, 11 Jul 2026 23:36:10 +0200 +Subject: dhcpv6: require known SERVERID when validating Reconfigure +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +dhcpv6_response_is_valid() previously set serverid_ok = true whenever +STATE_SERVER_ID was empty, on the assumption that an empty stored +server-id only happens for Solicit/Advertise where the client has not +yet bound to a server. The same code path is taken for Reconfigure +messages (req_msg_type == DHCPV6_MSG_UNKNOWN), so a Reconfigure +message that arrived while STATE_SERVER_ID was somehow empty (state +corruption, an early Reconfigure delivered before binding completes, +or a server-side bug) would be accepted without server identification. + +RFC 8415 §18.2.11 requires the SERVERID in a Reconfigure to match the +server the client received the lease from. Only set serverid_ok = true +on an empty stored server-id when we are not validating a Reconfigure; +for DHCPV6_MSG_UNKNOWN we leave it false so the response is dropped. + +Assisted-by: Claude:claude-opus-4-7 +Link: https://github.com/openwrt/odhcp6c/pull/160 +Signed-off-by: Hauke Mehrtens <hauke@hauke-m.de> + +(cherry picked from commit 0a4e51db30d5a13175cdda7830a5fcb882e88659) +[the message-type parameter is named 'type' in 24.10, not 'req_msg_type'] +--- + src/dhcpv6.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +--- a/src/dhcpv6.c ++++ b/src/dhcpv6.c +@@ -819,7 +819,7 @@ static bool dhcpv6_response_is_valid(con + if (server_id_len) + serverid_ok = (olen + 4U == server_id_len) && !memcmp( + &odata[-4], server_id, server_id_len); +- else ++ else if (type != DHCPV6_MSG_UNKNOWN) + serverid_ok = true; + } else if (otype == DHCPV6_OPT_AUTH && olen == -4 + + sizeof(struct dhcpv6_auth_reconfigure)) { diff --git a/package/network/ipv6/odhcp6c/patches/0011-odhcp6c-do-not-treat-DHCPv6-option-type-0-as-end-of-.patch b/package/network/ipv6/odhcp6c/patches/0011-odhcp6c-do-not-treat-DHCPv6-option-type-0-as-end-of-.patch new file mode 100644 index 0000000000..a92f47d562 --- /dev/null +++ b/package/network/ipv6/odhcp6c/patches/0011-odhcp6c-do-not-treat-DHCPv6-option-type-0-as-end-of-.patch @@ -0,0 +1,49 @@ +From 1374cbad07c63db19bba0f371c1a2f36167381dd Mon Sep 17 00:00:00 2001 +From: Hauke Mehrtens <hauke@hauke-m.de> +Date: Sat, 23 May 2026 14:46:55 +0200 +Subject: odhcp6c: do not treat DHCPv6 option type 0 as end-of-list +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +dhcpv6_for_each_option was structured as a chain of '&&' expressions +where each link both assigned a loop variable (otype, odata, olen) +and gated iteration on the assigned value being truthy. The middle +link, '((otype) = _o[0] << 8 | _o[1])', made iteration stop on +otype == 0. + +DHCPv6 has no end-of-list sentinel; option code 0 is just reserved +(RFC 8415 §21, IANA registry). A server (broken or hostile) that emits +an option with code 0 anywhere in the response would silently truncate +the client's view of the option list, masking everything that follows +— including STATUS, IA_NA / IA_PD, AUTH, etc. + +Switch the assignments inside the for-loop condition to use the comma +operator so all three (otype, odata, olen) are unconditionally +written, and gate iteration purely on the bounds check +'odata + olen <= end'. Callers see the same variables, in the same +order, just without the spurious type-0 truncation. + +Assisted-by: Claude:claude-opus-4-7 +Link: https://github.com/openwrt/odhcp6c/pull/160 +Signed-off-by: Hauke Mehrtens <hauke@hauke-m.de> +(cherry picked from commit 9177f236c2d74144a8a2cbdc7da0fcf2f582bc2c) +--- + src/odhcp6c.h | 6 ++++-- + 1 file changed, 4 insertions(+), 2 deletions(-) + +--- a/src/odhcp6c.h ++++ b/src/odhcp6c.h +@@ -267,8 +267,10 @@ struct dhcpv6_s46_rule { + + #define dhcpv6_for_each_option(start, end, otype, olen, odata)\ + for (uint8_t *_o = (uint8_t*)(start); _o + 4 <= (uint8_t*)(end) &&\ +- ((otype) = _o[0] << 8 | _o[1]) && ((odata) = (void*)&_o[4]) &&\ +- ((olen) = _o[2] << 8 | _o[3]) + (odata) <= (uint8_t*)(end); \ ++ ((otype) = _o[0] << 8 | _o[1],\ ++ (odata) = (void*)&_o[4],\ ++ (olen) = _o[2] << 8 | _o[3],\ ++ (odata) + (olen) <= (uint8_t*)(end)); \ + _o += 4 + (_o[2] << 8 | _o[3])) + + diff --git a/package/network/ipv6/odhcp6c/patches/0012-dhcpv6-enforce-monotonic-replay-counter-on-Reconfigu.patch b/package/network/ipv6/odhcp6c/patches/0012-dhcpv6-enforce-monotonic-replay-counter-on-Reconfigu.patch new file mode 100644 index 0000000000..cf94c18450 --- /dev/null +++ b/package/network/ipv6/odhcp6c/patches/0012-dhcpv6-enforce-monotonic-replay-counter-on-Reconfigu.patch @@ -0,0 +1,91 @@ +From 80c68194417ab73490fb99a8589d9c02d7fc5ede Mon Sep 17 00:00:00 2001 +From: Hauke Mehrtens <hauke@hauke-m.de> +Date: Sat, 11 Jul 2026 23:38:11 +0200 +Subject: dhcpv6: enforce monotonic replay counter on Reconfigure RKAP +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +RFC 8415 §20.4.3 requires the client to track the replay-detection +field of an authenticated Reconfigure message and to reject any +incoming Reconfigure whose replay value does not exceed the highest +one already accepted. Without this check, a captured Reconfigure can +be replayed at will: the HMAC is still valid because the message +itself has not changed, and the client will happily re-trigger +Renew / Rebind / Information-Request cycles on demand. + +Add a per-binding (reconf_replay, reconf_replay_seen) tuple, +initialised in dhcpv6_promote_server_cand() alongside reconf_key so a +fresh binding starts with a fresh window. Compare the wire replay +field (network byte order, decoded with be64toh()) against the stored +value before running the HMAC verification; only commit the value on +a verified HMAC so a forged message cannot move the counter forward. + +Assisted-by: Claude:claude-opus-4-7 +Link: https://github.com/openwrt/odhcp6c/pull/160 +Signed-off-by: Hauke Mehrtens <hauke@hauke-m.de> + +(cherry picked from commit df27a49c98d89cb945767763a47bfafe196165c8) +[24.10 has no per-candidate reconf_key and no auth_protocol selector: the replay + state is global alongside the existing global reconf_key, and is reset in + dhcpv6_promote_server_cand() when a new server binding is established] +--- + src/dhcpv6.c | 19 +++++++++++++++++++ + 1 file changed, 19 insertions(+) + +--- a/src/dhcpv6.c ++++ b/src/dhcpv6.c +@@ -15,6 +15,7 @@ + + #include <time.h> + #include <fcntl.h> ++#include <endian.h> + #include <errno.h> + #include <inttypes.h> + #include <stdlib.h> +@@ -111,6 +112,8 @@ static struct in6_addr server_addr = IN6 + + // Reconfigure key + static uint8_t reconf_key[16]; ++static uint64_t reconf_replay; ++static bool reconf_replay_seen; + + // client options + static unsigned int client_options = 0; +@@ -827,6 +830,16 @@ static bool dhcpv6_response_is_valid(con + if (r->protocol != 3 || r->algorithm != 1 || r->reconf_type != 2) + continue; + ++ /* RFC 8415 §20.4.3: the replay-detection field must be ++ * monotonically increasing per (client, server-id) pair. ++ * Drop any Reconfigure whose replay value does not exceed ++ * the highest one we have already accepted. */ ++ uint64_t replay; ++ memcpy(&replay, &r->replay, sizeof(replay)); ++ replay = be64toh(replay); ++ if (reconf_replay_seen && replay <= reconf_replay) ++ continue; ++ + md5_ctx_t md5; + uint8_t serverhash[16], secretbytes[64]; + uint32_t hash[4]; +@@ -855,6 +868,10 @@ static bool dhcpv6_response_is_valid(con + md5_end(hash, &md5); + + rcauth_ok = !memcmp(hash, serverhash, sizeof(hash)); ++ if (rcauth_ok) { ++ reconf_replay = replay; ++ reconf_replay_seen = true; ++ } + } else if (otype == DHCPV6_OPT_RECONF_MESSAGE && olen == 1) { + rcmsg = odata[0]; + } else if ((otype == DHCPV6_OPT_IA_PD || otype == DHCPV6_OPT_IA_NA)) { +@@ -1680,6 +1697,8 @@ int dhcpv6_promote_server_cand(void) + odhcp6c_add_state(STATE_SERVER_ID, hdr, sizeof(hdr)); + odhcp6c_add_state(STATE_SERVER_ID, cand->duid, cand->duid_len); + accept_reconfig = cand->wants_reconfigure; ++ reconf_replay = 0; ++ reconf_replay_seen = false; + + if (cand->ia_na_len) { + odhcp6c_add_state(STATE_IA_NA, cand->ia_na, cand->ia_na_len); diff --git a/package/network/ipv6/odhcp6c/patches/0013-odhcp6c-fix-handling-of-RFC6603-Prefix-Exclude-Optio.patch b/package/network/ipv6/odhcp6c/patches/0013-odhcp6c-fix-handling-of-RFC6603-Prefix-Exclude-Optio.patch new file mode 100644 index 0000000000..a1c4b1b306 --- /dev/null +++ b/package/network/ipv6/odhcp6c/patches/0013-odhcp6c-fix-handling-of-RFC6603-Prefix-Exclude-Optio.patch @@ -0,0 +1,120 @@ +From 35f1c268c8ffb2bfe2c7f4fa0415215ea532570b Mon Sep 17 00:00:00 2001 +From: Hauke Mehrtens <hauke@hauke-m.de> +Date: Sat, 11 Jul 2026 23:47:42 +0200 +Subject: odhcp6c: fix handling of RFC6603 Prefix Exclude Option + +Several bugs in the encoding and (more importantly) decoding of +DHCPV6_OPT_PD_EXCLUDE lead to the option being ignored in some received +messages, or the excluded subnet id being mangled to an incorrect value. + +Fix both encoding and decoding, being more explicit and slightly more +verbose for clarity. + +Signed-off-by: Mark H. Spatz <mark.h.spatz@gmail.com> +Link: https://github.com/openwrt/odhcp6c/pull/151 +Signed-off-by: Hauke Mehrtens <hauke@hauke-m.de> + +(cherry picked from commit 07d324ee7222c0e15b9975281f18236fdccc11bd) +[24.10 has no dedicated exclusion_length field (dcb53c4 is not backported), so the + port keeps reusing entry.priority / e[].priority for the exclusion length. The + upstream 'slen > 2' -> 'slen >= 2' guard change is a no-op here: 24.10 already + skips on 'slen < 2'. The added info() PD_EXCLUDE trace is dropped, as the log + helpers from bfd7597 are not backported. The computed option length is unchanged + (excl_opt_len == old ex_len), so the ia_pd buffer sizing is unaffected.] +--- + src/dhcpv6.c | 47 +++++++++++++++++++++++++++-------------------- + 1 file changed, 27 insertions(+), 20 deletions(-) + +--- a/src/dhcpv6.c ++++ b/src/dhcpv6.c +@@ -437,13 +437,16 @@ static void dhcpv6_send(enum dhcpv6_msg + if (e[j].iaid != iaid) + continue; + +- uint8_t ex_len = 0; +- if (e[j].priority > 0) +- ex_len = ((e[j].priority - e[j].length - 1) / 8) + 6; ++ uint8_t excl_subnet_id_nbits, excl_subnet_id_nbytes, excl_opt_len = 0; ++ if (e[j].priority > 0) { ++ excl_subnet_id_nbits = e[j].priority - e[j].length; ++ excl_subnet_id_nbytes = ((excl_subnet_id_nbits - 1) / 8) + 1; ++ excl_opt_len = excl_subnet_id_nbytes + 4 + 1; ++ } + + struct dhcpv6_ia_prefix p = { + .type = htons(DHCPV6_OPT_IA_PREFIX), +- .len = htons(sizeof(p) - 4U + ex_len), ++ .len = htons(sizeof(p) - 4U + excl_opt_len), + .prefix = e[j].length, + .addr = e[j].target + }; +@@ -456,20 +459,22 @@ static void dhcpv6_send(enum dhcpv6_msg + memcpy(ia_pd + ia_pd_len, &p, sizeof(p)); + ia_pd_len += sizeof(p); + +- if (ex_len) { ++ if (excl_opt_len) { + ia_pd[ia_pd_len++] = 0; + ia_pd[ia_pd_len++] = DHCPV6_OPT_PD_EXCLUDE; + ia_pd[ia_pd_len++] = 0; +- ia_pd[ia_pd_len++] = ex_len - 4; ++ ia_pd[ia_pd_len++] = excl_opt_len - 4; + ia_pd[ia_pd_len++] = e[j].priority; + +- uint32_t excl = ntohl(e[j].router.s6_addr32[1]); +- excl >>= (64 - e[j].priority); +- excl <<= 8 - ((e[j].priority - e[j].length) % 8); +- +- for (size_t i = ex_len - 5; i > 0; --i, excl >>= 8) +- ia_pd[ia_pd_len + i] = excl & 0xff; +- ia_pd_len += ex_len - 5; ++ uint32_t excluded_bits = ntohl(e[j].router.s6_addr32[1]); ++ excluded_bits >>= (64 - e[j].priority); /* Right align subnet ID bits */ ++ excluded_bits <<= (32 - excl_subnet_id_nbits); /* Left align subnet ID bits */ ++ ++ /* Copy subnet ID bits into the option MSB first */ ++ for (size_t k = 0; k < excl_subnet_id_nbytes; ++k) { ++ ia_pd[ia_pd_len++] = excluded_bits >> 24; ++ excluded_bits <<= 8; ++ } + } + + hdr->len = htons(ntohs(hdr->len) + ntohs(p.len) + 4U); +@@ -1408,7 +1413,9 @@ static unsigned int dhcpv6_parse_ia(void + if (stype != DHCPV6_OPT_PD_EXCLUDE || slen < 2) + continue; + ++ /* RFC 6603 §4.2 Prefix Exclude option */ + uint8_t elen = sdata[0]; ++ uint8_t *excl_subnet_id = &sdata[1]; + if (elen > 64) + elen = 64; + +@@ -1417,19 +1424,19 @@ static unsigned int dhcpv6_parse_ia(void + continue; + } + +- uint8_t bytes = ((elen - entry.length - 1) / 8) + 1; +- if (slen <= bytes) { ++ uint8_t excl_subnet_id_nbits = elen - entry.length; ++ uint8_t excl_subnet_id_nbytes = ((excl_subnet_id_nbits - 1) / 8) + 1; ++ if ((excl_subnet_id + excl_subnet_id_nbytes) > (sdata + slen)) { + ok = false; + continue; + } + + uint32_t exclude = 0; +- do { +- exclude = exclude << 8 | sdata[bytes]; +- } while (--bytes); +- +- exclude >>= 8 - ((elen - entry.length) % 8); +- exclude <<= 64 - elen; ++ /* Copy subnet ID bits out of the option MSB first */ ++ for (size_t k = 0; k < excl_subnet_id_nbytes; k++) ++ exclude = (exclude << 8) | excl_subnet_id[k]; ++ exclude >>= (8 * excl_subnet_id_nbytes) - excl_subnet_id_nbits; /* Right align subnet ID bits */ ++ exclude <<= (64 - elen); /* Shift subnet ID bits into the low-order bits of the prefix */ + + // Abusing router & priority fields for exclusion + entry.router = entry.target; |