[PATCH RFC] rhashtable: use unique lockdep classes for bucket locks
"syzbot" <[email protected]> Sun, 2 Aug 2026 21:21:10 +0000 (UTC)
| Newsgroups | dev.linux.lists.syzbot |
|---|---|
| Message-ID | <[email protected]> |
Lockdep reports an inconsistent lock state (`{INITIAL USE} -> {IN-NMI}`)
when a BPF program deletes an element from a `BPF_MAP_TYPE_RHASH` map in
NMI context:
inconsistent {INITIAL USE} -> {IN-NMI} usage.
...
Possible unsafe locking scenario:
CPU0
====
lock(rhashtable_bucket);
<Interrupt>
lock(rhashtable_bucket);
*** DEADLOCK ***
The `{INITIAL USE}` of the lock is registered during process creation when
`pidfs_add_pid()` inserts a new PID into the `pidfs_ino_ht` rhashtable and
acquires the bucket lock in normal process context. Later, a BPF program
attached to a hardware breakpoint executes in NMI context and calls
`bpf_map_delete_elem()` on a `BPF_MAP_TYPE_RHASH` map. This calls
`rhashtable_remove_fast()`, which acquires the bucket lock in NMI context,
triggering the lockdep warning.
This is a false positive caused by all `rhashtable` instances in the kernel
sharing the exact same lock class for their bucket locks. The bucket table
allocation function `bucket_table_alloc()` initializes the lockdep map for
the bucket locks using a single static key. Thus, lockdep assumes that the
bucket lock acquired by the BPF program in NMI context is the same lock
acquired by `pidfs` in normal process context, leading to a perceived
deadlock.
In reality, they operate on completely different `rhashtable` instances.
Furthermore, a real deadlock on the same `BPF_MAP_TYPE_RHASH` instance is
explicitly prevented by the BPF subsystem using
`bpf_disable_instrumentation()`, which increments the per-CPU
`bpf_prog_active` counter and prevents reentrancy.
To fix this, update the `rhashtable` API to allow different lock classes
for the bucket locks of different `rhashtable` instances. Extend the
initialization macros to generate a unique static `lock_class_key` for the
bucket locks and pass it down to `bucket_table_alloc()`.
For BPF maps, use `__lockdep_no_track__` to completely bypass lockdep
tracking for the bucket locks. This avoids issues with shared global keys
like `__lockdep_no_validate__` (which can cause invalid wait context
warnings if previously registered as a mutex by another subsystem) while
relying on BPF's internal NMI deadlock prevention mechanism.
Fixes: 16b4d3e2fb24 ("bpf: Implement resizable hashmap basic functions")
Assisted-by: Gemini:gemini-3.5-flash Gemini:gemini-3.1-pro-preview syzbot
Reported-by: [email protected]
Closes: https://syzkaller.appspot.com/bug?extid=2fb31ade2d0e920fea9a
Link: https://syzkaller.appspot.com/ai_job?id=31a9ea31-9998-4618-b127-6dbc2e463ee9
To: "Andrew Morton" <[email protected]>
To: "Andrii Nakryiko" <[email protected]>
To: "Alexei Starovoitov" <[email protected]>
To: <[email protected]>
To: "Daniel Borkmann" <[email protected]>
To: "Eduard Zingerman" <[email protected]>
To: "Herbert Xu" <[email protected]>
To: <[email protected]>
To: "Kumar Kartikeya Dwivedi" <[email protected]>
To: "Thomas Graf" <[email protected]>
To: "Mykyta Yatsenko" <[email protected]>
Cc: "Emil Tsalapatis" <[email protected]>
Cc: "Jiri Olsa" <[email protected]>
Cc: <[email protected]>
Cc: "Martin KaFai Lau" <[email protected]>
Cc: "Song Liu" <[email protected]>
Cc: "Yonghong Song" <[email protected]>
---
diff --git a/include/linux/rhashtable-types.h b/include/linux/rhashtable-types.h
index 57c11ec9d..059f21e37 100644
--- a/include/linux/rhashtable-types.h
+++ b/include/linux/rhashtable-types.h
@@ -94,6 +94,9 @@ struct rhashtable {
struct mutex mutex;
spinlock_t lock;
atomic_t nelems;
+#ifdef CONFIG_LOCKDEP
+ struct lock_class_key *dep_key;
+#endif
#ifdef CONFIG_MEM_ALLOC_PROFILING
struct alloc_tag *alloc_tag;
#endif
@@ -138,23 +141,27 @@ struct rhashtable_iter {
int __rhashtable_init_noprof(struct rhashtable *ht,
const struct rhashtable_params *params,
- struct lock_class_key *key);
+ struct lock_class_key *key,
+ struct lock_class_key *bucket_key);
#define rhashtable_init_noprof(ht, params) \
({ \
static struct lock_class_key __key; \
+ static struct lock_class_key __bucket_key; \
\
- __rhashtable_init_noprof(ht, params, &__key); \
+ __rhashtable_init_noprof(ht, params, &__key, &__bucket_key); \
})
#define rhashtable_init(...) alloc_hooks(rhashtable_init_noprof(__VA_ARGS__))
int __rhltable_init_noprof(struct rhltable *hlt,
const struct rhashtable_params *params,
- struct lock_class_key *key);
+ struct lock_class_key *key,
+ struct lock_class_key *bucket_key);
#define rhltable_init_noprof(hlt, params) \
({ \
static struct lock_class_key __key; \
+ static struct lock_class_key __bucket_key; \
\
- __rhltable_init_noprof(hlt, params, &__key); \
+ __rhltable_init_noprof(hlt, params, &__key, &__bucket_key); \
})
#define rhltable_init(...) alloc_hooks(rhltable_init_noprof(__VA_ARGS__))
diff --git a/kernel/bpf/hashtab.c b/kernel/bpf/hashtab.c
index 9f394e1aa..d7e2a601c 100644
--- a/kernel/bpf/hashtab.c
+++ b/kernel/bpf/hashtab.c
@@ -2822,7 +2822,19 @@ static struct bpf_map *rhtab_map_alloc(union bpf_attr *attr)
params.obj_cmpfn = rhtab_key_cmp_long;
}
- err = rhashtable_init(&rhtab->ht, ¶ms);
+#ifdef CONFIG_LOCKDEP
+#define bpf_rhashtable_init(ht, params) \
+ ({ \
+ static struct lock_class_key __key; \
+ \
+ __rhashtable_init_noprof(ht, params, &__key, \
+ &__lockdep_no_track__); \
+ })
+#else
+#define bpf_rhashtable_init(ht, params) rhashtable_init_noprof(ht, params)
+#endif
+ err = alloc_hooks(bpf_rhashtable_init(&rhtab->ht, ¶ms));
+#undef bpf_rhashtable_init
if (err)
goto free_rhtab;
diff --git a/lib/rhashtable.c b/lib/rhashtable.c
index d459bef24..f872136ca 100644
--- a/lib/rhashtable.c
+++ b/lib/rhashtable.c
@@ -205,7 +205,12 @@ static struct bucket_table *bucket_table_alloc(struct rhashtable *ht,
if (tbl == NULL)
return NULL;
- lockdep_init_map(&tbl->dep_map, "rhashtable_bucket", &__key, 0);
+#ifdef CONFIG_LOCKDEP
+ if (ht->dep_key)
+ lockdep_init_map(&tbl->dep_map, "rhashtable_bucket", ht->dep_key, 0);
+ else
+#endif
+ lockdep_init_map(&tbl->dep_map, "rhashtable_bucket", &__key, 0);
tbl->size = size;
@@ -1162,7 +1167,8 @@ static u32 rhashtable_jhash2(const void *key, u32 length, u32 seed)
*/
int __rhashtable_init_noprof(struct rhashtable *ht,
const struct rhashtable_params *params,
- struct lock_class_key *key)
+ struct lock_class_key *key,
+ struct lock_class_key *bucket_key)
{
struct bucket_table *tbl;
size_t size;
@@ -1176,6 +1182,10 @@ int __rhashtable_init_noprof(struct rhashtable *ht,
spin_lock_init(&ht->lock);
memcpy(&ht->p, params, sizeof(*params));
+#ifdef CONFIG_LOCKDEP
+ ht->dep_key = bucket_key;
+#endif
+
alloc_tag_record(ht->alloc_tag);
if (params->min_size)
@@ -1237,11 +1247,12 @@ EXPORT_SYMBOL_GPL(__rhashtable_init_noprof);
*/
int __rhltable_init_noprof(struct rhltable *hlt,
const struct rhashtable_params *params,
- struct lock_class_key *key)
+ struct lock_class_key *key,
+ struct lock_class_key *bucket_key)
{
int err;
- err = __rhashtable_init_noprof(&hlt->ht, params, key);
+ err = __rhashtable_init_noprof(&hlt->ht, params, key, bucket_key);
hlt->ht.rhlist = true;
return err;
}
base-commit: f5098b6bae761e346ebcd9da7f95622c04733cff
--
This is an AI-generated patch subject to moderation.
Reply with '#syz upstream' to Sign-off the patch as a human author
and send it to the upstream kernel mailing lists.
Reply with '#syz reject' to reject it ('#syz unreject' to undo).
See https://goo.gle/syzbot-ai-patches for information about AI-generated patches.
You can comment on the patch as usual, syzbot will try to address
the comments and send a new version of the patch if necessary.
syzbot engineers can be reached at [email protected].