git: c4b6df9a7cc9 - stable/15 - netinet6: Fix ND link-layer address option layout for IPoIB (stable/15)

From: Adrian Chadd <adrian_at_FreeBSD.org>
Date: Mon, 02 Nov 2026 15:49:29 UTC
The branch stable/15 has been updated by adrian:

URL: https://cgit.FreeBSD.org/src/commit/?id=c4b6df9a7cc977cbe842b4fa9499af15b48a1b88

commit c4b6df9a7cc977cbe842b4fa9499af15b48a1b88
Author:     Vinícius Ferrão <vinicius@ferrao.net.br>
AuthorDate: 2026-11-02 15:45:03 +0000
Commit:     Adrian Chadd <adrian@FreeBSD.org>
CommitDate: 2026-11-02 15:46:42 +0000

    netinet6: Fix ND link-layer address option layout for IPoIB (stable/15)
    
    RFC 4391 (IP over InfiniBand), section 9.3, lays the ND source/target
    link-layer address option out as type, length (3), two reserved zero octets,
    then the 20-octet IPoIB link-layer address. The ND code assumed the Ethernet
    layout (RFC 4861, section 4.6.1) everywhere and read/wrote the address directly
    after the option header, i.e. two octets early. The option-length sanity check
    computes 24 for both layouts for a 20-octet address, so the mismatch was silent.
    
    Against a conformant peer (e.g. Linux) this broke IPv6 over IPoIB in both
    directions: the address parsed from a received NS/NA option was shifted by two
    bytes, so the cached link-layer address embedded a GID that does not exist on
    the fabric and replies were sent nowhere, and the options we built carried our
    address at the wrong offset for the peer. IPv4/ARP was unaffected. This blocked
    everything IPv6 over IPoIB against Linux, including rdma_cm's IP-to-GID
    resolution and therefore NFS-over-RDMA-over-InfiniBand with IPv6.
    
    Add nd6_lladdr_opt_pad(), the analog of Linux's ndisc_addr_option_pad(): the
    number of reserved octets between the option header and the address (2 for
    IFT_INFINIBAND and IFT_INFINIBANDLAG, 0 otherwise). Apply it at the option parse
    sites (nd6_ns_input(), nd6_na_input(), nd6_rs_input(), nd6_ra_input(),
    icmp6_redirect_input()) and the option build sites (nd6_ns_output_fib(),
    nd6_na_output_fib(), icmp6_redirect_output()). Zero the redirect target option
    before filling it; previously its trailing pad bytes were left uninitialized on
    links with 20-octet addresses. No functional change for link types without
    reserved octets.
    
    This is a direct commit to stable/15, equivalent to main commit
    bac4760524a2a15ce75e35251f0c9cdf31732f3f (D58096). It could not be cherry-picked
    cleanly: the neighbor-discovery code diverged on main (the router advertisement
    parse was refactored into helpers), so the same fix is reimplemented against the
    stable/15 sources.
    
    PR: 296585
    Sponsored by: VersatusHPC
    
    Reviewed by:    pouria
    Differential Revision:  https://reviews.freebsd.org/D58132
---
 sys/netinet6/icmp6.c   |  9 +++++++--
 sys/netinet6/nd6.h     |  1 +
 sys/netinet6/nd6_nbr.c | 35 +++++++++++++++++++++++++++++++----
 sys/netinet6/nd6_rtr.c |  7 +++++--
 4 files changed, 44 insertions(+), 8 deletions(-)

diff --git a/sys/netinet6/icmp6.c b/sys/netinet6/icmp6.c
index ec2e5a22aa33..a2459bc82f23 100644
--- a/sys/netinet6/icmp6.c
+++ b/sys/netinet6/icmp6.c
@@ -2170,6 +2170,7 @@ icmp6_redirect_input(struct mbuf *m, int off)
 	char ip6buf[INET6_ADDRSTRLEN];
 	char *lladdr;
 	int icmp6len, is_onlink, is_router, lladdrlen;
+	u_int lladdr_pad;
 
 	M_ASSERTPKTHDR(m);
 	KASSERT(m->m_pkthdr.rcvif != NULL, ("%s: no rcvif", __func__));
@@ -2296,8 +2297,9 @@ icmp6_redirect_input(struct mbuf *m, int off)
 
 	lladdr = NULL;
 	lladdrlen = 0;
+	lladdr_pad = nd6_lladdr_opt_pad(ifp);
 	if (ndopts.nd_opts_tgt_lladdr) {
-		lladdr = (char *)(ndopts.nd_opts_tgt_lladdr + 1);
+		lladdr = (char *)(ndopts.nd_opts_tgt_lladdr + 1) + lladdr_pad;
 		lladdrlen = ndopts.nd_opts_tgt_lladdr->nd_opt_len << 3;
 	}
 
@@ -2490,6 +2492,7 @@ icmp6_redirect_output(struct mbuf *m0, struct nhop_object *nh)
 	{
 		/* target lladdr option */
 		int len;
+		u_int pad;
 		struct nd_opt_hdr *nd_opt;
 		char *lladdr;
 
@@ -2497,6 +2500,7 @@ icmp6_redirect_output(struct mbuf *m0, struct nhop_object *nh)
 		if (ln == NULL)
 			goto nolladdropt;
 
+		pad = nd6_lladdr_opt_pad(ifp);
 		len = sizeof(*nd_opt) + ifp->if_addrlen;
 		len = (len + 7) & ~7;	/* round by 8 */
 		/* safety check */
@@ -2505,9 +2509,10 @@ icmp6_redirect_output(struct mbuf *m0, struct nhop_object *nh)
 
 		if (ln->la_flags & LLE_VALID) {
 			nd_opt = (struct nd_opt_hdr *)p;
+			memset(nd_opt, 0, len);
 			nd_opt->nd_opt_type = ND_OPT_TARGET_LINKADDR;
 			nd_opt->nd_opt_len = len >> 3;
-			lladdr = (char *)(nd_opt + 1);
+			lladdr = (char *)(nd_opt + 1) + pad;
 			bcopy(ln->ll_addr, lladdr, ifp->if_addrlen);
 			p += len;
 		}
diff --git a/sys/netinet6/nd6.h b/sys/netinet6/nd6.h
index e484c709e29a..566b92fc783f 100644
--- a/sys/netinet6/nd6.h
+++ b/sys/netinet6/nd6.h
@@ -390,6 +390,7 @@ void nd6_ns_input(struct mbuf *, int, int);
 void nd6_ns_output(struct ifnet *, const struct in6_addr *,
 	const struct in6_addr *, const struct in6_addr *, uint8_t *);
 caddr_t nd6_ifptomac(struct ifnet *);
+u_int nd6_lladdr_opt_pad(struct ifnet *);
 void nd6_dad_init(void);
 void nd6_dad_start(struct ifaddr *, int);
 void nd6_dad_stop(struct ifaddr *);
diff --git a/sys/netinet6/nd6_nbr.c b/sys/netinet6/nd6_nbr.c
index 29151b29a071..d79bb17196a4 100644
--- a/sys/netinet6/nd6_nbr.c
+++ b/sys/netinet6/nd6_nbr.c
@@ -139,6 +139,7 @@ nd6_ns_input(struct mbuf *m, int off, int icmp6len)
 	char ip6bufs[INET6_ADDRSTRLEN], ip6bufd[INET6_ADDRSTRLEN];
 	char *lladdr;
 	int anycast, lladdrlen, proxy, rflag, tentative, tlladdr;
+	u_int lladdr_pad;
 
 	ifa = NULL;
 
@@ -226,8 +227,9 @@ nd6_ns_input(struct mbuf *m, int off, int icmp6len)
 
 	lladdr = NULL;
 	lladdrlen = 0;
+	lladdr_pad = nd6_lladdr_opt_pad(ifp);
 	if (ndopts.nd_opts_src_lladdr) {
-		lladdr = (char *)(ndopts.nd_opts_src_lladdr + 1);
+		lladdr = (char *)(ndopts.nd_opts_src_lladdr + 1) + lladdr_pad;
 		lladdrlen = ndopts.nd_opts_src_lladdr->nd_opt_len << 3;
 	}
 
@@ -559,6 +561,7 @@ nd6_ns_output_fib(struct ifnet *ifp, const struct in6_addr *saddr6,
 		struct nd_opt_hdr *nd_opt;
 		char *mac;
 		int optlen;
+		u_int pad;
 
 		mac = NULL;
 		if (ifp->if_carp)
@@ -568,6 +571,7 @@ nd6_ns_output_fib(struct ifnet *ifp, const struct in6_addr *saddr6,
 
 		if (mac != NULL) {
 			nd_opt = (struct nd_opt_hdr *)(nd_ns + 1);
+			pad = nd6_lladdr_opt_pad(ifp);
 			optlen = sizeof(struct nd_opt_hdr) + ifp->if_addrlen;
 			/* 8 byte alignments... */
 			optlen = (optlen + 7) & ~7;
@@ -577,7 +581,8 @@ nd6_ns_output_fib(struct ifnet *ifp, const struct in6_addr *saddr6,
 			bzero(nd_opt, optlen);
 			nd_opt->nd_opt_type = ND_OPT_SOURCE_LINKADDR;
 			nd_opt->nd_opt_len = optlen >> 3;
-			bcopy(mac, nd_opt + 1, ifp->if_addrlen);
+			memcpy((char *)(nd_opt + 1) + pad, mac,
+			    ifp->if_addrlen);
 		}
 	}
 	/*
@@ -662,6 +667,7 @@ nd6_na_input(struct mbuf *m, int off, int icmp6len)
 	size_t linkhdrsize;
 	int flags, is_override, is_router, is_solicited;
 	int lladdr_off, lladdrlen, checklink;
+	u_int lladdr_pad;
 	bool flush_holdchain = false;
 
 	NET_EPOCH_ASSERT();
@@ -730,8 +736,9 @@ nd6_na_input(struct mbuf *m, int off, int icmp6len)
 
 	lladdr = NULL;
 	lladdrlen = 0;
+	lladdr_pad = nd6_lladdr_opt_pad(ifp);
 	if (ndopts.nd_opts_tgt_lladdr) {
-		lladdr = (char *)(ndopts.nd_opts_tgt_lladdr + 1);
+		lladdr = (char *)(ndopts.nd_opts_tgt_lladdr + 1) + lladdr_pad;
 		lladdrlen = ndopts.nd_opts_tgt_lladdr->nd_opt_len << 3;
 	}
 
@@ -1072,6 +1079,7 @@ nd6_na_output_fib(struct ifnet *ifp, const struct in6_addr *daddr6_0,
 		}
 	}
 	if ((tlladdr & ND6_NA_OPT_LLA) && mac != NULL) {
+		u_int pad = nd6_lladdr_opt_pad(ifp);
 		int optlen = sizeof(struct nd_opt_hdr) + ifp->if_addrlen;
 		struct nd_opt_hdr *nd_opt = (struct nd_opt_hdr *)(nd_na + 1);
 
@@ -1084,7 +1092,7 @@ nd6_na_output_fib(struct ifnet *ifp, const struct in6_addr *daddr6_0,
 		bzero((caddr_t)nd_opt, optlen);
 		nd_opt->nd_opt_type = ND_OPT_TARGET_LINKADDR;
 		nd_opt->nd_opt_len = optlen >> 3;
-		bcopy(mac, (caddr_t)(nd_opt + 1), ifp->if_addrlen);
+		memcpy((char *)(nd_opt + 1) + pad, mac, ifp->if_addrlen);
 	} else
 		flags &= ~ND_NA_FLAG_OVERRIDE;
 
@@ -1141,6 +1149,25 @@ nd6_ifptomac(struct ifnet *ifp)
 	}
 }
 
+/*
+ * Number of reserved octets between the header of an ND link-layer
+ * address option and the link-layer address itself.  On IPoIB links
+ * two zero octets are prepended to the 20-octet link-layer address
+ * to fill the 24-octet option (RFC 4391, section 9.3); on all other
+ * link types the address immediately follows the option header.
+ */
+u_int
+nd6_lladdr_opt_pad(struct ifnet *ifp)
+{
+	switch (ifp->if_type) {
+	case IFT_INFINIBAND:
+	case IFT_INFINIBANDLAG:
+		return (2);
+	default:
+		return (0);
+	}
+}
+
 struct dadq {
 	TAILQ_ENTRY(dadq) dad_list;
 	struct ifaddr *dad_ifa;
diff --git a/sys/netinet6/nd6_rtr.c b/sys/netinet6/nd6_rtr.c
index 4735685a61cd..c497fc9fa6f1 100644
--- a/sys/netinet6/nd6_rtr.c
+++ b/sys/netinet6/nd6_rtr.c
@@ -169,6 +169,7 @@ nd6_rs_input(struct mbuf *m, int off, int icmp6len)
 	char ip6bufs[INET6_ADDRSTRLEN], ip6bufd[INET6_ADDRSTRLEN];
 	char *lladdr;
 	int lladdrlen;
+	u_int lladdr_pad;
 
 	ifp = m->m_pkthdr.rcvif;
 
@@ -223,8 +224,9 @@ nd6_rs_input(struct mbuf *m, int off, int icmp6len)
 
 	lladdr = NULL;
 	lladdrlen = 0;
+	lladdr_pad = nd6_lladdr_opt_pad(ifp);
 	if (ndopts.nd_opts_src_lladdr) {
-		lladdr = (char *)(ndopts.nd_opts_src_lladdr + 1);
+		lladdr = (char *)(ndopts.nd_opts_src_lladdr + 1) + lladdr_pad;
 		lladdrlen = ndopts.nd_opts_src_lladdr->nd_opt_len << 3;
 	}
 
@@ -577,9 +579,10 @@ nd6_ra_input(struct mbuf *m, int off, int icmp6len)
     {
 	char *lladdr = NULL;
 	int lladdrlen = 0;
+	u_int lladdr_pad = nd6_lladdr_opt_pad(ifp);
 
 	if (ndopts.nd_opts_src_lladdr) {
-		lladdr = (char *)(ndopts.nd_opts_src_lladdr + 1);
+		lladdr = (char *)(ndopts.nd_opts_src_lladdr + 1) + lladdr_pad;
 		lladdrlen = ndopts.nd_opts_src_lladdr->nd_opt_len << 3;
 	}