new file mode 100644
@@ -0,0 +1,213 @@
+From 44c72b548ed11da60ff3b8d43711e74d64032526 Mon Sep 17 00:00:00 2001
+From: "Robin H. Johnson" <robbat2@gentoo.org>
+Date: Sun, 17 May 2026 15:52:03 -0700
+Subject: [PATCH] feat: harden radvdump (CVE-2026-48715)
+
+- drop root after startup & validate
+- ensure buffers are zeroed between parsed packets
+- Jumbo packets were truncated in parsing due to outdated MSG_SIZE_RECV
+- Route Information option length field was not sufficiently validated
+
+Thanks to @TristanInSec for finding the Route Information
+length validation issue.
+
+Signed-off-by: Robin H. Johnson <robbat2@orbis-terrarum.net>
+Reference: https://github.com/radvd-project/radvd/security/advisories/GHSA-52px-gh9p-m379
+
+CVE: CVE-2026-48715
+Upstream-Status: Backport [https://github.com/radvd-project/radvd/commit/068bde13e3fd6a5fcdb6859e6a2acd293a325dc5]
+
+Changes: Added the missing '-u username' argument to the help output.
+
+Signed-off-by: Roland Kovacs <roland.kovacs@est.tech>
+---
+ radvdump.8.man | 10 +++++++
+ radvdump.c | 78 +++++++++++++++++++++++++++++++++++++++++++-------
+ 2 files changed, 78 insertions(+), 10 deletions(-)
+
+diff --git a/radvdump.8.man b/radvdump.8.man
+index ea9932c..25dbad8 100644
+--- a/radvdump.8.man
++++ b/radvdump.8.man
+@@ -20,6 +20,7 @@ radvdump \- dump router advertisements
+ .B radvdump
+ .B "[ \-vhfe ]"
+ .BI "[ \-d " debuglevel " ]"
++.BI "[ \-u " username " ]"
+
+ .SH DESCRIPTION
+ .B radvdump
+@@ -62,6 +63,15 @@ debugging level of 0 completely turns off debugging.
+
+ The default debugging level is 0.
+
++.TP
++.BR "\-u " username, " \-\-username " username
++If specified, drops root privileges and changes user ID to
++.I username
++and group ID to the primary group of
++.IR username .
++This is recommended for security reasons.
++Defaults to "nobody", pass "" or "root" to not drop privileges by default.
++
+ .SH FILES
+
+ .nf
+diff --git a/radvdump.c b/radvdump.c
+index 0a20272..a939970 100644
+--- a/radvdump.c
++++ b/radvdump.c
+@@ -17,11 +17,18 @@
+ #include "config.h"
+ #include "includes.h"
+
+-static char usage_str[] = "[-vhfe] [-d level]";
++static char usage_str[] = "[-vhfe] [-d level] [-u username]";
+
+ #ifdef HAVE_GETOPT_LONG
+-struct option prog_opt[] = {{"debug", 1, 0, 'd'}, {"file-format", 0, 0, 'f'}, {"exclude-defaults", 0, 0, 'e'},
+- {"version", 0, 0, 'v'}, {"help", 0, 0, 'h'}, {NULL, 0, 0, 0}};
++struct option prog_opt[] = {
++ {"debug", 1, 0, 'd'},
++ {"username", 1, 0, 'u'},
++ {"file-format", 0, 0, 'f'},
++ {"exclude-defaults", 0, 0, 'e'},
++ {"version", 0, 0, 'v'},
++ {"help", 0, 0, 'h'},
++ {NULL, 0, 0, 0}
++};
+ #endif
+
+ int sock = -1;
+@@ -39,12 +46,14 @@ int main(int argc, char *argv[])
+ int opt_idx;
+ #endif
+ char const *pname = ((pname = strrchr(argv[0], '/')) != NULL) ? pname + 1 : argv[0];
++ char username[256];
++ strncpy(username, "nobody", 255);
+
+ /* parse args */
+ #ifdef HAVE_GETOPT_LONG
+- while ((c = getopt_long(argc, argv, "d:fehv", prog_opt, &opt_idx)) > 0)
++ while ((c = getopt_long(argc, argv, "d:fehvu:", prog_opt, &opt_idx)) > 0)
+ #else
+- while ((c = getopt(argc, argv, "d:fehv")) > 0)
++ while ((c = getopt(argc, argv, "d:fehvu:")) > 0)
+ #endif
+ {
+ switch (c) {
+@@ -59,6 +68,9 @@ int main(int argc, char *argv[])
+ case 'v':
+ version();
+ break;
++ case 'u':
++ strncpy(username, optarg, 255);
++ break;
+ case 'h':
+ usage(pname);
+ #ifdef HAVE_GETOPT_LONG
+@@ -83,12 +95,29 @@ int main(int argc, char *argv[])
+ exit(1);
+ }
+
++ if(strncmp(username, "", 255) == 0 || strncmp(username, "-", 255) == 0) {
++ // Must opt out of security
++ } else {
++ if (drop_root_privileges(username) < 0) {
++ perror("drop_root_privileges");
++ flog(LOG_ERR, "unable to drop root privileges");
++ exit(1);
++ }
++ dlog(LOG_DEBUG, 5, "running as user: %s/%d", username, geteuid());
++ }
++
++#define _MSG_SIZE MSG_SIZE_RECV
++#define _CHDR_SIZE CMSG_SPACE(sizeof(struct in6_pktinfo)) + CMSG_SPACE(sizeof(int))
+ for (;;) {
+- unsigned char msg[MSG_SIZE_RECV];
++ unsigned char msg[_MSG_SIZE];
+ int hoplimit = 0;
+ struct in6_pktinfo *pkt_info = NULL;
+ struct sockaddr_in6 rcv_addr;
+- unsigned char chdr[CMSG_SPACE(sizeof(struct in6_pktinfo)) + CMSG_SPACE(sizeof(int))];
++ unsigned char chdr[_CHDR_SIZE];
++ // safety; completely zero buffers for each receive
++ memset(&msg, 0, _MSG_SIZE);
++ memset(&chdr, 0, _CHDR_SIZE);
++
+ int len = recv_rs_ra(sock, msg, &rcv_addr, &pkt_info, &hoplimit, chdr);
+ if (len > 0) {
+ struct icmp6_hdr *icmph;
+@@ -120,7 +149,7 @@ int main(int argc, char *argv[])
+ } else if (icmph->icmp6_type == ND_ROUTER_ADVERT)
+ print_ff(msg, len, &rcv_addr, hoplimit, (unsigned int)pkt_info->ipi6_ifindex, edefs);
+ } else if (len == 0) {
+- flog(LOG_ERR, "received zero lenght packet");
++ flog(LOG_ERR, "received zero length packet");
+ exit(1);
+ } else {
+ flog(LOG_ERR, "recv_rs_ra: %s", strerror(errno));
+@@ -202,7 +231,8 @@ static void print_ff(unsigned char *msg, int len, struct sockaddr_in6 *addr, int
+ flog(LOG_ERR, "option length greater than total"
+ " length in RA (type %d, optlen %d, len %d)",
+ (int)*opt_str, optlen, len);
+- break;
++ // Do not process anything further in this RA.
++ goto end_of_interface;
+ }
+
+ switch (*opt_str) {
+@@ -289,7 +319,8 @@ static void print_ff(unsigned char *msg, int len, struct sockaddr_in6 *addr, int
+ flog(LOG_ERR, "option length greater than total"
+ " length in RA (type %d, optlen %d, len %d)",
+ (int)*opt_str, optlen, orig_len);
+- break;
++ // Do not process anything further in this RA.
++ goto end_of_interface;
+ }
+
+ switch (*opt_str) {
+@@ -345,6 +376,32 @@ static void print_ff(unsigned char *msg, int len, struct sockaddr_in6 *addr, int
+ } else {
+ struct in6_addr addr;
+ memset(&addr, 0, sizeof(addr));
++ /* RFC4191 section 2.3 (reformatted)
++ Length 8-bit unsigned integer.
++
++ The length of the option (including the Type and Length
++ fields) in units of 8 octets.
++
++ The Length field is 1, 2, or 3 depending on the Prefix
++ Length.
++
++ If Prefix Length is greater than 64, then Length must be 3.
++
++ If Prefix Length is greater than 0, then Length must be 2 or
++ 3.
++
++ If Prefix Length is zero, then Length must be 1, 2, or 3.
++ */
++ if (
++ (rinfo->nd_opt_ri_len > 3)
++ || (rinfo->nd_opt_ri_len == 0)
++ || (rinfo->nd_opt_ri_prefix_len > 64 && rinfo->nd_opt_ri_len < 3)
++ || (rinfo->nd_opt_ri_prefix_len > 0 && rinfo->nd_opt_ri_len < 1)
++ ) {
++ flog(LOG_ERR, "Invalid Route Information option length %d from %s", rinfo->nd_opt_ri_len, addr_str);
++ printf("# Invalid Route Information option length %d, per RFC4191 section 2.3 ", rinfo->nd_opt_ri_len);
++ break;
++ }
+ if (rinfo->nd_opt_ri_len > 1)
+ memcpy(&addr, &rinfo->nd_opt_ri_prefix, (rinfo->nd_opt_ri_len - 1) * 8);
+ addrtostr(&addr, prefix_str, sizeof(prefix_str));
+@@ -444,6 +501,7 @@ static void print_ff(unsigned char *msg, int len, struct sockaddr_in6 *addr, int
+ opt_str += optlen;
+ }
+
++end_of_interface:
+ printf("}; # End of interface definition\n");
+
+ fflush(stdout);
+--
+2.34.1
+
new file mode 100644
@@ -0,0 +1,106 @@
+From 3b7602ee0da3c8313e147e539e0efc01dd34a0df Mon Sep 17 00:00:00 2001
+From: "Robin H. Johnson" <robbat2@gentoo.org>
+Date: Sun, 17 May 2026 16:21:39 -0700
+Subject: [PATCH] refactor: make drop_root_privileges reusable
+
+Signed-off-by: Robin H. Johnson <robbat2@gentoo.org>
+
+Upstream-Status: Backport [https://github.com/radvd-project/radvd/commit/11c92dafc392dab1843964316950e297fbd35d33]
+
+Note: dependency of CVE-2026-48715
+
+Signed-off-by: Roland Kovacs <roland.kovacs@est.tech>
+---
+ radvd.c | 16 ----------------
+ radvd.h | 1 +
+ util.c | 31 +++++++++++++++++++++++++++++++
+ 3 files changed, 32 insertions(+), 16 deletions(-)
+
+diff --git a/radvd.c b/radvd.c
+index 22255b1..e807121 100644
+--- a/radvd.c
++++ b/radvd.c
+@@ -79,7 +79,6 @@ static volatile int sigterm_received = 0;
+ static volatile int sigusr1_received = 0;
+
+ static int check_conffile_perm(const char *, const char *);
+-static int drop_root_privileges(const char *);
+ static int open_and_lock_pid_file(char const *daemon_pid_file_ident);
+ static int write_pid_file(char const *daemon_pid_file_ident, pid_t pid);
+ static pid_t daemonp(char const *daemon_pid_file_ident);
+@@ -841,21 +840,6 @@ static void reset_prefix_lifetimes_foo(struct Interface *iface, void *data)
+
+ static void reset_prefix_lifetimes(struct Interface *ifaces) { for_each_iface(ifaces, reset_prefix_lifetimes_foo, 0); }
+
+-static int drop_root_privileges(const char *username)
+-{
+- struct passwd *pw = getpwnam(username);
+- if (pw) {
+- if (initgroups(username, pw->pw_gid) != 0 || setgid(pw->pw_gid) != 0 || setuid(pw->pw_uid) != 0) {
+- flog(LOG_ERR, "Couldn't change to '%.32s' uid=%d gid=%d", username, pw->pw_uid, pw->pw_gid);
+- return -1;
+- }
+- } else {
+- flog(LOG_ERR, "Couldn't find user '%.32s'", username);
+- return -1;
+- }
+- return 0;
+-}
+-
+ static int check_conffile_perm(const char *username, const char *conf_file)
+ {
+ FILE *fp = fopen(conf_file, "r");
+diff --git a/radvd.h b/radvd.h
+index 2a61c75..4276bbe 100644
+--- a/radvd.h
++++ b/radvd.h
+@@ -341,6 +341,7 @@ struct safe_buffer_list *new_safe_buffer_list(void);
+ void safe_buffer_list_free(struct safe_buffer_list *sbl);
+ struct safe_buffer_list *safe_buffer_list_append(struct safe_buffer_list *sbl);
+ void safe_buffer_list_to_safe_buffer(struct safe_buffer_list *sbl, struct safe_buffer *sb);
++int drop_root_privileges(const char *);
+
+ /* privsep.c */
+ int privsep_interface_curhlim(const char *iface, uint32_t hlim);
+diff --git a/util.c b/util.c
+index b160cc9..012f16b 100644
+--- a/util.c
++++ b/util.c
+@@ -283,3 +283,34 @@ struct in6_addr get_prefix6(struct in6_addr const *addr, struct in6_addr const *
+
+ return prefix;
+ }
++
++int drop_root_privileges(const char *username)
++{
++ struct passwd *pw = getpwnam(username);
++ if (pw) {
++ if (initgroups(username, pw->pw_gid) != 0 || setgid(pw->pw_gid) != 0 || setuid(pw->pw_uid) != 0) {
++ flog(LOG_ERR, "Couldn't change to '%.32s' uid=%d gid=%d", username, pw->pw_uid, pw->pw_gid);
++ return -1;
++ }
++ // If the target 0 was zero; then the later checks would return false
++ // positives. This can take place if explicitly running as '-u root'.
++ if(pw->pw_uid == 0) {
++ return 0;
++ }
++ // Validate that we cannot get root again
++ if (setuid(0) != -1) {
++ flog(LOG_ERR, "Drop root privileged unsuccessful, setuid regained root");
++ return -1;
++ }
++
++ if (seteuid(0) != -1) {
++ flog(LOG_ERR, "Drop root privileged unsuccessful, seteuid regained root");
++ return -1;
++ }
++
++ } else {
++ flog(LOG_ERR, "Couldn't find user '%.32s'", username);
++ return -1;
++ }
++ return 0;
++}
+--
+2.34.1
+
@@ -21,6 +21,8 @@ SRC_URI = "http://v6web.litech.org/radvd/dist/radvd-${PV}.tar.gz \
file://radvd.default \
file://radvd.conf \
file://0001-Reverts-the-include.h-change-in-46883f8a1a02fe42040d.patch \
+ file://CVE-2026-48715_dep.patch \
+ file://CVE-2026-48715.patch \
"
SRC_URI[sha256sum] = "c36470706fec3a9e6bed394ffea08acaff5dac647848d26b96bb9b9c65d58da0"
Prior to version 2.21, the `radvdump` utility shipped with radvd contains a stack buffer overflow in the Route Information option parser. When processing a crafted ICMPv6 Router Advertisement, `print_ff()` copies up to 2032 bytes from attacker-controlled packet data into a 16-byte `struct in6_addr` on the stack, overflowing by up to 2016 bytes. Signed-off-by: Roland Kovacs <roland.kovacs@est.tech> --- .../radvd/files/CVE-2026-48715.patch | 213 ++++++++++++++++++ .../radvd/files/CVE-2026-48715_dep.patch | 106 +++++++++ .../recipes-daemons/radvd/radvd_2.19.bb | 2 + 3 files changed, 321 insertions(+) create mode 100644 meta-networking/recipes-daemons/radvd/files/CVE-2026-48715.patch create mode 100644 meta-networking/recipes-daemons/radvd/files/CVE-2026-48715_dep.patch