diff mbox series

[meta-networking,scarthgap] radvd: fix CVE-2026-48715

Message ID 20260709111137.129370-1-roland.kovacs@est.tech
State New
Headers show
Series [meta-networking,scarthgap] radvd: fix CVE-2026-48715 | expand

Commit Message

Roland Kovacs July 9, 2026, 11:11 a.m. UTC
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
diff mbox series

Patch

diff --git a/meta-networking/recipes-daemons/radvd/files/CVE-2026-48715.patch b/meta-networking/recipes-daemons/radvd/files/CVE-2026-48715.patch
new file mode 100644
index 0000000000..e15ae4aaba
--- /dev/null
+++ b/meta-networking/recipes-daemons/radvd/files/CVE-2026-48715.patch
@@ -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
+
diff --git a/meta-networking/recipes-daemons/radvd/files/CVE-2026-48715_dep.patch b/meta-networking/recipes-daemons/radvd/files/CVE-2026-48715_dep.patch
new file mode 100644
index 0000000000..eaabb0f243
--- /dev/null
+++ b/meta-networking/recipes-daemons/radvd/files/CVE-2026-48715_dep.patch
@@ -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
+
diff --git a/meta-networking/recipes-daemons/radvd/radvd_2.19.bb b/meta-networking/recipes-daemons/radvd/radvd_2.19.bb
index a9b5f79424..7f73143649 100644
--- a/meta-networking/recipes-daemons/radvd/radvd_2.19.bb
+++ b/meta-networking/recipes-daemons/radvd/radvd_2.19.bb
@@ -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"