[RFC net-next v2 2/3] bonding: build NS probe headers before inserting VLAN tags

From: Xiang Mei (Microsoft)

Date: Wed Jul 29 2026 - 19:02:12 EST


bond_ns_send() passes the skb from ndisc_ns_create() to
bond_handle_vlan() before any header exists: ndisc_ns_create() returns
only the ICMPv6 message, and the IPv6 and link-layer headers are pushed
later by ndisc_send_skb(). Tagging that bare message causes two
failures:

- Corruption: bond_handle_vlan() inserts tags at mac_len = ETH_HLEN,
expecting an Ethernet header. arp_create() provides one,
ndisc_ns_create() does not, so each tag lands 12 bytes into the ICMPv6
message, inside the target address. Every config with two or more
stacked VLANs emits a probe no peer can answer.

- Panic: each tag also eats the headroom reserved for the IPv6 header
(LL_RESERVED_SPACE(dev) + sizeof(struct ipv6hdr) = 56 for a plain
Ethernet slave, leaving 56 - 4 * (N - 1) for N VLANs). Six leave
36 < 40 and the ip6_nd_hdr() push underflows skb->head:

skbuff: skb_under_panic: text:ffffffff85eef44b len:84 put:40
head:ffff88801400b740 data:ffff88801400b73c
tail:0x50 end:0x180 dev:veth0
kernel BUG at net/core/skbuff.c:214!
Oops: invalid opcode: 0000 [#1] SMP KASAN NOPTI
Workqueue: bond0 bond_arp_monitor
RIP: 0010:skb_panic+0x142/0x230
Call Trace:
skb_push (net/core/skbuff.c:224)
ndisc_send_skb (net/ipv6/ndisc.c:454 net/ipv6/ndisc.c:506)
bond_ns_send (drivers/net/bonding/bond_main.c:3255)
bond_ns_send_all (drivers/net/bonding/bond_main.c:3313)
bond_arp_monitor (drivers/net/bonding/bond_main.c:3458)
process_one_work (kernel/workqueue.c:3322)
worker_thread (kernel/workqueue.c:3405)
kthread (kernel/kthread.c:436)
Kernel panic - not syncing: Fatal exception

Build the probe in the order the stack uses: bond_ns_build_ip6hdr()
pushes the IPv6 header, the probe traverses NF_INET_LOCAL_OUT as a bare
IPv6 packet, then bond_ns_add_llhdr() pushes the link-layer header,
bond_handle_vlan() adds the tags, and it is transmitted on the slave
being tested. NF_INET_LOCAL_OUT must run before tagging so netfilter
sees the IPv6 packet ndisc_send_skb() feeds it rather than a half-built
Ethernet frame; ndisc_attach_dst() supplies the dst
ip6_route_me_harder() dereferences unconditionally.

The tags now land at a real Ethernet header, so the target address is
intact, and pushing the headers first lets skb_vlan_push() grow the
headroom itself, so no push underflows skb->head. nf_hook() returns 1
on NF_ACCEPT with the skb still owned by the caller; any other verdict
means netfilter consumed it.

Fixes: 4e24be018eb9 ("bonding: add new parameter ns_targets")
Cc: stable@xxxxxxxxxxxxxxx
Reported-by: AutonomousCodeSecurity@xxxxxxxxxxxxx
Signed-off-by: Xiang Mei (Microsoft) <xmei5@xxxxxxx>
---
drivers/net/bonding/bond_main.c | 106 +++++++++++++++++++++++++++++++-
1 file changed, 103 insertions(+), 3 deletions(-)

diff --git a/drivers/net/bonding/bond_main.c b/drivers/net/bonding/bond_main.c
index 522eab060f9e..d5a7a7dd1eb2 100644
--- a/drivers/net/bonding/bond_main.c
+++ b/drivers/net/bonding/bond_main.c
@@ -90,6 +90,8 @@
#include <net/tls.h>
#endif
#include <net/ip6_route.h>
+#include <net/ip6_checksum.h>
+#include <linux/netfilter.h>
#include <net/netdev_lock.h>
#include <net/xdp.h>

@@ -3232,13 +3234,75 @@ static int bond_arp_rcv(const struct sk_buff *skb, struct bonding *bond,
}

#if IS_ENABLED(CONFIG_IPV6)
+static int bond_ns_xmit_finish(struct net *net, struct sock *sk,
+ struct sk_buff *skb)
+{
+ return dev_queue_xmit(skb);
+}
+
+static void bond_ns_build_ip6hdr(struct sk_buff *skb, struct net_device *dev,
+ const struct in6_addr *saddr,
+ const struct in6_addr *daddr)
+{
+ struct icmp6hdr *icmp6h = icmp6_hdr(skb);
+ unsigned int len = skb->len;
+ struct inet6_dev *idev;
+ struct ipv6hdr *hdr;
+ unsigned int tclass;
+
+ icmp6h->icmp6_cksum = csum_ipv6_magic(saddr, daddr, len,
+ IPPROTO_ICMPV6,
+ csum_partial(icmp6h, len, 0));
+
+ rcu_read_lock();
+ idev = __in6_dev_get(dev);
+ tclass = idev ? READ_ONCE(idev->cnf.ndisc_tclass) : 0;
+ rcu_read_unlock();
+
+ skb_push(skb, sizeof(*hdr));
+ skb_reset_network_header(skb);
+ hdr = ipv6_hdr(skb);
+
+ ip6_flow_hdr(hdr, tclass, 0);
+ hdr->payload_len = htons(len);
+ hdr->nexthdr = IPPROTO_ICMPV6;
+ hdr->hop_limit = 255; /* RFC 4861, 7.1.1 */
+ hdr->saddr = *saddr;
+ hdr->daddr = *daddr;
+}
+
+/* Push the Ethernet header after the LOCAL_OUT hooks, as arp_create() and the
+ * IPv6 output path do.
+ */
+static int bond_ns_add_llhdr(struct sk_buff *skb, struct net_device *dev,
+ const struct in6_addr *daddr)
+{
+ unsigned char ha[MAX_ADDR_LEN];
+ int err;
+
+ err = ndisc_mc_map(daddr, ha, dev, 1);
+ if (err < 0)
+ return err;
+
+ err = dev_hard_header(skb, dev, ETH_P_IPV6, ha, dev->dev_addr,
+ skb->len);
+ if (err < 0)
+ return err;
+
+ skb_reset_mac_header(skb);
+ return 0;
+}
+
static void bond_ns_send(struct slave *slave, const struct in6_addr *daddr,
const struct in6_addr *saddr, struct bond_vlan_tag *tags)
{
struct net_device *bond_dev = slave->bond->dev;
struct net_device *slave_dev = slave->dev;
+ struct inet6_dev *idev;
struct in6_addr mcaddr;
struct sk_buff *skb;
+ struct net *net;
+ int ret;

slave_dbg(bond_dev, slave_dev, "NS on slave: dst %pI6c src %pI6c\n",
daddr, saddr);
@@ -3250,10 +3314,46 @@ static void bond_ns_send(struct slave *slave, const struct in6_addr *daddr,
}

addrconf_addr_solict_mult(daddr, &mcaddr);
- if (bond_handle_vlan(slave, tags, skb)) {
- slave_update_last_tx(slave);
- ndisc_send_skb(skb, &mcaddr, saddr);
+
+ /* ip6_route_me_harder() in the LOCAL_OUT path dereferences skb_dst() */
+ if (ndisc_attach_dst(skb, &mcaddr, saddr)) {
+ net_err_ratelimited("NS route allocation failed\n");
+ kfree_skb(skb);
+ return;
}
+
+ bond_ns_build_ip6hdr(skb, slave_dev, saddr, &mcaddr);
+
+ rcu_read_lock();
+ net = dev_net_rcu(slave_dev);
+ idev = __in6_dev_get(slave_dev);
+ IP6_INC_STATS(net, idev, IPSTATS_MIB_OUTREQUESTS);
+
+ /* nf_hook() returns 1 on NF_ACCEPT with @skb still ours; any other
+ * verdict means netfilter consumed it.
+ */
+ ret = nf_hook(NFPROTO_IPV6, NF_INET_LOCAL_OUT, net, NULL, skb, NULL,
+ slave_dev, bond_ns_xmit_finish);
+ if (ret == 1) {
+ ICMP6MSGOUT_INC_STATS(net, idev, NDISC_NEIGHBOUR_SOLICITATION);
+ ICMP6_INC_STATS(net, idev, ICMP6_MIB_OUTMSGS);
+ }
+ rcu_read_unlock();
+
+ if (ret != 1)
+ return;
+
+ if (bond_ns_add_llhdr(skb, slave_dev, &mcaddr) < 0) {
+ net_err_ratelimited("failed to build NS link-layer header\n");
+ kfree_skb(skb);
+ return;
+ }
+
+ if (!bond_handle_vlan(slave, tags, skb))
+ return;
+
+ slave_update_last_tx(slave);
+ dev_queue_xmit(skb);
}

static void bond_ns_send_all(struct bonding *bond, struct slave *slave)
--
2.43.0