* [PATCH nf v8 1/3] netfilter: nf_dup: preserve guard across NFQUEUE reinjection
2026-10-15 7:43 [PATCH nf v8 0/3] netfilter: bound asynchronous packet duplication Zihan Xi
@ 2026-10-15 7:43 ` Zihan Xi
2026-10-15 7:56 ` sashiko-bot
2026-10-15 7:43 ` [PATCH nf v8 2/3] netfilter: nf_dup_netdev: bound asynchronous duplication Zihan Xi
` (3 subsequent siblings)
4 siblings, 1 reply; 11+ messages in thread
From: Zihan Xi @ 2026-10-15 7:43 UTC (permalink / raw)
To: netfilter-devel, virtualization, intel-wired-lan, bpf
Cc: pablo, fw, phil, davem, edumazet, pabeni, horms, kaber, jengelh,
andrew+netdev, mst, jasowangio, eperezma, xuanzhuo,
aleksander.lobakin, ast, daniel, hawk, john.fastabend, sdf,
magnus.karlsson, maciej.fijalkowski, willemb, alice.kernel,
eilaimemedsnaimel, imv4bel, shiming.cheng, sd, vega, root, zihanx
NFQUEUE can suspend traversal of a duplicated packet while the task-local
duplication guard is set. The output call then returns and clears the guard
before a later verdict resumes hook traversal.
Save the guard in the queue entry and restore it while reinjection continues
hook traversal. Preserve and restore the verdict task's prior state, including
nested synchronous reinjection.
Fixes: cd58bcd9787e ("netfilter: xt_TEE: have cloned packet travel through Xtables too")
Cc: stable@vger.kernel.org
Reported-by: Vega <vega@nebusec.ai>
Assisted-by: LLM
Co-developed-by: Luxing Yin <root@tr0jan.top>
Signed-off-by: Luxing Yin <root@tr0jan.top>
Signed-off-by: Zihan Xi <zihanx@nebusec.ai>
---
include/net/netfilter/nf_queue.h | 1 +
net/netfilter/nf_queue.c | 1 +
net/netfilter/nfnetlink_queue.c | 15 ++++++++++++---
3 files changed, 14 insertions(+), 3 deletions(-)
diff --git a/include/net/netfilter/nf_queue.h b/include/net/netfilter/nf_queue.h
index fc3e81c07364..039622d48905 100644
--- a/include/net/netfilter/nf_queue.h
+++ b/include/net/netfilter/nf_queue.h
@@ -23,6 +23,7 @@ struct nf_queue_entry {
struct net_device *physout;
#endif
struct nf_hook_state state;
+ bool in_nf_duplicate;
bool nf_ct_is_unconfirmed;
u16 size; /* sizeof(entry) + saved route keys */
diff --git a/net/netfilter/nf_queue.c b/net/netfilter/nf_queue.c
index 73363ceedebe..44f182eba953 100644
--- a/net/netfilter/nf_queue.c
+++ b/net/netfilter/nf_queue.c
@@ -219,6 +219,7 @@ static int __nf_queue(struct sk_buff *skb, const struct nf_hook_state *state,
*entry = (struct nf_queue_entry) {
.skb = skb,
.skb_dev = skb->dev,
+ .in_nf_duplicate = current->in_nf_duplicate,
.state = *state,
.hook_index = index,
.size = sizeof(*entry) + route_key_size,
diff --git a/net/netfilter/nfnetlink_queue.c b/net/netfilter/nfnetlink_queue.c
index a3bc00280051..9a8f55498fd5 100644
--- a/net/netfilter/nfnetlink_queue.c
+++ b/net/netfilter/nfnetlink_queue.c
@@ -43,6 +43,7 @@
#include <net/sock.h>
#include <net/tcp_states.h>
#include <net/netfilter/nf_queue.h>
+#include <net/net_debug.h>
#include <net/netns/generic.h>
#include <linux/atomic.h>
@@ -368,17 +369,24 @@ static void nf_reinject(struct nf_queue_entry *entry, unsigned int verdict)
unsigned int i;
int err;
u8 pf;
+ bool old_in_nf_duplicate;
+
+ DEBUG_NET_WARN_ONCE(current->in_nf_duplicate,
+ "%s called with duplication guard set", __func__);
+ local_bh_disable();
+ old_in_nf_duplicate = current->in_nf_duplicate;
net = entry->state.net;
pf = entry->state.pf;
+ current->in_nf_duplicate = entry->in_nf_duplicate;
hooks = nf_hook_entries_head(net, pf, entry->state.hook);
i = entry->hook_index;
if (!hooks || i >= hooks->num_hook_entries) {
kfree_skb_reason(skb, SKB_DROP_REASON_NETFILTER_DROP);
nf_queue_entry_free(entry);
- return;
+ goto out;
}
hook_entry = &hooks->hooks[i];
@@ -401,9 +409,7 @@ static void nf_reinject(struct nf_queue_entry *entry, unsigned int verdict)
switch (verdict & NF_VERDICT_MASK) {
case NF_ACCEPT:
case NF_STOP:
- local_bh_disable();
entry->state.okfn(entry->state.net, entry->state.sk, skb);
- local_bh_enable();
break;
case NF_QUEUE:
err = nf_queue(skb, &entry->state, i, verdict);
@@ -417,6 +423,9 @@ static void nf_reinject(struct nf_queue_entry *entry, unsigned int verdict)
}
nf_queue_entry_free(entry);
+out:
+ current->in_nf_duplicate = old_in_nf_duplicate;
+ local_bh_enable();
}
/* return true if the entry has an unconfirmed conntrack attached that isn't owned by us
--
2.43.0
^ permalink raw reply related [flat|nested] 11+ messages in thread* Re: [PATCH nf v8 1/3] netfilter: nf_dup: preserve guard across NFQUEUE reinjection
2026-10-15 7:43 ` [PATCH nf v8 1/3] netfilter: nf_dup: preserve guard across NFQUEUE reinjection Zihan Xi
@ 2026-10-15 7:56 ` sashiko-bot
2026-10-15 8:13 ` zihan xi
0 siblings, 1 reply; 11+ messages in thread
From: sashiko-bot @ 2026-10-15 7:56 UTC (permalink / raw)
To: Zihan Xi, virtualization, intel-wired-lan, bpf
Cc: Michael S. Tsirkin, Eugenio Perez
Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [High] netfilter: nf_queue synchronous bypass triggers false DEBUG_NET_WARN_ONCE
--
commit 6ab1ebe7cd71aef0c99fc87d28ca2657ffc20779
Author: Zihan Xi <zihanx@nebusec.ai>
netfilter: nf_dup: preserve guard across NFQUEUE reinjection
This commit saves the task-local in_nf_duplicate guard into struct
nf_queue_entry when queuing a packet and restores it during reinjection. It
also adds a warning in nf_reinject() to ensure it is not called with the
guard already set.
> diff --git a/net/netfilter/nfnetlink_queue.c b/net/netfilter/nfnetlink_queue.c
> index 477fc632657f4..1ec9c1fbae843 100644
> --- a/net/netfilter/nfnetlink_queue.c
> +++ b/net/netfilter/nfnetlink_queue.c
[ ... ]
> @@ -369,17 +370,24 @@ static void nf_reinject(struct nf_queue_entry *entry, unsigned int verdict)
> unsigned int i;
> int err;
> u8 pf;
> + bool old_in_nf_duplicate;
> +
> + DEBUG_NET_WARN_ONCE(current->in_nf_duplicate,
> + "%s called with duplication guard set", __func__);
[Severity: High]
Does this warning falsely trigger when a packet is synchronously reinjected
during queue bypass?
When an NFQUEUE rule is configured with queue-bypass (fail-open), packets
are synchronously reinjected if the netlink queue is full. This synchronous
reinjection path (nfqnl_enqueue_packet() -> nfqnl_reinject() ->
nf_reinject()) executes in the same task context as the original hook
traversal.
If the packet was originally duplicated via xt_TEE or nft_dup,
current->in_nf_duplicate will already be true.
Because this can be triggered by remote traffic intentionally filling a
bypass-enabled queue with duplicated packets, could this introduce a remotely
triggerable regression on systems with panic_on_warn enabled?
> + local_bh_disable();
> + old_in_nf_duplicate = current->in_nf_duplicate;
>
> net = entry->state.net;
> pf = entry->state.pf;
--
Sashiko AI review · https://sashiko.dev/#/patchset/cover.1792048855.git.zihanx@nebusec.ai?part=1
^ permalink raw reply [flat|nested] 11+ messages in thread* Re: [PATCH nf v8 1/3] netfilter: nf_dup: preserve guard across NFQUEUE reinjection
2026-10-15 7:56 ` sashiko-bot
@ 2026-10-15 8:13 ` zihan xi
0 siblings, 0 replies; 11+ messages in thread
From: zihan xi @ 2026-10-15 8:13 UTC (permalink / raw)
To: sashiko-reviews
Cc: virtualization, intel-wired-lan, bpf, Michael S. Tsirkin,
Eugenio Perez
On Thu, Oct 15, 2026 at 3:56 PM <sashiko-bot@kernel.org> wrote:
>
> Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
> - [High] netfilter: nf_queue synchronous bypass triggers false DEBUG_NET_WARN_ONCE
> --
>
> commit 6ab1ebe7cd71aef0c99fc87d28ca2657ffc20779
> Author: Zihan Xi <zihanx@nebusec.ai>
>
> netfilter: nf_dup: preserve guard across NFQUEUE reinjection
>
> This commit saves the task-local in_nf_duplicate guard into struct
> nf_queue_entry when queuing a packet and restores it during reinjection. It
> also adds a warning in nf_reinject() to ensure it is not called with the
> guard already set.
>
> > diff --git a/net/netfilter/nfnetlink_queue.c b/net/netfilter/nfnetlink_queue.c
> > index 477fc632657f4..1ec9c1fbae843 100644
> > --- a/net/netfilter/nfnetlink_queue.c
> > +++ b/net/netfilter/nfnetlink_queue.c
> [ ... ]
> > @@ -369,17 +370,24 @@ static void nf_reinject(struct nf_queue_entry *entry, unsigned int verdict)
> > unsigned int i;
> > int err;
> > u8 pf;
> > + bool old_in_nf_duplicate;
> > +
> > + DEBUG_NET_WARN_ONCE(current->in_nf_duplicate,
> > + "%s called with duplication guard set", __func__);
>
> [Severity: High]
> Does this warning falsely trigger when a packet is synchronously reinjected
> during queue bypass?
>
> When an NFQUEUE rule is configured with queue-bypass (fail-open), packets
> are synchronously reinjected if the netlink queue is full. This synchronous
> reinjection path (nfqnl_enqueue_packet() -> nfqnl_reinject() ->
> nf_reinject()) executes in the same task context as the original hook
> traversal.
>
> If the packet was originally duplicated via xt_TEE or nft_dup,
> current->in_nf_duplicate will already be true.
>
> Because this can be triggered by remote traffic intentionally filling a
> bypass-enabled queue with duplicated packets, could this introduce a remotely
> triggerable regression on systems with panic_on_warn enabled?
>
> > + local_bh_disable();
> > + old_in_nf_duplicate = current->in_nf_duplicate;
> >
> > net = entry->state.net;
> > pf = entry->state.pf;
>
> --
> Sashiko AI review · https://sashiko.dev/#/patchset/cover.1792048855.git.zihanx@nebusec.ai?part=1
Thanks for catching this. The warning can fire on synchronous NFQUEUE
fail-open reinjection, where both guard values
are set. I’ve narrowed the local check to warn only if the
current-task guard is set but the queue entry did not
record it. I’ll test the queue-full fail-open path before sending an
updated series.
^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH nf v8 2/3] netfilter: nf_dup_netdev: bound asynchronous duplication
2026-10-15 7:43 [PATCH nf v8 0/3] netfilter: bound asynchronous packet duplication Zihan Xi
2026-10-15 7:43 ` [PATCH nf v8 1/3] netfilter: nf_dup: preserve guard across NFQUEUE reinjection Zihan Xi
@ 2026-10-15 7:43 ` Zihan Xi
2026-10-15 7:58 ` sashiko-bot
2026-10-15 7:43 ` [PATCH nf v8 3/3] netfilter: nf_dup: prevent TTL and hop-limit underflow Zihan Xi
` (2 subsequent siblings)
4 siblings, 1 reply; 11+ messages in thread
From: Zihan Xi @ 2026-10-15 7:43 UTC (permalink / raw)
To: netfilter-devel, virtualization, intel-wired-lan, bpf
Cc: pablo, fw, phil, davem, edumazet, pabeni, horms, kaber, jengelh,
andrew+netdev, mst, jasowangio, eperezma, xuanzhuo,
aleksander.lobakin, ast, daniel, hawk, john.fastabend, sdf,
magnus.karlsson, maciej.fijalkowski, willemb, alice.kernel,
eilaimemedsnaimel, imv4bel, shiming.cheng, sd, vega, root, zihanx
The task-local transmit recursion counter covers synchronous
dev_queue_xmit() recursion, but does not follow a duplicated skb through
asynchronous receive paths. Carry a bounded duplication depth with the skb
and preserve it across skb and XDP conversions.
Store the depth in the sk_buff headers group. __copy_skb_header() copies
that group with a single memcpy(), so skb_clone() preserves the depth.
Propagate it explicitly when veth converts an skb to an XDP buffer and
when XDP buffers or frames are converted back to skbs in the core and
virtio paths. Preserve it through zero-copy XDP frame conversion as well.
Clear the depth when GRO reuses an skb and when AF_XDP or libeth
reinitializes a recycled XDP buffer, so a fresh packet does not inherit
stale recursion state.
Keep the existing recursion limit and reject MIRRED flow offload where
the software depth cannot be enforced.
Fixes: 502061f81d3e ("netfilter: nf_tables: add packet duplication to the netdev family")
Cc: stable@vger.kernel.org
Reported-by: Vega <vega@nebusec.ai>
Assisted-by: LLM
Co-developed-by: Luxing Yin <root@tr0jan.top>
Signed-off-by: Luxing Yin <root@tr0jan.top>
Signed-off-by: Zihan Xi <zihanx@nebusec.ai>
---
changes in v8:
- explain how skb_clone() inherits the depth through the sk_buff headers
group and identify the explicit skb/XDP propagation and reset paths
- split the v7 NFQUEUE/netdev proposal and add the ingress
TTL/hop-limit limit as a separate patch
- preserve the IPv4/IPv6 recursion guard across NFQUEUE reinjection
- bound netdev dup depth across software skb/XDP paths
- stop IPv4 TTL and IPv6 hop-limit underflow before cloning
- limit the netdev claim to software skb/XDP paths and document the
MIRRED offload restriction
- v7 Link:
https://lore.kernel.org/all/cover.1791688819.git.zihanx@nebusec.ai/
changes in v7:
- replace the v6 user-namespace setup restriction with NFQUEUE state
capture and restoration
- use a separate reinjection flag checked only in task context, and scope
the netdev dup marker to its BH-disabled transmit section
- cover queued IPv4/IPv6 duplication and nft_dup_netdev clones; restore the
verdict task's previous state on every exit path
- keep the claim limited to NFQUEUE reinjection, not independent device
receive paths
- add the netdev frontend Fixes tag for its separate NFQUEUE trigger path
- v6 Link:
https://lore.kernel.org/all/82abaa776053bb3e716095a783722452d87fa039.1791429178.git.zihanx@nebusec.ai/
changes in v6:
- reroll the unchanged four-file setup-time restriction on nf.git main at
ab699d296e06
- do not present v5 build or runtime results as v6 validation
- v5 Link:
https://lore.kernel.org/all/20260929125832.25316-1-zihanx@nebusec.ai/
changes in v5:
- describe the patch as a non-initial-userns setup restriction, not a
general recursion fix; state that init_user_ns behavior is unchanged
- remove path-specific trigger details and run-specific metrics from the
public commit and cover prose
- drop d877f07112f1 from Fixes: because it added a later frontend, not the
shared duplication behavior addressed by this restriction
- record the v5 build and setup-time validation, distinguishing them from
earlier unpatched-baseline and root-OOM runs
- v4 Link:
https://lore.kernel.org/all/cover.1790408011.git.zihanx@nebusec.ai/
changes in v4:
- add Fixes tags for TEE, IPv4/IPv6 nft dup, and netdev nft dup
frontends
- clarify that fcd53c51d037 only added a synchronous guard
- clarify that this restricts non-initial user namespaces and does not
fix asynchronous recursion in init_user_ns-owned network namespaces
- state explicitly that the netdev loop was not runtime-reproduced
- v3 Link:
https://lore.kernel.org/all/cover.1790042930.git.zihanx@nebusec.ai/
changes in v3:
- reroll the fix against the latest nf.git main after no follow-up on v2
- add the Co-developed-by trailer and matching Signed-off-by
- v2 Link:
https://lore.kernel.org/all/cover.1788425393.git.zihanx@nebusec.ai/
changes in v2:
- drop the persistent struct sk_buff::nf_duplicated field and nf_copy()
change from v1
- disable IPv4/IPv6 duplication in non-initial user namespaces
- v1 Link:
https://lore.kernel.org/all/cover.1787903722.git.zihanx@nebusec.ai/
---
drivers/net/veth.c | 3 +++
drivers/net/virtio_net.c | 3 +++
include/linux/skbuff.h | 9 +++++++++
include/net/libeth/xdp.h | 6 ++++--
include/net/xdp.h | 25 +++++++++++++++++++++++++
net/core/gro.c | 3 +++
net/core/xdp.c | 10 ++++++++++
net/netfilter/nf_dup_netdev.c | 11 ++++++++++-
net/xdp/xsk_buff_pool.c | 2 ++
9 files changed, 69 insertions(+), 3 deletions(-)
diff --git a/drivers/net/veth.c b/drivers/net/veth.c
index 71227d0389aa..e24a81a14301 100644
--- a/drivers/net/veth.c
+++ b/drivers/net/veth.c
@@ -771,6 +771,9 @@ static int veth_convert_skb_to_xdp_buff(struct veth_rq *rq,
xdp_init_buff(xdp, frame_sz, &rq->xdp_rxq);
xdp_prepare_buff(xdp, skb->head, skb_headroom(skb),
skb_headlen(skb), true);
+#if IS_ENABLED(CONFIG_NF_TABLES)
+ xdp_buff_set_nf_dup_netdev(xdp, skb->nf_dup_netdev);
+#endif
if (skb_shinfo(skb)->nr_frags) {
skb_shinfo(skb)->xdp_frags_size = skb->data_len;
diff --git a/drivers/net/virtio_net.c b/drivers/net/virtio_net.c
index daab43ac92ce..c78bcef1ef11 100644
--- a/drivers/net/virtio_net.c
+++ b/drivers/net/virtio_net.c
@@ -2083,6 +2083,9 @@ static struct sk_buff *build_skb_from_xdp_buff(struct net_device *dev,
data_len = xdp->data_end - xdp->data;
skb_reserve(skb, headroom);
__skb_put(skb, data_len);
+#if IS_ENABLED(CONFIG_NF_TABLES)
+ skb->nf_dup_netdev = xdp_buff_get_nf_dup_netdev(xdp);
+#endif
metasize = xdp->data - xdp->data_meta;
metasize = metasize > 0 ? metasize : 0;
diff --git a/include/linux/skbuff.h b/include/linux/skbuff.h
index a3ff380d43f0..45eb9aeabfe7 100644
--- a/include/linux/skbuff.h
+++ b/include/linux/skbuff.h
@@ -815,6 +815,7 @@ enum skb_tstamp_type {
* @peeked: this packet has been seen already, so stats have been
* done for it, don't do them again
* @nf_trace: netfilter packet trace flag
+ * @nf_dup_netdev: netdev duplication depth
* @protocol: Packet protocol from driver
* @destructor: Destruct function
* @tcp_tsorted_anchor: list structure for TCP (tp->tsorted_sent_queue)
@@ -1012,6 +1013,9 @@ struct sk_buff {
#if IS_ENABLED(CONFIG_NETFILTER_XT_TARGET_TRACE) || IS_ENABLED(CONFIG_NF_TABLES)
__u8 nf_trace:1;
#endif
+#if IS_ENABLED(CONFIG_NF_TABLES)
+ __u8 nf_dup_netdev:2; /* netdev duplication depth */
+#endif
#ifdef CONFIG_NET_SWITCHDEV
__u8 offload_fwd_mark:1;
__u8 offload_l3_fwd_mark:1;
@@ -4602,6 +4606,11 @@ static inline bool skb_metadata_differs(const struct sk_buff *skb_a,
u8 len_a = skb_metadata_len(skb_a);
u8 len_b = skb_metadata_len(skb_b);
+#if IS_ENABLED(CONFIG_NF_TABLES)
+ if (skb_a->nf_dup_netdev != skb_b->nf_dup_netdev)
+ return true;
+#endif
+
if (!(len_a | len_b))
return false;
diff --git a/include/net/libeth/xdp.h b/include/net/libeth/xdp.h
index 898723ab62e8..bccc244c19b9 100644
--- a/include/net/libeth/xdp.h
+++ b/include/net/libeth/xdp.h
@@ -1181,10 +1181,12 @@ libeth_xdp_init_buff(struct libeth_xdp_buff *dst,
const struct libeth_xdp_buff_stash *src,
struct xdp_rxq_info *rxq)
{
- if (likely(!src->data))
+ if (likely(!src->data)) {
dst->data = NULL;
- else
+ dst->base.flags = 0;
+ } else {
libeth_xdp_load_stash(dst, src);
+ }
dst->base.rxq = rxq;
}
diff --git a/include/net/xdp.h b/include/net/xdp.h
index 07231adfb5f8..40ca174bb6b1 100644
--- a/include/net/xdp.h
+++ b/include/net/xdp.h
@@ -81,6 +81,9 @@ enum xdp_buff_flags {
* XDP program is not attached.
*/
XDP_FLAGS_FRAGS_UNREADABLE = BIT(2),
+ /* Netfilter netdev-dup recursion depth, carried through XDP frames. */
+ XDP_FLAGS_NF_DUP_NETDEV_SHIFT = 3,
+ XDP_FLAGS_NF_DUP_NETDEV_MASK = 3U << XDP_FLAGS_NF_DUP_NETDEV_SHIFT,
};
struct xdp_buff {
@@ -106,6 +109,21 @@ struct xdp_buff {
};
};
+static __always_inline u8
+xdp_buff_get_nf_dup_netdev(const struct xdp_buff *xdp)
+{
+ return (xdp->flags & XDP_FLAGS_NF_DUP_NETDEV_MASK) >>
+ XDP_FLAGS_NF_DUP_NETDEV_SHIFT;
+}
+
+static __always_inline void
+xdp_buff_set_nf_dup_netdev(struct xdp_buff *xdp, u8 depth)
+{
+ xdp->flags &= ~XDP_FLAGS_NF_DUP_NETDEV_MASK;
+ xdp->flags |= (depth << XDP_FLAGS_NF_DUP_NETDEV_SHIFT) &
+ XDP_FLAGS_NF_DUP_NETDEV_MASK;
+}
+
static __always_inline void xdp_reinit_buff(struct xdp_buff *xdp)
{
xdp->flags = 0;
@@ -310,6 +328,13 @@ struct xdp_frame {
u32 flags; /* supported values defined in xdp_buff_flags */
};
+static __always_inline u8
+xdp_frame_get_nf_dup_netdev(const struct xdp_frame *frame)
+{
+ return (frame->flags & XDP_FLAGS_NF_DUP_NETDEV_MASK) >>
+ XDP_FLAGS_NF_DUP_NETDEV_SHIFT;
+}
+
static __always_inline bool xdp_frame_has_frags(const struct xdp_frame *frame)
{
return !!(frame->flags & XDP_FLAGS_HAS_FRAGS);
diff --git a/net/core/gro.c b/net/core/gro.c
index 29b4d02bf519..71044dba7cc2 100644
--- a/net/core/gro.c
+++ b/net/core/gro.c
@@ -671,6 +671,9 @@ static void napi_reuse_skb(struct napi_struct *napi, struct sk_buff *skb)
skb->encapsulation = 0;
skb->ip_summed = CHECKSUM_NONE;
+#if IS_ENABLED(CONFIG_NF_TABLES)
+ skb->nf_dup_netdev = 0;
+#endif
shinfo = skb_shinfo(skb);
shinfo->gso_type = 0;
diff --git a/net/core/xdp.c b/net/core/xdp.c
index 1d679e8fd649..e9c66525c955 100644
--- a/net/core/xdp.c
+++ b/net/core/xdp.c
@@ -605,6 +605,7 @@ struct xdp_frame *xdp_convert_zc_to_xdp_frame(struct xdp_buff *xdp)
xdpf->headroom = metasize;
xdpf->metasize = metasize;
xdpf->frame_sz = PAGE_SIZE;
+ xdpf->flags = xdp->flags & XDP_FLAGS_NF_DUP_NETDEV_MASK;
xdpf->mem_type = MEM_TYPE_PAGE_ORDER0;
xsk_buff_free(xdp);
@@ -658,6 +659,9 @@ struct sk_buff *xdp_build_skb_from_buff(const struct xdp_buff *xdp)
skb_mark_for_recycle(skb);
skb_record_rx_queue(skb, rxq->queue_index);
+#if IS_ENABLED(CONFIG_NF_TABLES)
+ skb->nf_dup_netdev = xdp_buff_get_nf_dup_netdev(xdp);
+#endif
if (unlikely(nr_frags)) {
u32 tsize;
@@ -773,6 +777,9 @@ struct sk_buff *xdp_build_skb_from_zc(struct xdp_buff *xdp)
}
skb_record_rx_queue(skb, rxq->queue_index);
+#if IS_ENABLED(CONFIG_NF_TABLES)
+ skb->nf_dup_netdev = xdp_buff_get_nf_dup_netdev(xdp);
+#endif
if (unlikely(xdp_buff_has_frags(xdp)) &&
unlikely(!xdp_copy_frags_from_zc(skb, xdp, pp))) {
@@ -819,6 +826,9 @@ struct sk_buff *__xdp_build_skb_from_frame(struct xdp_frame *xdpf,
skb_reserve(skb, headroom);
__skb_put(skb, xdpf->len);
+#if IS_ENABLED(CONFIG_NF_TABLES)
+ skb->nf_dup_netdev = xdp_frame_get_nf_dup_netdev(xdpf);
+#endif
if (xdpf->metasize)
skb_metadata_set(skb, xdpf->metasize);
diff --git a/net/netfilter/nf_dup_netdev.c b/net/netfilter/nf_dup_netdev.c
index c6bd5c29bed6..0671df72fbfa 100644
--- a/net/netfilter/nf_dup_netdev.c
+++ b/net/netfilter/nf_dup_netdev.c
@@ -58,13 +58,18 @@ void nf_dup_netdev_egress(const struct nft_pktinfo *pkt, int oif)
struct net_device *dev;
struct sk_buff *skb;
+ if (pkt->skb->nf_dup_netdev > NF_RECURSION_LIMIT)
+ return;
+
dev = dev_get_by_index_rcu(nft_net(pkt), oif);
if (dev == NULL)
return;
skb = skb_clone(pkt->skb, GFP_ATOMIC);
- if (skb)
+ if (skb) {
+ skb->nf_dup_netdev = pkt->skb->nf_dup_netdev + 1;
nf_do_netdev_egress(skb, dev, nft_hook(pkt));
+ }
}
EXPORT_SYMBOL_GPL(nf_dup_netdev_egress);
@@ -75,6 +80,10 @@ int nft_fwd_dup_netdev_offload(struct nft_offload_ctx *ctx,
struct flow_action_entry *entry;
struct net_device *dev;
+ /* The netdev duplication limit is carried in the skb. */
+ if (id == FLOW_ACTION_MIRRED)
+ return -EOPNOTSUPP;
+
dev = dev_get_by_index(ctx->net, oif);
if (!dev)
return -EOPNOTSUPP;
diff --git a/net/xdp/xsk_buff_pool.c b/net/xdp/xsk_buff_pool.c
index 9d2d94f1fb75..db46901a7ca6 100644
--- a/net/xdp/xsk_buff_pool.c
+++ b/net/xdp/xsk_buff_pool.c
@@ -633,6 +633,7 @@ static u32 xp_alloc_new_from_fq(struct xsk_buff_pool *pool, struct xdp_buff **xd
}
xskb = xp_get_xskb(pool, addr);
+ xskb->xdp.flags = 0;
*xdp = &xskb->xdp;
xdp++;
@@ -653,6 +654,7 @@ static u32 xp_alloc_reused(struct xsk_buff_pool *pool, struct xdp_buff **xdp, u3
while (i--) {
xskb = list_first_entry(&pool->free_list, struct xdp_buff_xsk, list_node);
list_del_init(&xskb->list_node);
+ xskb->xdp.flags = 0;
*xdp = &xskb->xdp;
xdp++;
--
2.43.0
^ permalink raw reply related [flat|nested] 11+ messages in thread* Re: [PATCH nf v8 2/3] netfilter: nf_dup_netdev: bound asynchronous duplication
2026-10-15 7:43 ` [PATCH nf v8 2/3] netfilter: nf_dup_netdev: bound asynchronous duplication Zihan Xi
@ 2026-10-15 7:58 ` sashiko-bot
0 siblings, 0 replies; 11+ messages in thread
From: sashiko-bot @ 2026-10-15 7:58 UTC (permalink / raw)
To: Zihan Xi, virtualization; +Cc: Michael S. Tsirkin, Eugenio Perez
> The task-local transmit recursion counter covers synchronous
> dev_queue_xmit() recursion, but does not follow a duplicated skb through
> asynchronous receive paths. Carry a bounded duplication depth with the skb
> and preserve it across skb and XDP conversions.
>
> Store the depth in the sk_buff headers group. __copy_skb_header() copies
> that group with a single memcpy(), so skb_clone() preserves the depth.
> Propagate it explicitly when veth converts an skb to an XDP buffer and
> when XDP buffers or frames are converted back to skbs in the core and
> virtio paths. Preserve it through zero-copy XDP frame conversion as well.
>
> Clear the depth when GRO reuses an skb and when AF_XDP or libeth
> reinitializes a recycled XDP buffer, so a fresh packet does not inherit
> stale recursion state.
>
> Keep the existing recursion limit and reject MIRRED flow offload where
> the software depth cannot be enforced.
>
> Fixes: 502061f81d3e ("netfilter: nf_tables: add packet duplication to the netdev family")
> Cc: stable@vger.kernel.org
> Reported-by: Vega <vega@nebusec.ai>
> Assisted-by: LLM
> Co-developed-by: Luxing Yin <root@tr0jan.top>
> Signed-off-by: Luxing Yin <root@tr0jan.top>
> Signed-off-by: Zihan Xi <zihanx@nebusec.ai>
Sashiko has reviewed this patch and found no issues. It looks great!
--
Sashiko AI review · https://sashiko.dev/#/patchset/cover.1792048855.git.zihanx@nebusec.ai?part=2
^ permalink raw reply [flat|nested] 11+ messages in thread
* [PATCH nf v8 3/3] netfilter: nf_dup: prevent TTL and hop-limit underflow
2026-10-15 7:43 [PATCH nf v8 0/3] netfilter: bound asynchronous packet duplication Zihan Xi
2026-10-15 7:43 ` [PATCH nf v8 1/3] netfilter: nf_dup: preserve guard across NFQUEUE reinjection Zihan Xi
2026-10-15 7:43 ` [PATCH nf v8 2/3] netfilter: nf_dup_netdev: bound asynchronous duplication Zihan Xi
@ 2026-10-15 7:43 ` Zihan Xi
2026-10-15 7:51 ` sashiko-bot
2026-10-15 7:49 ` [PATCH nf v8 0/3] netfilter: bound asynchronous packet duplication netdev-bot+sinfo
2026-10-15 10:39 ` Florian Westphal
4 siblings, 1 reply; 11+ messages in thread
From: Zihan Xi @ 2026-10-15 7:43 UTC (permalink / raw)
To: netfilter-devel, virtualization, intel-wired-lan, bpf
Cc: pablo, fw, phil, davem, edumazet, pabeni, horms, kaber, jengelh,
andrew+netdev, mst, jasowangio, eperezma, xuanzhuo,
aleksander.lobakin, ast, daniel, hawk, john.fastabend, sdf,
magnus.karlsson, maciej.fijalkowski, willemb, alice.kernel,
eilaimemedsnaimel, imv4bel, shiming.cheng, sd, vega, root, zihanx
The IPv4 and IPv6 duplication helpers decrement TTL or hop limit for
copies processed at PRE_ROUTING or LOCAL_IN. Stop duplicating when the
value is at or below one, before a subsequent decrement can wrap.
Fixes: e281b19897dc ("netfilter: xtables: inclusion of xt_TEE")
Cc: stable@vger.kernel.org
Reported-by: Vega <vega@nebusec.ai>
Assisted-by: LLM
Co-developed-by: Luxing Yin <root@tr0jan.top>
Signed-off-by: Luxing Yin <root@tr0jan.top>
Signed-off-by: Zihan Xi <zihanx@nebusec.ai>
---
net/ipv4/netfilter/nf_dup_ipv4.c | 3 +++
net/ipv6/netfilter/nf_dup_ipv6.c | 3 +++
2 files changed, 6 insertions(+)
diff --git a/net/ipv4/netfilter/nf_dup_ipv4.c b/net/ipv4/netfilter/nf_dup_ipv4.c
index 9a773502f10a..5f9d6c473fb0 100644
--- a/net/ipv4/netfilter/nf_dup_ipv4.c
+++ b/net/ipv4/netfilter/nf_dup_ipv4.c
@@ -56,6 +56,9 @@ void nf_dup_ipv4(struct net *net, struct sk_buff *skb, unsigned int hooknum,
local_bh_disable();
if (current->in_nf_duplicate)
goto out;
+ if ((hooknum == NF_INET_PRE_ROUTING || hooknum == NF_INET_LOCAL_IN) &&
+ ip_hdr(skb)->ttl <= 1)
+ goto out;
/*
* Copy the skb, and route the copy. Will later return %XT_CONTINUE for
* the original skb, which should continue on its way as if nothing has
diff --git a/net/ipv6/netfilter/nf_dup_ipv6.c b/net/ipv6/netfilter/nf_dup_ipv6.c
index 6da3102b7c1b..5095f9623001 100644
--- a/net/ipv6/netfilter/nf_dup_ipv6.c
+++ b/net/ipv6/netfilter/nf_dup_ipv6.c
@@ -50,6 +50,9 @@ void nf_dup_ipv6(struct net *net, struct sk_buff *skb, unsigned int hooknum,
local_bh_disable();
if (current->in_nf_duplicate)
goto out;
+ if ((hooknum == NF_INET_PRE_ROUTING || hooknum == NF_INET_LOCAL_IN) &&
+ ipv6_hdr(skb)->hop_limit <= 1)
+ goto out;
skb = pskb_copy(skb, GFP_ATOMIC);
if (skb == NULL)
goto out;
--
2.43.0
^ permalink raw reply related [flat|nested] 11+ messages in thread* Re: [PATCH nf v8 0/3] netfilter: bound asynchronous packet duplication
2026-10-15 7:43 [PATCH nf v8 0/3] netfilter: bound asynchronous packet duplication Zihan Xi
` (2 preceding siblings ...)
2026-10-15 7:43 ` [PATCH nf v8 3/3] netfilter: nf_dup: prevent TTL and hop-limit underflow Zihan Xi
@ 2026-10-15 7:49 ` netdev-bot+sinfo
2026-10-15 10:39 ` Florian Westphal
4 siblings, 0 replies; 11+ messages in thread
From: netdev-bot+sinfo @ 2026-10-15 7:49 UTC (permalink / raw)
To: Zihan Xi
Cc: netfilter-devel, virtualization, intel-wired-lan, bpf, pablo, fw,
phil, davem, edumazet, pabeni, horms, kaber, jengelh,
andrew+netdev, mst, jasowangio, eperezma, xuanzhuo,
aleksander.lobakin, ast, daniel, hawk, john.fastabend, sdf,
magnus.karlsson, maciej.fijalkowski, willemb, alice.kernel,
eilaimemedsnaimel, imv4bel, shiming.cheng, sd, vega, root
Hi!
This is an automated message. This series looks like a fix, but its
commit messages seem to be missing some information:
- How the issue was discovered, e.g. hit in production, hit during
development, syzbot report, manual code inspection, LLM or static
analysis tool scan.
Please do not repost the series just to address the above. Instead,
reply to this email with the missing information, so that reviewers
can take it into account. If the series needs another revision for
other reasons, please include the information in the commit messages
then.
The evaluation is done by an LLM so it may be wrong, if you think
that is the case please reply and explain.
^ permalink raw reply [flat|nested] 11+ messages in thread* Re: [PATCH nf v8 0/3] netfilter: bound asynchronous packet duplication
2026-10-15 7:43 [PATCH nf v8 0/3] netfilter: bound asynchronous packet duplication Zihan Xi
` (3 preceding siblings ...)
2026-10-15 7:49 ` [PATCH nf v8 0/3] netfilter: bound asynchronous packet duplication netdev-bot+sinfo
@ 2026-10-15 10:39 ` Florian Westphal
2026-10-15 11:33 ` zihan xi
4 siblings, 1 reply; 11+ messages in thread
From: Florian Westphal @ 2026-10-15 10:39 UTC (permalink / raw)
To: Zihan Xi
Cc: netfilter-devel, virtualization, intel-wired-lan, bpf, pablo,
phil, davem, edumazet, pabeni, horms, kaber, jengelh,
andrew+netdev, mst, jasowangio, eperezma, xuanzhuo,
aleksander.lobakin, ast, daniel, hawk, john.fastabend, sdf,
magnus.karlsson, maciej.fijalkowski, willemb, alice.kernel,
eilaimemedsnaimel, imv4bel, shiming.cheng, sd, vega, root
Zihan Xi <zihanx@nebusec.ai> wrote:
> Hi Linux kernel maintainers,
>
> We found and validated an issue in netfilter packet duplication. The bug
> is reachable by an unprivileged user in user and network namespaces.
> Bounded tests exercised the fixes. Non-duplication packet semantics are
> intended to remain unchanged. MIRRED flow offload for affected netdev dup
> rules is rejected because it cannot enforce the duplication-depth limit.
>
> We will provide detailed information about the bug
> in this email, along with a PoC to trigger it.
Series makes no sense to me, sorry. I dislike the 2nd patch, I see no
reason why we need to make changes all over the place for this...?
Also, if we go with 2nd patch, why do we still need the first?
> The IPv4 and IPv6 duplication helpers use a task-local guard while a cloned
> packet traverses output hooks. If NFQUEUE suspends that traversal, the
> output call can return and clear the guard before a later verdict resumes
> the remaining hooks. The resumed packet can reach another duplication
> consumer without the original guard. This can cause repeated packet
> processing and consume CPU and network resources.
"Don't do that, then"?
I think the kernel should not crash when misused, but I don't think
kernel should have to do extreme stunts to detect such loops.
There are probably many other ways to make kernel allocate lots of
memory or waste cpu cycles.
E.g. via create use of veth and bridges, sched plug, teql, ...
> Patch 1 records the
> guard in the queue entry and restores it during reinjection. It keeps
> bottom halves disabled during the reinjection traversal, restoring the
> verdict task state before reenabling them.
Its small enough, so I have no objections to this patch.
> The netdev nft dup path has a separate synchronous transmit recursion
> counter. It does not accompany a duplicated skb through asynchronous
> software receive paths. Patch 2 carries a bounded duplication depth through
> skb and supported XDP conversions, resets it on reused buffers, and rejects
> MIRRED flow offload where the software depth cannot be enforced. This scope
> covers software skb/XDP continuity, not packet loops through physical
> hardware or userspace device round trips.
I think it would be better to first go to the drawing board and make ONE
facility that governs the recursion part.
From a quick glance, the difference is historical from time when
per-module pcpu counters were used, so nf_dup netdev could not see the state
of nft_dup toggle. Now that its in task struct anyway, maybe its time to
just zap the egress recursion counter and first make a patch that
uses the bool toggle for both egress and normal dup?
I would like to remove code, not add more.
> At PRE_ROUTING and LOCAL_IN, the IPv4 and IPv6 helpers decrement TTL or
> hop limit on the copy. Patch 3 stops creating copies when the value is
> <= 1, so the decrement cannot reach zero or wrap a zero value to the
> maximum.
Thats unrelated problem. There are other places that do TTL/HL
decrements. I think it makes sense to clean this up in -next and
add common helpers and then use those.
^ permalink raw reply [flat|nested] 11+ messages in thread* Re: [PATCH nf v8 0/3] netfilter: bound asynchronous packet duplication
2026-10-15 10:39 ` Florian Westphal
@ 2026-10-15 11:33 ` zihan xi
0 siblings, 0 replies; 11+ messages in thread
From: zihan xi @ 2026-10-15 11:33 UTC (permalink / raw)
To: Florian Westphal
Cc: netfilter-devel, virtualization, intel-wired-lan, bpf, pablo,
phil, davem, edumazet, pabeni, horms, kaber, jengelh,
andrew+netdev, mst, jasowangio, eperezma, xuanzhuo,
aleksander.lobakin, ast, daniel, hawk, john.fastabend, sdf,
magnus.karlsson, maciej.fijalkowski, willemb, alice.kernel,
eilaimemedsnaimel, imv4bel, shiming.cheng, sd, vega, root
On Thu, Oct 15, 2026 at 6:39 PM Florian Westphal <fw@strlen.de> wrote:
>
> Zihan Xi <zihanx@nebusec.ai> wrote:
> > Hi Linux kernel maintainers,
> >
> > We found and validated an issue in netfilter packet duplication. The bug
> > is reachable by an unprivileged user in user and network namespaces.
> > Bounded tests exercised the fixes. Non-duplication packet semantics are
> > intended to remain unchanged. MIRRED flow offload for affected netdev dup
> > rules is rejected because it cannot enforce the duplication-depth limit.
> >
> > We will provide detailed information about the bug
> > in this email, along with a PoC to trigger it.
>
> Series makes no sense to me, sorry. I dislike the 2nd patch, I see no
> reason why we need to make changes all over the place for this...?
>
> Also, if we go with 2nd patch, why do we still need the first?
>
> > The IPv4 and IPv6 duplication helpers use a task-local guard while a cloned
> > packet traverses output hooks. If NFQUEUE suspends that traversal, the
> > output call can return and clear the guard before a later verdict resumes
> > the remaining hooks. The resumed packet can reach another duplication
> > consumer without the original guard. This can cause repeated packet
> > processing and consume CPU and network resources.
>
> "Don't do that, then"?
>
> I think the kernel should not crash when misused, but I don't think
> kernel should have to do extreme stunts to detect such loops.
>
> There are probably many other ways to make kernel allocate lots of
> memory or waste cpu cycles.
>
> E.g. via create use of veth and bridges, sched plug, teql, ...
>
> > Patch 1 records the
> > guard in the queue entry and restores it during reinjection. It keeps
> > bottom halves disabled during the reinjection traversal, restoring the
> > verdict task state before reenabling them.
>
> Its small enough, so I have no objections to this patch.
>
> > The netdev nft dup path has a separate synchronous transmit recursion
> > counter. It does not accompany a duplicated skb through asynchronous
> > software receive paths. Patch 2 carries a bounded duplication depth through
> > skb and supported XDP conversions, resets it on reused buffers, and rejects
> > MIRRED flow offload where the software depth cannot be enforced. This scope
> > covers software skb/XDP continuity, not packet loops through physical
> > hardware or userspace device round trips.
>
> I think it would be better to first go to the drawing board and make ONE
> facility that governs the recursion part.
>
> From a quick glance, the difference is historical from time when
> per-module pcpu counters were used, so nf_dup netdev could not see the state
> of nft_dup toggle. Now that its in task struct anyway, maybe its time to
> just zap the egress recursion counter and first make a patch that
> uses the bool toggle for both egress and normal dup?
>
> I would like to remove code, not add more.
>
> > At PRE_ROUTING and LOCAL_IN, the IPv4 and IPv6 helpers decrement TTL or
> > hop limit on the copy. Patch 3 stops creating copies when the value is
> > <= 1, so the decrement cannot reach zero or wrap a zero value to the
> > maximum.
>
> Thats unrelated problem. There are other places that do TTL/HL
> decrements. I think it makes sense to clean this up in -next and
> add common helpers and then use those.
Hi Florian,
Thanks for taking the time to review this series. Your comments make
clear that I have not yet presented a sufficiently simple, coherent
design, particularly why both patches 1 and 2 are needed.
I will go back to the recursion-guard design and first evaluate
whether the existing current->in_nf_duplicate flag can be used by both
the IPv4/IPv6 and netdev duplication paths, replacing the separate
egress recursion counter. I will check the effects on the counter’s
other users and verify the relevant egress, bottom-half, and
asynchronous receive paths before deciding whether a task-local flag
is sufficient. I will not assume that it covers asynchronous paths
without evidence.
I understand that you have no objection to patch 1 on its own. I will
nevertheless check whether the unified design makes its NFQUEUE
queue-entry state redundant; if not, I will keep that change limited
to the NFQUEUE state-lifetimeissue.
I agree that the TTL/hop-limit change is separate from this series. I
will leave it out and consider it independently alongside the
common-helper work in -next.
I also take your point about scope: the goal should be a simple,
coherent recursion guard for this duplication behavior, not a general
mechanism for detecting every possible source of CPU or memory
consumption. I will return with the path analysis and a smaller design
before sending another series.
Best regards,
Zihan
^ permalink raw reply [flat|nested] 11+ messages in thread