From: Pablo Neira Ayuso <pablo@netfilter.org>
To: Zihan Xi <zihanx@nebusec.ai>
Cc: netfilter-devel@vger.kernel.org, coreteam@netfilter.org,
fw@strlen.de, phil@nwl.cc, netdev@vger.kernel.org,
linux-kernel@vger.kernel.org, davem@davemloft.net,
edumazet@google.com, kuba@kernel.org, pabeni@redhat.com,
horms@kernel.org
Subject: Re: [PATCH nf v3 1/1] netfilter: x_tables: avoid holding mutex over faultable user copies
Date: Fri, 9 Oct 2026 01:54:43 +0200 [thread overview]
Message-ID: <asgtQ3E2LVWqfV8D@chamomile> (raw)
In-Reply-To: <f88fc502b17b4022105a99eea407525807ee573e.1789885804.git.zihanx@nebusec.ai>
On Sun, Sep 20, 2026 at 11:58:22AM +0000, Zihan Xi wrote:
> 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 may sleep indefinitely while the mutex is held,
> blocking unrelated table and registry operations.
>
> Disable page faults while each locked user copy runs. For GET_ENTRIES,
> validate the table and requested size while holding the table mutex, then
> release it before faulting in the output range. Reacquire the mutex and
> revalidate the table and size before the nofault copy. If a nofault copy
> fails, fault the range outside the lock and retry the operation up to three
> times after the initial attempt. A failed fault-in returns -EFAULT;
> repeated nofault failures are limited to four attempts in total and then
> return -EFAULT. This avoids discarding a locked counter snapshot on the
> common first fault. Move GET_INFO's fixed-size copy outside the table locks
> and apply the same fault-safe handling to the IPv4/IPv6 compat GET_ENTRIES
> paths.
>
> The ebtables GET paths use a separate ebt_mutex and are outside this
> IPv4/IPv6/ARP series.
I keep spinning on this, and I am not sure this fix is the right thing
to do. There is no single caller of this pagefault_disable/enable() mm
subsystem function in the net folder.
Would it be feasible to turn this global family mutex into a per-netns
family mutex? xt_find_table_lock() could be revamped to make it?
The iteration over xt_templates[] still needs to hold the global
family lock, but only to create the table in that netns.
> Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2")
> 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 v3:
> - Validate table and requested size before fault-in, then revalidate the
> table and size after lock reacquisition in native and IPv4/IPv6 compat
> GET_ENTRIES paths.
> - Make retry semantics explicit: one initial nofault attempt followed by
> at most three retries. Keep fault-in and retry decisions outside family
> mutexes; failed fault-in and exhausted retries return -EFAULT.
> - Keep the fixed-size GET_INFO copy outside table locks and brace compat
> lookup error arms.
> - Clarify ARP-only runtime coverage, privilege scope, helper build,
> holder/waiter evidence, QEMU configuration, and the NOT RUN
> crash-log status.
> - Regenerate the numbered patch and cover with LF line endings; use the
> standard 0001-*.patch filename.
> - v2 Link:
> https://lore.kernel.org/all/cover.1788961415.git.zihanx@nebusec.ai/
> changes in v2:
> - Rebase onto current nf.git after 0bd7ed1a3263c
> ("netfilter: arp_tables: remove the 32bit compat interface").
> ARP GET_INFO and GET_ENTRIES use native paths only; IPv4 and IPv6
> retain compat handling.
> - Drop hung_task_panic and the 10-second hung_task timeout from the
> reproducer, as pointed out by Pablo Neira Ayuso; observe holder/waiter
> wchan instead.
> - v1 Link:
> https://lore.kernel.org/all/cover.1788244146.git.zihanx@nebusec.ai/
>
> net/ipv4/netfilter/arp_tables.c | 49 ++++++++++++++++---
> net/ipv4/netfilter/ip_tables.c | 87 +++++++++++++++++++++++++++++----
> net/ipv6/netfilter/ip6_tables.c | 87 +++++++++++++++++++++++++++++----
> 3 files changed, 197 insertions(+), 26 deletions(-)
>
> diff --git a/net/ipv4/netfilter/arp_tables.c b/net/ipv4/netfilter/arp_tables.c
> index db307fa..a857e67 100644
> --- a/net/ipv4/netfilter/arp_tables.c
7> +++ 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;
> + unsigned int retries = 0;
>
> if (*len < sizeof(get))
> return -EINVAL;
> @@ -779,21 +782,51 @@ 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)) {
> + ret = PTR_ERR(t);
> + goto out;
> + }
> +
> + if (get.size != t->private->size) {
> + ret = -EAGAIN;
> + module_put(t->me);
> + xt_table_unlock(t);
> + goto out;
> + }
> +
> + module_put(t->me);
> + xt_table_unlock(t);
> +
> + /* Fault in only after validating the table and requested size. */
> + if (fault_in_safe_writeable((char __user *)uptr->entrytable,
> + get.size))
> + return -EFAULT;
> +
> t = xt_find_table_lock(net, NFPROTO_ARP, get.name);
> if (!IS_ERR(t)) {
> const struct xt_table_info *private = t->private;
>
> - if (get.size == private->size)
> + if (get.size != private->size) {
> + ret = -EAGAIN;
> + } else {
> ret = copy_entries_to_user(private->size,
> t, uptr->entrytable);
> - else
> - ret = -EAGAIN;
> + }
>
> module_put(t->me);
> xt_table_unlock(t);
> } else
> ret = PTR_ERR(t);
>
> +out:
> + /* Allow three retries after the initial nofault copy. */
> + if (ret == -EFAULT && retries < 3) {
> + retries++;
> + goto retry;
> + }
> +
> return ret;
> }
>
> diff --git a/net/ipv4/netfilter/ip_tables.c b/net/ipv4/netfilter/ip_tables.c
> index 809441c..90f4a26 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;
> + unsigned int retries = 0;
>
> if (*len < sizeof(get))
> return -EINVAL;
> @@ -1013,20 +1016,50 @@ 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)) {
> + ret = PTR_ERR(t);
> + goto out;
> + }
> +
> + if (get.size != t->private->size) {
> + ret = -EAGAIN;
> + module_put(t->me);
> + xt_table_unlock(t);
> + goto out;
> + }
> +
> + module_put(t->me);
> + xt_table_unlock(t);
> +
> + /* Fault in only after validating the table and requested size. */
> + if (fault_in_safe_writeable((char __user *)uptr->entrytable,
> + get.size))
> + return -EFAULT;
> +
> t = xt_find_table_lock(net, AF_INET, get.name);
> if (!IS_ERR(t)) {
> const struct xt_table_info *private = t->private;
> - if (get.size == private->size)
> + if (get.size != private->size) {
> + ret = -EAGAIN;
> + } else {
> ret = copy_entries_to_user(private->size,
> t, uptr->entrytable);
> - else
> - ret = -EAGAIN;
> + }
>
> module_put(t->me);
> xt_table_unlock(t);
> } else
> ret = PTR_ERR(t);
>
> +out:
> + /* Allow three retries after the initial nofault copy. */
> + if (ret == -EFAULT && retries < 3) {
> + retries++;
> + goto retry;
> + }
> +
> return ret;
> }
>
> @@ -1561,12 +1594,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 +1614,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;
> + unsigned int retries = 0;
>
> if (*len < sizeof(get))
> return -EINVAL;
> @@ -1591,6 +1627,32 @@ 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)) {
> + const struct xt_table_info *private = t->private;
> + struct xt_table_info info;
> +
> + ret = compat_table_info(private, &info);
> + if (!ret && get.size != info.size)
> + ret = -EAGAIN;
> +
> + xt_compat_flush_offsets(AF_INET);
> + module_put(t->me);
> + xt_table_unlock(t);
> + } else {
> + ret = PTR_ERR(t);
> + }
> + xt_compat_unlock(AF_INET);
> + if (ret)
> + goto out;
> +
> + /* Fault in only after validating the table and requested size. */
> + if (fault_in_safe_writeable((char __user *)uptr->entrytable,
> + get.size))
> + return -EFAULT;
> +
> xt_compat_lock(AF_INET);
> t = xt_find_table_lock(net, AF_INET, get.name);
> if (!IS_ERR(t)) {
> @@ -1606,10 +1668,17 @@ compat_get_entries(struct net *net, struct compat_ipt_get_entries __user *uptr,
> xt_compat_flush_offsets(AF_INET);
> module_put(t->me);
> xt_table_unlock(t);
> - } else
> + } else {
> ret = PTR_ERR(t);
> + }
>
> xt_compat_unlock(AF_INET);
> +out:
> + /* Allow three retries after the initial nofault copy. */
> + if (ret == -EFAULT && retries < 3) {
> + retries++;
> + goto retry;
> + }
> return ret;
> }
> #endif
> diff --git a/net/ipv6/netfilter/ip6_tables.c b/net/ipv6/netfilter/ip6_tables.c
> index f42fb96..4952bfd 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>
> @@ -840,6 +841,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++){
> @@ -877,6 +879,7 @@ copy_entries_to_user(unsigned int total_size,
> }
>
> free_counters:
> + pagefault_enable();
> vfree(counters);
> return ret;
> }
> @@ -959,6 +962,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;
> @@ -976,7 +980,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;
> @@ -997,10 +1000,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);
> @@ -1010,6 +1010,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;
> }
>
> @@ -1020,6 +1022,7 @@ get_entries(struct net *net, struct ip6t_get_entries __user *uptr,
> int ret;
> struct ip6t_get_entries get;
> struct xt_table *t;
> + unsigned int retries = 0;
>
> if (*len < sizeof(get))
> return -EINVAL;
> @@ -1030,20 +1033,50 @@ 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)) {
> + ret = PTR_ERR(t);
> + goto out;
> + }
> +
> + if (get.size != t->private->size) {
> + ret = -EAGAIN;
> + module_put(t->me);
> + xt_table_unlock(t);
> + goto out;
> + }
> +
> + module_put(t->me);
> + xt_table_unlock(t);
> +
> + /* Fault in only after validating the table and requested size. */
> + if (fault_in_safe_writeable((char __user *)uptr->entrytable,
> + get.size))
> + return -EFAULT;
> +
> t = xt_find_table_lock(net, AF_INET6, get.name);
> if (!IS_ERR(t)) {
> struct xt_table_info *private = t->private;
> - if (get.size == private->size)
> + if (get.size != private->size) {
> + ret = -EAGAIN;
> + } else {
> ret = copy_entries_to_user(private->size,
> t, uptr->entrytable);
> - else
> - ret = -EAGAIN;
> + }
>
> module_put(t->me);
> xt_table_unlock(t);
> } else
> ret = PTR_ERR(t);
>
> +out:
> + /* Allow three retries after the initial nofault copy. */
> + if (ret == -EFAULT && retries < 3) {
> + retries++;
> + goto retry;
> + }
> +
> return ret;
> }
>
> @@ -1570,12 +1603,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;
> @@ -1588,6 +1623,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;
> + unsigned int retries = 0;
>
> if (*len < sizeof(get))
> return -EINVAL;
> @@ -1600,6 +1636,32 @@ 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)) {
> + const struct xt_table_info *private = t->private;
> + struct xt_table_info info;
> +
> + ret = compat_table_info(private, &info);
> + if (!ret && get.size != info.size)
> + ret = -EAGAIN;
> +
> + xt_compat_flush_offsets(AF_INET6);
> + module_put(t->me);
> + xt_table_unlock(t);
> + } else {
> + ret = PTR_ERR(t);
> + }
> + xt_compat_unlock(AF_INET6);
> + if (ret)
> + goto out;
> +
> + /* Fault in only after validating the table and requested size. */
> + if (fault_in_safe_writeable((char __user *)uptr->entrytable,
> + get.size))
> + return -EFAULT;
> +
> xt_compat_lock(AF_INET6);
> t = xt_find_table_lock(net, AF_INET6, get.name);
> if (!IS_ERR(t)) {
> @@ -1615,10 +1677,17 @@ compat_get_entries(struct net *net, struct compat_ip6t_get_entries __user *uptr,
> xt_compat_flush_offsets(AF_INET6);
> module_put(t->me);
> xt_table_unlock(t);
> - } else
> + } else {
> ret = PTR_ERR(t);
> + }
>
> xt_compat_unlock(AF_INET6);
> +out:
> + /* Allow three retries after the initial nofault copy. */
> + if (ret == -EFAULT && retries < 3) {
> + retries++;
> + goto retry;
> + }
> return ret;
> }
> #endif
> --
> 2.55.0.windows.3
>
>
next prev parent reply other threads:[~2026-10-08 23:54 UTC|newest]
Thread overview: 6+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-09-20 11:58 [PATCH nf v3 0/1] netfilter: x_tables: avoid holding mutex over faultable user copies Zihan Xi
2026-09-20 11:58 ` [PATCH nf v3 1/1] " Zihan Xi
2026-10-08 23:54 ` Pablo Neira Ayuso [this message]
2026-10-09 0:07 ` Florian Westphal
2026-10-09 0:14 ` Pablo Neira Ayuso
2026-10-09 1:09 ` zihan xi
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=asgtQ3E2LVWqfV8D@chamomile \
--to=pablo@netfilter.org \
--cc=coreteam@netfilter.org \
--cc=davem@davemloft.net \
--cc=edumazet@google.com \
--cc=fw@strlen.de \
--cc=horms@kernel.org \
--cc=kuba@kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=netdev@vger.kernel.org \
--cc=netfilter-devel@vger.kernel.org \
--cc=pabeni@redhat.com \
--cc=phil@nwl.cc \
--cc=zihanx@nebusec.ai \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox