[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, &params);
+#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, &params));
+#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].