Commit 1cf1e9f2e8 for frr

commit 1cf1e9f2e8c78f8b882c5be20091535af3e2d65d
Author: Dilshad Tanveer <dilshad@labs.digiscrypt.com>
Date:   Thu Oct 15 12:47:54 2026 +0530

    zebra: bound ND option walk to the received packet length

    rtadv_process_optional() advanced through the RA option list by
    8 * nd_opt_len without checking that many bytes were actually
    received, so a zero length spun forever and an oversized length
    underflowed the unsigned counter and read option headers (and a
    source link-layer MAC) off the end of the receive buffer.

    Require a full option header to remain and compute the length once.
    A zero or out-of-range option length now makes rtadv_process_optional()
    return false, and rtadv_process_advert() drops the whole RA, since
    RFC 4861 4.6 treats a zero-length option as invalidating the message.

    Signed-off-by: Dilshad Tanveer <dilshad@labs.digiscrypt.com>

diff --git a/zebra/rtadv.c b/zebra/rtadv.c
index 86560aed58..7e2032f05a 100644
--- a/zebra/rtadv.c
+++ b/zebra/rtadv.c
@@ -734,15 +734,30 @@ static const char *rtadv_optionalhdr2str(uint8_t opt_type)
  * which is when a interface does not have a LL
  * v6 address.  We still need to be able to install
  * the mac address for v4 to v6 resolution
+ *
+ * Returns false when an option is malformed, in which case RFC 4861 4.6
+ * says the whole message is invalid and the caller should drop it.
  */
-static void rtadv_process_optional(uint8_t *optional, unsigned int len,
+static bool rtadv_process_optional(uint8_t *optional, unsigned int len,
 				   struct interface *ifp,
 				   struct sockaddr_in6 *addr)
 {
 	char *mac;

-	while (len > 0) {
+	while (len >= sizeof(struct nd_opt_hdr)) {
 		struct nd_opt_hdr *opt_hdr = (struct nd_opt_hdr *)optional;
+		unsigned int opt_len = 8 * opt_hdr->nd_opt_len;
+
+		/*
+		 * RFC 4861 4.6 options are a multiple of 8 octets and never
+		 * zero. A zero length would spin here forever, and a length
+		 * past the bytes we received would wrap the unsigned counter
+		 * below and keep reading off the end of the packet buffer.
+		 * Either way the option framing is broken, so report the
+		 * message as invalid.
+		 */
+		if (opt_len == 0 || opt_len > len)
+			return false;

 		switch(opt_hdr->nd_opt_type) {
 		case ND_OPT_SOURCE_LINKADDR:
@@ -761,9 +776,11 @@ static void rtadv_process_optional(uint8_t *optional, unsigned int len,
 			break;
 		}

-		len -= 8 * opt_hdr->nd_opt_len;
-		optional += 8 * opt_hdr->nd_opt_len;
+		len -= opt_len;
+		optional += opt_len;
 	}
+
+	return true;
 }

 static void rtadv_process_advert(uint8_t *msg, unsigned int len,
@@ -789,9 +806,16 @@ static void rtadv_process_advert(uint8_t *msg, unsigned int len,
 	}

 	if (!IN6_IS_ADDR_LINKLOCAL(&addr->sin6_addr)) {
-		rtadv_process_optional(msg + sizeof(struct nd_router_advert),
-				       len - sizeof(struct nd_router_advert),
-				       ifp, addr);
+		if (!rtadv_process_optional(msg + sizeof(struct nd_router_advert),
+					    len - sizeof(struct nd_router_advert),
+					    ifp, addr)) {
+			if (IS_ZEBRA_DEBUG_PACKET)
+				zlog_debug(
+					"%s(%s:%u): Rx RA with invalid option, dropping message from %s",
+					ifp->name, ifp->vrf->name, ifp->ifindex,
+					addr_str);
+			return;
+		}
 		if (IS_ZEBRA_DEBUG_PACKET)
 			zlog_debug(
 				"%s(%s:%u): Rx RA with non-linklocal source address from %s",