Re: netfilter: nf_nat: race in nf_nat_setup_info() corrupts nat_bysource list (GPF / soft lockup)

Vimal Agrawal <[email protected]>
Newsgroups gmane.comp.security.firewalls.netfilter.devel
Message-ID <CALkUMdRTdv9rHqgNO29mWNk6VDbMM8X0kJ8DP5pdWM+nJOpH1w@mail.gmail.com>
Hi Florian,

The specific precondition — br_flood() cloning skbs that share one
unconfirmed ct — is stock br_netfilter behavior, not anything in our
custom module; nf_ct_drop_unconfirmed()'s own comment already names
this exact case (br_netfilter/multicast routing). The only thing our
module does is decide, per-packet, whether to route a given skb
through NFQUEUE — which is exactly what the NFQUEUE API is for, not an
unusual or abusive use of it. What's missing isn't anything in our
module; it's that nf_ct_drop_unconfirmed()'s "am I the exclusive owner
of this unconfirmed ct" check only runs at NFQUEUE-enqueue time. A
sibling clone that never goes through NFQUEUE — e.g. one that takes
the plain synchronous forward path while another clone of the same ct
is off in NFQUEUE + reinject — never hits that check at all, and stock
kernel code doesn't check it anywhere else on that path either.

Agreed, and we're not disputing that this is not a complete fix. But
auditing and converting every extension-add call site and every
non-atomic ct->status write across nf_conntrack_core.c, every ALG
helper (ftp/sip/pptp/h323/irc), and ctnetlink is a much larger effort
than we can responsibly take on as part of resolving two specific
production crashes. We'd like to keep the scope of this fix limited to
nf_nat_setup_info() — the exact function both crashes faulted in, with
live disassembly confirming the fault site in both cases — rather than
trying to fix the whole class in one patch. A broader fix (e.g. a
common checkpoint that prevents the sharing from ever reaching any of
these unguarded sites, generalizing what nf_ct_drop_unconfirmed()
already does at NFQUEUE-enqueue time) seems like the right direction
longer-term, but feels like it deserves its own design discussion
rather than being folded into a fix for this specific bug report.

With that scope, here's what we're proposing (corrected from your
sketch: test_and_set_bit() takes the bit number first, address second
— yours had them swapped — and this wires up the actual drop on the
losing path, which your version fell through past):

+++ b/net/netfilter/nf_nat_core.c
@@ -839,17 +839,25 @@ nf_nat_setup_info(struct nf_conn *ct,
                                      &ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple);
                lock = &nf_nat_locks[srchash % CONNTRACK_LOCKS];
                spin_lock_bh(lock);
+               /* Concurrent callers can reach here for the same,
+                * still-unconfirmed ct (skb_clone()/GSO segmentation/
+                * nf_ct_attach() can share one ct across skbs processed on
+                * different CPUs before it is confirmed). test_and_set_bit()
+                * makes the check-and-mark atomic under this same lock, so
+                * only the first caller links the node; a losing caller
+                * drops instead of double-linking and corrupting the list.
+                */
+               if (test_and_set_bit(IPS_SRC_NAT_DONE_BIT, &ct->status)) {
+                       spin_unlock_bh(lock);
+                       return NF_DROP;
+               }
                hlist_add_head_rcu(&ct->nat_bysource,
                                   &nf_nat_bysource[srchash]);
                spin_unlock_bh(lock);
+       } else {
+               set_bit(IPS_DST_NAT_DONE_BIT, &ct->status);
        }

-       /* It's done. */
-       if (maniptype == NF_NAT_MANIP_DST)
-               ct->status |= IPS_DST_NAT_DONE;
-       else
-               ct->status |= IPS_SRC_NAT_DONE;
-
        return NF_ACCEPT;
 }
 EXPORT_SYMBOL(nf_nat_setup_info);

This makes the check-and-mark atomic under the same lock that protects
the list insert, so a losing concurrent caller drops instead of
double-linking nat_bysource. Does keeping this fix scoped to
nf_nat_setup_info() seem reasonable to you, with the broader
extension/status-write question left open for separate discussion?

I can send the formal patch thereafter.

Vimal

On Mon, Aug 17, 2026 at 8:43 PM Florian Westphal <[email protected]> wrote:
>
> Vimal Agrawal <[email protected]> wrote:
> > I do see some conntrack updates passed nf_nat_initialized check in
> > nf_nat_setup_info so ideally it should not do anything for the second
> > or other run for the same conntrack.
> > How about locking out the whole of nf_nat_setup_info on ct->lock. i..e
> > spin_lock_bh(&ct->lock) at the start of the function and unlock before
> > return.
>
> It would be good to understand that this isn't a 3rd party module bug
> first.
>
> As I said, conntrack doesn't expect unconfirmed conntracks racing
> on multiple CPUs.
>
> Minimal hack (not even compile tested):
> diff --git a/net/netfilter/nf_nat_core.c b/net/netfilter/nf_nat_core.c
> --- a/net/netfilter/nf_nat_core.c
> +++ b/net/netfilter/nf_nat_core.c
> @@ -815,16 +815,12 @@ nf_nat_setup_info(struct nf_conn *ct,
>                                       &ct->tuplehash[IP_CT_DIR_ORIGINAL].tuple);
>                 lock = &nf_nat_locks[srchash % CONNTRACK_LOCKS];
>                 spin_lock_bh(lock);
> -               hlist_add_head_rcu(&ct->nat_bysource,
> -                                  &nf_nat_bysource[srchash]);
> +               if (!test_and_set_bit(&ct->status, IPS_SRC_NAT_DONE_BIT))
> +                       hlist_add_head_rcu(&ct->nat_bysource,
> +                                          &nf_nat_bysource[srchash]);
>                 spin_unlock_bh(lock);
> -       }
> -
> -       /* It's done. */
> -       if (maniptype == NF_NAT_MANIP_DST)
> -               ct->status |= IPS_DST_NAT_DONE;
> -       else
> -               ct->status |= IPS_SRC_NAT_DONE;
> +       } else
> +               set_bit(&ct->status, IPS_DST_NAT_DONE_BIT);
>
>         return NF_ACCEPT;
>  }
>
> But its not enough, if we assume racing unconfirmed conntracks are now allowed, then
> all places adding conntrack extensions are buggy, and all direct ct->status changes
> (not using set_bit APIs) are buggy too.
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.