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

From: Wolfram Sang

Date: Thu Sep 17 2026 - 05:59:26 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_single_lock; done

Signed-off-by: Wolfram Sang <wsa+renesas@xxxxxxxxxxxxxxxxxxxx>
Reviewed-by: Geert Uytterhoeven <geert+renesas@xxxxxxxxx>
---

Changes since v7:
* renamed debugfs-files to be more specific
* reworded commit message slightly
* added tag

Thanks to Geert for the comments!


drivers/hwspinlock/hwspinlock_core.c | 131 +++++++++++++++++++++++++++
1 file changed, 131 insertions(+)

diff --git a/drivers/hwspinlock/hwspinlock_core.c b/drivers/hwspinlock/hwspinlock_core.c
index 79eb23183434..d5570c21ac12 100644
--- a/drivers/hwspinlock/hwspinlock_core.c
+++ b/drivers/hwspinlock/hwspinlock_core.c
@@ -914,6 +914,120 @@ 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 int fops_request_set(void *data, u64 id)
+{
+ if (id > UINT_MAX)
+ return -EINVAL;
+
+ return hwspin_lock_request_specific(id) ? 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;
+
+ if (id > UINT_MAX)
+ return -EINVAL;
+
+ hwlock = xa_load(&hwspinlocks, 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. Be aware that you might need another properly
+ * requested lock on a provider to ensure that its clocks etc are enabled!
+ */
+static int fops_trylock_raw_set(void *data, u64 id)
+{
+ struct hwspinlock *hwlock;
+
+ if (id > UINT_MAX)
+ return -EINVAL;
+
+ hwlock = xa_load(&hwspinlocks, 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;
+
+ if (id > UINT_MAX)
+ return -EINVAL;
+
+ hwlock = xa_load(&hwspinlocks, 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_single_lock_set(void *data, u64 id)
+{
+ struct hwspinlock *hwlock;
+ int ret;
+
+ if (id > UINT_MAX)
+ return -EINVAL;
+
+ hwlock = hwspin_lock_request_specific(id);
+ if (!hwlock)
+ return -ENOENT;
+
+ /* Try twice to see if unlocking was also successful */
+ for (int i = 0; i < 2; i++) {
+ ret = hwspin_trylock_raw(hwlock);
+ if (ret) {
+ hwspin_lock_free(hwlock);
+ return ret;
+ }
+
+ ret = hwspin_trylock_raw(hwlock);
+ if (ret != -EBUSY) {
+ if (ret == 0)
+ hwspin_unlock_raw(hwlock);
+ hwspin_lock_free(hwlock);
+ return -EACCES;
+ }
+
+ hwspin_unlock_raw(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_single_lock, NULL, fops_test_single_lock_set, "%llu\n");
+
+static void hwspin_lock_low_level_debug_init(struct dentry *rootdir)
+{
+ debugfs_create_file("request_lock", 0200, rootdir, NULL, &fops_request);
+ debugfs_create_file("free_lock", 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_single_lock", 0200, rootdir, NULL, &fops_test_single_lock);
+}
+#endif
+
/*
* subsys_initcall() is used here but controllers may already have been
* registered earlier or will be later. The rationale is that debugfs is
@@ -926,6 +1040,23 @@ 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.53.0