* [PATCH nf v8 0/3] netfilter: bound asynchronous packet duplication
@ 2026-10-15 7:43 Zihan Xi
2026-10-15 7:43 ` [PATCH nf v8 1/3] netfilter: nf_dup: preserve guard across NFQUEUE reinjection Zihan Xi
` (4 more replies)
0 siblings, 5 replies; 9+ 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
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.
---- details below ----
Bug details:
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. 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.
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.
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. The three patches address the NFQUEUE reinjection, software
netdev receive, and ingress TTL/hop-limit paths, respectively. The patch
does not intend to change packet semantics for unrelated traffic; it also
rejects affected MIRRED flow offload because it cannot enforce the
netdev duplication-depth limit.
Validation:
The v8 series was built on nf.git main at
ab699d296e066eb64f51d2a8a1d42237a3b186c8 and booted in a 2-vCPU,
2-GiB QEMU guest as 7.3.0-rc5-g9a13bdbfd108. In bounded user/network
namespace runs, the IPv4 and IPv6 nft dup/NFQUEUE tests each completed
three callbacks with three successful verdicts and no verdict failures.
A separate IPv4 run sent three UDP datagrams and completed nine callbacks
with nine successful verdicts; the nft counters showed nine visits at
each configured dup rule. These are bounded validation runs.
For netdev dup, a native veth/XDP_TX-to-XDP_PASS test reached the
first four depth-coded rules once each and did not reach the next two.
The XDP_PASS control test without the transmit loop reached its single
dup rule four times. The IPv4 TTL test observed eight matching packets
and no TTL-0 or TTL-255 packets. The filtered IPv6 test observed eight
matching packets and no hop-limit-0 packets; its one hop-limit-255 count
was unrelated NDP traffic and is not evidence for the dup path. An IPv6
xt_TEE/NFQUEUE test ran in a non-initial user/network namespace: one UDP
datagram produced two callbacks, both accepted successfully. The guest
test setup used ip6tables-legacy and pre-created its xtables lock file
with mode 0666 so the unprivileged test process could open the lock.
An unpatched-baseline bounded run by an unprivileged user in user and
network namespaces reached 1,018,782 NFQUEUE callbacks in 61.643
seconds. The callback counter is incremented before nfq_set_verdict(),
so this is a callback count, not a successful-verdict count. This
observed repeated packet processing and resource consumption.
Reproducer:
gcc -O2 -o nfq_accept poc.c -lnetfilter_queue
unshare -Urn /bin/bash
The NFQUEUE trigger requires a userspace verdict process, while the netdev
test installs XDP programs and exercises veth skb/XDP conversion. Those
setup and verdict interactions are not expressible as a standalone
packetdrill exchange. The reproducer below covers the NFQUEUE path; the
separate bounded TTL/hop-limit and netdev/XDP validation results are
summarized above.
The following commands ran as an unprivileged user in a new user/network
namespace. First create a veth pair and assign 198.18.0.1/24 to v0 and
198.18.0.2/24 to v1, then bring both links up. Install these output hooks:
nft add table ip t
nft 'add chain ip t dup1 { type filter hook output priority raw; policy accept; }'
nft 'add chain ip t queue1 { type filter hook output priority mangle; policy accept; }'
nft 'add chain ip t dup2 { type filter hook output priority filter; policy accept; }'
nft add rule ip t dup1 udp dport 5555 counter dup to 198.18.0.2 device v0
nft add rule ip t queue1 udp dport 5555 queue num 0
nft add rule ip t dup2 udp dport 5555 counter dup to 198.18.0.2 device v0
Start the verdict process in the background, send three UDP datagrams to
198.18.0.2 port 5555, then wait for the helper to exit:
./nfq_accept 1 4 &
q=$!
python3 -c 'import socket; s=socket.socket(socket.AF_INET,socket.SOCK_DGRAM); [s.sendto(b"x",("198.18.0.2",5555)) for _ in range(3)]'
wait "$q"
The included helper returns NF_ACCEPT for each queued packet and reports
callback and verdict counts.
For the IPv6 xt_TEE test, the same helper was run in a non-initial
user/network namespace after creating a veth pair with fd00::1/64 on v0
and fd00::2/64 on v1. The guest test fixture pre-created
/run/xtables.lock with mode 0666 as guest root:
touch /run/xtables.lock
chmod 0666 /run/xtables.lock
The rules and one-packet trigger were:
ip6tables-legacy -t mangle -A OUTPUT -p udp -m udp --dport 5556 -j TEE --gateway fd00::2
ip6tables-legacy -t mangle -A OUTPUT -p udp -m udp --dport 5556 -j NFQUEUE --queue-num 0
./nfq_accept 1 6 &
q=$!
python3 -c 'import socket; s=socket.socket(socket.AF_INET6,socket.SOCK_DGRAM); s.sendto(b"x",("fd00::2",5556))'
wait "$q"
We run the PoC in a 2 vCPU, 2 GB RAM x86 QEMU environment.
------BEGIN poc.c------
#include <arpa/inet.h>
#include <errno.h>
#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
#include <unistd.h>
#define NF_ACCEPT 1
#define NF_DROP 0
#define NF_REPEAT 4
#define NF_STOP 5
#include <libnetfilter_queue/libnetfilter_queue.h>
static volatile sig_atomic_t stop;
static unsigned long callbacks, verdict_ok, verdict_fail, repeats, drops;
static const char *mode = "accept";
static struct nfq_q_handle *queue;
static void alarm_handler(int sig) { (void)sig; stop = 1; }
static int cb(struct nfq_q_handle *qh, struct nfgenmsg *nfmsg,
struct nfq_data *nfa, void *data)
{
(void)nfmsg; (void)data;
struct nfqnl_msg_packet_hdr *ph = nfq_get_msg_packet_hdr(nfa);
if (!ph) return 0;
uint32_t id = ntohl(ph->packet_id);
callbacks++;
if (!strcmp(mode, "slow")) usleep(100000);
uint32_t verdict = NF_ACCEPT;
if (!strcmp(mode, "repeat-first") && callbacks == 1) { verdict = NF_REPEAT; repeats++; }
else if (!strcmp(mode, "drop")) { verdict = NF_DROP; drops++; }
else if (!strcmp(mode, "stop")) verdict = NF_STOP;
int rv = nfq_set_verdict(qh, id, verdict, 0, NULL);
if (rv >= 0) verdict_ok++;
else verdict_fail++;
return 0;
}
int main(int argc, char **argv)
{
int seconds = argc > 1 ? atoi(argv[1]) : 3;
signal(SIGALRM, alarm_handler); alarm(seconds);
int family = argc > 2 && atoi(argv[2]) == 6 ? AF_INET6 : AF_INET;
if (argc > 2 && atoi(argv[2]) == 7) family = 7;
if (argc > 3) mode = argv[3];
struct nfq_handle *h = nfq_open();
if (!h) { perror("nfq_open"); return 1; }
if (nfq_unbind_pf(h, family) < 0) perror("nfq_unbind_pf");
if (nfq_bind_pf(h, family) < 0) { perror("nfq_bind_pf"); return 2; }
queue = nfq_create_queue(h, 0, cb, NULL);
if (!queue) { perror("nfq_create_queue"); return 3; }
if (!strcmp(mode, "slow") && nfq_set_queue_maxlen(queue, 1) < 0) { perror("nfq_set_queue_maxlen"); return 4; }
if (nfq_set_mode(queue, NFQNL_COPY_PACKET, 0xffff) < 0) { perror("nfq_set_mode"); return 4; }
int fd = nfq_fd(h); char buf[65536];
while (!stop) {
int n = recv(fd, buf, sizeof(buf), MSG_DONTWAIT);
if (n >= 0) { nfq_handle_packet(h, buf, n); continue; }
if (errno == EAGAIN || errno == EWOULDBLOCK) { usleep(1000); continue; }
if (errno == EINTR) continue;
perror("recv"); break;
}
printf("callbacks=%lu verdict_ok=%lu verdict_fail=%lu repeat=%lu drop=%lu", callbacks, verdict_ok, verdict_fail, repeats, drops);
nfq_destroy_queue(queue); nfq_close(h);
return verdict_fail ? 5 : 0;
}
------END poc.c--------
Changes in v8:
- split the v7 NFQUEUE/netdev proposal into focused patches and add a
third patch for the ingress TTL/hop-limit limit
- 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/
Best regards,
Zihan Xi
Zihan Xi (3):
netfilter: nf_dup: preserve guard across NFQUEUE reinjection
netfilter: nf_dup_netdev: bound asynchronous duplication
netfilter: nf_dup: prevent TTL and hop-limit underflow
drivers/net/veth.c | 3 +++
drivers/net/virtio_net.c | 3 +++
include/linux/skbuff.h | 9 +++++++++
include/net/libeth/xdp.h | 6 ++++--
include/net/netfilter/nf_queue.h | 1 +
include/net/xdp.h | 25 +++++++++++++++++++++++++
net/core/gro.c | 3 +++
net/core/xdp.c | 10 ++++++++++
net/ipv4/netfilter/nf_dup_ipv4.c | 3 +++
net/ipv6/netfilter/nf_dup_ipv6.c | 3 +++
net/netfilter/nf_dup_netdev.c | 11 ++++++++++-
net/netfilter/nf_queue.c | 1 +
net/netfilter/nfnetlink_queue.c | 15 ++++++++++++---
net/xdp/xsk_buff_pool.c | 2 ++
14 files changed, 89 insertions(+), 6 deletions(-)
--
2.43.0
^ permalink raw reply [flat|nested] 9+ messages in thread
* [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; 9+ 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] 9+ 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:43 ` [PATCH nf v8 3/3] netfilter: nf_dup: prevent TTL and hop-limit underflow Zihan Xi
` (2 subsequent siblings)
4 siblings, 0 replies; 9+ 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] 9+ 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:49 ` [PATCH nf v8 0/3] netfilter: bound asynchronous packet duplication netdev-bot+sinfo
2026-10-15 10:39 ` Florian Westphal
4 siblings, 0 replies; 9+ 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] 9+ 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; 9+ 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] 9+ 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; 9+ 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] 9+ 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; 9+ 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] 9+ 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; 9+ 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] 9+ 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; 9+ 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] 9+ messages in thread
end of thread, other threads:[~2026-10-15 15:55 UTC | newest]
Thread overview: 9+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
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:56 ` sashiko-bot
2026-10-15 8:13 ` 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 ` [PATCH nf v8 3/3] netfilter: nf_dup: prevent TTL and hop-limit underflow Zihan Xi
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
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox