* [v2 PATCH] rhashtable: Add rhashtable_flush_and_free helper @ 2026-09-12 10:16 Herbert Xu 2026-09-12 18:32 ` Florian Westphal 0 siblings, 1 reply; 7+ messages in thread From: Herbert Xu @ 2026-09-12 10:16 UTC (permalink / raw) To: Florian Westphal, kadlec, linux-crypto This patch adds the helper rhashtable_flush_and_free and its rhltable counter-part. The intended user is netfilter: https://sashiko.dev/#/patchset/20260828152256.8759-1-fw%40strlen.de The hash table will briefly become NULL during the flush call, in order to avoid race conditions against concurrent insertions and removals which may schedule new rehashes. The rest of the code has been updated to handle ht->tbl being NULL. In order to ensure that rehashes are fully disabled during the flush, the self-rearming in rht_deferred_worker is moved inside the mutex. Incidentally this allows the removal of the special hack where it had to bypass the IRQ work. However, as the existing rhashtable_free_and_destroy helper does not set the hash table to NULL, change it so that it uses disable_work_sync instead. If concurrent insertions and removals hit the old table during the call, they will act directly on the old table. If they see a NULL ht->tbl they shall fail immediately. Concurrent insertions and removals that see the new table will act on it. After setting ht->tbl to NULL, the new helper will wait for concurrent insertions and removals on the old ht->tbl to complete, and then cancel existing rehashes. Once rehashes are completely gone, the new table will be installed. Finally entries in the old table are freed. Signed-off-by: Herbert Xu <herbert@gondor.apana.org.au> diff --git a/include/linux/rhashtable.h b/include/linux/rhashtable.h index 6c5e6d9accba..edff647303bb 100644 --- a/include/linux/rhashtable.h +++ b/include/linux/rhashtable.h @@ -255,6 +255,11 @@ void rhashtable_free_and_destroy(struct rhashtable *ht, void *arg); void rhashtable_destroy(struct rhashtable *ht); + +void rhashtable_flush_and_free(struct rhashtable *ht, + void (*free_fn)(void *ptr, void *arg), + void *arg); + struct rhash_lock_head __rcu **rht_bucket_nested( const struct bucket_table *tbl, unsigned int hash); struct rhash_lock_head __rcu **__rht_bucket_nested( @@ -621,6 +626,9 @@ static __always_inline struct rhash_head *__rhashtable_lookup( BUILD_BUG_ON(!__builtin_constant_p(freq)); tbl = rht_dereference_rcu(ht->tbl, ht); + if (!tbl) + goto out; + restart: hash = rht_key_hashfn(ht, tbl, key, params); bkt = rht_bucket(tbl, hash); @@ -644,6 +652,7 @@ static __always_inline struct rhash_head *__rhashtable_lookup( if (unlikely(tbl)) goto restart; +out: return NULL; } @@ -763,12 +772,15 @@ static __always_inline void *__rhashtable_insert_fast( struct rhash_head *head; unsigned long flags; unsigned int hash; + void *data = NULL; int elasticity; - void *data; rcu_read_lock(); tbl = rht_dereference_rcu(ht->tbl, ht); + if (!tbl) + goto out; + hash = rht_head_hashfn(ht, tbl, obj, params); elasticity = RHT_ELASTICITY; bkt = rht_bucket_insert(ht, tbl, hash); @@ -1127,11 +1139,13 @@ static __always_inline int __rhashtable_remove_fast( const struct rhashtable_params params, bool rhlist) { struct bucket_table *tbl; - int err; + int err = -ENOENT; rcu_read_lock(); tbl = rht_dereference_rcu(ht->tbl, ht); + if (!tbl) + goto out; /* Because we have already taken (and released) the bucket * lock in old_tbl, if we find that future_tbl is not yet @@ -1143,6 +1157,7 @@ static __always_inline int __rhashtable_remove_fast( (tbl = rht_dereference_rcu(tbl->future_tbl, ht))) ; +out: rcu_read_unlock(); return err; @@ -1262,11 +1277,13 @@ static __always_inline int rhashtable_replace_fast( const struct rhashtable_params params) { struct bucket_table *tbl; - int err; + int err = -ENOENT; rcu_read_lock(); tbl = rht_dereference_rcu(ht->tbl, ht); + if (!tbl) + goto out; /* Because we have already taken (and released) the bucket * lock in old_tbl, if we find that future_tbl is not yet @@ -1278,6 +1295,7 @@ static __always_inline int rhashtable_replace_fast( (tbl = rht_dereference_rcu(tbl->future_tbl, ht))) ; +out: rcu_read_unlock(); return err; @@ -1331,4 +1349,19 @@ static inline void rhltable_destroy(struct rhltable *hlt) rhltable_free_and_destroy(hlt, NULL, NULL); } +/** + * rhltable_flush_and_free - unlink and free all elements in the hash list table + * @hlt: the hash list table to destroy + * @free_fn: callback to release resources of element + * @arg: pointer passed to free_fn + * + * See documentation for rhashtable_flush_and_free. + */ +static inline void rhltable_flush_and_free(struct rhltable *hlt, + void (*free_fn)(void *ptr, + void *arg), + void *arg) +{ + rhashtable_flush_and_free(&hlt->ht, free_fn, arg); +} #endif /* _LINUX_RHASHTABLE_H */ diff --git a/lib/rhashtable.c b/lib/rhashtable.c index 5da0e53a8d42..cf44e30f92cd 100644 --- a/lib/rhashtable.c +++ b/lib/rhashtable.c @@ -435,6 +435,9 @@ static void rht_deferred_worker(struct work_struct *work) mutex_lock_nested(&ht->mutex, 1); tbl = rht_dereference(ht->tbl, ht); + if (!tbl) + goto out; + tbl = rhashtable_last_table(ht, tbl); if (rht_grow_above_75(ht, tbl)) @@ -451,18 +454,11 @@ static void rht_deferred_worker(struct work_struct *work) err = err ?: nerr; } - mutex_unlock(&ht->mutex); - - /* - * Re-arm via @run_work, not @run_irq_work. - * rhashtable_free_and_destroy() drains async work as irq_work_sync() - * followed by cancel_work_sync(). If this site queued irq_work while - * cancel_work_sync() was waiting for us, irq_work_sync() would already - * have returned and the stale irq_work could fire post-teardown. - * cancel_work_sync() natively handles self-requeue on @run_work. - */ if (err) - schedule_work(&ht->run_work); + irq_work_queue(&ht->run_irq_work); + +out: + mutex_unlock(&ht->mutex); } /* @@ -489,6 +485,8 @@ static int rhashtable_insert_rehash(struct rhashtable *ht, int err; old_tbl = rht_dereference_rcu(ht->tbl, ht); + if (!old_tbl) + return -EAGAIN; size = tbl->size; @@ -636,6 +634,8 @@ static void *rhashtable_try_insert(struct rhashtable *ht, const void *key, void *data; new_tbl = rcu_dereference(ht->tbl); + if (!new_tbl) + return NULL; do { tbl = new_tbl; @@ -779,6 +779,8 @@ void *rhashtable_next_key(struct rhashtable *ht, const void *prev_key) return ERR_PTR(-EOPNOTSUPP); tbl = rht_dereference_rcu(ht->tbl, ht); + if (!tbl) + return NULL; do { he = __rhashtable_next_in_table(ht, tbl, prev_key); if (!IS_ERR_OR_NULL(he)) @@ -820,13 +822,15 @@ void rhashtable_walk_enter(struct rhashtable *ht, struct rhashtable_iter *iter) iter->p = NULL; iter->slot = 0; iter->skip = 0; - iter->end_of_table = 0; spin_lock(&ht->lock); iter->walker.tbl = rcu_dereference_protected(ht->tbl, lockdep_is_held(&ht->lock)); - list_add(&iter->walker.list, &iter->walker.tbl->walkers); + if (iter->walker.tbl) + list_add(&iter->walker.list, &iter->walker.tbl->walkers); spin_unlock(&ht->lock); + + iter->end_of_table = !iter->walker.tbl; } EXPORT_SYMBOL_GPL(rhashtable_walk_enter); @@ -880,6 +884,10 @@ int rhashtable_walk_start_check(struct rhashtable_iter *iter) return 0; if (!iter->walker.tbl) { iter->walker.tbl = rht_dereference_rcu(ht->tbl, ht); + if (!iter->walker.tbl) { + iter->end_of_table = true; + return 0; + } iter->slot = 0; iter->skip = 0; iter->p = NULL; @@ -1276,45 +1284,12 @@ static void rhashtable_free_one(struct rhashtable *ht, struct rhash_head *obj, } while (list); } -/** - * rhashtable_free_and_destroy - free elements and destroy hash table - * @ht: the hash table to destroy - * @free_fn: callback to release resources of element - * @arg: pointer passed to free_fn - * - * Stops an eventual async resize. If defined, invokes free_fn for each - * element to releasal resources. Please note that RCU protected - * readers may still be accessing the elements. Releasing of resources - * must occur in a compatible manner. Then frees the bucket array. - * - * This function will eventually sleep to wait for an async resize - * to complete. The caller is responsible that no further write operations - * occurs in parallel. - * - * After cancel_work_sync() has returned, the deferred rehash worker is - * quiesced and, per the contract above, no other concurrent access to the - * rhashtable is possible. The tables are therefore owned exclusively by - * this function and can be walked without ht->mutex held. - */ -void rhashtable_free_and_destroy(struct rhashtable *ht, - void (*free_fn)(void *ptr, void *arg), - void *arg) +static void rhashtable_free(struct rhashtable *ht, struct bucket_table *tbl, + void (*free_fn)(void *ptr, void *arg), void *arg) { - struct bucket_table *tbl, *next_tbl; + struct bucket_table *next_tbl; unsigned int i; - irq_work_sync(&ht->run_irq_work); - cancel_work_sync(&ht->run_work); - - /* - * Do NOT take ht->mutex here. The rehash worker establishes - * ht->mutex -> fs_reclaim via GFP_KERNEL bucket allocation under - * the mutex; callers on the reclaim path (e.g. simple_xattr_ht_free() - * from evict() under the dcache shrinker for shmem/kernfs/pidfs - * inodes) would otherwise close a circular dependency - * fs_reclaim -> ht->mutex. - */ - tbl = rcu_dereference_raw(ht->tbl); restart: if (free_fn) { for (i = 0; i < tbl->size; i++) { @@ -1339,6 +1314,36 @@ void rhashtable_free_and_destroy(struct rhashtable *ht, goto restart; } } + +/** + * rhashtable_free_and_destroy - free elements and destroy hash table + * @ht: the hash table to destroy + * @free_fn: callback to release resources of element + * @arg: pointer passed to free_fn + * + * Stops an eventual async resize. If defined, invokes free_fn for each + * element to releasal resources. Please note that RCU protected + * readers may still be accessing the elements. Releasing of resources + * must occur in a compatible manner. Then frees the bucket array. + * + * This function will eventually sleep to wait for an async resize + * to complete. The caller is responsible that no further write operations + * occurs in parallel. + * + * After disable_work_sync() has returned, the deferred rehash worker is + * quiesced and, per the contract above, no other concurrent access to the + * rhashtable is possible. The tables are therefore owned exclusively by + * this function and can be walked without ht->mutex held. + */ +void rhashtable_free_and_destroy(struct rhashtable *ht, + void (*free_fn)(void *ptr, void *arg), + void *arg) +{ + disable_work_sync(&ht->run_work); + irq_work_sync(&ht->run_irq_work); + + rhashtable_free(ht, rcu_dereference_raw(ht->tbl), free_fn, arg); +} EXPORT_SYMBOL_GPL(rhashtable_free_and_destroy); void rhashtable_destroy(struct rhashtable *ht) @@ -1415,3 +1420,55 @@ struct rhash_lock_head __rcu **rht_bucket_nested_insert( } EXPORT_SYMBOL_GPL(rht_bucket_nested_insert); + +/** + * rhashtable_flush_and_free - detach and discard all current elements + * @ht: the hash table to flush + * @free_fn: callback to release resources of an element, may be %NULL + * @arg: pointer passed to free_fn + * + * Swaps the bucket table backing @ht for a new, empty table. + * + * The detached table is then walked and every element found is + * unlinked, and, if @free_fn is given, handed to it for release. + * + * Unlike rhashtable_destroy(), @ht is left fully initialized and may + * continue to be used for lookups, insertions, and removals. + * + * This function may sleep, it cannot be called from atomic context or + * RCU read-side critical sections. + * + * The caller must not make concurrent rhashtable_flush_and_free calls. + * That is, a subsequent call can only be made once the first call has + * returned. + */ +void rhashtable_flush_and_free(struct rhashtable *ht, + void (*free_fn)(void *ptr, void *arg), + void *arg) +{ + struct bucket_table *old_tbl, *new_tbl; + + new_tbl = bucket_table_alloc(ht, rounded_hashtable_size(&ht->p), + GFP_KERNEL); + if (!new_tbl) + new_tbl = bucket_table_alloc(ht, ht->p.min_size, + GFP_KERNEL | __GFP_NOFAIL); + + mutex_lock(&ht->mutex); + old_tbl = rht_dereference(ht->tbl, ht); + RCU_INIT_POINTER(ht->tbl, NULL); + mutex_unlock(&ht->mutex); + + /* Wait for insertions/removals on the old ht->tbl to complete. */ + synchronize_rcu(); + + /* Cancel resizes/rehashes. */ + irq_work_sync(&ht->run_irq_work); + cancel_work_sync(&ht->run_work); + + /* No need for mutex as rehashes have all stopped. */ + rcu_assign_pointer(ht->tbl, new_tbl); + + rhashtable_free(ht, old_tbl, free_fn, arg); +} +EXPORT_SYMBOL_GPL(rhashtable_flush_and_free); Cheers, -- Email: Herbert Xu <herbert@gondor.apana.org.au> Home Page: http://gondor.apana.org.au/~herbert/ PGP Key: http://gondor.apana.org.au/~herbert/pubkey.txt ^ permalink raw reply related [flat|nested] 7+ messages in thread
* Re: [v2 PATCH] rhashtable: Add rhashtable_flush_and_free helper 2026-09-12 10:16 [v2 PATCH] rhashtable: Add rhashtable_flush_and_free helper Herbert Xu @ 2026-09-12 18:32 ` Florian Westphal 2026-09-12 23:27 ` Herbert Xu 0 siblings, 1 reply; 7+ messages in thread From: Florian Westphal @ 2026-09-12 18:32 UTC (permalink / raw) To: Herbert Xu; +Cc: kadlec, linux-crypto Herbert Xu <herbert@gondor.apana.org.au> wrote: > This patch adds the helper rhashtable_flush_and_free and its > rhltable counter-part. The intended user is netfilter: > > https://sashiko.dev/#/patchset/20260828152256.8759-1-fw%40strlen.de Thanks for working on this. > The hash table will briefly become NULL during the flush call, > in order to avoid race conditions against concurrent insertions > and removals which may schedule new rehashes. When you send next version, could you elaborate a bit why this is required? rht_deferred_worker grabs the ht mutex, so I don't understand this problem. Worst case is a single, "useless" rehash of a most likely empty (or at least mostly empty) table. I don't see why the mutex_lock(&ht->mutex); old_tbl = rcu_replace_pointer(ht->tbl, new_tbl, lockdep_rht_mutex_is_held(ht)); is bad. It avoids the NULL checks and concurrent insertions will work. From your previous comments, there are two cases: 1. rehash gets queued, then flush was called (before worker runs). This result in a "useless" rehash of a mostly empty table, but I don't see the problem with this (flushes are rare). 2. flush -> table replaced with empty one; *then* a rehash gets queued (e.g. because of parallel mass insert into the new, now not-so-empty-anymore table). In this second case, the behaviour is the same as if no flush would have happened. -EBUSY is always possible regardless of a flush happening or not. Also, a possible "wrong" -EBUSY is certainly better than guaranteed -EBUSY during the synchronize_rcu() call? That said, if it is really needed then this "guarenteed -EBUSY" is fine for my use case: for insertions from kernel/(iptables -j SET target), failure is already tolerated / acceptable, and insertions from userspace serialize on the same netlink mutex as the flush request. ^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [v2 PATCH] rhashtable: Add rhashtable_flush_and_free helper 2026-09-12 18:32 ` Florian Westphal @ 2026-09-12 23:27 ` Herbert Xu 2026-09-13 0:12 ` [v3 " Herbert Xu 0 siblings, 1 reply; 7+ messages in thread From: Herbert Xu @ 2026-09-12 23:27 UTC (permalink / raw) To: Florian Westphal; +Cc: kadlec, linux-crypto On Sat, Sep 12, 2026 at 08:32:44PM +0200, Florian Westphal wrote: > > When you send next version, could you elaborate a bit why this is > required? rht_deferred_worker grabs the ht mutex, so I don't understand > this problem. Worst case is a single, > "useless" rehash of a most likely empty (or at least mostly empty) table. You're right. I mistakenly thought that a scheduled rehash may cause a rehash without resizing the table, but that's not how it works since only insertions can trigger a direct rehash, and that doesn't even happen in the delayed worker. Let me see if I can simplify this. Thanks, -- Email: Herbert Xu <herbert@gondor.apana.org.au> Home Page: http://gondor.apana.org.au/~herbert/ PGP Key: http://gondor.apana.org.au/~herbert/pubkey.txt ^ permalink raw reply [flat|nested] 7+ messages in thread
* [v3 PATCH] rhashtable: Add rhashtable_flush_and_free helper 2026-09-12 23:27 ` Herbert Xu @ 2026-09-13 0:12 ` Herbert Xu 2026-09-13 1:34 ` [v4 " Herbert Xu 0 siblings, 1 reply; 7+ messages in thread From: Herbert Xu @ 2026-09-13 0:12 UTC (permalink / raw) To: Florian Westphal; +Cc: kadlec, linux-crypto v3 makes the following changes: - Return -ESTALE when insertion sees a NULL ht->tbl so the entry doesn't leak. - Reset nelems to zero before publishing new ht->tbl. - Detach all walkers from bucket table before freeing. Note that I've kept the two-stage ht->tbl update, because even though it's not required for rehashing, I can't find a way to reset nelems properly without doing this. The issue is again that insertions occur locklessly and it updates nelems. So we must quiesce existing insertions before we can update nelems. ---8<--- This patch adds the helper rhashtable_flush_and_free and its rhltable counter-part. The intended user is netfilter: https://sashiko.dev/#/patchset/20260828152256.8759-1-fw%40strlen.de The hash table briefly becomes NULL during the flush call in order to quiesce insertions on the old table so that ht->nelems can be reset to zero safely. The rest of the code is updated to handle ht->tbl being NULL. In order to ensure that rehashes are fully disabled during the flush, the self-rearming in rht_deferred_worker is moved inside the mutex. Incidentally, this allows the removal of the special hack where it had to bypass the IRQ work. However, as the existing rhashtable_free_and_destroy helper does not set the hash table to NULL, change it to use disable_work_sync instead. If concurrent insertions and removals hit the old table during the call, they act directly on the old table. If they see a NULL ht->tbl, they fail immediately. Concurrent insertions and removals that see the new table act on it. After setting ht->tbl to NULL, the new helper waits for concurrent insertions and removals on the old ht->tbl to complete, and then cancels existing rehashes. Once rehashes are completely gone, ht->nelems is reset to zero, and the new table is installed. Finally, walkers are detached from the old table, and the entries in the old table are freed. Signed-off-by: Herbert Xu <herbert@gondor.apana.org.au> diff --git a/include/linux/rhashtable.h b/include/linux/rhashtable.h index 6c5e6d9accba..43d6e2cc8be2 100644 --- a/include/linux/rhashtable.h +++ b/include/linux/rhashtable.h @@ -255,6 +255,11 @@ void rhashtable_free_and_destroy(struct rhashtable *ht, void *arg); void rhashtable_destroy(struct rhashtable *ht); + +void rhashtable_flush_and_free(struct rhashtable *ht, + void (*free_fn)(void *ptr, void *arg), + void *arg); + struct rhash_lock_head __rcu **rht_bucket_nested( const struct bucket_table *tbl, unsigned int hash); struct rhash_lock_head __rcu **__rht_bucket_nested( @@ -621,6 +626,9 @@ static __always_inline struct rhash_head *__rhashtable_lookup( BUILD_BUG_ON(!__builtin_constant_p(freq)); tbl = rht_dereference_rcu(ht->tbl, ht); + if (!tbl) + goto out; + restart: hash = rht_key_hashfn(ht, tbl, key, params); bkt = rht_bucket(tbl, hash); @@ -644,6 +652,7 @@ static __always_inline struct rhash_head *__rhashtable_lookup( if (unlikely(tbl)) goto restart; +out: return NULL; } @@ -759,16 +768,19 @@ static __always_inline void *__rhashtable_insert_fast( }; struct rhash_lock_head __rcu **bkt; struct rhash_head __rcu **pprev; + void *data = ERR_PTR(-ESTALE); struct bucket_table *tbl; struct rhash_head *head; unsigned long flags; unsigned int hash; int elasticity; - void *data; rcu_read_lock(); tbl = rht_dereference_rcu(ht->tbl, ht); + if (!tbl) + goto out; + hash = rht_head_hashfn(ht, tbl, obj, params); elasticity = RHT_ELASTICITY; bkt = rht_bucket_insert(ht, tbl, hash); @@ -1127,11 +1139,13 @@ static __always_inline int __rhashtable_remove_fast( const struct rhashtable_params params, bool rhlist) { struct bucket_table *tbl; - int err; + int err = -ENOENT; rcu_read_lock(); tbl = rht_dereference_rcu(ht->tbl, ht); + if (!tbl) + goto out; /* Because we have already taken (and released) the bucket * lock in old_tbl, if we find that future_tbl is not yet @@ -1143,6 +1157,7 @@ static __always_inline int __rhashtable_remove_fast( (tbl = rht_dereference_rcu(tbl->future_tbl, ht))) ; +out: rcu_read_unlock(); return err; @@ -1262,11 +1277,13 @@ static __always_inline int rhashtable_replace_fast( const struct rhashtable_params params) { struct bucket_table *tbl; - int err; + int err = -ENOENT; rcu_read_lock(); tbl = rht_dereference_rcu(ht->tbl, ht); + if (!tbl) + goto out; /* Because we have already taken (and released) the bucket * lock in old_tbl, if we find that future_tbl is not yet @@ -1278,6 +1295,7 @@ static __always_inline int rhashtable_replace_fast( (tbl = rht_dereference_rcu(tbl->future_tbl, ht))) ; +out: rcu_read_unlock(); return err; @@ -1331,4 +1349,19 @@ static inline void rhltable_destroy(struct rhltable *hlt) rhltable_free_and_destroy(hlt, NULL, NULL); } +/** + * rhltable_flush_and_free - unlink and free all elements in the hash list table + * @hlt: the hash list table to destroy + * @free_fn: callback to release resources of element + * @arg: pointer passed to free_fn + * + * See documentation for rhashtable_flush_and_free. + */ +static inline void rhltable_flush_and_free(struct rhltable *hlt, + void (*free_fn)(void *ptr, + void *arg), + void *arg) +{ + rhashtable_flush_and_free(&hlt->ht, free_fn, arg); +} #endif /* _LINUX_RHASHTABLE_H */ diff --git a/lib/rhashtable.c b/lib/rhashtable.c index 5da0e53a8d42..a1616ec9905c 100644 --- a/lib/rhashtable.c +++ b/lib/rhashtable.c @@ -435,6 +435,9 @@ static void rht_deferred_worker(struct work_struct *work) mutex_lock_nested(&ht->mutex, 1); tbl = rht_dereference(ht->tbl, ht); + if (!tbl) + goto out; + tbl = rhashtable_last_table(ht, tbl); if (rht_grow_above_75(ht, tbl)) @@ -451,18 +454,11 @@ static void rht_deferred_worker(struct work_struct *work) err = err ?: nerr; } - mutex_unlock(&ht->mutex); - - /* - * Re-arm via @run_work, not @run_irq_work. - * rhashtable_free_and_destroy() drains async work as irq_work_sync() - * followed by cancel_work_sync(). If this site queued irq_work while - * cancel_work_sync() was waiting for us, irq_work_sync() would already - * have returned and the stale irq_work could fire post-teardown. - * cancel_work_sync() natively handles self-requeue on @run_work. - */ if (err) - schedule_work(&ht->run_work); + irq_work_queue(&ht->run_irq_work); + +out: + mutex_unlock(&ht->mutex); } /* @@ -489,6 +485,8 @@ static int rhashtable_insert_rehash(struct rhashtable *ht, int err; old_tbl = rht_dereference_rcu(ht->tbl, ht); + if (!old_tbl) + return -EAGAIN; size = tbl->size; @@ -636,6 +634,8 @@ static void *rhashtable_try_insert(struct rhashtable *ht, const void *key, void *data; new_tbl = rcu_dereference(ht->tbl); + if (!new_tbl) + return ERR_PTR(-ESTALE); do { tbl = new_tbl; @@ -779,6 +779,8 @@ void *rhashtable_next_key(struct rhashtable *ht, const void *prev_key) return ERR_PTR(-EOPNOTSUPP); tbl = rht_dereference_rcu(ht->tbl, ht); + if (!tbl) + return NULL; do { he = __rhashtable_next_in_table(ht, tbl, prev_key); if (!IS_ERR_OR_NULL(he)) @@ -820,13 +822,15 @@ void rhashtable_walk_enter(struct rhashtable *ht, struct rhashtable_iter *iter) iter->p = NULL; iter->slot = 0; iter->skip = 0; - iter->end_of_table = 0; spin_lock(&ht->lock); iter->walker.tbl = rcu_dereference_protected(ht->tbl, lockdep_is_held(&ht->lock)); - list_add(&iter->walker.list, &iter->walker.tbl->walkers); + if (iter->walker.tbl) + list_add(&iter->walker.list, &iter->walker.tbl->walkers); spin_unlock(&ht->lock); + + iter->end_of_table = !iter->walker.tbl; } EXPORT_SYMBOL_GPL(rhashtable_walk_enter); @@ -880,6 +884,10 @@ int rhashtable_walk_start_check(struct rhashtable_iter *iter) return 0; if (!iter->walker.tbl) { iter->walker.tbl = rht_dereference_rcu(ht->tbl, ht); + if (!iter->walker.tbl) { + iter->end_of_table = true; + return 0; + } iter->slot = 0; iter->skip = 0; iter->p = NULL; @@ -1276,46 +1284,24 @@ static void rhashtable_free_one(struct rhashtable *ht, struct rhash_head *obj, } while (list); } -/** - * rhashtable_free_and_destroy - free elements and destroy hash table - * @ht: the hash table to destroy - * @free_fn: callback to release resources of element - * @arg: pointer passed to free_fn - * - * Stops an eventual async resize. If defined, invokes free_fn for each - * element to releasal resources. Please note that RCU protected - * readers may still be accessing the elements. Releasing of resources - * must occur in a compatible manner. Then frees the bucket array. - * - * This function will eventually sleep to wait for an async resize - * to complete. The caller is responsible that no further write operations - * occurs in parallel. - * - * After cancel_work_sync() has returned, the deferred rehash worker is - * quiesced and, per the contract above, no other concurrent access to the - * rhashtable is possible. The tables are therefore owned exclusively by - * this function and can be walked without ht->mutex held. - */ -void rhashtable_free_and_destroy(struct rhashtable *ht, - void (*free_fn)(void *ptr, void *arg), - void *arg) +static void rhashtable_free(struct rhashtable *ht, struct bucket_table *tbl, + void (*free_fn)(void *ptr, void *arg), void *arg) { - struct bucket_table *tbl, *next_tbl; + struct rhashtable_walker *walker; + struct bucket_table *next_tbl; unsigned int i; - irq_work_sync(&ht->run_irq_work); - cancel_work_sync(&ht->run_work); - - /* - * Do NOT take ht->mutex here. The rehash worker establishes - * ht->mutex -> fs_reclaim via GFP_KERNEL bucket allocation under - * the mutex; callers on the reclaim path (e.g. simple_xattr_ht_free() - * from evict() under the dcache shrinker for shmem/kernfs/pidfs - * inodes) would otherwise close a circular dependency - * fs_reclaim -> ht->mutex. - */ - tbl = rcu_dereference_raw(ht->tbl); restart: + spin_lock(&ht->lock); + list_for_each_entry(walker, &tbl->walkers, list) { + struct rhashtable_iter *iter; + + walker->tbl = NULL; + iter = container_of(walker, struct rhashtable_iter, walker); + iter->end_of_table = true; + } + spin_unlock(&ht->lock); + if (free_fn) { for (i = 0; i < tbl->size; i++) { struct rhash_head *pos, *next; @@ -1339,6 +1325,36 @@ void rhashtable_free_and_destroy(struct rhashtable *ht, goto restart; } } + +/** + * rhashtable_free_and_destroy - free elements and destroy hash table + * @ht: the hash table to destroy + * @free_fn: callback to release resources of element + * @arg: pointer passed to free_fn + * + * Stops an eventual async resize. If defined, invokes free_fn for each + * element to releasal resources. Please note that RCU protected + * readers may still be accessing the elements. Releasing of resources + * must occur in a compatible manner. Then frees the bucket array. + * + * This function will eventually sleep to wait for an async resize + * to complete. The caller is responsible that no further write operations + * occurs in parallel. + * + * After disable_work_sync() has returned, the deferred rehash worker is + * quiesced and, per the contract above, no other concurrent access to the + * rhashtable is possible. The tables are therefore owned exclusively by + * this function and can be walked without ht->mutex held. + */ +void rhashtable_free_and_destroy(struct rhashtable *ht, + void (*free_fn)(void *ptr, void *arg), + void *arg) +{ + disable_work_sync(&ht->run_work); + irq_work_sync(&ht->run_irq_work); + + rhashtable_free(ht, rcu_dereference_raw(ht->tbl), free_fn, arg); +} EXPORT_SYMBOL_GPL(rhashtable_free_and_destroy); void rhashtable_destroy(struct rhashtable *ht) @@ -1415,3 +1431,57 @@ struct rhash_lock_head __rcu **rht_bucket_nested_insert( } EXPORT_SYMBOL_GPL(rht_bucket_nested_insert); + +/** + * rhashtable_flush_and_free - detach and discard all current elements + * @ht: the hash table to flush + * @free_fn: callback to release resources of an element, may be %NULL + * @arg: pointer passed to free_fn + * + * Swaps the bucket table backing @ht for a new, empty table. + * + * The detached table is then walked and every element found is + * unlinked, and, if @free_fn is given, handed to it for release. + * + * Unlike rhashtable_destroy(), @ht is left fully initialized and may + * continue to be used for lookups, insertions, and removals. + * + * This function may sleep, it cannot be called from atomic context or + * RCU read-side critical sections. + * + * The caller must not make concurrent rhashtable_flush_and_free calls. + * That is, a subsequent call can only be made once the first call has + * returned. + */ +void rhashtable_flush_and_free(struct rhashtable *ht, + void (*free_fn)(void *ptr, void *arg), + void *arg) +{ + struct bucket_table *old_tbl, *new_tbl; + + new_tbl = bucket_table_alloc(ht, rounded_hashtable_size(&ht->p), + GFP_KERNEL); + if (!new_tbl) + new_tbl = bucket_table_alloc(ht, ht->p.min_size, + GFP_KERNEL | __GFP_NOFAIL); + + mutex_lock(&ht->mutex); + old_tbl = rht_dereference(ht->tbl, ht); + RCU_INIT_POINTER(ht->tbl, NULL); + mutex_unlock(&ht->mutex); + + /* Wait for insertions/removals on the old ht->tbl to complete. */ + synchronize_rcu(); + + /* Cancel resizes/rehashes. */ + irq_work_sync(&ht->run_irq_work); + cancel_work_sync(&ht->run_work); + + atomic_set(&ht->nelems, 0); + + /* No need for mutex as rehashes have all stopped. */ + rcu_assign_pointer(ht->tbl, new_tbl); + + rhashtable_free(ht, old_tbl, free_fn, arg); +} +EXPORT_SYMBOL_GPL(rhashtable_flush_and_free); Cheers, -- Email: Herbert Xu <herbert@gondor.apana.org.au> Home Page: http://gondor.apana.org.au/~herbert/ PGP Key: http://gondor.apana.org.au/~herbert/pubkey.txt ^ permalink raw reply related [flat|nested] 7+ messages in thread
* [v4 PATCH] rhashtable: Add rhashtable_flush_and_free helper 2026-09-13 0:12 ` [v3 " Herbert Xu @ 2026-09-13 1:34 ` Herbert Xu 2026-09-13 2:53 ` [v5 " Herbert Xu 0 siblings, 1 reply; 7+ messages in thread From: Herbert Xu @ 2026-09-13 1:34 UTC (permalink / raw) To: Florian Westphal; +Cc: kadlec, linux-crypto v4 makes the following changes: - Set iter->p to NULL when detaching walkers. - Wait for another RCU grace period after detaching walkers. ---8<--- This patch adds the helper rhashtable_flush_and_free and its rhltable counter-part. The intended user is netfilter: https://sashiko.dev/#/patchset/20260828152256.8759-1-fw%40strlen.de The hash table briefly becomes NULL during the flush call in order to quiesce insertions on the old table so that ht->nelems can be reset to zero safely. The rest of the code is updated to handle ht->tbl being NULL. In order to ensure that rehashes are fully disabled during the flush, the self-rearming in rht_deferred_worker is moved inside the mutex. Incidentally, this allows the removal of the special hack where it had to bypass the IRQ work. However, as the existing rhashtable_free_and_destroy helper does not set the hash table to NULL, change it to use disable_work_sync instead. If concurrent insertions and removals hit the old table during the call, they act directly on the old table. If they see a NULL ht->tbl, they fail immediately. Concurrent insertions and removals that see the new table act on it. After setting ht->tbl to NULL, the new helper waits for concurrent insertions and removals on the old ht->tbl to complete, and then cancels existing rehashes. Once rehashes are completely gone, ht->nelems is reset to zero, and the new table is installed. Finally, walkers are detached from the old table, and the entries in the old table are freed. Signed-off-by: Herbert Xu <herbert@gondor.apana.org.au> diff --git a/include/linux/rhashtable.h b/include/linux/rhashtable.h index 6c5e6d9accba..9b31ebd340e3 100644 --- a/include/linux/rhashtable.h +++ b/include/linux/rhashtable.h @@ -255,6 +255,10 @@ void rhashtable_free_and_destroy(struct rhashtable *ht, void *arg); void rhashtable_destroy(struct rhashtable *ht); +void rhashtable_flush_and_free(struct rhashtable *ht, + void (*free_fn)(void *ptr, void *arg), + void *arg); + struct rhash_lock_head __rcu **rht_bucket_nested( const struct bucket_table *tbl, unsigned int hash); struct rhash_lock_head __rcu **__rht_bucket_nested( @@ -621,6 +625,9 @@ static __always_inline struct rhash_head *__rhashtable_lookup( BUILD_BUG_ON(!__builtin_constant_p(freq)); tbl = rht_dereference_rcu(ht->tbl, ht); + if (!tbl) + goto out; + restart: hash = rht_key_hashfn(ht, tbl, key, params); bkt = rht_bucket(tbl, hash); @@ -644,6 +651,7 @@ static __always_inline struct rhash_head *__rhashtable_lookup( if (unlikely(tbl)) goto restart; +out: return NULL; } @@ -759,16 +767,19 @@ static __always_inline void *__rhashtable_insert_fast( }; struct rhash_lock_head __rcu **bkt; struct rhash_head __rcu **pprev; + void *data = ERR_PTR(-ESTALE); struct bucket_table *tbl; struct rhash_head *head; unsigned long flags; unsigned int hash; int elasticity; - void *data; rcu_read_lock(); tbl = rht_dereference_rcu(ht->tbl, ht); + if (!tbl) + goto out; + hash = rht_head_hashfn(ht, tbl, obj, params); elasticity = RHT_ELASTICITY; bkt = rht_bucket_insert(ht, tbl, hash); @@ -1127,11 +1138,13 @@ static __always_inline int __rhashtable_remove_fast( const struct rhashtable_params params, bool rhlist) { struct bucket_table *tbl; - int err; + int err = -ENOENT; rcu_read_lock(); tbl = rht_dereference_rcu(ht->tbl, ht); + if (!tbl) + goto out; /* Because we have already taken (and released) the bucket * lock in old_tbl, if we find that future_tbl is not yet @@ -1143,6 +1156,7 @@ static __always_inline int __rhashtable_remove_fast( (tbl = rht_dereference_rcu(tbl->future_tbl, ht))) ; +out: rcu_read_unlock(); return err; @@ -1262,11 +1276,13 @@ static __always_inline int rhashtable_replace_fast( const struct rhashtable_params params) { struct bucket_table *tbl; - int err; + int err = -ENOENT; rcu_read_lock(); tbl = rht_dereference_rcu(ht->tbl, ht); + if (!tbl) + goto out; /* Because we have already taken (and released) the bucket * lock in old_tbl, if we find that future_tbl is not yet @@ -1278,6 +1294,7 @@ static __always_inline int rhashtable_replace_fast( (tbl = rht_dereference_rcu(tbl->future_tbl, ht))) ; +out: rcu_read_unlock(); return err; @@ -1331,4 +1348,19 @@ static inline void rhltable_destroy(struct rhltable *hlt) rhltable_free_and_destroy(hlt, NULL, NULL); } +/** + * rhltable_flush_and_free - unlink and free all elements in the hash list table + * @hlt: the hash list table to destroy + * @free_fn: callback to release resources of element + * @arg: pointer passed to free_fn + * + * See documentation for rhashtable_flush_and_free. + */ +static inline void rhltable_flush_and_free(struct rhltable *hlt, + void (*free_fn)(void *ptr, + void *arg), + void *arg) +{ + rhashtable_flush_and_free(&hlt->ht, free_fn, arg); +} #endif /* _LINUX_RHASHTABLE_H */ diff --git a/lib/rhashtable.c b/lib/rhashtable.c index 5da0e53a8d42..6f39fdce0233 100644 --- a/lib/rhashtable.c +++ b/lib/rhashtable.c @@ -435,6 +435,9 @@ static void rht_deferred_worker(struct work_struct *work) mutex_lock_nested(&ht->mutex, 1); tbl = rht_dereference(ht->tbl, ht); + if (!tbl) + goto out; + tbl = rhashtable_last_table(ht, tbl); if (rht_grow_above_75(ht, tbl)) @@ -451,18 +454,11 @@ static void rht_deferred_worker(struct work_struct *work) err = err ?: nerr; } - mutex_unlock(&ht->mutex); - - /* - * Re-arm via @run_work, not @run_irq_work. - * rhashtable_free_and_destroy() drains async work as irq_work_sync() - * followed by cancel_work_sync(). If this site queued irq_work while - * cancel_work_sync() was waiting for us, irq_work_sync() would already - * have returned and the stale irq_work could fire post-teardown. - * cancel_work_sync() natively handles self-requeue on @run_work. - */ if (err) - schedule_work(&ht->run_work); + irq_work_queue(&ht->run_irq_work); + +out: + mutex_unlock(&ht->mutex); } /* @@ -489,6 +485,8 @@ static int rhashtable_insert_rehash(struct rhashtable *ht, int err; old_tbl = rht_dereference_rcu(ht->tbl, ht); + if (!old_tbl) + return -EAGAIN; size = tbl->size; @@ -636,6 +634,8 @@ static void *rhashtable_try_insert(struct rhashtable *ht, const void *key, void *data; new_tbl = rcu_dereference(ht->tbl); + if (!new_tbl) + return ERR_PTR(-ESTALE); do { tbl = new_tbl; @@ -779,6 +779,8 @@ void *rhashtable_next_key(struct rhashtable *ht, const void *prev_key) return ERR_PTR(-EOPNOTSUPP); tbl = rht_dereference_rcu(ht->tbl, ht); + if (!tbl) + return NULL; do { he = __rhashtable_next_in_table(ht, tbl, prev_key); if (!IS_ERR_OR_NULL(he)) @@ -820,13 +822,15 @@ void rhashtable_walk_enter(struct rhashtable *ht, struct rhashtable_iter *iter) iter->p = NULL; iter->slot = 0; iter->skip = 0; - iter->end_of_table = 0; spin_lock(&ht->lock); iter->walker.tbl = rcu_dereference_protected(ht->tbl, lockdep_is_held(&ht->lock)); - list_add(&iter->walker.list, &iter->walker.tbl->walkers); + if (iter->walker.tbl) + list_add(&iter->walker.list, &iter->walker.tbl->walkers); spin_unlock(&ht->lock); + + iter->end_of_table = !iter->walker.tbl; } EXPORT_SYMBOL_GPL(rhashtable_walk_enter); @@ -880,6 +884,10 @@ int rhashtable_walk_start_check(struct rhashtable_iter *iter) return 0; if (!iter->walker.tbl) { iter->walker.tbl = rht_dereference_rcu(ht->tbl, ht); + if (!iter->walker.tbl) { + iter->end_of_table = true; + return 0; + } iter->slot = 0; iter->skip = 0; iter->p = NULL; @@ -1276,45 +1284,12 @@ static void rhashtable_free_one(struct rhashtable *ht, struct rhash_head *obj, } while (list); } -/** - * rhashtable_free_and_destroy - free elements and destroy hash table - * @ht: the hash table to destroy - * @free_fn: callback to release resources of element - * @arg: pointer passed to free_fn - * - * Stops an eventual async resize. If defined, invokes free_fn for each - * element to releasal resources. Please note that RCU protected - * readers may still be accessing the elements. Releasing of resources - * must occur in a compatible manner. Then frees the bucket array. - * - * This function will eventually sleep to wait for an async resize - * to complete. The caller is responsible that no further write operations - * occurs in parallel. - * - * After cancel_work_sync() has returned, the deferred rehash worker is - * quiesced and, per the contract above, no other concurrent access to the - * rhashtable is possible. The tables are therefore owned exclusively by - * this function and can be walked without ht->mutex held. - */ -void rhashtable_free_and_destroy(struct rhashtable *ht, - void (*free_fn)(void *ptr, void *arg), - void *arg) +static void rhashtable_free(struct rhashtable *ht, struct bucket_table *tbl, + void (*free_fn)(void *ptr, void *arg), void *arg) { - struct bucket_table *tbl, *next_tbl; + struct bucket_table *next_tbl; unsigned int i; - irq_work_sync(&ht->run_irq_work); - cancel_work_sync(&ht->run_work); - - /* - * Do NOT take ht->mutex here. The rehash worker establishes - * ht->mutex -> fs_reclaim via GFP_KERNEL bucket allocation under - * the mutex; callers on the reclaim path (e.g. simple_xattr_ht_free() - * from evict() under the dcache shrinker for shmem/kernfs/pidfs - * inodes) would otherwise close a circular dependency - * fs_reclaim -> ht->mutex. - */ - tbl = rcu_dereference_raw(ht->tbl); restart: if (free_fn) { for (i = 0; i < tbl->size; i++) { @@ -1339,6 +1314,36 @@ void rhashtable_free_and_destroy(struct rhashtable *ht, goto restart; } } + +/** + * rhashtable_free_and_destroy - free elements and destroy hash table + * @ht: the hash table to destroy + * @free_fn: callback to release resources of element + * @arg: pointer passed to free_fn + * + * Stops an eventual async resize. If defined, invokes free_fn for each + * element to releasal resources. Please note that RCU protected + * readers may still be accessing the elements. Releasing of resources + * must occur in a compatible manner. Then frees the bucket array. + * + * This function will eventually sleep to wait for an async resize + * to complete. The caller is responsible that no further write operations + * occurs in parallel. + * + * After disable_work_sync() has returned, the deferred rehash worker is + * quiesced and, per the contract above, no other concurrent access to the + * rhashtable is possible. The tables are therefore owned exclusively by + * this function and can be walked without ht->mutex held. + */ +void rhashtable_free_and_destroy(struct rhashtable *ht, + void (*free_fn)(void *ptr, void *arg), + void *arg) +{ + disable_work_sync(&ht->run_work); + irq_work_sync(&ht->run_irq_work); + + rhashtable_free(ht, rcu_dereference_raw(ht->tbl), free_fn, arg); +} EXPORT_SYMBOL_GPL(rhashtable_free_and_destroy); void rhashtable_destroy(struct rhashtable *ht) @@ -1415,3 +1420,78 @@ struct rhash_lock_head __rcu **rht_bucket_nested_insert( } EXPORT_SYMBOL_GPL(rht_bucket_nested_insert); + +/** + * rhashtable_flush_and_free - detach and discard all current elements + * @ht: the hash table to flush + * @free_fn: callback to release resources of an element, may be %NULL + * @arg: pointer passed to free_fn + * + * Swaps the bucket table backing @ht for a new, empty table. + * + * The detached table is then walked and every element found is + * unlinked, and, if @free_fn is given, handed to it for release. + * + * Unlike rhashtable_destroy(), @ht is left fully initialized and may + * continue to be used for lookups, insertions, and removals. + * + * This function may sleep, it cannot be called from atomic context or + * RCU read-side critical sections. + * + * The caller must not make concurrent rhashtable_flush_and_free calls. + * That is, a subsequent call can only be made once the first call has + * returned. + */ +void rhashtable_flush_and_free(struct rhashtable *ht, + void (*free_fn)(void *ptr, void *arg), + void *arg) +{ + struct bucket_table *tbl, *old_tbl, *new_tbl; + struct rhashtable_walker *walker; + + new_tbl = bucket_table_alloc(ht, rounded_hashtable_size(&ht->p), + GFP_KERNEL); + if (!new_tbl) + new_tbl = bucket_table_alloc(ht, ht->p.min_size, + GFP_KERNEL | __GFP_NOFAIL); + + mutex_lock(&ht->mutex); + old_tbl = rht_dereference(ht->tbl, ht); + RCU_INIT_POINTER(ht->tbl, NULL); + mutex_unlock(&ht->mutex); + + /* Wait for insertions/removals on the old ht->tbl to complete. */ + synchronize_rcu(); + + /* Cancel resizes/rehashes. */ + irq_work_sync(&ht->run_irq_work); + cancel_work_sync(&ht->run_work); + + atomic_set(&ht->nelems, 0); + + /* No need for mutex as rehashes have all stopped. */ + rcu_assign_pointer(ht->tbl, new_tbl); + + /* Detach walkers from the old hash table. */ + tbl = old_tbl; + spin_lock(&ht->lock); + do { + list_for_each_entry(walker, &tbl->walkers, list) { + struct rhashtable_iter *iter = container_of( + walker, struct rhashtable_iter, walker); + + walker->tbl = NULL; + iter->end_of_table = true; + iter->p = NULL; + } + + tbl = rcu_dereference_raw(tbl->future_tbl); + } while (tbl); + spin_unlock(&ht->lock); + + /* Wait for walkers on old table to complete. */ + synchronize_rcu(); + + rhashtable_free(ht, old_tbl, free_fn, arg); +} +EXPORT_SYMBOL_GPL(rhashtable_flush_and_free); Cheers, -- Email: Herbert Xu <herbert@gondor.apana.org.au> Home Page: http://gondor.apana.org.au/~herbert/ PGP Key: http://gondor.apana.org.au/~herbert/pubkey.txt ^ permalink raw reply related [flat|nested] 7+ messages in thread
* [v5 PATCH] rhashtable: Add rhashtable_flush_and_free helper 2026-09-13 1:34 ` [v4 " Herbert Xu @ 2026-09-13 2:53 ` Herbert Xu 2026-09-13 6:31 ` Florian Westphal 0 siblings, 1 reply; 7+ messages in thread From: Herbert Xu @ 2026-09-13 2:53 UTC (permalink / raw) To: Florian Westphal; +Cc: kadlec, linux-crypto v5 makes the following changes: - Add bucket_table dying flag to fix walker race. ---8<--- This patch adds the helper rhashtable_flush_and_free and its rhltable counter-part. The intended user is netfilter: https://sashiko.dev/#/patchset/20260828152256.8759-1-fw%40strlen.de The hash table briefly becomes NULL during the flush call in order to quiesce insertions on the old table so that ht->nelems can be reset to zero safely. The rest of the code is updated to handle ht->tbl being NULL. In order to ensure that rehashes are fully disabled during the flush, the self-rearming in rht_deferred_worker is moved inside the mutex. Incidentally, this allows the removal of the special hack where it had to bypass the IRQ work. However, as the existing rhashtable_free_and_destroy helper does not set the hash table to NULL, change it to use disable_work_sync instead. If concurrent insertions and removals hit the old table during the call, they act directly on the old table. If they see a NULL ht->tbl, they fail immediately. Concurrent insertions and removals that see the new table act on it. After setting ht->tbl to NULL, the new helper waits for concurrent insertions and removals on the old ht->tbl to complete, and then cancels existing rehashes. Once rehashes are completely gone, ht->nelems is reset to zero, and the new table is installed. Walkers are detached from the old table, and the dying flag is set on the old table so that other walkers cannot reattach themselves to the dying table. Replace the rcu_head_after_call_rcu check in rhashtable_walk_stop with the dying flag. Finally, the entries in the old table are freed. Signed-off-by: Herbert Xu <herbert@gondor.apana.org.au> diff --git a/include/linux/rhashtable.h b/include/linux/rhashtable.h index 6c5e6d9accba..cb6f5aeab538 100644 --- a/include/linux/rhashtable.h +++ b/include/linux/rhashtable.h @@ -67,6 +67,7 @@ struct rhash_lock_head {}; * @nest: Number of bits of first-level nested table. * @rehash: Current bucket being rehashed * @hash_rnd: Random seed to fold into hash + * @dying: True if table is about to be freed * @walkers: List of active walkers * @rcu: RCU structure for freeing the table * @future_tbl: Table under construction during rehashing @@ -77,6 +78,7 @@ struct bucket_table { unsigned int size; unsigned int nest; u32 hash_rnd; + u32 dying; struct list_head walkers; struct rcu_head rcu; @@ -255,6 +257,10 @@ void rhashtable_free_and_destroy(struct rhashtable *ht, void *arg); void rhashtable_destroy(struct rhashtable *ht); +void rhashtable_flush_and_free(struct rhashtable *ht, + void (*free_fn)(void *ptr, void *arg), + void *arg); + struct rhash_lock_head __rcu **rht_bucket_nested( const struct bucket_table *tbl, unsigned int hash); struct rhash_lock_head __rcu **__rht_bucket_nested( @@ -621,6 +627,9 @@ static __always_inline struct rhash_head *__rhashtable_lookup( BUILD_BUG_ON(!__builtin_constant_p(freq)); tbl = rht_dereference_rcu(ht->tbl, ht); + if (!tbl) + goto out; + restart: hash = rht_key_hashfn(ht, tbl, key, params); bkt = rht_bucket(tbl, hash); @@ -644,6 +653,7 @@ static __always_inline struct rhash_head *__rhashtable_lookup( if (unlikely(tbl)) goto restart; +out: return NULL; } @@ -759,16 +769,19 @@ static __always_inline void *__rhashtable_insert_fast( }; struct rhash_lock_head __rcu **bkt; struct rhash_head __rcu **pprev; + void *data = ERR_PTR(-ESTALE); struct bucket_table *tbl; struct rhash_head *head; unsigned long flags; unsigned int hash; int elasticity; - void *data; rcu_read_lock(); tbl = rht_dereference_rcu(ht->tbl, ht); + if (!tbl) + goto out; + hash = rht_head_hashfn(ht, tbl, obj, params); elasticity = RHT_ELASTICITY; bkt = rht_bucket_insert(ht, tbl, hash); @@ -1127,11 +1140,13 @@ static __always_inline int __rhashtable_remove_fast( const struct rhashtable_params params, bool rhlist) { struct bucket_table *tbl; - int err; + int err = -ENOENT; rcu_read_lock(); tbl = rht_dereference_rcu(ht->tbl, ht); + if (!tbl) + goto out; /* Because we have already taken (and released) the bucket * lock in old_tbl, if we find that future_tbl is not yet @@ -1143,6 +1158,7 @@ static __always_inline int __rhashtable_remove_fast( (tbl = rht_dereference_rcu(tbl->future_tbl, ht))) ; +out: rcu_read_unlock(); return err; @@ -1262,11 +1278,13 @@ static __always_inline int rhashtable_replace_fast( const struct rhashtable_params params) { struct bucket_table *tbl; - int err; + int err = -ENOENT; rcu_read_lock(); tbl = rht_dereference_rcu(ht->tbl, ht); + if (!tbl) + goto out; /* Because we have already taken (and released) the bucket * lock in old_tbl, if we find that future_tbl is not yet @@ -1278,6 +1296,7 @@ static __always_inline int rhashtable_replace_fast( (tbl = rht_dereference_rcu(tbl->future_tbl, ht))) ; +out: rcu_read_unlock(); return err; @@ -1331,4 +1350,19 @@ static inline void rhltable_destroy(struct rhltable *hlt) rhltable_free_and_destroy(hlt, NULL, NULL); } +/** + * rhltable_flush_and_free - unlink and free all elements in the hash list table + * @hlt: the hash list table to destroy + * @free_fn: callback to release resources of element + * @arg: pointer passed to free_fn + * + * See documentation for rhashtable_flush_and_free. + */ +static inline void rhltable_flush_and_free(struct rhltable *hlt, + void (*free_fn)(void *ptr, + void *arg), + void *arg) +{ + rhashtable_flush_and_free(&hlt->ht, free_fn, arg); +} #endif /* _LINUX_RHASHTABLE_H */ diff --git a/lib/rhashtable.c b/lib/rhashtable.c index 5da0e53a8d42..c8bf0eed00b5 100644 --- a/lib/rhashtable.c +++ b/lib/rhashtable.c @@ -350,6 +350,8 @@ static int rhashtable_rehash_table(struct rhashtable *ht) /* Publish the new table pointer. */ rcu_assign_pointer(ht->tbl, new_tbl); + old_tbl->dying = true; + spin_lock(&ht->lock); list_for_each_entry(walker, &old_tbl->walkers, list) walker->tbl = NULL; @@ -435,6 +437,9 @@ static void rht_deferred_worker(struct work_struct *work) mutex_lock_nested(&ht->mutex, 1); tbl = rht_dereference(ht->tbl, ht); + if (!tbl) + goto out; + tbl = rhashtable_last_table(ht, tbl); if (rht_grow_above_75(ht, tbl)) @@ -451,18 +456,11 @@ static void rht_deferred_worker(struct work_struct *work) err = err ?: nerr; } - mutex_unlock(&ht->mutex); - - /* - * Re-arm via @run_work, not @run_irq_work. - * rhashtable_free_and_destroy() drains async work as irq_work_sync() - * followed by cancel_work_sync(). If this site queued irq_work while - * cancel_work_sync() was waiting for us, irq_work_sync() would already - * have returned and the stale irq_work could fire post-teardown. - * cancel_work_sync() natively handles self-requeue on @run_work. - */ if (err) - schedule_work(&ht->run_work); + irq_work_queue(&ht->run_irq_work); + +out: + mutex_unlock(&ht->mutex); } /* @@ -489,6 +487,8 @@ static int rhashtable_insert_rehash(struct rhashtable *ht, int err; old_tbl = rht_dereference_rcu(ht->tbl, ht); + if (!old_tbl) + return -EAGAIN; size = tbl->size; @@ -636,6 +636,8 @@ static void *rhashtable_try_insert(struct rhashtable *ht, const void *key, void *data; new_tbl = rcu_dereference(ht->tbl); + if (!new_tbl) + return ERR_PTR(-ESTALE); do { tbl = new_tbl; @@ -779,6 +781,8 @@ void *rhashtable_next_key(struct rhashtable *ht, const void *prev_key) return ERR_PTR(-EOPNOTSUPP); tbl = rht_dereference_rcu(ht->tbl, ht); + if (!tbl) + return NULL; do { he = __rhashtable_next_in_table(ht, tbl, prev_key); if (!IS_ERR_OR_NULL(he)) @@ -820,13 +824,15 @@ void rhashtable_walk_enter(struct rhashtable *ht, struct rhashtable_iter *iter) iter->p = NULL; iter->slot = 0; iter->skip = 0; - iter->end_of_table = 0; spin_lock(&ht->lock); iter->walker.tbl = rcu_dereference_protected(ht->tbl, lockdep_is_held(&ht->lock)); - list_add(&iter->walker.list, &iter->walker.tbl->walkers); + if (iter->walker.tbl) + list_add(&iter->walker.list, &iter->walker.tbl->walkers); spin_unlock(&ht->lock); + + iter->end_of_table = !iter->walker.tbl; } EXPORT_SYMBOL_GPL(rhashtable_walk_enter); @@ -880,6 +886,10 @@ int rhashtable_walk_start_check(struct rhashtable_iter *iter) return 0; if (!iter->walker.tbl) { iter->walker.tbl = rht_dereference_rcu(ht->tbl, ht); + if (!iter->walker.tbl) { + iter->end_of_table = true; + return 0; + } iter->slot = 0; iter->skip = 0; iter->p = NULL; @@ -1091,7 +1101,7 @@ void rhashtable_walk_stop(struct rhashtable_iter *iter) ht = iter->ht; spin_lock(&ht->lock); - if (rcu_head_after_call_rcu(&tbl->rcu, bucket_table_free_rcu)) + if (tbl->dying) /* This bucket table is being freed, don't re-link it. */ iter->walker.tbl = NULL; else @@ -1276,45 +1286,12 @@ static void rhashtable_free_one(struct rhashtable *ht, struct rhash_head *obj, } while (list); } -/** - * rhashtable_free_and_destroy - free elements and destroy hash table - * @ht: the hash table to destroy - * @free_fn: callback to release resources of element - * @arg: pointer passed to free_fn - * - * Stops an eventual async resize. If defined, invokes free_fn for each - * element to releasal resources. Please note that RCU protected - * readers may still be accessing the elements. Releasing of resources - * must occur in a compatible manner. Then frees the bucket array. - * - * This function will eventually sleep to wait for an async resize - * to complete. The caller is responsible that no further write operations - * occurs in parallel. - * - * After cancel_work_sync() has returned, the deferred rehash worker is - * quiesced and, per the contract above, no other concurrent access to the - * rhashtable is possible. The tables are therefore owned exclusively by - * this function and can be walked without ht->mutex held. - */ -void rhashtable_free_and_destroy(struct rhashtable *ht, - void (*free_fn)(void *ptr, void *arg), - void *arg) +static void rhashtable_free(struct rhashtable *ht, struct bucket_table *tbl, + void (*free_fn)(void *ptr, void *arg), void *arg) { - struct bucket_table *tbl, *next_tbl; + struct bucket_table *next_tbl; unsigned int i; - irq_work_sync(&ht->run_irq_work); - cancel_work_sync(&ht->run_work); - - /* - * Do NOT take ht->mutex here. The rehash worker establishes - * ht->mutex -> fs_reclaim via GFP_KERNEL bucket allocation under - * the mutex; callers on the reclaim path (e.g. simple_xattr_ht_free() - * from evict() under the dcache shrinker for shmem/kernfs/pidfs - * inodes) would otherwise close a circular dependency - * fs_reclaim -> ht->mutex. - */ - tbl = rcu_dereference_raw(ht->tbl); restart: if (free_fn) { for (i = 0; i < tbl->size; i++) { @@ -1339,6 +1316,36 @@ void rhashtable_free_and_destroy(struct rhashtable *ht, goto restart; } } + +/** + * rhashtable_free_and_destroy - free elements and destroy hash table + * @ht: the hash table to destroy + * @free_fn: callback to release resources of element + * @arg: pointer passed to free_fn + * + * Stops an eventual async resize. If defined, invokes free_fn for each + * element to releasal resources. Please note that RCU protected + * readers may still be accessing the elements. Releasing of resources + * must occur in a compatible manner. Then frees the bucket array. + * + * This function will eventually sleep to wait for an async resize + * to complete. The caller is responsible that no further write operations + * occurs in parallel. + * + * After disable_work_sync() has returned, the deferred rehash worker is + * quiesced and, per the contract above, no other concurrent access to the + * rhashtable is possible. The tables are therefore owned exclusively by + * this function and can be walked without ht->mutex held. + */ +void rhashtable_free_and_destroy(struct rhashtable *ht, + void (*free_fn)(void *ptr, void *arg), + void *arg) +{ + disable_work_sync(&ht->run_work); + irq_work_sync(&ht->run_irq_work); + + rhashtable_free(ht, rcu_dereference_raw(ht->tbl), free_fn, arg); +} EXPORT_SYMBOL_GPL(rhashtable_free_and_destroy); void rhashtable_destroy(struct rhashtable *ht) @@ -1415,3 +1422,79 @@ struct rhash_lock_head __rcu **rht_bucket_nested_insert( } EXPORT_SYMBOL_GPL(rht_bucket_nested_insert); + +/** + * rhashtable_flush_and_free - detach and discard all current elements + * @ht: the hash table to flush + * @free_fn: callback to release resources of an element, may be %NULL + * @arg: pointer passed to free_fn + * + * Swaps the bucket table backing @ht for a new, empty table. + * + * The detached table is then walked and every element found is + * unlinked, and, if @free_fn is given, handed to it for release. + * + * Unlike rhashtable_destroy(), @ht is left fully initialized and may + * continue to be used for lookups, insertions, and removals. + * + * This function may sleep, it cannot be called from atomic context or + * RCU read-side critical sections. + * + * The caller must not make concurrent rhashtable_flush_and_free calls. + * That is, a subsequent call can only be made once the first call has + * returned. + */ +void rhashtable_flush_and_free(struct rhashtable *ht, + void (*free_fn)(void *ptr, void *arg), + void *arg) +{ + struct bucket_table *tbl, *old_tbl, *new_tbl; + struct rhashtable_walker *walker; + + new_tbl = bucket_table_alloc(ht, rounded_hashtable_size(&ht->p), + GFP_KERNEL); + if (!new_tbl) + new_tbl = bucket_table_alloc(ht, ht->p.min_size, + GFP_KERNEL | __GFP_NOFAIL); + + mutex_lock(&ht->mutex); + old_tbl = rht_dereference(ht->tbl, ht); + RCU_INIT_POINTER(ht->tbl, NULL); + mutex_unlock(&ht->mutex); + + /* Wait for insertions/removals on the old ht->tbl to complete. */ + synchronize_rcu(); + + /* Cancel resizes/rehashes. */ + irq_work_sync(&ht->run_irq_work); + cancel_work_sync(&ht->run_work); + + atomic_set(&ht->nelems, 0); + + /* No need for mutex as rehashes have all stopped. */ + rcu_assign_pointer(ht->tbl, new_tbl); + + /* Detach walkers from the old hash table. */ + tbl = old_tbl; + spin_lock(&ht->lock); + do { + tbl->dying = true; + list_for_each_entry(walker, &tbl->walkers, list) { + struct rhashtable_iter *iter = container_of( + walker, struct rhashtable_iter, walker); + + walker->tbl = NULL; + iter->end_of_table = true; + iter->p = NULL; + } + + tbl = rcu_dereference_raw(tbl->future_tbl); + } while (tbl); + spin_unlock(&ht->lock); + + /* Wait for walkers on old table to complete. */ + synchronize_rcu(); + + rhashtable_free(ht, old_tbl, free_fn, arg); +} +EXPORT_SYMBOL_GPL(rhashtable_flush_and_free); Cheers, -- Email: Herbert Xu <herbert@gondor.apana.org.au> Home Page: http://gondor.apana.org.au/~herbert/ PGP Key: http://gondor.apana.org.au/~herbert/pubkey.txt ^ permalink raw reply related [flat|nested] 7+ messages in thread
* Re: [v5 PATCH] rhashtable: Add rhashtable_flush_and_free helper 2026-09-13 2:53 ` [v5 " Herbert Xu @ 2026-09-13 6:31 ` Florian Westphal 0 siblings, 0 replies; 7+ messages in thread From: Florian Westphal @ 2026-09-13 6:31 UTC (permalink / raw) To: Herbert Xu; +Cc: kadlec, linux-crypto Herbert Xu <herbert@gondor.apana.org.au> wrote: > v5 makes the following changes: > > - Add bucket_table dying flag to fix walker race. Thanks, I will rebase my ipset patches on top of this on monday and will get back to you, ^ permalink raw reply [flat|nested] 7+ messages in thread
end of thread, other threads:[~2026-09-13 6:37 UTC | newest] Thread overview: 7+ messages (download: mbox.gz follow: Atom feed -- links below jump to the message on this page -- 2026-09-12 10:16 [v2 PATCH] rhashtable: Add rhashtable_flush_and_free helper Herbert Xu 2026-09-12 18:32 ` Florian Westphal 2026-09-12 23:27 ` Herbert Xu 2026-09-13 0:12 ` [v3 " Herbert Xu 2026-09-13 1:34 ` [v4 " Herbert Xu 2026-09-13 2:53 ` [v5 " Herbert Xu 2026-09-13 6:31 ` Florian Westphal
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox