[PATCH nf v2 1/1] netfilter: x_tables: avoid holding mutex over faultable user copies

From: Zihan Xi

Date: Wed Sep 09 2026 - 12:00:10 EST


The legacy IPv4, IPv6 and ARP table GET_INFO and GET_ENTRIES paths hold
the per-family xtables mutexes while copying table data to userspace. A
faultable destination can therefore sleep indefinitely with the mutex held,
blocking unrelated table and registry operations.

Disable page faults during the locked copy, release the lock, fault in the
output range, and retry once. Move GET_INFO's fixed-size copy outside the
table locks and apply the same retry handling to the remaining IPv4/IPv6
compat GET_ENTRIES paths.

Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2")
Cc: stable@xxxxxxxxxxxxxxx
Reported-by: Vega <vega@xxxxxxxxxx>
Assisted-by: LLM
Signed-off-by: Zihan Xi <zihanx@xxxxxxxxxx>
---
changes in v2:
- Rebase onto current nf.git after 0bd7ed1a3263c ("netfilter:
arp_tables: remove the 32bit compat interface"). ARP GET_INFO
and GET_ENTRIES are updated on the native paths only. IPv4 and
IPv6 still include the compat GET_ENTRIES retry.
- Drop hung_task_panic and the 10-second hung_task timeout from the
reproducer, as pointed out by Pablo Neira Ayuso. Observe the stall
through holder/waiter wchan instead.
- v1 Link: https://lore.kernel.org/all/cover.1788244146.git.zihanx@xxxxxxxxxx/

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

diff --git a/net/ipv4/netfilter/arp_tables.c b/net/ipv4/netfilter/arp_tables.c
index db307fa49f3f6..7b43269aa6b21 100644
--- a/net/ipv4/netfilter/arp_tables.c
+++ b/net/ipv4/netfilter/arp_tables.c
@@ -23,6 +23,7 @@
#include <linux/init.h>
#include <linux/mutex.h>
#include <linux/err.h>
+#include <linux/pagemap.h>
#include <net/sock.h>
#include <linux/uaccess.h>

@@ -695,6 +696,7 @@ static int copy_entries_to_user(unsigned int total_size,

loc_cpu_entry = private->entries;

+ pagefault_disable();
/* FIXME: use iterator macros --RR */
/* ... then go back and fix counters and names */
for (off = 0, num = 0; off < total_size; off += e->next_offset, num++){
@@ -719,12 +721,14 @@ static int copy_entries_to_user(unsigned int total_size,
}

free_counters:
+ pagefault_enable();
vfree(counters);
return ret;
}

static int get_info(struct net *net, void __user *user, const int *len)
{
+ struct arpt_getinfo info;
char name[XT_TABLE_MAXNAMELEN];
struct xt_table *t;
int ret;
@@ -738,7 +742,6 @@ static int get_info(struct net *net, void __user *user, const int *len)
name[XT_TABLE_MAXNAMELEN-1] = '\0';
t = xt_request_find_table_lock(net, NFPROTO_ARP, name);
if (!IS_ERR(t)) {
- struct arpt_getinfo info;
const struct xt_table_info *private = t->private;

memset(&info, 0, sizeof(info));
@@ -751,15 +754,14 @@ static int get_info(struct net *net, void __user *user, const int *len)
info.size = private->size;
strscpy(info.name, name);

- if (copy_to_user(user, &info, *len) != 0)
- ret = -EFAULT;
- else
- ret = 0;
+ ret = 0;
xt_table_unlock(t);
module_put(t->me);
} else
ret = PTR_ERR(t);

+ if (!ret && copy_to_user(user, &info, *len) != 0)
+ ret = -EFAULT;
return ret;
}

@@ -769,6 +771,7 @@ static int get_entries(struct net *net, struct arpt_get_entries __user *uptr,
int ret;
struct arpt_get_entries get;
struct xt_table *t;
+ bool faulted = false;

if (*len < sizeof(get))
return -EINVAL;
@@ -779,6 +782,7 @@ static int get_entries(struct net *net, struct arpt_get_entries __user *uptr,

get.name[sizeof(get.name) - 1] = '\0';

+ retry:
t = xt_find_table_lock(net, NFPROTO_ARP, get.name);
if (!IS_ERR(t)) {
const struct xt_table_info *private = t->private;
@@ -794,6 +798,14 @@ static int get_entries(struct net *net, struct arpt_get_entries __user *uptr,
} else
ret = PTR_ERR(t);

+ if (ret == -EFAULT && !faulted) {
+ faulted = true;
+ if (fault_in_writeable((char __user *)uptr->entrytable,
+ get.size))
+ return -EFAULT;
+ goto retry;
+ }
+
return ret;
}

diff --git a/net/ipv4/netfilter/ip_tables.c b/net/ipv4/netfilter/ip_tables.c
index 809441cedcedc..e029072b07501 100644
--- a/net/ipv4/netfilter/ip_tables.c
+++ b/net/ipv4/netfilter/ip_tables.c
@@ -21,6 +21,7 @@
#include <linux/proc_fs.h>
#include <linux/err.h>
#include <linux/cpumask.h>
+#include <linux/pagemap.h>

#include <linux/netfilter/x_tables.h>
#include <linux/netfilter_ipv4/ip_tables.h>
@@ -824,6 +825,7 @@ copy_entries_to_user(unsigned int total_size,

loc_cpu_entry = private->entries;

+ pagefault_disable();
/* FIXME: use iterator macros --RR */
/* ... then go back and fix counters and names */
for (off = 0, num = 0; off < total_size; off += e->next_offset, num++){
@@ -861,6 +863,7 @@ copy_entries_to_user(unsigned int total_size,
}

free_counters:
+ pagefault_enable();
vfree(counters);
return ret;
}
@@ -943,6 +946,7 @@ static int compat_table_info(const struct xt_table_info *info,

static int get_info(struct net *net, void __user *user, const int *len)
{
+ struct ipt_getinfo info;
char name[XT_TABLE_MAXNAMELEN];
struct xt_table *t;
int ret;
@@ -960,7 +964,6 @@ static int get_info(struct net *net, void __user *user, const int *len)
#endif
t = xt_request_find_table_lock(net, AF_INET, name);
if (!IS_ERR(t)) {
- struct ipt_getinfo info;
const struct xt_table_info *private = t->private;
#ifdef CONFIG_NETFILTER_XTABLES_COMPAT
struct xt_table_info tmp;
@@ -981,10 +984,7 @@ static int get_info(struct net *net, void __user *user, const int *len)
info.size = private->size;
strscpy(info.name, name);

- if (copy_to_user(user, &info, *len) != 0)
- ret = -EFAULT;
- else
- ret = 0;
+ ret = 0;

xt_table_unlock(t);
module_put(t->me);
@@ -994,6 +994,8 @@ static int get_info(struct net *net, void __user *user, const int *len)
if (in_compat_syscall())
xt_compat_unlock(AF_INET);
#endif
+ if (!ret && copy_to_user(user, &info, *len) != 0)
+ ret = -EFAULT;
return ret;
}

@@ -1004,6 +1006,7 @@ get_entries(struct net *net, struct ipt_get_entries __user *uptr,
int ret;
struct ipt_get_entries get;
struct xt_table *t;
+ bool faulted = false;

if (*len < sizeof(get))
return -EINVAL;
@@ -1013,6 +1016,7 @@ get_entries(struct net *net, struct ipt_get_entries __user *uptr,
return -EINVAL;
get.name[sizeof(get.name) - 1] = '\0';

+ retry:
t = xt_find_table_lock(net, AF_INET, get.name);
if (!IS_ERR(t)) {
const struct xt_table_info *private = t->private;
@@ -1027,6 +1031,14 @@ get_entries(struct net *net, struct ipt_get_entries __user *uptr,
} else
ret = PTR_ERR(t);

+ if (ret == -EFAULT && !faulted) {
+ faulted = true;
+ if (fault_in_writeable((char __user *)uptr->entrytable,
+ get.size))
+ return -EFAULT;
+ goto retry;
+ }
+
return ret;
}

@@ -1561,12 +1573,14 @@ compat_copy_entries_to_user(unsigned int total_size, struct xt_table *table,

pos = userptr;
size = total_size;
+ pagefault_disable();
xt_entry_foreach(iter, private->entries, total_size) {
ret = compat_copy_entry_to_user(iter, &pos,
&size, counters, i++);
if (ret != 0)
break;
}
+ pagefault_enable();

vfree(counters);
return ret;
@@ -1579,6 +1593,7 @@ compat_get_entries(struct net *net, struct compat_ipt_get_entries __user *uptr,
int ret;
struct compat_ipt_get_entries get;
struct xt_table *t;
+ bool faulted = false;

if (*len < sizeof(get))
return -EINVAL;
@@ -1591,6 +1606,7 @@ compat_get_entries(struct net *net, struct compat_ipt_get_entries __user *uptr,

get.name[sizeof(get.name) - 1] = '\0';

+ retry:
xt_compat_lock(AF_INET);
t = xt_find_table_lock(net, AF_INET, get.name);
if (!IS_ERR(t)) {
@@ -1610,6 +1626,13 @@ compat_get_entries(struct net *net, struct compat_ipt_get_entries __user *uptr,
ret = PTR_ERR(t);

xt_compat_unlock(AF_INET);
+ if (ret == -EFAULT && !faulted) {
+ faulted = true;
+ if (fault_in_writeable((char __user *)uptr->entrytable,
+ get.size))
+ return -EFAULT;
+ goto retry;
+ }
return ret;
}
#endif
diff --git a/net/ipv6/netfilter/ip6_tables.c b/net/ipv6/netfilter/ip6_tables.c
index 313c4aac377aa..a64fcd3a9948c 100644
--- a/net/ipv6/netfilter/ip6_tables.c
+++ b/net/ipv6/netfilter/ip6_tables.c
@@ -25,6 +25,7 @@
#include <linux/proc_fs.h>
#include <linux/err.h>
#include <linux/cpumask.h>
+#include <linux/pagemap.h>

#include <linux/netfilter_ipv6/ip6_tables.h>
#include <linux/netfilter/x_tables.h>
@@ -845,6 +846,7 @@ copy_entries_to_user(unsigned int total_size,

loc_cpu_entry = private->entries;

+ pagefault_disable();
/* FIXME: use iterator macros --RR */
/* ... then go back and fix counters and names */
for (off = 0, num = 0; off < total_size; off += e->next_offset, num++){
@@ -882,6 +884,7 @@ copy_entries_to_user(unsigned int total_size,
}

free_counters:
+ pagefault_enable();
vfree(counters);
return ret;
}
@@ -964,6 +967,7 @@ static int compat_table_info(const struct xt_table_info *info,

static int get_info(struct net *net, void __user *user, const int *len)
{
+ struct ip6t_getinfo info;
char name[XT_TABLE_MAXNAMELEN];
struct xt_table *t;
int ret;
@@ -981,7 +985,6 @@ static int get_info(struct net *net, void __user *user, const int *len)
#endif
t = xt_request_find_table_lock(net, AF_INET6, name);
if (!IS_ERR(t)) {
- struct ip6t_getinfo info;
const struct xt_table_info *private = t->private;
#ifdef CONFIG_NETFILTER_XTABLES_COMPAT
struct xt_table_info tmp;
@@ -1002,10 +1005,7 @@ static int get_info(struct net *net, void __user *user, const int *len)
info.size = private->size;
strcpy(info.name, name);

- if (copy_to_user(user, &info, *len) != 0)
- ret = -EFAULT;
- else
- ret = 0;
+ ret = 0;

xt_table_unlock(t);
module_put(t->me);
@@ -1015,6 +1015,8 @@ static int get_info(struct net *net, void __user *user, const int *len)
if (in_compat_syscall())
xt_compat_unlock(AF_INET6);
#endif
+ if (!ret && copy_to_user(user, &info, *len) != 0)
+ ret = -EFAULT;
return ret;
}

@@ -1025,6 +1027,7 @@ get_entries(struct net *net, struct ip6t_get_entries __user *uptr,
int ret;
struct ip6t_get_entries get;
struct xt_table *t;
+ bool faulted = false;

if (*len < sizeof(get))
return -EINVAL;
@@ -1035,6 +1038,7 @@ get_entries(struct net *net, struct ip6t_get_entries __user *uptr,

get.name[sizeof(get.name) - 1] = '\0';

+ retry:
t = xt_find_table_lock(net, AF_INET6, get.name);
if (!IS_ERR(t)) {
struct xt_table_info *private = t->private;
@@ -1049,6 +1053,14 @@ get_entries(struct net *net, struct ip6t_get_entries __user *uptr,
} else
ret = PTR_ERR(t);

+ if (ret == -EFAULT && !faulted) {
+ faulted = true;
+ if (fault_in_writeable((char __user *)uptr->entrytable,
+ get.size))
+ return -EFAULT;
+ goto retry;
+ }
+
return ret;
}

@@ -1575,12 +1587,14 @@ compat_copy_entries_to_user(unsigned int total_size, struct xt_table *table,

pos = userptr;
size = total_size;
+ pagefault_disable();
xt_entry_foreach(iter, private->entries, total_size) {
ret = compat_copy_entry_to_user(iter, &pos,
&size, counters, i++);
if (ret != 0)
break;
}
+ pagefault_enable();

vfree(counters);
return ret;
@@ -1593,6 +1607,7 @@ compat_get_entries(struct net *net, struct compat_ip6t_get_entries __user *uptr,
int ret;
struct compat_ip6t_get_entries get;
struct xt_table *t;
+ bool faulted = false;

if (*len < sizeof(get))
return -EINVAL;
@@ -1605,6 +1620,7 @@ compat_get_entries(struct net *net, struct compat_ip6t_get_entries __user *uptr,

get.name[sizeof(get.name) - 1] = '\0';

+ retry:
xt_compat_lock(AF_INET6);
t = xt_find_table_lock(net, AF_INET6, get.name);
if (!IS_ERR(t)) {
@@ -1624,6 +1640,13 @@ compat_get_entries(struct net *net, struct compat_ip6t_get_entries __user *uptr,
ret = PTR_ERR(t);

xt_compat_unlock(AF_INET6);
+ if (ret == -EFAULT && !faulted) {
+ faulted = true;
+ if (fault_in_writeable((char __user *)uptr->entrytable,
+ get.size))
+ return -EFAULT;
+ goto retry;
+ }
return ret;
}
#endif
--
2.43.0