* [PATCH net-next v2 1/4] net: xfrm: interface: restore dst_orig on every xfrm interface
2026-11-12 20:40 [PATCH net-next v2 0/4] net: drop the stale dst when steering to another device Jamal Hadi Salim
@ 2026-11-12 20:40 ` Jamal Hadi Salim
2026-11-12 20:40 ` [PATCH net-next v2 2/4] net: dst: keep the LWT metadata of a dropped route Jamal Hadi Salim
` (2 subsequent siblings)
3 siblings, 0 replies; 6+ messages in thread
From: Jamal Hadi Salim @ 2026-11-12 20:40 UTC (permalink / raw)
To: netdev
Cc: Jamal Hadi Salim, Steffen Klassert, Herbert Xu, David S. Miller,
Eric Dumazet, Jakub Kicinski, Paolo Abeni, Simon Horman,
Alexei Starovoitov, Daniel Borkmann, Andrii Nakryiko,
Eduard Zingerman, Kumar Kartikeya Dwivedi, Martin KaFai Lau,
Song Liu, Yonghong Song, Jiri Olsa, Emil Tsalapatis,
Ihor Solodrai, John Fastabend, Stanislav Fomichev, bpf,
Jiri Pirko, Victor Nogueira
Found while testing the dst release for the two producers that steer a
packet to another device, fixed later in this series. One of the consumers
of the metadata such a release installs is an xfrm interface, which
restores the original route before the xfrm lookup; the swap warned on the
metadata dst still installed in the skb, and only the collect_md arm
restored the route at all.
The issue:
An xfrm interface takes the if_id and the underlying device either from its
configuration or, in collect_md mode, from the xfrm metadata on the skb.
When that metadata also carries the route the packet arrived through,
xfrmi_xmit2() puts the route back on the skb, because the xfrm lookup takes
the address family from it. skb_dst_set() expects the dst slot to be empty
for a reference counted dst and warns when it is not, so the route landed
over a metadata dst that was still there; a kernel built with
CONFIG_DEBUG_NET=y panics on the first packet that takes this path when
panic_on_warn is set.
Two more defects sit in the same place. An interface with a configured
if_id never restores the route: it takes the metadata dst as it is, whose
address family is AF_UNSPEC, so the lookup finds no policy and the packet
is dropped as a link failure instead of being transformed. And the restore
moved the route out of the metadata dst, which a clone of the skb shares,
so the clone that reached the interface first left the original packet with
nothing to restore.
The same slot overwrite is in the unstable helper a tc-bpf program uses to
attach that metadata: bpf_skb_set_xfrm_info() records the route as dst_orig
and installs a per-cpu metadata dst over it. The per-cpu dst is shared by
every invocation of the program, so the restore cannot keep anything in it.
The Fix:
Restore dst_orig in both arms of xfrmi_xmit2(), taking a reference with
dst_clone() so the metadata dst keeps its own, and empty the counted slot
with skb_dst_drop() before the route is installed. The helper allocates a
metadata dst per skb instead of a per-cpu one, so the route it records
belongs to that skb and is released with it; the per-cpu allocation is no
longer used and goes away.
Conditions to recreate the bug:
A tc-bpf program that records the route as dst_orig with
bpf_skb_set_xfrm_info() and redirects the packet to an xfrm interface is
the shortest way to hit the overwrite. A netdev egress fwd rule that steers
a packet routed with an xfrm encapsulation into an interface reaches the
same place once the dst release earlier in this series keeps the route:
ip link add xf0 type xfrm if_id 42
ip link set xf0 up
ip neigh replace 10.20.0.1 lladdr 02:aa:00:00:00:ab \
dev va nud permanent
ip route add 10.20.0.1/32 dev va encap xfrm if_id 42
nft add rule netdev t c fwd ip to 10.1.0.2 device xf0
Send one packet to 10.20.0.1. With CONFIG_DEBUG_NET=y and panic_on_warn=1
the kernel panics in xfrmi_xmit() with the metadata dst still installed
while the route is put back. The interface with the configured if_id drops
the packet as a link failure without consulting the outbound policy, and a
clone made after the metadata was attached consumes the route before the
original packet arrives.
Reviewed-by: Victor Nogueira <victor@mojatatu.com>
Signed-off-by: Jamal Hadi Salim <jhs@mojatatu.com>
---
include/net/xfrm.h | 2 --
net/core/filter.c | 8 --------
net/xfrm/xfrm_interface_bpf.c | 16 +++++-----------
net/xfrm/xfrm_interface_core.c | 22 ++++++++++++----------
4 files changed, 17 insertions(+), 31 deletions(-)
diff --git a/include/net/xfrm.h b/include/net/xfrm.h
index 445223a8dc6f..1866e5a3b911 100644
--- a/include/net/xfrm.h
+++ b/include/net/xfrm.h
@@ -2370,8 +2370,6 @@ static inline bool xfrm6_local_dontfrag(const struct sock *sk)
#if (IS_BUILTIN(CONFIG_XFRM_INTERFACE) && IS_ENABLED(CONFIG_DEBUG_INFO_BTF)) || \
(IS_MODULE(CONFIG_XFRM_INTERFACE) && IS_ENABLED(CONFIG_DEBUG_INFO_BTF_MODULES))
-extern struct metadata_dst __percpu *xfrm_bpf_md_dst;
-
int register_xfrm_interface_bpf(void);
#else
diff --git a/net/core/filter.c b/net/core/filter.c
index 9b8d5191c4ba..2d574ae52c4b 100644
--- a/net/core/filter.c
+++ b/net/core/filter.c
@@ -6353,14 +6353,6 @@ static const struct bpf_func_proto bpf_bind_proto = {
#ifdef CONFIG_XFRM
-#if (IS_BUILTIN(CONFIG_XFRM_INTERFACE) && IS_ENABLED(CONFIG_DEBUG_INFO_BTF)) || \
- (IS_MODULE(CONFIG_XFRM_INTERFACE) && IS_ENABLED(CONFIG_DEBUG_INFO_BTF_MODULES))
-
-struct metadata_dst __percpu *xfrm_bpf_md_dst;
-EXPORT_SYMBOL_GPL(xfrm_bpf_md_dst);
-
-#endif
-
BPF_CALL_5(bpf_skb_get_xfrm_state, struct sk_buff *, skb, u32, index,
struct bpf_xfrm_state *, to, u32, size, u64, flags)
{
diff --git a/net/xfrm/xfrm_interface_bpf.c b/net/xfrm/xfrm_interface_bpf.c
index 5ea15037ebd1..8e83c624b774 100644
--- a/net/xfrm/xfrm_interface_bpf.c
+++ b/net/xfrm/xfrm_interface_bpf.c
@@ -68,16 +68,9 @@ __bpf_kfunc int bpf_skb_set_xfrm_info(struct __sk_buff *skb_ctx, const struct bp
if (unlikely(skb_metadata_dst(skb)))
return -EINVAL;
- if (!xfrm_bpf_md_dst) {
- struct metadata_dst __percpu *tmp;
-
- tmp = metadata_dst_alloc_percpu(0, METADATA_XFRM, GFP_ATOMIC);
- if (!tmp)
- return -ENOMEM;
- if (cmpxchg(&xfrm_bpf_md_dst, NULL, tmp))
- metadata_dst_free_percpu(tmp);
- }
- md_dst = this_cpu_ptr(xfrm_bpf_md_dst);
+ md_dst = metadata_dst_alloc(0, METADATA_XFRM, GFP_ATOMIC);
+ if (unlikely(!md_dst))
+ return -ENOMEM;
info = &md_dst->u.xfrm_info;
@@ -86,7 +79,8 @@ __bpf_kfunc int bpf_skb_set_xfrm_info(struct __sk_buff *skb_ctx, const struct bp
skb_dst_force(skb);
info->dst_orig = skb_dst(skb);
- dst_hold((struct dst_entry *)md_dst);
+ /* The allocated dst owns one reference, handed to the skb here. */
+ skb_dstref_steal(skb);
skb_dst_set(skb, (struct dst_entry *)md_dst);
return 0;
}
diff --git a/net/xfrm/xfrm_interface_core.c b/net/xfrm/xfrm_interface_core.c
index 9835fbe63ccb..896180f8d171 100644
--- a/net/xfrm/xfrm_interface_core.c
+++ b/net/xfrm/xfrm_interface_core.c
@@ -423,6 +423,7 @@ static int xfrmi_rcv_cb(struct sk_buff *skb, int err)
static int
xfrmi_xmit2(struct sk_buff *skb, struct net_device *dev, struct flowi *fl)
{
+ struct xfrm_md_info *md_info = skb_xfrm_md_info(skb);
struct xfrm_if *xi = netdev_priv(dev);
struct dst_entry *dst = skb_dst(skb);
unsigned int length = skb->len;
@@ -433,25 +434,26 @@ xfrmi_xmit2(struct sk_buff *skb, struct net_device *dev, struct flowi *fl)
int mtu;
if (xi->p.collect_md) {
- struct xfrm_md_info *md_info = skb_xfrm_md_info(skb);
-
if (unlikely(!md_info))
return -EINVAL;
if_id = md_info->if_id;
fl->flowi_oif = md_info->link;
- if (md_info->dst_orig) {
- struct dst_entry *tmp_dst = dst;
-
- dst = md_info->dst_orig;
- skb_dst_set(skb, dst);
- md_info->dst_orig = NULL;
- dst_release(tmp_dst);
- }
} else {
if_id = xi->p.if_id;
}
+ /* The metadata dst carries the route the packet is still to be
+ * transformed for, and the lookup below takes the address family from
+ * the dst it is given. Put that route back, taking our own reference:
+ * the metadata dst keeps its own, as a clone of this skb shares it.
+ */
+ if (md_info && md_info->dst_orig) {
+ dst = dst_clone(md_info->dst_orig);
+ skb_dst_drop(skb);
+ skb_dst_set(skb, dst);
+ }
+
dst_hold(dst);
dst = xfrm_lookup_with_ifid(xi->net, dst, fl, NULL, 0, if_id);
if (IS_ERR(dst)) {
--
2.43.0
^ permalink raw reply related [flat|nested] 6+ messages in thread* [PATCH net-next v2 2/4] net: dst: keep the LWT metadata of a dropped route
2026-11-12 20:40 [PATCH net-next v2 0/4] net: drop the stale dst when steering to another device Jamal Hadi Salim
2026-11-12 20:40 ` [PATCH net-next v2 1/4] net: xfrm: interface: restore dst_orig on every xfrm interface Jamal Hadi Salim
@ 2026-11-12 20:40 ` Jamal Hadi Salim
2026-11-12 20:40 ` [PATCH net-next v2 3/4] netfilter: nft_fwd_netdev: drop the stale dst when steering to a device Jamal Hadi Salim
2026-11-12 20:40 ` [PATCH net-next v2 4/4] bpf: drop the stale dst when redirecting to an explicit nexthop Jamal Hadi Salim
3 siblings, 0 replies; 6+ messages in thread
From: Jamal Hadi Salim @ 2026-11-12 20:40 UTC (permalink / raw)
To: netdev
Cc: Jamal Hadi Salim, David S. Miller, Eric Dumazet, Jakub Kicinski,
Paolo Abeni, Simon Horman, bpf, Jiri Pirko, Victor Nogueira
Found while working on the two producers that steer a packet to another
device's neighbour, fixed later in this series. Both release the route
dst on the way, and a route installed with an encapsulation carries the
parameters a device downstream needs: an ip or ip6 encapsulation holds an
ip_tunnel_info, where skb_tunnel_info() reads it, and an xfrm encapsulation
holds an xfrm_md_info, where skb_xfrm_md_info() reads it. The release
discards them, so a tunnel or an xfrm interface downstream has nothing left
to build the outer header with, or to select its if_id.
The Fix:
Add skb_dst_drop_keep_metadata(), which moves those parameters onto a
metadata dst of its own before the route is released. The ip and ip6
payload is copied field by field; the route's dst cache belongs to its
lwtstate and is shared by every packet that uses the route, so it is not
duplicated: the copy starts with an empty cache, which costs the tunnel one
route lookup. The xfrm payload keeps the route itself, because the xfrm
lookup reads its family; the counted reference the skb already holds moves
into the metadata dst, which releases it once it has been consumed.
The helper owns the whole operation: a dst that is not a route is not
dropped, because a metadata dst is the encapsulation itself. Its callers
must not transmit with the stale route, so a failed copy is reported and
the skb is left untouched for its owner to drop.
Conditions to recreate the bug:
Install a route with an encapsulation (ip route add ... encap ip id <id>
dst <remote>, or encap xfrm if_id <id>), steer a packet to another
device's neighbour with a netdev egress fwd rule or
bpf_redirect_neigh(), and release the route dst before neigh_xmit() hands
the packet on. A tunnel or xfrm device in the path then finds no
parameters in skb_tunnel_info() or skb_xfrm_md_info() and drops the packet
instead of using them.
Reviewed-by: Victor Nogueira <victor@mojatatu.com>
Signed-off-by: Jamal Hadi Salim <jhs@mojatatu.com>
---
include/net/dst_metadata.h | 1 +
net/core/dst.c | 72 ++++++++++++++++++++++++++++++++++++++
2 files changed, 73 insertions(+)
diff --git a/include/net/dst_metadata.h b/include/net/dst_metadata.h
index f45d1e3163f0..32d74b4f009f 100644
--- a/include/net/dst_metadata.h
+++ b/include/net/dst_metadata.h
@@ -136,6 +136,7 @@ struct metadata_dst *metadata_dst_alloc(u8 optslen, enum metadata_type type,
void metadata_dst_free_percpu(struct metadata_dst __percpu *md_dst);
struct metadata_dst __percpu *
metadata_dst_alloc_percpu(u8 optslen, enum metadata_type type, gfp_t flags);
+bool skb_dst_drop_keep_metadata(struct sk_buff *skb);
static inline struct metadata_dst *tun_rx_dst(int md_size)
{
diff --git a/net/core/dst.c b/net/core/dst.c
index 092861133023..daa003979d59 100644
--- a/net/core/dst.c
+++ b/net/core/dst.c
@@ -353,3 +353,75 @@ void metadata_dst_free_percpu(struct metadata_dst __percpu *md_dst)
free_percpu(md_dst);
}
EXPORT_SYMBOL_GPL(metadata_dst_free_percpu);
+
+/**
+ * skb_dst_drop_keep_metadata - release a route dst, keeping its LWT metadata
+ * @skb: buffer whose dst is to be released
+ *
+ * A route installed with an encapsulation carries the parameters a downstream
+ * device needs in dst->lwtstate, where skb_tunnel_info() and skb_xfrm_md_info()
+ * find them. Releasing such a route would discard them, so move them onto a
+ * metadata dst of their own first. The route is not duplicated: the copy
+ * starts with an empty cache, and for xfrm the route is kept because the
+ * lookup needs its family.
+ *
+ * A dst that is metadata is left in place. False means no copy could be
+ * allocated and the caller must drop the skb.
+ */
+bool skb_dst_drop_keep_metadata(struct sk_buff *skb)
+{
+ struct dst_entry *old = skb_dst(skb);
+ struct metadata_dst *md_dst = NULL;
+
+ if (!skb_valid_dst(skb))
+ return true;
+
+ if (old->lwtstate && old->lwtstate->type == LWTUNNEL_ENCAP_XFRM) {
+ struct xfrm_md_info *src = lwt_xfrm_info(old->lwtstate);
+
+ md_dst = metadata_dst_alloc(0, METADATA_XFRM, GFP_ATOMIC);
+ if (!md_dst)
+ return false;
+
+ /* Count the route so it outlives the skb, then move the
+ * reference into the metadata dst.
+ */
+ if (!skb_dst_force(skb)) {
+ metadata_dst_free(md_dst);
+ return false;
+ }
+
+ md_dst->u.xfrm_info.if_id = src->if_id;
+ md_dst->u.xfrm_info.link = src->link;
+ md_dst->u.xfrm_info.dst_orig = old;
+ skb_dstref_steal(skb);
+ skb_dst_set(skb, &md_dst->dst);
+ return true;
+ }
+
+ if (old->lwtstate &&
+ (old->lwtstate->type == LWTUNNEL_ENCAP_IP ||
+ old->lwtstate->type == LWTUNNEL_ENCAP_IP6)) {
+ struct ip_tunnel_info *src = lwt_tun_info(old->lwtstate);
+ struct ip_tunnel_info *dst;
+
+ md_dst = metadata_dst_alloc(src->options_len,
+ METADATA_IP_TUNNEL, GFP_ATOMIC);
+ if (!md_dst)
+ return false;
+
+ dst = &md_dst->u.tun_info;
+ dst->key = src->key;
+ dst->encap = src->encap;
+ dst->options_len = src->options_len;
+ dst->mode = src->mode;
+ memcpy(ip_tunnel_info_opts(dst), ip_tunnel_info_opts(src),
+ src->options_len);
+ }
+
+ skb_dst_drop(skb);
+ if (md_dst)
+ skb_dst_set(skb, &md_dst->dst);
+ return true;
+}
+EXPORT_SYMBOL_GPL(skb_dst_drop_keep_metadata);
--
2.43.0
^ permalink raw reply related [flat|nested] 6+ messages in thread* [PATCH net-next v2 3/4] netfilter: nft_fwd_netdev: drop the stale dst when steering to a device
2026-11-12 20:40 [PATCH net-next v2 0/4] net: drop the stale dst when steering to another device Jamal Hadi Salim
2026-11-12 20:40 ` [PATCH net-next v2 1/4] net: xfrm: interface: restore dst_orig on every xfrm interface Jamal Hadi Salim
2026-11-12 20:40 ` [PATCH net-next v2 2/4] net: dst: keep the LWT metadata of a dropped route Jamal Hadi Salim
@ 2026-11-12 20:40 ` Jamal Hadi Salim
2026-11-13 9:47 ` Pablo Neira Ayuso
2026-11-12 20:40 ` [PATCH net-next v2 4/4] bpf: drop the stale dst when redirecting to an explicit nexthop Jamal Hadi Salim
3 siblings, 1 reply; 6+ messages in thread
From: Jamal Hadi Salim @ 2026-11-12 20:40 UTC (permalink / raw)
To: netdev
Cc: Jamal Hadi Salim, Pablo Neira Ayuso, Florian Westphal,
Phil Sutter, David S. Miller, Eric Dumazet, Jakub Kicinski,
Paolo Abeni, Simon Horman, netfilter-devel, coreteam, Jiri Pirko,
Victor Nogueira, Sashiko
This is a followup of the review of the teql arp-queue fix as reported
by Sashiko.
The issue:
The neighbour variant of fwd rewrites skb->dev to the configured output
device and calls neigh_xmit(), but leaves the skb's dst untouched. If that
device's neighbour for the configured nexthop is incomplete, the packet is
parked on the neighbour's arp_queue while the stale dst still describes the
device the packet was routed through. Draining the queue re-looks the
neighbour up from that dst, so the frame is built with the routed device's
link-layer header and then transmitted on the configured device. The packet
leaves with another device's addresses.
The Fix:
Release the dst before the device switch. Both lookup inputs the neighbour
transmission needs, the device and the nexthop, are supplied by the rule,
so the route dst is never consulted. Without it the queue drain keeps the
parking neighbour and the frame carries the configured device's addresses.
The release goes through skb_dst_drop_keep_metadata(), so encapsulation
parameters a route carries are handed over to a metadata dst instead of
being lost, and a metadata dst already on the packet is left in place. If
that copy cannot be allocated the packet is dropped, never sent with the
stale route.
Conditions to recreate the bug:
Two network namespaces, A (10.10.0.1/24) and B (10.1.0.1/24) in one, their
veth peers in the other with only A's peer addressed (10.10.0.2/24) so B's
nexthop cannot be resolved by ARP. Resolve A's neighbour for 10.10.0.2 as
permanent, then load this ruleset on A:
table netdev t {
chain c {
type filter hook egress device va priority 0
policy accept
fwd ip to 10.1.0.2 device vb
}
}
Send one packet to 10.10.0.2. It is parked on B's incomplete neighbour
for 10.1.0.2; once that neighbour becomes valid the frame leaves B
carrying A's source address and A's neighbour as the destination.
Reported-by: Sashiko (nipa) <sashiko-bot@kernel.org>
Closes: https://netdev-ai.bots.linux.dev/sashiko/#/patchset/QDISC-6HS9.v3.r0.20261103062649-2@mojatatu.com
Link: https://lore.kernel.org/netdev/QDISC-6HS9.v3.r0.20261103062649-2@mojatatu.com/
Link: https://lore.kernel.org/netdev/avTfQjFQYR2NNdRg@chamomile/
Reviewed-by: Victor Nogueira <victor@mojatatu.com>
Signed-off-by: Jamal Hadi Salim <jhs@mojatatu.com>
---
v1 -> v2:
- Do not drop a dst that is not a route: a DST_METADATA dst is the tunnel
template a tunnel helper attached to the packet, and dropping it discards
the encapsulation parameters. Considered comment from Pablo Neira Ayuso.
- Keep a route's encapsulation parameters as well: a route installed with an
ip, ip6 or xfrm encapsulation carries them in dst->lwtstate, which a drop
would discard before a downstream tunnel or xfrm device could use them.
The dst release now goes through a helper that moves them onto a metadata
dst first (new 2/4).
net/netfilter/nft_fwd_netdev.c | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/net/netfilter/nft_fwd_netdev.c b/net/netfilter/nft_fwd_netdev.c
index a48c2f765bba..3f2ffda1998c 100644
--- a/net/netfilter/nft_fwd_netdev.c
+++ b/net/netfilter/nft_fwd_netdev.c
@@ -14,6 +14,7 @@
#include <net/netfilter/nf_tables.h>
#include <net/netfilter/nf_tables_offload.h>
#include <net/netfilter/nf_dup_netdev.h>
+#include <net/dst_metadata.h>
#include <net/neighbour.h>
#include <net/ip.h>
@@ -159,6 +160,11 @@ static void nft_fwd_neigh_eval(const struct nft_expr *expr,
goto out;
}
+ if (!skb_dst_drop_keep_metadata(skb)) {
+ verdict = NF_DROP;
+ goto out;
+ }
+
local_bh_disable();
if (nf_dev_xmit_recursion()) {
local_bh_enable();
--
2.43.0
^ permalink raw reply related [flat|nested] 6+ messages in thread* Re: [PATCH net-next v2 3/4] netfilter: nft_fwd_netdev: drop the stale dst when steering to a device
2026-11-12 20:40 ` [PATCH net-next v2 3/4] netfilter: nft_fwd_netdev: drop the stale dst when steering to a device Jamal Hadi Salim
@ 2026-11-13 9:47 ` Pablo Neira Ayuso
0 siblings, 0 replies; 6+ messages in thread
From: Pablo Neira Ayuso @ 2026-11-13 9:47 UTC (permalink / raw)
To: Jamal Hadi Salim
Cc: netdev, Florian Westphal, Phil Sutter, David S. Miller,
Eric Dumazet, Jakub Kicinski, Paolo Abeni, Simon Horman,
netfilter-devel, coreteam, Jiri Pirko, Victor Nogueira, Sashiko
On Thu, Nov 12, 2026 at 03:40:48PM -0500, Jamal Hadi Salim wrote:
> This is a followup of the review of the teql arp-queue fix as reported
> by Sashiko.
>
> The issue:
> The neighbour variant of fwd rewrites skb->dev to the configured output
> device and calls neigh_xmit(), but leaves the skb's dst untouched. If that
> device's neighbour for the configured nexthop is incomplete, the packet is
> parked on the neighbour's arp_queue while the stale dst still describes the
> device the packet was routed through. Draining the queue re-looks the
> neighbour up from that dst, so the frame is built with the routed device's
> link-layer header and then transmitted on the configured device. The packet
> leaves with another device's addresses.
>
> The Fix:
> Release the dst before the device switch. Both lookup inputs the neighbour
> transmission needs, the device and the nexthop, are supplied by the rule,
> so the route dst is never consulted. Without it the queue drain keeps the
> parking neighbour and the frame carries the configured device's addresses.
>
> The release goes through skb_dst_drop_keep_metadata(), so encapsulation
> parameters a route carries are handed over to a metadata dst instead of
> being lost, and a metadata dst already on the packet is left in place. If
> that copy cannot be allocated the packet is dropped, never sent with the
> stale route.
>
> Conditions to recreate the bug:
> Two network namespaces, A (10.10.0.1/24) and B (10.1.0.1/24) in one, their
> veth peers in the other with only A's peer addressed (10.10.0.2/24) so B's
> nexthop cannot be resolved by ARP. Resolve A's neighbour for 10.10.0.2 as
> permanent, then load this ruleset on A:
>
> table netdev t {
> chain c {
> type filter hook egress device va priority 0
> policy accept
> fwd ip to 10.1.0.2 device vb
> }
> }
>
> Send one packet to 10.10.0.2. It is parked on B's incomplete neighbour
> for 10.1.0.2; once that neighbour becomes valid the frame leaves B
> carrying A's source address and A's neighbour as the destination.
>
> Reported-by: Sashiko (nipa) <sashiko-bot@kernel.org>
> Closes: https://netdev-ai.bots.linux.dev/sashiko/#/patchset/QDISC-6HS9.v3.r0.20261103062649-2@mojatatu.com
> Link: https://lore.kernel.org/netdev/QDISC-6HS9.v3.r0.20261103062649-2@mojatatu.com/
> Link: https://lore.kernel.org/netdev/avTfQjFQYR2NNdRg@chamomile/
> Reviewed-by: Victor Nogueira <victor@mojatatu.com>
> Signed-off-by: Jamal Hadi Salim <jhs@mojatatu.com>
Reviewed-by: Pablo Neira Ayuso <pablo@netfilter.org>
^ permalink raw reply [flat|nested] 6+ messages in thread
* [PATCH net-next v2 4/4] bpf: drop the stale dst when redirecting to an explicit nexthop
2026-11-12 20:40 [PATCH net-next v2 0/4] net: drop the stale dst when steering to another device Jamal Hadi Salim
` (2 preceding siblings ...)
2026-11-12 20:40 ` [PATCH net-next v2 3/4] netfilter: nft_fwd_netdev: drop the stale dst when steering to a device Jamal Hadi Salim
@ 2026-11-12 20:40 ` Jamal Hadi Salim
3 siblings, 0 replies; 6+ messages in thread
From: Jamal Hadi Salim @ 2026-11-12 20:40 UTC (permalink / raw)
To: netdev
Cc: Jamal Hadi Salim, Daniel Borkmann, John Fastabend,
Stanislav Fomichev, Martin KaFai Lau, Alexei Starovoitov,
Andrii Nakryiko, Eduard Zingerman, Kumar Kartikeya Dwivedi,
Song Liu, Yonghong Song, Jiri Olsa, Emil Tsalapatis,
Ihor Solodrai, David S. Miller, Eric Dumazet, Jakub Kicinski,
Paolo Abeni, Simon Horman, bpf, Jiri Pirko, Victor Nogueira,
Sashiko
This is a followup of the review of the teql arp-queue fix as reported
by Sashiko.
The issue:
__bpf_redirect_neigh_v4() and __bpf_redirect_neigh_v6() install a dst for
the redirect target only when the program lets the helper do the FIB
lookup. With BPF_F_NEXTHOP the program supplies the nexthop, the lookup is
skipped, and whatever dst the packet arrived with stays attached while
bpf_out_neigh_v{4,6}() switch skb->dev to the target device. If that
device's neighbour is incomplete the packet is parked on its arp_queue with
the original route's dst; draining the queue re-looks the neighbour up from
that dst and builds the frame with the original device's link-layer header,
then transmits it on the target device.
The Fix:
Release the dst when the nexthop is supplied. That branch never consults
the route dst, so nothing that describes the route is lost, and the queue
drain keeps the target device's parking neighbour.
The release goes through skb_dst_drop_keep_metadata(), so encapsulation
parameters a route carries are handed over to a metadata dst instead of
being lost, and a metadata dst already on the packet is left in place. Both
wrappers take their existing drop path when that copy cannot be allocated,
so a packet is never sent with the stale route.
Conditions to recreate the bug:
Two network namespaces, A (10.10.0.1/24) and B (10.1.0.1/24) in one, their
veth peers in the other with only A's peer addressed (10.10.0.2/24) so B's
nexthop cannot be resolved by ARP. Resolve A's neighbour for 10.10.0.2 as
permanent and attach a cls_bpf egress program to A, with vb the index of B:
struct bpf_redir_neigh nh = { .nh_family = AF_INET,
.ipv4_nh = htonl(0x0a010002) };
return bpf_redirect_neigh(vb, &nh, sizeof(nh), 0);
Send one packet to 10.10.0.2. It is parked on B's incomplete neighbour
for 10.1.0.2; once that neighbour becomes valid the frame leaves B
carrying A's source address and A's neighbour as the destination.
Reported-by: Sashiko (nipa) <sashiko-bot@kernel.org>
Closes: https://netdev-ai.bots.linux.dev/sashiko/#/patchset/QDISC-6HS9.v3.r0.20261103062649-2@mojatatu.com
Link: https://lore.kernel.org/netdev/QDISC-6HS9.v3.r0.20261103062649-2@mojatatu.com/
Link: https://lore.kernel.org/netdev/avTfQjFQYR2NNdRg@chamomile/
Reviewed-by: Victor Nogueira <victor@mojatatu.com>
Signed-off-by: Jamal Hadi Salim <jhs@mojatatu.com>
---
v1 -> v2:
- Do not drop a dst that is not a route, and keep the encapsulation
parameters a route carries: the release goes through a helper that moves
them onto a metadata dst first (new 2/4), and a copy that cannot be
allocated drops the packet instead of sending it with the stale route.
net/core/filter.c | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/net/core/filter.c b/net/core/filter.c
index 2d574ae52c4b..4980fb8db299 100644
--- a/net/core/filter.c
+++ b/net/core/filter.c
@@ -2318,6 +2318,8 @@ static int __bpf_redirect_neigh_v6(struct sk_buff *skb, struct net_device *dev,
skb_dst_set(skb, dst);
} else if (nh->nh_family != AF_INET6) {
goto out_drop;
+ } else if (!skb_dst_drop_keep_metadata(skb)) {
+ goto out_drop;
}
err = bpf_out_neigh_v6(net, skb, dev, nh);
@@ -2433,6 +2435,8 @@ static int __bpf_redirect_neigh_v4(struct sk_buff *skb, struct net_device *dev,
skb_dst_drop(skb);
skb_dst_set(skb, &rt->dst);
+ } else if (!skb_dst_drop_keep_metadata(skb)) {
+ goto out_drop;
}
err = bpf_out_neigh_v4(net, skb, dev, nh);
--
2.43.0
^ permalink raw reply related [flat|nested] 6+ messages in thread