[PATCH v4 3/3] hwspinlock: add low level debug capabilities

From: Wolfram Sang

Date: Sat Jul 18 2026 - 12:31:42 EST


While developing the hwspinlock core or a device driver, it can be
really helpful to change states from userspace. Add a debugsfs interface
to request, free, lock, unlock, and test hwspinlocks. Because this is a
potentially dangerous interface, it can only be enabled via source code.
Similar to writeable attributes for clocks where this idea stems from.
The created files take a single id for a single lock. To test all locks
in a system, one could use this oneliner while in the debugfs directory:

for l in $(cut -d: -f1 hwspinlock_summary); do echo $l > test_lock; done

Signed-off-by: Wolfram Sang <wsa+renesas@xxxxxxxxxxxxxxxxxxxx>
---
drivers/hwspinlock/hwspinlock_core.c | 122 +++++++++++++++++++++++++++
1 file changed, 122 insertions(+)

diff --git a/drivers/hwspinlock/hwspinlock_core.c b/drivers/hwspinlock/hwspinlock_core.c
index 84e9aae6ef44..052dea7e4850 100644
--- a/drivers/hwspinlock/hwspinlock_core.c
+++ b/drivers/hwspinlock/hwspinlock_core.c
@@ -908,6 +908,110 @@ static const struct seq_operations hwspinlock_sops = {
};
DEFINE_SEQ_ATTRIBUTE(hwspinlock);

+/*
+ * This can be dangerous, therefore don't provide any real compile time
+ * configuration option for this feature.
+ * People who want to use this will need to modify the source code directly.
+ */
+#undef HWSPINLOCK_LOW_LEVEL_DEBUG
+#ifdef HWSPINLOCK_LOW_LEVEL_DEBUG
+
+static struct hwspinlock *hwspin_lock_ll_debug_get(u64 id)
+{
+ struct hwspinlock *hwlock;
+ unsigned long index = id;
+
+ rcu_read_lock();
+ hwlock = xa_find(&hwspinlocks, &index, ULONG_MAX, XA_PRESENT);
+ rcu_read_unlock();
+
+ return hwlock;
+}
+
+static int fops_request_set(void *data, u64 id)
+{
+ struct hwspinlock *hwlock = hwspin_lock_request_specific(id);
+
+ return hwlock ? 0 : -ENOENT;
+}
+DEFINE_DEBUGFS_ATTRIBUTE(fops_request, NULL, fops_request_set, "%llu\n");
+
+static int fops_free_set(void *data, u64 id)
+{
+ struct hwspinlock *hwlock = hwspin_lock_ll_debug_get(id);
+
+ return hwlock ? hwspin_lock_free(hwlock) : -ENOENT;
+}
+DEFINE_DEBUGFS_ATTRIBUTE(fops_free, NULL, fops_free_set, "%llu\n");
+
+/*
+ * It is intentionally allowed to lock/free hwspinlocks which have not been
+ * requested before. Like error injection, these inconsistent states can be
+ * very useful for debugging.
+ */
+static int fops_trylock_raw_set(void *data, u64 id)
+{
+ struct hwspinlock *hwlock = hwspin_lock_ll_debug_get(id);
+
+ return hwlock ? hwspin_trylock_raw(hwlock) : -ENOENT;
+}
+DEFINE_DEBUGFS_ATTRIBUTE(fops_trylock_raw, NULL, fops_trylock_raw_set, "%llu\n");
+
+static int fops_unlock_raw_set(void *data, u64 id)
+{
+ struct hwspinlock *hwlock = hwspin_lock_ll_debug_get(id);
+
+ if (hwlock)
+ hwspin_unlock_raw(hwlock);
+
+ return 0;
+}
+DEFINE_DEBUGFS_ATTRIBUTE(fops_unlock_raw, NULL, fops_unlock_raw_set, "%llu\n");
+
+static int fops_test_set(void *data, u64 id)
+{
+ struct hwspinlock *hwlock = hwspin_lock_request_specific(id);
+ int ret;
+
+ if (!hwlock)
+ return -ENOENT;
+
+ /* Try twice to see if unlocking was also successful */
+ for (int i = 0; i < 2; i++) {
+ ret = hwspin_trylock(hwlock);
+ if (ret) {
+ hwspin_lock_free(hwlock);
+ return ret;
+ }
+
+ ret = hwspin_trylock(hwlock);
+ if (ret != -EBUSY) {
+ hwspin_lock_free(hwlock);
+ return -EACCES;
+ }
+
+ hwspin_unlock(hwlock);
+ }
+
+ ret = hwspin_lock_free(hwlock);
+ if (ret)
+ return ret;
+
+ pr_info("Successfully tested lock %llu\n", id);
+ return 0;
+}
+DEFINE_DEBUGFS_ATTRIBUTE(fops_test, NULL, fops_test_set, "%llu\n");
+
+static void hwspin_lock_low_level_debug_init(struct dentry *rootdir)
+{
+ debugfs_create_file("request", 0200, rootdir, NULL, &fops_request);
+ debugfs_create_file("free", 0200, rootdir, NULL, &fops_free);
+ debugfs_create_file("lock", 0200, rootdir, NULL, &fops_trylock_raw);
+ debugfs_create_file("unlock", 0200, rootdir, NULL, &fops_unlock_raw);
+ debugfs_create_file("test_lock", 0200, rootdir, NULL, &fops_test);
+}
+#endif
+
/*
* subsys_initcall() is used here but controllers may already have been
* registered earlier or will be later. The rationale is that debugfs is
@@ -920,6 +1024,24 @@ static int __init hwspin_lock_init(void)

debugfs_create_file("hwspinlock_summary", 0444, hwspinlock_debugfs,
NULL, &hwspinlock_fops);
+
+#ifdef HWSPINLOCK_LOW_LEVEL_DEBUG
+ pr_warn("**********************************************************\n");
+ pr_warn("** NOTICE NOTICE NOTICE NOTICE NOTICE NOTICE NOTICE **\n");
+ pr_warn("** **\n");
+ pr_warn("** Low Level Debug for hwspinlocks is compiled in! **\n");
+ pr_warn("** **\n");
+ pr_warn("** This means safety, security, stability can be easily **\n");
+ pr_warn("** compromised from userspace! **\n");
+ pr_warn("** **\n");
+ pr_warn("** If you see this message and you are not debugging **\n");
+ pr_warn("** the kernel, report this immediately to your vendor! **\n");
+ pr_warn("** **\n");
+ pr_warn("** NOTICE NOTICE NOTICE NOTICE NOTICE NOTICE NOTICE **\n");
+ pr_warn("**********************************************************\n");
+ hwspin_lock_low_level_debug_init(hwspinlock_debugfs);
+#endif
+
return 0;
}
subsys_initcall(hwspin_lock_init);
--
2.51.0