From: Shakeel Butt <shakeel.butt@linux.dev>
To: Peter Zijlstra <peterz@infradead.org>,
Ingo Molnar <mingo@redhat.com>, Will Deacon <will@kernel.org>,
Boqun Feng <boqun@kernel.org>, Waiman Long <longman@redhat.com>
Cc: Paul McKenney <paulmck@kernel.org>,
Greg Kroah-Hartman <gregkh@linuxfoundation.org>,
Tejun Heo <tj@kernel.org>,
Christian Brauner <christian@brauner.io>,
Sebastian Andrzej Siewior <bigeasy@linutronix.de>,
Johannes Weiner <hannes@cmpxchg.org>,
Jonathan Corbet <corbet@lwn.net>,
Meta kernel team <kernel-team@meta.com>,
cgroups@vger.kernel.org, driver-core@lists.linux.dev,
linux-kernel@vger.kernel.org
Subject: [PATCH 5/7] locking/selftests: add KUnit tests for lock holder tracking
Date: Tue, 22 Sep 2026 22:01:22 -0700 [thread overview]
Message-ID: <320446364440ae5fa150f855afa2f0357eafb784.1790139577.git.shakeel.butt@linux.dev> (raw)
In-Reply-To: <cover.1790139577.git.shakeel.butt@linux.dev>
Add 16 KUnit tests that check task_nr_tracked_locks(current) always
equals the number of opted-in locks the task holds. They cover:
- locks that did not opt in, which must not count
- the mutex, rw_semaphore and percpu_rw_semaphore lock and trylock
calls, including the killable, interruptible, freezable and io ones
- downgrade_write(), the cleanup.h guards, and nested locks released
out of order
- the percpu_rwsem_release()/percpu_rwsem_acquire() hand-off
- re-init clearing the opt-in
Five tests use a helper thread that holds a lock. They check that a
failed trylock does not count, that a lock taken right as the helper
releases it (usually through the slow path) counts exactly once, and
that each task has its own count.
Three cases cover what the opt-in changes besides the count: that an
opted-in but unheld lock still reads unlocked, which the sticky bit
would otherwise break for rwsem_is_locked() and its callers; that
*_track_holder() refuses a lock that is already held; and that the
non_owner rwsem calls warn and do not count. The last needs
CONFIG_DEBUG_LOCK_ALLOC, as without it they are plain down_read() and
up_read(), so it skips otherwise.
Tests compare against the count at the start of the test, not zero.
Passes on UML, x86_64, x86_64 with PROVE_LOCKING + DEBUG_MUTEXES +
DEBUG_RWSEMS (where the mutex fast path is compiled out), and x86_64
PREEMPT_RT:
tools/testing/kunit/kunit.py run --kunitconfig=kernel/locking/ lockholder
Signed-off-by: Shakeel Butt <shakeel.butt@linux.dev>
---
kernel/locking/.kunitconfig | 4 +
kernel/locking/Makefile | 1 +
kernel/locking/lockholder_kunit.c | 741 ++++++++++++++++++++++++++++++
lib/Kconfig.debug | 13 +
4 files changed, 759 insertions(+)
create mode 100644 kernel/locking/.kunitconfig
create mode 100644 kernel/locking/lockholder_kunit.c
diff --git a/kernel/locking/.kunitconfig b/kernel/locking/.kunitconfig
new file mode 100644
index 000000000000..ed243843b8b0
--- /dev/null
+++ b/kernel/locking/.kunitconfig
@@ -0,0 +1,4 @@
+CONFIG_KUNIT=y
+CONFIG_SMP=y
+CONFIG_TRACK_LOCK_HOLDERS=y
+CONFIG_TRACK_LOCK_HOLDERS_KUNIT_TEST=y
diff --git a/kernel/locking/Makefile b/kernel/locking/Makefile
index a0945beb304c..d23b754df40f 100644
--- a/kernel/locking/Makefile
+++ b/kernel/locking/Makefile
@@ -10,6 +10,7 @@ CONTEXT_ANALYSIS_rwsem.o := y
obj-y += mutex.o semaphore.o rwsem.o percpu-rwsem.o
obj-$(CONFIG_TRACK_LOCK_HOLDERS) += lockholder.o
+obj-$(CONFIG_TRACK_LOCK_HOLDERS_KUNIT_TEST) += lockholder_kunit.o
# Avoid recursion lockdep -> sanitizer -> ... -> lockdep & improve performance.
KASAN_SANITIZE_lockdep.o := n
diff --git a/kernel/locking/lockholder_kunit.c b/kernel/locking/lockholder_kunit.c
new file mode 100644
index 000000000000..6b3c3a66f7df
--- /dev/null
+++ b/kernel/locking/lockholder_kunit.c
@@ -0,0 +1,741 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * KUnit tests for sleeping lock holder tracking: check that
+ * task_nr_tracked_locks(current) always equals the number of opted-in
+ * locks the task holds.
+ */
+#include <kunit/test.h>
+#include <linux/completion.h>
+#include <linux/delay.h>
+#include <linux/kthread.h>
+#include <linux/lockholder.h>
+#include <linux/mutex.h>
+#include <linux/percpu-rwsem.h>
+#include <linux/rwsem.h>
+#include <linux/sched.h>
+
+struct lockholder_ctx {
+ struct mutex tmutex;
+ struct mutex tmutex2;
+ struct mutex pmutex;
+ struct rw_semaphore tsem;
+ struct rw_semaphore tsem2;
+ struct rw_semaphore psem;
+ struct percpu_rw_semaphore tpcpu;
+ struct percpu_rw_semaphore ppcpu;
+ bool pcpu_ready;
+
+ /* Count at test start. Tests compare against it, not against zero. */
+ unsigned int base;
+
+ /* Helper thread that holds a lock until told to release it. */
+ struct completion helper_holds;
+ struct completion helper_may_release;
+ struct completion helper_done;
+ unsigned int helper_depth_held;
+ unsigned int helper_depth_after;
+};
+
+/* Check the count relative to the start of the test. */
+#define EXPECT_DEPTH(test, ctx, n) \
+ KUNIT_EXPECT_EQ((test), (ctx)->base + (unsigned int)(n), \
+ task_nr_tracked_locks(current))
+
+#define ASSERT_DEPTH(test, ctx, n) \
+ KUNIT_ASSERT_EQ((test), (ctx)->base + (unsigned int)(n), \
+ task_nr_tracked_locks(current))
+
+static int lockholder_init(struct kunit *test)
+{
+ struct lockholder_ctx *ctx;
+
+ ctx = kunit_kzalloc(test, sizeof(*ctx), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, ctx);
+
+ mutex_init(&ctx->tmutex);
+ mutex_init(&ctx->tmutex2);
+ mutex_init(&ctx->pmutex);
+ init_rwsem(&ctx->tsem);
+ init_rwsem(&ctx->tsem2);
+ init_rwsem(&ctx->psem);
+
+ mutex_track_holder(&ctx->tmutex);
+ mutex_track_holder(&ctx->tmutex2);
+ rwsem_track_holder(&ctx->tsem);
+ rwsem_track_holder(&ctx->tsem2);
+
+ KUNIT_ASSERT_EQ(test, 0, percpu_init_rwsem(&ctx->tpcpu));
+ if (percpu_init_rwsem(&ctx->ppcpu)) {
+ percpu_free_rwsem(&ctx->tpcpu);
+ KUNIT_ASSERT_TRUE(test, false);
+ }
+ ctx->pcpu_ready = true;
+ percpu_rwsem_track_holder(&ctx->tpcpu);
+
+ init_completion(&ctx->helper_holds);
+ init_completion(&ctx->helper_may_release);
+ init_completion(&ctx->helper_done);
+
+ ctx->base = task_nr_tracked_locks(current);
+ test->priv = ctx;
+ return 0;
+}
+
+static void lockholder_exit(struct kunit *test)
+{
+ struct lockholder_ctx *ctx = test->priv;
+
+ if (ctx && ctx->pcpu_ready) {
+ percpu_free_rwsem(&ctx->tpcpu);
+ percpu_free_rwsem(&ctx->ppcpu);
+ ctx->pcpu_ready = false;
+ }
+}
+
+/* Locks that did not opt in are never counted. */
+static void untracked_locks_are_not_counted(struct kunit *test)
+{
+ struct lockholder_ctx *ctx = test->priv;
+
+ mutex_lock(&ctx->pmutex);
+ EXPECT_DEPTH(test, ctx, 0);
+ mutex_unlock(&ctx->pmutex);
+ EXPECT_DEPTH(test, ctx, 0);
+
+ KUNIT_EXPECT_TRUE(test, mutex_trylock(&ctx->pmutex));
+ EXPECT_DEPTH(test, ctx, 0);
+ mutex_unlock(&ctx->pmutex);
+
+ down_read(&ctx->psem);
+ EXPECT_DEPTH(test, ctx, 0);
+ up_read(&ctx->psem);
+
+ down_write(&ctx->psem);
+ EXPECT_DEPTH(test, ctx, 0);
+ up_write(&ctx->psem);
+ EXPECT_DEPTH(test, ctx, 0);
+
+ percpu_down_read(&ctx->ppcpu);
+ EXPECT_DEPTH(test, ctx, 0);
+ percpu_up_read(&ctx->ppcpu);
+
+ percpu_down_write(&ctx->ppcpu);
+ EXPECT_DEPTH(test, ctx, 0);
+ percpu_up_write(&ctx->ppcpu);
+ EXPECT_DEPTH(test, ctx, 0);
+}
+
+static void tracked_mutex_is_counted(struct kunit *test)
+{
+ struct lockholder_ctx *ctx = test->priv;
+
+ mutex_lock(&ctx->tmutex);
+ EXPECT_DEPTH(test, ctx, 1);
+ mutex_unlock(&ctx->tmutex);
+ EXPECT_DEPTH(test, ctx, 0);
+
+ KUNIT_ASSERT_EQ(test, 0, mutex_lock_interruptible(&ctx->tmutex));
+ EXPECT_DEPTH(test, ctx, 1);
+ mutex_unlock(&ctx->tmutex);
+ EXPECT_DEPTH(test, ctx, 0);
+
+ KUNIT_ASSERT_EQ(test, 0, mutex_lock_killable(&ctx->tmutex));
+ EXPECT_DEPTH(test, ctx, 1);
+ mutex_unlock(&ctx->tmutex);
+ EXPECT_DEPTH(test, ctx, 0);
+
+ mutex_lock_io(&ctx->tmutex);
+ EXPECT_DEPTH(test, ctx, 1);
+ mutex_unlock(&ctx->tmutex);
+ EXPECT_DEPTH(test, ctx, 0);
+
+ KUNIT_ASSERT_TRUE(test, mutex_trylock(&ctx->tmutex));
+ EXPECT_DEPTH(test, ctx, 1);
+ mutex_unlock(&ctx->tmutex);
+ EXPECT_DEPTH(test, ctx, 0);
+}
+
+static void tracked_rwsem_read_is_counted(struct kunit *test)
+{
+ struct lockholder_ctx *ctx = test->priv;
+
+ down_read(&ctx->tsem);
+ EXPECT_DEPTH(test, ctx, 1);
+ up_read(&ctx->tsem);
+ EXPECT_DEPTH(test, ctx, 0);
+
+ KUNIT_ASSERT_EQ(test, 0, down_read_killable(&ctx->tsem));
+ EXPECT_DEPTH(test, ctx, 1);
+ up_read(&ctx->tsem);
+ EXPECT_DEPTH(test, ctx, 0);
+
+ KUNIT_ASSERT_EQ(test, 0, down_read_interruptible(&ctx->tsem));
+ EXPECT_DEPTH(test, ctx, 1);
+ up_read(&ctx->tsem);
+ EXPECT_DEPTH(test, ctx, 0);
+
+ KUNIT_ASSERT_EQ(test, 1, down_read_trylock(&ctx->tsem));
+ EXPECT_DEPTH(test, ctx, 1);
+ up_read(&ctx->tsem);
+ EXPECT_DEPTH(test, ctx, 0);
+}
+
+static void tracked_rwsem_write_is_counted(struct kunit *test)
+{
+ struct lockholder_ctx *ctx = test->priv;
+
+ down_write(&ctx->tsem);
+ EXPECT_DEPTH(test, ctx, 1);
+ up_write(&ctx->tsem);
+ EXPECT_DEPTH(test, ctx, 0);
+
+ KUNIT_ASSERT_EQ(test, 0, down_write_killable(&ctx->tsem));
+ EXPECT_DEPTH(test, ctx, 1);
+ up_write(&ctx->tsem);
+ EXPECT_DEPTH(test, ctx, 0);
+
+ KUNIT_ASSERT_EQ(test, 1, down_write_trylock(&ctx->tsem));
+ EXPECT_DEPTH(test, ctx, 1);
+ up_write(&ctx->tsem);
+ EXPECT_DEPTH(test, ctx, 0);
+}
+
+static void tracked_percpu_rwsem_is_counted(struct kunit *test)
+{
+ struct lockholder_ctx *ctx = test->priv;
+
+ percpu_down_read(&ctx->tpcpu);
+ EXPECT_DEPTH(test, ctx, 1);
+ percpu_up_read(&ctx->tpcpu);
+ EXPECT_DEPTH(test, ctx, 0);
+
+ percpu_down_read_freezable(&ctx->tpcpu, false);
+ EXPECT_DEPTH(test, ctx, 1);
+ percpu_up_read(&ctx->tpcpu);
+ EXPECT_DEPTH(test, ctx, 0);
+
+ KUNIT_ASSERT_TRUE(test, percpu_down_read_trylock(&ctx->tpcpu));
+ EXPECT_DEPTH(test, ctx, 1);
+ percpu_up_read(&ctx->tpcpu);
+ EXPECT_DEPTH(test, ctx, 0);
+
+ percpu_down_write(&ctx->tpcpu);
+ EXPECT_DEPTH(test, ctx, 1);
+ percpu_up_write(&ctx->tpcpu);
+ EXPECT_DEPTH(test, ctx, 0);
+
+ scoped_guard(percpu_read, &ctx->tpcpu)
+ EXPECT_DEPTH(test, ctx, 1);
+ EXPECT_DEPTH(test, ctx, 0);
+
+ scoped_guard(percpu_write, &ctx->tpcpu)
+ EXPECT_DEPTH(test, ctx, 1);
+ EXPECT_DEPTH(test, ctx, 0);
+}
+
+/*
+ * percpu_rwsem_release()/percpu_rwsem_acquire() move the count with the
+ * lock.
+ */
+static void percpu_rwsem_handover_moves_the_count(struct kunit *test)
+{
+ struct lockholder_ctx *ctx = test->priv;
+
+ percpu_down_write(&ctx->tpcpu);
+ ASSERT_DEPTH(test, ctx, 1);
+
+ /* As if returning to user space with the lock held. */
+ percpu_rwsem_release(&ctx->tpcpu, _THIS_IP_);
+ EXPECT_DEPTH(test, ctx, 0);
+
+ /* Take it back before releasing it. */
+ percpu_rwsem_acquire(&ctx->tpcpu, false, _THIS_IP_);
+ EXPECT_DEPTH(test, ctx, 1);
+
+ percpu_up_write(&ctx->tpcpu);
+ EXPECT_DEPTH(test, ctx, 0);
+}
+
+/* The percpu read slow path, used right after a writer, is counted too. */
+static void percpu_rwsem_slow_read_path_is_counted(struct kunit *test)
+{
+ struct lockholder_ctx *ctx = test->priv;
+
+ /* A writer forces readers onto the slow path for a while. */
+ percpu_down_write(&ctx->tpcpu);
+ ASSERT_DEPTH(test, ctx, 1);
+ percpu_up_write(&ctx->tpcpu);
+ ASSERT_DEPTH(test, ctx, 0);
+
+ /*
+ * rcu_sync stays non-idle for a grace period after a writer, so
+ * this most likely uses the slow path.
+ */
+ percpu_down_read(&ctx->tpcpu);
+ EXPECT_DEPTH(test, ctx, 1);
+ percpu_up_read(&ctx->tpcpu);
+ EXPECT_DEPTH(test, ctx, 0);
+
+ KUNIT_EXPECT_TRUE(test, percpu_down_read_trylock(&ctx->tpcpu));
+ EXPECT_DEPTH(test, ctx, 1);
+ percpu_up_read(&ctx->tpcpu);
+ EXPECT_DEPTH(test, ctx, 0);
+}
+
+/* downgrade_write() keeps the lock held, so the count does not change. */
+static void downgrade_write_keeps_the_count(struct kunit *test)
+{
+ struct lockholder_ctx *ctx = test->priv;
+
+ down_write(&ctx->tsem);
+ ASSERT_DEPTH(test, ctx, 1);
+
+ downgrade_write(&ctx->tsem);
+ EXPECT_DEPTH(test, ctx, 1);
+
+ up_read(&ctx->tsem);
+ EXPECT_DEPTH(test, ctx, 0);
+}
+
+/* Tracked locks add up; untracked ones in between do not count. */
+static void nested_locks_stack(struct kunit *test)
+{
+ struct lockholder_ctx *ctx = test->priv;
+
+ mutex_lock(&ctx->tmutex);
+ EXPECT_DEPTH(test, ctx, 1);
+
+ mutex_lock(&ctx->pmutex);
+ EXPECT_DEPTH(test, ctx, 1);
+
+ down_read(&ctx->tsem);
+ EXPECT_DEPTH(test, ctx, 2);
+
+ down_write(&ctx->psem);
+ EXPECT_DEPTH(test, ctx, 2);
+
+ mutex_lock(&ctx->tmutex2);
+ EXPECT_DEPTH(test, ctx, 3);
+
+ down_read(&ctx->tsem2);
+ EXPECT_DEPTH(test, ctx, 4);
+
+ percpu_down_read(&ctx->tpcpu);
+ EXPECT_DEPTH(test, ctx, 5);
+
+ percpu_down_read(&ctx->ppcpu);
+ EXPECT_DEPTH(test, ctx, 5);
+
+ /* Release in a different order than taken. */
+ up_read(&ctx->tsem);
+ EXPECT_DEPTH(test, ctx, 4);
+ percpu_up_read(&ctx->ppcpu);
+ EXPECT_DEPTH(test, ctx, 4);
+ up_write(&ctx->psem);
+ EXPECT_DEPTH(test, ctx, 4);
+ percpu_up_read(&ctx->tpcpu);
+ EXPECT_DEPTH(test, ctx, 3);
+ up_read(&ctx->tsem2);
+ EXPECT_DEPTH(test, ctx, 2);
+ mutex_unlock(&ctx->pmutex);
+ EXPECT_DEPTH(test, ctx, 2);
+ mutex_unlock(&ctx->tmutex2);
+ EXPECT_DEPTH(test, ctx, 1);
+ mutex_unlock(&ctx->tmutex);
+ EXPECT_DEPTH(test, ctx, 0);
+}
+
+/* cleanup.h guards are counted too. */
+static void guards_are_counted(struct kunit *test)
+{
+ struct lockholder_ctx *ctx = test->priv;
+
+ scoped_guard(mutex, &ctx->tmutex)
+ EXPECT_DEPTH(test, ctx, 1);
+ EXPECT_DEPTH(test, ctx, 0);
+
+ scoped_guard(rwsem_read, &ctx->tsem)
+ EXPECT_DEPTH(test, ctx, 1);
+ EXPECT_DEPTH(test, ctx, 0);
+
+ scoped_guard(rwsem_write, &ctx->tsem)
+ EXPECT_DEPTH(test, ctx, 1);
+ EXPECT_DEPTH(test, ctx, 0);
+}
+
+/*
+ * The helper thread takes a lock and records its own count. It is used to
+ * create contention and to check that counts are per task.
+ */
+enum helper_lock {
+ HELPER_TMUTEX,
+ HELPER_TSEM_WRITE,
+ HELPER_TPCPU_WRITE,
+};
+
+struct helper_arg {
+ struct lockholder_ctx *ctx;
+ enum helper_lock which;
+};
+
+static int lockholder_helper(void *data)
+{
+ struct helper_arg *arg = data;
+ struct lockholder_ctx *ctx = arg->ctx;
+
+ switch (arg->which) {
+ case HELPER_TMUTEX:
+ mutex_lock(&ctx->tmutex);
+ break;
+ case HELPER_TSEM_WRITE:
+ down_write(&ctx->tsem);
+ break;
+ case HELPER_TPCPU_WRITE:
+ percpu_down_write(&ctx->tpcpu);
+ break;
+ }
+
+ ctx->helper_depth_held = task_nr_tracked_locks(current);
+ complete(&ctx->helper_holds);
+
+ wait_for_completion(&ctx->helper_may_release);
+
+ switch (arg->which) {
+ case HELPER_TMUTEX:
+ mutex_unlock(&ctx->tmutex);
+ break;
+ case HELPER_TSEM_WRITE:
+ up_write(&ctx->tsem);
+ break;
+ case HELPER_TPCPU_WRITE:
+ percpu_up_write(&ctx->tpcpu);
+ break;
+ }
+
+ ctx->helper_depth_after = task_nr_tracked_locks(current);
+ complete(&ctx->helper_done);
+
+ /* Wait for kthread_stop(). */
+ while (!kthread_should_stop())
+ schedule_timeout_interruptible(HZ / 10);
+ return 0;
+}
+
+static struct task_struct *start_helper(struct kunit *test,
+ enum helper_lock which)
+{
+ struct lockholder_ctx *ctx = test->priv;
+ struct helper_arg *arg;
+ struct task_struct *t;
+
+ arg = kunit_kzalloc(test, sizeof(*arg), GFP_KERNEL);
+ KUNIT_ASSERT_NOT_NULL(test, arg);
+ arg->ctx = ctx;
+ arg->which = which;
+
+ t = kthread_run(lockholder_helper, arg, "lockholder_kunit");
+ KUNIT_ASSERT_FALSE(test, IS_ERR(t));
+
+ wait_for_completion(&ctx->helper_holds);
+ return t;
+}
+
+static void stop_helper(struct kunit *test, struct task_struct *t)
+{
+ struct lockholder_ctx *ctx = test->priv;
+
+ complete(&ctx->helper_may_release);
+ wait_for_completion(&ctx->helper_done);
+ kthread_stop(t);
+}
+
+/* Each task has its own count, and a new task starts at zero. */
+static void the_count_is_per_task(struct kunit *test)
+{
+ struct lockholder_ctx *ctx = test->priv;
+ struct task_struct *t;
+
+ down_read(&ctx->tsem2);
+ ASSERT_DEPTH(test, ctx, 1);
+
+ t = start_helper(test, HELPER_TMUTEX);
+
+ /* The helper's lock is not in our count. */
+ EXPECT_DEPTH(test, ctx, 1);
+ /* The helper started from zero, not from our count. */
+ KUNIT_EXPECT_EQ(test, 1u, ctx->helper_depth_held);
+
+ stop_helper(test, t);
+ KUNIT_EXPECT_EQ(test, 0u, ctx->helper_depth_after);
+
+ EXPECT_DEPTH(test, ctx, 1);
+ up_read(&ctx->tsem2);
+ EXPECT_DEPTH(test, ctx, 0);
+}
+
+/* A failed trylock is not counted. */
+static void failed_trylock_is_not_counted(struct kunit *test)
+{
+ struct lockholder_ctx *ctx = test->priv;
+ struct task_struct *t;
+
+ t = start_helper(test, HELPER_TSEM_WRITE);
+
+ KUNIT_EXPECT_EQ(test, 0, down_read_trylock(&ctx->tsem));
+ EXPECT_DEPTH(test, ctx, 0);
+
+ KUNIT_EXPECT_EQ(test, 0, down_write_trylock(&ctx->tsem));
+ EXPECT_DEPTH(test, ctx, 0);
+
+ stop_helper(test, t);
+
+ /* Once the lock is free, the trylock succeeds and counts. */
+ KUNIT_ASSERT_EQ(test, 1, down_read_trylock(&ctx->tsem));
+ EXPECT_DEPTH(test, ctx, 1);
+ up_read(&ctx->tsem);
+ EXPECT_DEPTH(test, ctx, 0);
+}
+
+/*
+ * A failed mutex_trylock() is not counted, and a contended mutex_lock()
+ * is counted exactly once: not zero (slow path missed) and not twice
+ * (fast and slow path both counted).
+ */
+static void contended_mutex_is_counted_once(struct kunit *test)
+{
+ struct lockholder_ctx *ctx = test->priv;
+ struct task_struct *t;
+
+ t = start_helper(test, HELPER_TMUTEX);
+
+ KUNIT_EXPECT_FALSE(test, mutex_trylock(&ctx->tmutex));
+ EXPECT_DEPTH(test, ctx, 0);
+
+ /*
+ * Let the helper release the lock. mutex_lock() below most likely
+ * finds it still held and takes the slow path.
+ */
+ complete(&ctx->helper_may_release);
+
+ mutex_lock(&ctx->tmutex);
+ EXPECT_DEPTH(test, ctx, 1);
+ mutex_unlock(&ctx->tmutex);
+ EXPECT_DEPTH(test, ctx, 0);
+
+ wait_for_completion(&ctx->helper_done);
+ kthread_stop(t);
+}
+
+/* The same for a contended rwsem. */
+static void contended_rwsem_is_counted_once(struct kunit *test)
+{
+ struct lockholder_ctx *ctx = test->priv;
+ struct task_struct *t;
+
+ t = start_helper(test, HELPER_TSEM_WRITE);
+
+ complete(&ctx->helper_may_release);
+
+ down_write(&ctx->tsem);
+ EXPECT_DEPTH(test, ctx, 1);
+ up_write(&ctx->tsem);
+ EXPECT_DEPTH(test, ctx, 0);
+
+ down_read(&ctx->tsem);
+ EXPECT_DEPTH(test, ctx, 1);
+ up_read(&ctx->tsem);
+ EXPECT_DEPTH(test, ctx, 0);
+
+ wait_for_completion(&ctx->helper_done);
+ kthread_stop(t);
+}
+
+/* The same for a percpu_rw_semaphore, starting with a failed read trylock. */
+static void contended_percpu_rwsem_is_counted_once(struct kunit *test)
+{
+ struct lockholder_ctx *ctx = test->priv;
+ struct task_struct *t;
+
+ t = start_helper(test, HELPER_TPCPU_WRITE);
+ KUNIT_EXPECT_EQ(test, 1u, ctx->helper_depth_held);
+
+ KUNIT_EXPECT_FALSE(test, percpu_down_read_trylock(&ctx->tpcpu));
+ EXPECT_DEPTH(test, ctx, 0);
+
+ complete(&ctx->helper_may_release);
+
+ percpu_down_read(&ctx->tpcpu);
+ EXPECT_DEPTH(test, ctx, 1);
+ percpu_up_read(&ctx->tpcpu);
+ EXPECT_DEPTH(test, ctx, 0);
+
+ percpu_down_write(&ctx->tpcpu);
+ EXPECT_DEPTH(test, ctx, 1);
+ percpu_up_write(&ctx->tpcpu);
+ EXPECT_DEPTH(test, ctx, 0);
+
+ wait_for_completion(&ctx->helper_done);
+ KUNIT_EXPECT_EQ(test, 0u, ctx->helper_depth_after);
+ kthread_stop(t);
+}
+
+/* Initializing a lock again clears its opt-in. */
+/*
+ * The opt-in is a sticky bit in the same word that says whether the lock is
+ * held, so an opted-in lock that nobody holds must still read as unlocked.
+ */
+static void opted_in_lock_still_reads_unlocked(struct kunit *test)
+{
+ struct lockholder_ctx *ctx = test->priv;
+
+ KUNIT_EXPECT_FALSE(test, rwsem_is_locked(&ctx->tsem));
+ KUNIT_EXPECT_FALSE(test, mutex_is_locked(&ctx->tmutex));
+ KUNIT_EXPECT_FALSE(test, percpu_is_read_locked(&ctx->tpcpu));
+ KUNIT_EXPECT_FALSE(test, percpu_is_write_locked(&ctx->tpcpu));
+
+ down_read(&ctx->tsem);
+ KUNIT_EXPECT_TRUE(test, rwsem_is_locked(&ctx->tsem));
+ up_read(&ctx->tsem);
+ KUNIT_EXPECT_FALSE(test, rwsem_is_locked(&ctx->tsem));
+
+ down_write(&ctx->tsem);
+ KUNIT_EXPECT_TRUE(test, rwsem_is_locked(&ctx->tsem));
+ up_write(&ctx->tsem);
+ KUNIT_EXPECT_FALSE(test, rwsem_is_locked(&ctx->tsem));
+
+ mutex_lock(&ctx->tmutex);
+ KUNIT_EXPECT_TRUE(test, mutex_is_locked(&ctx->tmutex));
+ mutex_unlock(&ctx->tmutex);
+ KUNIT_EXPECT_FALSE(test, mutex_is_locked(&ctx->tmutex));
+
+ percpu_down_write(&ctx->tpcpu);
+ KUNIT_EXPECT_TRUE(test, percpu_is_write_locked(&ctx->tpcpu));
+ percpu_up_write(&ctx->tpcpu);
+ KUNIT_EXPECT_FALSE(test, percpu_is_write_locked(&ctx->tpcpu));
+}
+
+/* Opting in a lock that is already held is refused, and warns. */
+static void track_holder_on_a_held_lock_is_refused(struct kunit *test)
+{
+ struct lockholder_ctx *ctx = test->priv;
+
+ mutex_lock(&ctx->pmutex);
+ down_read(&ctx->psem);
+ percpu_down_read(&ctx->ppcpu);
+ EXPECT_DEPTH(test, ctx, 0);
+
+ kunit_warning_suppress(test) {
+ mutex_track_holder(&ctx->pmutex);
+ rwsem_track_holder(&ctx->psem);
+ percpu_rwsem_track_holder(&ctx->ppcpu);
+ KUNIT_EXPECT_SUPPRESSED_WARNING_COUNT(test, 3);
+ }
+
+ /* Refused, so the locks are still untracked and nothing is counted. */
+ EXPECT_DEPTH(test, ctx, 0);
+ percpu_up_read(&ctx->ppcpu);
+ up_read(&ctx->psem);
+ mutex_unlock(&ctx->pmutex);
+ EXPECT_DEPTH(test, ctx, 0);
+
+ mutex_lock(&ctx->pmutex);
+ EXPECT_DEPTH(test, ctx, 0);
+ mutex_unlock(&ctx->pmutex);
+}
+
+/*
+ * The non_owner calls can run in different tasks, which a per-task count
+ * cannot follow. Both sides warn on a tracked rwsem and neither counts.
+ *
+ * Without CONFIG_DEBUG_LOCK_ALLOC they are plain down_read()/up_read(),
+ * which are counted and balanced, so there is nothing to test.
+ */
+static void non_owner_on_a_tracked_rwsem_warns(struct kunit *test)
+{
+ struct lockholder_ctx *ctx = test->priv;
+
+ if (!IS_ENABLED(CONFIG_DEBUG_LOCK_ALLOC))
+ kunit_skip(test, "non_owner API needs CONFIG_DEBUG_LOCK_ALLOC");
+
+ kunit_warning_suppress(test) {
+ down_read_non_owner(&ctx->tsem);
+ EXPECT_DEPTH(test, ctx, 0);
+ up_read_non_owner(&ctx->tsem);
+ KUNIT_EXPECT_SUPPRESSED_WARNING_COUNT(test, 2);
+ }
+ EXPECT_DEPTH(test, ctx, 0);
+
+ /* An untracked rwsem uses them without warning, and is not counted. */
+ down_read_non_owner(&ctx->psem);
+ EXPECT_DEPTH(test, ctx, 0);
+ up_read_non_owner(&ctx->psem);
+ EXPECT_DEPTH(test, ctx, 0);
+}
+
+static void reinit_clears_the_opt_in(struct kunit *test)
+{
+ struct lockholder_ctx *ctx = test->priv;
+
+ down_read(&ctx->tsem);
+ EXPECT_DEPTH(test, ctx, 1);
+ up_read(&ctx->tsem);
+
+ init_rwsem(&ctx->tsem);
+ down_read(&ctx->tsem);
+ EXPECT_DEPTH(test, ctx, 0);
+ up_read(&ctx->tsem);
+
+ percpu_down_read(&ctx->tpcpu);
+ EXPECT_DEPTH(test, ctx, 1);
+ percpu_up_read(&ctx->tpcpu);
+
+ percpu_free_rwsem(&ctx->tpcpu);
+ KUNIT_ASSERT_EQ(test, 0, percpu_init_rwsem(&ctx->tpcpu));
+ percpu_down_read(&ctx->tpcpu);
+ EXPECT_DEPTH(test, ctx, 0);
+ percpu_up_read(&ctx->tpcpu);
+
+ mutex_lock(&ctx->tmutex);
+ EXPECT_DEPTH(test, ctx, 1);
+ mutex_unlock(&ctx->tmutex);
+
+ mutex_init(&ctx->tmutex);
+ mutex_lock(&ctx->tmutex);
+ EXPECT_DEPTH(test, ctx, 0);
+ mutex_unlock(&ctx->tmutex);
+ EXPECT_DEPTH(test, ctx, 0);
+}
+
+static struct kunit_case lockholder_test_cases[] = {
+ KUNIT_CASE(untracked_locks_are_not_counted),
+ KUNIT_CASE(tracked_mutex_is_counted),
+ KUNIT_CASE(tracked_rwsem_read_is_counted),
+ KUNIT_CASE(tracked_rwsem_write_is_counted),
+ KUNIT_CASE(tracked_percpu_rwsem_is_counted),
+ KUNIT_CASE(percpu_rwsem_handover_moves_the_count),
+ KUNIT_CASE(percpu_rwsem_slow_read_path_is_counted),
+ KUNIT_CASE(downgrade_write_keeps_the_count),
+ KUNIT_CASE(nested_locks_stack),
+ KUNIT_CASE(guards_are_counted),
+ KUNIT_CASE(the_count_is_per_task),
+ KUNIT_CASE(failed_trylock_is_not_counted),
+ KUNIT_CASE(contended_mutex_is_counted_once),
+ KUNIT_CASE(contended_rwsem_is_counted_once),
+ KUNIT_CASE(contended_percpu_rwsem_is_counted_once),
+ KUNIT_CASE(reinit_clears_the_opt_in),
+ KUNIT_CASE(opted_in_lock_still_reads_unlocked),
+ KUNIT_CASE(track_holder_on_a_held_lock_is_refused),
+ KUNIT_CASE(non_owner_on_a_tracked_rwsem_warns),
+ {}
+};
+
+static struct kunit_suite lockholder_test_suite = {
+ .name = "lockholder",
+ .init = lockholder_init,
+ .exit = lockholder_exit,
+ .test_cases = lockholder_test_cases,
+};
+kunit_test_suite(lockholder_test_suite);
+
+MODULE_DESCRIPTION("KUnit tests for sleeping lock holder tracking");
+MODULE_LICENSE("GPL");
diff --git a/lib/Kconfig.debug b/lib/Kconfig.debug
index 163192b2ed7f..67fc86b4fc6f 100644
--- a/lib/Kconfig.debug
+++ b/lib/Kconfig.debug
@@ -1672,6 +1672,19 @@ config WW_MUTEX_SELFTEST
Say M if you want these self tests to build as a module.
Say N if you are unsure.
+config TRACK_LOCK_HOLDERS_KUNIT_TEST
+ tristate "KUnit tests for sleeping lock holder tracking" if !KUNIT_ALL_TESTS
+ depends on KUNIT && TRACK_LOCK_HOLDERS
+ default KUNIT_ALL_TESTS
+ help
+ Check that task_nr_tracked_locks() always equals the number of
+ opted-in locks the task holds.
+
+ For more information on KUnit and unit tests in general, refer to
+ the KUnit documentation in Documentation/dev-tools/kunit/.
+
+ If unsure, say N.
+
config SCF_TORTURE_TEST
tristate "torture tests for smp_call_function*()"
depends on DEBUG_KERNEL
--
2.53.0-Meta
next prev parent reply other threads:[~2026-09-23 5:01 UTC|newest]
Thread overview: 8+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-09-23 5:01 [PATCH 0/7] locking: opt-in tracking of sleeping lock holders Shakeel Butt
2026-09-23 5:01 ` [PATCH 1/7] locking: add " Shakeel Butt
2026-09-23 5:01 ` [PATCH 2/7] locking/rwsem: track holders of opted-in rw_semaphores Shakeel Butt
2026-09-23 5:01 ` [PATCH 3/7] locking/mutex: track holders of opted-in mutexes Shakeel Butt
2026-09-23 5:01 ` [PATCH 4/7] locking/percpu-rwsem: track holders of opted-in percpu_rw_semaphores Shakeel Butt
2026-09-23 5:01 ` Shakeel Butt [this message]
2026-09-23 5:01 ` [PATCH 6/7] Documentation/locking: document sleeping lock holder tracking Shakeel Butt
2026-09-23 5:01 ` [PATCH 7/7] kernfs, cgroup: track holders of the cgroupfs locks Shakeel Butt
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=320446364440ae5fa150f855afa2f0357eafb784.1790139577.git.shakeel.butt@linux.dev \
--to=shakeel.butt@linux.dev \
--cc=bigeasy@linutronix.de \
--cc=boqun@kernel.org \
--cc=cgroups@vger.kernel.org \
--cc=christian@brauner.io \
--cc=corbet@lwn.net \
--cc=driver-core@lists.linux.dev \
--cc=gregkh@linuxfoundation.org \
--cc=hannes@cmpxchg.org \
--cc=kernel-team@meta.com \
--cc=linux-kernel@vger.kernel.org \
--cc=longman@redhat.com \
--cc=mingo@redhat.com \
--cc=paulmck@kernel.org \
--cc=peterz@infradead.org \
--cc=tj@kernel.org \
--cc=will@kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®