radvd: fix CVE-2026-48715

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>
Signed-off-by: Anuj Mittal <anuj.mittal@oss.qualcomm.com>
This commit is contained in:
Roland Kovacs
2026-07-09 13:11:50 +02:00
committed by Anuj Mittal
parent 0a044f3363
commit 8cba40ff88
3 changed files with 321 additions and 0 deletions
@@ -0,0 +1,213 @@
From 545487bba7b8bcf447ca119f45eb6a1b8eb70dc7 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 0ac39e4..a58c655 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 bb918c8..032d705 100644
--- a/radvdump.c
+++ b/radvdump.c
@@ -23,11 +23,18 @@
#define bswap64(y) (y)
#endif
-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;
@@ -45,12 +52,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) {
@@ -65,6 +74,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
@@ -89,12 +101,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;
@@ -126,7 +155,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));
@@ -208,7 +237,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) {
@@ -320,7 +350,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) {
@@ -376,6 +407,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));
@@ -519,6 +576,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
@@ -0,0 +1,106 @@
From c0efc056a45a75977a1f400e8023dcdacab84c07 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 3784b76..0d3fa1d 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);
@@ -862,21 +861,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 38f6533..ae4f471 100644
--- a/radvd.h
+++ b/radvd.h
@@ -381,6 +381,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 c033794..f18385e 100644
--- a/util.c
+++ b/util.c
@@ -287,3 +287,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
@@ -20,6 +20,8 @@ SRC_URI = "http://v6web.litech.org/radvd/dist/radvd-${PV}.tar.gz \
file://volatiles.03_radvd \ file://volatiles.03_radvd \
file://radvd.default \ file://radvd.default \
file://radvd.conf \ file://radvd.conf \
file://CVE-2026-48715_dep.patch \
file://CVE-2026-48715.patch \
" "
SRC_URI[sha256sum] = "af37c5a81d59f3bdc00d83056606ffa1810d4550beed6caa4f81181246494220" SRC_URI[sha256sum] = "af37c5a81d59f3bdc00d83056606ffa1810d4550beed6caa4f81181246494220"