summaryrefslogtreecommitdiff
path: root/lib/rhashtable.c
diff options
context:
space:
mode:
Diffstat (limited to 'lib/rhashtable.c')
-rw-r--r--lib/rhashtable.c193
1 files changed, 173 insertions, 20 deletions
diff --git a/lib/rhashtable.c b/lib/rhashtable.c
index 6074ed5f66f3..6362896e4f09 100644
--- a/lib/rhashtable.c
+++ b/lib/rhashtable.c
@@ -14,6 +14,7 @@
#include <linux/atomic.h>
#include <linux/kernel.h>
#include <linux/init.h>
+#include <linux/irq_work.h>
#include <linux/log2.h>
#include <linux/sched.h>
#include <linux/rculist.h>
@@ -25,6 +26,7 @@
#include <linux/rhashtable.h>
#include <linux/err.h>
#include <linux/export.h>
+#include <linux/workqueue.h>
#define HASH_DEFAULT_SIZE 64UL
#define HASH_MIN_SIZE 4U
@@ -114,6 +116,14 @@ static void bucket_table_free(const struct bucket_table *tbl)
kvfree(tbl);
}
+static void bucket_table_free_atomic(const struct bucket_table *tbl)
+{
+ if (tbl->nest)
+ nested_bucket_table_free(tbl);
+
+ kvfree_atomic(tbl);
+}
+
static void bucket_table_free_rcu(struct rcu_head *head)
{
bucket_table_free(container_of(head, struct bucket_table, rcu));
@@ -441,10 +451,33 @@ static void rht_deferred_worker(struct work_struct *work)
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);
}
+/*
+ * Insert-path callers can run under a raw spinlock (e.g. an insecure_elasticity
+ * user). Calling schedule_work() under that lock records caller_lock ->
+ * pool->lock -> pi_lock -> rq->__lock, closing a locking cycle if any of
+ * these is acquired in the reverse direction elsewhere. Bounce through
+ * irq_work so the schedule_work() runs with the caller's lock no longer held.
+ */
+static void rht_deferred_irq_work(struct irq_work *irq_work)
+{
+ struct rhashtable *ht = container_of(irq_work, struct rhashtable,
+ run_irq_work);
+
+ schedule_work(&ht->run_work);
+}
+
static int rhashtable_insert_rehash(struct rhashtable *ht,
struct bucket_table *tbl)
{
@@ -473,11 +506,11 @@ static int rhashtable_insert_rehash(struct rhashtable *ht,
err = rhashtable_rehash_attach(ht, tbl, new_tbl);
if (err) {
- bucket_table_free(new_tbl);
+ bucket_table_free_atomic(new_tbl);
if (err == -EEXIST)
err = 0;
} else
- schedule_work(&ht->run_work);
+ irq_work_queue(&ht->run_irq_work);
return err;
@@ -488,7 +521,7 @@ fail:
/* Schedule async rehash to retry allocation in process context. */
if (err == -ENOMEM)
- schedule_work(&ht->run_work);
+ irq_work_queue(&ht->run_irq_work);
return err;
}
@@ -538,7 +571,7 @@ static void *rhashtable_lookup_one(struct rhashtable *ht,
return NULL;
}
- if (elasticity <= 0)
+ if (elasticity <= 0 && !ht->p.insecure_elasticity)
return ERR_PTR(-EAGAIN);
return ERR_PTR(-ENOENT);
@@ -568,7 +601,8 @@ static struct bucket_table *rhashtable_insert_one(
if (unlikely(rht_grow_above_max(ht, tbl)))
return ERR_PTR(-E2BIG);
- if (unlikely(rht_grow_above_100(ht, tbl)))
+ if (unlikely(rht_grow_above_100(ht, tbl)) &&
+ !ht->p.insecure_elasticity)
return ERR_PTR(-EAGAIN);
head = rht_ptr(bkt, tbl, hash);
@@ -629,7 +663,7 @@ static void *rhashtable_try_insert(struct rhashtable *ht, const void *key,
rht_unlock(tbl, bkt, flags);
if (inserted && rht_grow_above_75(ht, tbl))
- schedule_work(&ht->run_work);
+ irq_work_queue(&ht->run_irq_work);
}
} while (!IS_ERR_OR_NULL(new_tbl));
@@ -655,6 +689,108 @@ void *rhashtable_insert_slow(struct rhashtable *ht, const void *key,
}
EXPORT_SYMBOL_GPL(rhashtable_insert_slow);
+/* Scan one element forward from prev_key's position in @tbl.
+ * Returns first rhash_head whose bucket > prev_key's bucket, or the
+ * element immediately after prev_key inside prev_key's bucket.
+ * Returns the first element if prev_key is NULL, NULL when @tbl is
+ * exhausted, or ERR_PTR(-ENOENT) if prev_key is not found in @tbl.
+ */
+static struct rhash_head *__rhashtable_next_in_table(
+ struct rhashtable *ht, struct bucket_table *tbl,
+ const void *prev_key)
+{
+ struct rhashtable_compare_arg arg = { .ht = ht, .key = prev_key };
+ const struct rhashtable_params params = ht->p;
+ struct rhash_head *he;
+ unsigned int b = 0;
+ bool found = false;
+
+ if (prev_key) {
+ b = rht_key_hashfn(ht, tbl, prev_key, params);
+ rht_for_each_rcu(he, tbl, b) {
+ bool match = params.obj_cmpfn
+ ? !params.obj_cmpfn(&arg, rht_obj(ht, he))
+ : !rhashtable_compare(&arg, rht_obj(ht, he));
+ if (found) {
+ if (match)
+ continue;
+ return he;
+ }
+ if (match)
+ found = true;
+ }
+ if (!found)
+ return ERR_PTR(-ENOENT);
+ b++;
+ }
+
+ for (; b < tbl->size; b++)
+ rht_for_each_rcu(he, tbl, b)
+ return he;
+ return NULL;
+}
+
+/**
+ * rhashtable_next_key - return next element after a given key
+ * @ht: hash table
+ * @prev_key: pointer to previous key, or NULL for the first element
+ *
+ * WARNING: this walk is highly unstable. Unlike rhashtable_walk_*(),
+ * it cannot detect a concurrent resize or rehash, so a full iteration
+ * is NOT guaranteed to terminate under adversarial or sustained
+ * rehashing. Callers MUST tolerate skipped and duplicated elements and
+ * SHOULD bound their loop externally.
+ *
+ * Returns the next element in best-effort iteration order, walking the
+ * @tbl chain (including any future_tbl in flight). Caller must hold RCU.
+ *
+ * Pass @prev_key == NULL to obtain the first element. To iterate, set
+ * @prev_key to the key of the previously returned element on each call,
+ * and stop when NULL is returned.
+ *
+ * Best-effort semantics:
+ * - Across the tbl->future_tbl chain, an element being migrated may
+ * transiently appear in both tables and be observed twice.
+ * - Concurrent inserts may or may not be observed.
+ * - Termination of a full iteration loop is NOT guaranteed under
+ * adversarial continuous rehash; callers MUST tolerate skips and
+ * repeats and SHOULD bound their loop externally.
+ * - Behavior on tables that contain duplicate keys is undefined:
+ * duplicates may be skipped, repeated, or trap the walk in a
+ * cycle. Callers requiring duplicate-key iteration must use
+ * rhashtable_walk_*() instead.
+ * - rhltable instances are not supported and return
+ * ERR_PTR(-EOPNOTSUPP).
+ * - If prev_key was concurrently deleted and is not present in any
+ * in-flight table, returns ERR_PTR(-ENOENT).
+ *
+ * Returns entry of the next element, or NULL when iteration is exhausted,
+ * or ERR_PTR(-ENOENT) if prev_key is not found, or
+ * ERR_PTR(-EOPNOTSUPP) if @ht is an rhltable.
+ */
+void *rhashtable_next_key(struct rhashtable *ht, const void *prev_key)
+{
+ struct bucket_table *tbl;
+ struct rhash_head *he;
+
+ if (unlikely(ht->rhlist))
+ return ERR_PTR(-EOPNOTSUPP);
+
+ tbl = rht_dereference_rcu(ht->tbl, ht);
+ do {
+ he = __rhashtable_next_in_table(ht, tbl, prev_key);
+ if (!IS_ERR_OR_NULL(he))
+ return rht_obj(ht, he);
+ if (!he)
+ prev_key = NULL;
+ /* See any new future_tbl attached during a rehash. */
+ smp_rmb();
+ tbl = rht_dereference_rcu(tbl->future_tbl, ht);
+ } while (tbl);
+ return he; /* NULL or -ENOENT */
+}
+EXPORT_SYMBOL_GPL(rhashtable_next_key);
+
/**
* rhashtable_walk_enter - Initialise an iterator
* @ht: Table to walk over
@@ -744,6 +880,7 @@ int rhashtable_walk_start_check(struct rhashtable_iter *iter)
iter->walker.tbl = rht_dereference_rcu(ht->tbl, ht);
iter->slot = 0;
iter->skip = 0;
+ iter->p = NULL;
return -EAGAIN;
}
@@ -1025,8 +1162,9 @@ static u32 rhashtable_jhash2(const void *key, u32 length, u32 seed)
* .obj_hashfn = my_hash_fn,
* };
*/
-int rhashtable_init_noprof(struct rhashtable *ht,
- const struct rhashtable_params *params)
+int __rhashtable_init_noprof(struct rhashtable *ht,
+ const struct rhashtable_params *params,
+ struct lock_class_key *key)
{
struct bucket_table *tbl;
size_t size;
@@ -1036,7 +1174,7 @@ int rhashtable_init_noprof(struct rhashtable *ht,
return -EINVAL;
memset(ht, 0, sizeof(*ht));
- mutex_init(&ht->mutex);
+ mutex_init_with_key(&ht->mutex, key);
spin_lock_init(&ht->lock);
memcpy(&ht->p, params, sizeof(*params));
@@ -1084,10 +1222,11 @@ int rhashtable_init_noprof(struct rhashtable *ht,
RCU_INIT_POINTER(ht->tbl, tbl);
INIT_WORK(&ht->run_work, rht_deferred_worker);
+ init_irq_work(&ht->run_irq_work, rht_deferred_irq_work);
return 0;
}
-EXPORT_SYMBOL_GPL(rhashtable_init_noprof);
+EXPORT_SYMBOL_GPL(__rhashtable_init_noprof);
/**
* rhltable_init - initialize a new hash list table
@@ -1098,15 +1237,17 @@ EXPORT_SYMBOL_GPL(rhashtable_init_noprof);
*
* See documentation for rhashtable_init.
*/
-int rhltable_init_noprof(struct rhltable *hlt, const struct rhashtable_params *params)
+int __rhltable_init_noprof(struct rhltable *hlt,
+ const struct rhashtable_params *params,
+ struct lock_class_key *key)
{
int err;
- err = rhashtable_init_noprof(&hlt->ht, params);
+ err = __rhashtable_init_noprof(&hlt->ht, params, key);
hlt->ht.rhlist = true;
return err;
}
-EXPORT_SYMBOL_GPL(rhltable_init_noprof);
+EXPORT_SYMBOL_GPL(__rhltable_init_noprof);
static void rhashtable_free_one(struct rhashtable *ht, struct rhash_head *obj,
void (*free_fn)(void *ptr, void *arg),
@@ -1122,7 +1263,7 @@ static void rhashtable_free_one(struct rhashtable *ht, struct rhash_head *obj,
list = container_of(obj, struct rhlist_head, rhead);
do {
obj = &list->rhead;
- list = rht_dereference(list->next, ht);
+ list = rcu_dereference_raw(list->next);
free_fn(rht_obj(ht, obj), arg);
} while (list);
}
@@ -1141,6 +1282,11 @@ static void rhashtable_free_one(struct rhashtable *ht, struct rhash_head *obj,
* 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),
@@ -1149,10 +1295,18 @@ void rhashtable_free_and_destroy(struct rhashtable *ht,
struct bucket_table *tbl, *next_tbl;
unsigned int i;
+ irq_work_sync(&ht->run_irq_work);
cancel_work_sync(&ht->run_work);
- mutex_lock(&ht->mutex);
- tbl = rht_dereference(ht->tbl, ht);
+ /*
+ * 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++) {
@@ -1161,22 +1315,21 @@ restart:
cond_resched();
for (pos = rht_ptr_exclusive(rht_bucket(tbl, i)),
next = !rht_is_a_nulls(pos) ?
- rht_dereference(pos->next, ht) : NULL;
+ rcu_dereference_raw(pos->next) : NULL;
!rht_is_a_nulls(pos);
pos = next,
next = !rht_is_a_nulls(pos) ?
- rht_dereference(pos->next, ht) : NULL)
+ rcu_dereference_raw(pos->next) : NULL)
rhashtable_free_one(ht, pos, free_fn, arg);
}
}
- next_tbl = rht_dereference(tbl->future_tbl, ht);
+ next_tbl = rcu_dereference_raw(tbl->future_tbl);
bucket_table_free(tbl);
if (next_tbl) {
tbl = next_tbl;
goto restart;
}
- mutex_unlock(&ht->mutex);
}
EXPORT_SYMBOL_GPL(rhashtable_free_and_destroy);