All of lore.kernel.org
 help / color / mirror / Atom feed
* netfilter: nf_nat: race in nf_nat_setup_info() corrupts nat_bysource list (GPF / soft lockup)
@ 2026-08-16 14:00 Vimal Agrawal
  2026-08-16 14:25 ` Florian Westphal
  0 siblings, 1 reply; 2+ messages in thread
From: Vimal Agrawal @ 2026-08-16 14:00 UTC (permalink / raw)
  To: netfilter-devel
  Cc: Pablo Neira Ayuso, Jozsef Kadlecsik, Florian Westphal, coreteam,
	Vimal Agrawal

Hi,

Our custom module can decide to send the first packet of a conntrack
to userspace via NFQUEUE while the rest of the packets for that
conntrack follow the standard kernel forward path.
In the case of a bridge, br_flood() clones skbs, and those clones
share one unconfirmed conntrack for the initial packet(s) (via
skb_clone()'s __nf_copy(), which just bumps the refcount and copies
the same ct pointer).

br_flood() clones the skb; Skb1 and Skb2 share one unconfirmed ct:
  Skb1 (cpu1): -> nfqueue -> [userspace verdict] -> reinject -> SNAT
-> confirm (cpu2)
  Skb2 (cpu1): -> SNAT -> confirm (cpu1)
Skb1's reinject-triggered SNAT (cpu2) races Skb2's synchronous SNAT
(cpu1) on the same, still-unconfirmed ct.

Because of this, SNAT for the two skbs sharing the unconfirmed
conntrack races across two different CPUs. nf_nat_setup_info() sees on
both CPUs that the conntrack isn't yet SNAT-initialized, and both try
to initialize it — pushing the same conntrack node onto the
nat_bysource hash chain twice, corrupting the list.
We've seen two instances of this in production: a soft lockup and a
crash due to an illegal address, both while traversing the
nat_bysource chain. Backtrace from the GPF instance:
[321916.534145] [   T6617] general protection fault, probably for
non-canonical address 0x15000bffffffb6: 0000 [#1] SMP NOPTI
[321916.567351] [   T6617] CPU: 0 PID: 6617 Comm: snort Kdump: loaded
Tainted: P        W  O       6.6.49 #1
[321916.595688] [   T6617] Hardware name: Sophos XGS/XGS, BIOS V112 02/09/2022
[321916.615439] [   T6617] RIP: 0010:nf_nat_setup_info+0x8b6/0xba0 [nf_nat]
[321916.634338] [   T6617] Code: ed 75 15 e9 f2 fb ff ff 4d 8b ad 90
00 00 00 4d 85 ed 0f 84 e2 fb ff ff 49 81 ed 90 00 00 00 0f 84 d5 fb
ff ff 0f b6 44 24 56 <41> 38 45 46 75 d8 49 8b 45 20 49 8b 55 28 48 33
44 24 30 48 33 54
...
[321917.017416] [   T6617]  nf_nat_masquerade_ipv4+0x11d/0x200 [nf_nat]
[321917.035180] [   T6617]  ? do_accept6+0x150/0x150 [xt_SFOS_SNAT]
[321917.051796] [   T6617]  do_masq4+0x49/0x70 [xt_SFOS_SNAT]
[321917.066699] [   T6617]  ? do_accept6+0x150/0x150 [xt_SFOS_SNAT]
[321917.083312] [   T6617]  rule_lookup+0xc9/0x130 [sfos_rules_framework]
[321917.101646] [   T6617]  snat_tg+0x235/0x300 [xt_SFOS_SNAT]
[321917.116827] [   T6617]  ipt_do_table+0x2b9/0x410 [ip_tables]
[321917.132578] [   T6617]  nf_nat_inet_fn+0x14f/0x260 [nf_nat]
[321917.148044] [   T6617]  nf_nat_ipv4_out+0x4d/0xe0 [nf_nat]
[321917.163221] [   T6617]  nf_reinject+0x177/0x1d0
Disassembly of the faulting/looping instruction shows it's inside
find_appropriate_src()'s inlined hlist_for_each_entry_rcu() walk of
nf_nat_bysource[h] (called from get_unique_tuple(), itself called from
nf_nat_setup_info() before the vulnerable insert). Both are static,
single-call-site functions, so they're fully inlined and never appear
as separate symbols — which is why the crash attributes entirely to
nf_nat_setup_info:
373    hlist_for_each_entry_rcu(ct, &nf_nat_bysource[h], nat_bysource) {
   0x0000000000000daa <+2170>:  48 8b 15 00 00 00 00  mov    0x0(%rip),%rdx
   0x0000000000000db1 <+2177>:  89 c0                mov    %eax,%eax
   ...
357    return (t->dst.protonum == tuple->dst.protonum &&
   0x0000000000000de1 <+2225>:  0f b6 44 24 56        movzbl 0x56(%rsp),%eax
   0x0000000000000de6 <+2230>:  41 38 45 46           cmp    %al,0x46(%r13)
Basically, nf_nat_setup_info() is not CPU-race-safe against two
callers concurrently operating on the same, still-unconfirmed ct.

This kernel (6.6) already carries the fix for the analogous race at
confirm time — commit 3a1ce9793855935a2d2b4d9dc5003d42f797f6fc
("netfilter: conntrack: fix cloned unconfirmed skb->_nfct race in
__nf_conntrack_confirm"), which detects this exact
shared-unconfirmed-ct-via-clone precondition and drops the losing skb.
But nf_nat_setup_info() runs before confirm, on the same shared ct,
and has no equivalent guard — so the identical precondition
(bridge-flood/broadcast clones sharing one unconfirmed ct across CPUs)
is still exploitable earlier in the pipeline, at the NAT-setup stage.

We have not been able to catch the nf_nat_setup_info() double-insert
directly via kprobe so far — the window there is narrow. We do,
however, see the __nf_conntrack_confirm() WARN_ON_ONCE ("Another skb
with the same unconfirmed conntrack may win the race...") fire live in
production whenever there's a race for confirmation, which confirms
the underlying precondition — clones sharing one unconfirmed ct racing
across CPUs — is genuinely occurring on this box, not just inferred
from the crash dumps. We just haven't yet been able to force the
earlier, nf_nat_setup_info()-stage race into a controlled
reproduction.

I think this needs a fix in nf_nat_setup_info(). Wanted to get initial
feedback before I start working on a patch.

Thanks,
Vimal

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

end of thread, other threads:[~2026-08-16 14:25 UTC | newest]

Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-16 14:00 netfilter: nf_nat: race in nf_nat_setup_info() corrupts nat_bysource list (GPF / soft lockup) Vimal Agrawal
2026-08-16 14:25 ` Florian Westphal

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.