The pernet flowtable statistics (net->ft.stat) are released when the netns exits, but the flowtables attached to a dying netns are torn down asynchronously afterwards: the tc conntrack action frees its flowtable from an RCU-delayed workqueue item (tcf_ct_flow_table_cleanup_work()), and nf_tables releases flowtables from the transaction release work. nf_flow_table_free() ends with a garbage-collection pass that queues offload works; flow_offload_queue_work() increments the pernet counters when scheduling them and the handlers decrement the counters again once they complete. The flushes in nf_flow_table_free() only drain those workqueues when hardware offload is in use, so after free_percpu() the stray workers still this_cpu_inc()/this_cpu_dec() into freed percpu memory. Account flowtables at init time and defer the release of the statistics to the last nf_flow_table_free() of a dismantled netns, after draining the offload workqueues. A netns without flowtables keeps the synchronous release. Fixes: b038177636f8 ("netfilter: nf_flow_table: count pending offload workqueue tasks") Cc: stable@vger.kernel.org Signed-off-by: Qihang --- include/net/netfilter/nf_flow_table.h | 1 + include/net/netns/flow_table.h | 11 +++++++ net/netfilter/nf_flow_table_core.c | 44 ++++++++++++++++++++++++++- net/netfilter/nf_flow_table_offload.c | 7 +++++ 4 files changed, 62 insertions(+), 1 deletion(-) diff --git a/include/net/netfilter/nf_flow_table.h b/include/net/netfilter/nf_flow_table.h index 5b22d31cb1407..91ffa3072a0cc 100644 --- a/include/net/netfilter/nf_flow_table.h +++ b/include/net/netfilter/nf_flow_table.h @@ -376,6 +376,7 @@ void nf_flow_offload_stats(struct nf_flowtable *flowtable, void nf_flow_table_offload_flush(struct nf_flowtable *flowtable); void nf_flow_table_offload_flush_cleanup(struct nf_flowtable *flowtable); +void nf_flow_table_offload_drain(void); int nf_flow_table_offload_setup(struct nf_flowtable *flowtable, struct net_device *dev, diff --git a/include/net/netns/flow_table.h b/include/net/netns/flow_table.h index 1c5fc657e2675..6d2dc0d1f06c7 100644 --- a/include/net/netns/flow_table.h +++ b/include/net/netns/flow_table.h @@ -2,6 +2,9 @@ #ifndef __NETNS_FLOW_TABLE_H #define __NETNS_FLOW_TABLE_H +#include +#include + struct nf_flow_table_stat { unsigned int count_wq_add; unsigned int count_wq_del; @@ -9,6 +12,14 @@ struct nf_flow_table_stat { }; struct netns_ft { + /* Protects nr_ft and exiting. */ + spinlock_t lock; + /* Number of flowtables attached to this netns. */ + int nr_ft; + /* Set at netns exit; the statistics are then released by the + * last flowtable teardown. + */ + bool exiting; struct nf_flow_table_stat __percpu *stat; }; #endif diff --git a/net/netfilter/nf_flow_table_core.c b/net/netfilter/nf_flow_table_core.c index 0d72dc8ae47a1..de0a9f0e4f7b9 100644 --- a/net/netfilter/nf_flow_table_core.c +++ b/net/netfilter/nf_flow_table_core.c @@ -715,6 +715,7 @@ EXPORT_SYMBOL_GPL(nf_flow_dnat_port); int nf_flow_table_init(struct nf_flowtable *flowtable) { + struct net *net; int err; INIT_DELAYED_WORK(&flowtable->gc_work, nf_flow_offload_work_gc); @@ -733,6 +734,13 @@ int nf_flow_table_init(struct nf_flowtable *flowtable) list_add(&flowtable->list, &flowtables); mutex_unlock(&flowtable_lock); + net = read_pnet(&flowtable->net); + if (net) { + spin_lock(&net->ft.lock); + net->ft.nr_ft++; + spin_unlock(&net->ft.lock); + } + return 0; } EXPORT_SYMBOL_GPL(nf_flow_table_init); @@ -771,6 +779,8 @@ EXPORT_SYMBOL_GPL(nf_flow_table_cleanup); void nf_flow_table_free(struct nf_flowtable *flow_table) { + struct net *net; + mutex_lock(&flowtable_lock); list_del(&flow_table->list); mutex_unlock(&flowtable_lock); @@ -781,19 +791,51 @@ void nf_flow_table_free(struct nf_flowtable *flow_table) nf_flow_table_iterate(flow_table, nf_flow_table_do_cleanup, NULL); nf_flow_table_gc_run(flow_table); nf_flow_table_offload_flush_cleanup(flow_table); + + /* The gc pass above can still queue offload works whose handlers + * decrement the pernet statistics. Drain them all. + */ + nf_flow_table_offload_drain(); rhashtable_destroy(&flow_table->rhashtable); + + net = read_pnet(&flow_table->net); + if (net) { + bool release; + + spin_lock(&net->ft.lock); + release = net->ft.exiting && !--net->ft.nr_ft; + spin_unlock(&net->ft.lock); + + /* Released by the last flowtable teardown, see + * nf_flow_table_fini_net(). + */ + if (release) + free_percpu(net->ft.stat); + } } EXPORT_SYMBOL_GPL(nf_flow_table_free); static int nf_flow_table_init_net(struct net *net) { + spin_lock_init(&net->ft.lock); net->ft.stat = alloc_percpu(struct nf_flow_table_stat); return net->ft.stat ? 0 : -ENOMEM; } static void nf_flow_table_fini_net(struct net *net) { - free_percpu(net->ft.stat); + bool release; + + spin_lock(&net->ft.lock); + net->ft.exiting = true; + release = !net->ft.nr_ft; + spin_unlock(&net->ft.lock); + + /* Pending flowtables still update the statistics from workqueue + * context; the last one releases them, see nf_flow_table_free(). + */ + if (release) + free_percpu(net->ft.stat); } static int nf_flow_table_pernet_init(struct net *net) diff --git a/net/netfilter/nf_flow_table_offload.c b/net/netfilter/nf_flow_table_offload.c index 4365859220e6a..6ac28802ef632 100644 --- a/net/netfilter/nf_flow_table_offload.c +++ b/net/netfilter/nf_flow_table_offload.c @@ -1180,6 +1180,13 @@ void nf_flow_table_offload_flush(struct nf_flowtable *flowtable) } } +void nf_flow_table_offload_drain(void) +{ + flush_workqueue(nf_flow_offload_add_wq); + flush_workqueue(nf_flow_offload_del_wq); + flush_workqueue(nf_flow_offload_stats_wq); +} + static int nf_flow_table_block_setup(struct nf_flowtable *flowtable, struct flow_block_offload *bo, enum flow_block_command cmd) -- 2.54.0 (Apple Git-157)