|
| 1 | +From 53b3e60edb674b442b2b3bbdba484667b0f47a5d Mon Sep 17 00:00:00 2001 |
| 2 | +From: Adrian Bente <adibente@gmail.com> |
| 3 | +Date: Thu, 28 May 2026 10:08:51 +0300 |
| 4 | +Subject: [PATCH] netfilter: flowtable: fix offloaded ct timeout never being extended |
| 5 | + |
| 6 | +OpenWrt has recently migrated many platforms to kernel 6.18. On the |
| 7 | +MediaTek platform, which supports hardware network offloading, WiFi |
| 8 | +connections accelerated via the WED path were observed to drop after |
| 9 | +roughly 300 seconds. |
| 10 | + |
| 11 | +After several debugging sessions, assisted by the Claude LLM, the |
| 12 | +problem was narrowed down as follows: |
| 13 | + |
| 14 | +nf_flow_table_extend_ct_timeout() extends ct->timeout for offloaded |
| 15 | +flows using: |
| 16 | + |
| 17 | + cmpxchg(&ct->timeout, expires, new_timeout); |
| 18 | + |
| 19 | +'expires' comes from nf_ct_expires(ct) and is a relative value, while |
| 20 | +ct->timeout holds an absolute timestamp. The two are never equal, so |
| 21 | +the cmpxchg always fails and the timeout is never extended. |
| 22 | + |
| 23 | +This goes unnoticed for most flows, but a long-lived hardware (WED) |
| 24 | +offloaded flow on MediaTek MT7986 eventually has ct->timeout decay to |
| 25 | +zero, the conntrack entry is reaped and the connection breaks. |
| 26 | + |
| 27 | +Open-code the relative value from a single READ_ONCE(ct->timeout) |
| 28 | +snapshot and compare against that same absolute snapshot in the |
| 29 | +cmpxchg, so the timeout extension actually takes effect while the |
| 30 | +datapath remains authoritative if it updates ct->timeout concurrently. |
| 31 | + |
| 32 | +Fixes: 03428ca5cee9 ("netfilter: conntrack: rework offload nf_conn timeout extension logic") |
| 33 | +Cc: stable@vger.kernel.org |
| 34 | +Suggested-by: Florian Westphal <fw@strlen.de> |
| 35 | +Signed-off-by: Adrian Bente <adibente@gmail.com> |
| 36 | +Signed-off-by: Pablo Neira Ayuso <pablo@netfilter.org> |
| 37 | +--- |
| 38 | + net/netfilter/nf_flow_table_core.c | 13 +++++++++---- |
| 39 | + 1 file changed, 9 insertions(+), 4 deletions(-) |
| 40 | + |
| 41 | +--- a/net/netfilter/nf_flow_table_core.c |
| 42 | ++++ b/net/netfilter/nf_flow_table_core.c |
| 43 | +@@ -500,8 +500,13 @@ static u32 nf_flow_table_tcp_timeout(con |
| 44 | + */ |
| 45 | + static void nf_flow_table_extend_ct_timeout(struct nf_conn *ct) |
| 46 | + { |
| 47 | +- static const u32 min_timeout = 5 * 60 * HZ; |
| 48 | +- u32 expires = nf_ct_expires(ct); |
| 49 | ++ static const s32 min_timeout = 5 * 60 * HZ; |
| 50 | ++ u32 ct_timeout = READ_ONCE(ct->timeout); |
| 51 | ++ s32 expires; |
| 52 | ++ |
| 53 | ++ expires = ct_timeout - nfct_time_stamp; |
| 54 | ++ if (expires <= 0) /* already expired */ |
| 55 | ++ return; |
| 56 | + |
| 57 | + /* normal case: large enough timeout, nothing to do. */ |
| 58 | + if (likely(expires >= min_timeout)) |
| 59 | +@@ -519,7 +524,7 @@ static void nf_flow_table_extend_ct_time |
| 60 | + if (nf_ct_is_confirmed(ct) && |
| 61 | + test_bit(IPS_OFFLOAD_BIT, &ct->status)) { |
| 62 | + u8 l4proto = nf_ct_protonum(ct); |
| 63 | +- u32 new_timeout = true; |
| 64 | ++ u32 new_timeout = 1; |
| 65 | + |
| 66 | + switch (l4proto) { |
| 67 | + case IPPROTO_UDP: |
| 68 | +@@ -544,7 +549,7 @@ static void nf_flow_table_extend_ct_time |
| 69 | + */ |
| 70 | + if (new_timeout) { |
| 71 | + new_timeout += nfct_time_stamp; |
| 72 | +- cmpxchg(&ct->timeout, expires, new_timeout); |
| 73 | ++ cmpxchg(&ct->timeout, ct_timeout, new_timeout); |
| 74 | + } |
| 75 | + } |
| 76 | + |
0 commit comments