Add kselftest cases for the revocable API.

The test consists of three parts:
- A kernel module (revocable_test.ko) that creates a debugfs file.
- A user-space C program (revocable_test) that uses the kselftest
  harness to interact with the debugfs file.
- An orchestrating shell script (test-revocable.sh) that loads the
  module, runs the C program, and unloads the module.

The test cases cover the following scenarios:
- Basic: Verifies that a consumer can successfully access the resource.
- Revocation: Verifies that after the provider revokes the resource,
  the consumer correctly receives a NULL pointer on a subsequent access.
- Try Access Macro: Same as "Revocation" but uses the macro level
  helpers.

Signed-off-by: Tzung-Bi Shih <[email protected]>
---
v8:
- Squash:
  - 4d7dc4d1a62d revocable: Fix races in revocable_alloc() using RCU
  - 377563ce0653 revocable: fix SRCU index corruption by requiring 
caller-provided storage
- Change accordingly due to its dependency "revocable: Revocable resource
  management" changes.
- Move:
  - tools/testing/selftests/drivers/base/revocable/ ->
    tools/testing/selftests/revocable/.

v7: https://lore.kernel.org/all/[email protected]
- "2025" -> "2026" in copyright.
- Rename the test name "macro" -> "try_access_macro".

v6: 
https://lore.kernel.org/chrome-platform/[email protected]
- Rename REVOCABLE_TRY_ACCESS_WITH() -> REVOCABLE_TRY_ACCESS_SCOPED().
- Add tests for new REVOCABLE_TRY_ACCESS_WITH().

v5: 
https://lore.kernel.org/chrome-platform/[email protected]
- No changes.

v4: 
https://lore.kernel.org/chrome-platform/[email protected]
- REVOCABLE() -> REVOCABLE_TRY_ACCESS_WITH().
- revocable_release() -> revocable_withdraw_access().

v3: 
https://lore.kernel.org/chrome-platform/[email protected]
- No changes.

v2: 
https://lore.kernel.org/chrome-platform/[email protected]
- New in the series.

 MAINTAINERS                                   |   1 +
 tools/testing/selftests/revocable/Makefile    |   7 +
 .../selftests/revocable/revocable_test.c      | 177 +++++++++++++
 .../selftests/revocable/revocable_test.h      |  20 ++
 .../selftests/revocable/test-revocable.sh     |  34 +++
 .../selftests/revocable/test_modules/Makefile |  10 +
 .../revocable/test_modules/revocable_test.c   | 234 ++++++++++++++++++
 7 files changed, 483 insertions(+)
 create mode 100644 tools/testing/selftests/revocable/Makefile
 create mode 100644 tools/testing/selftests/revocable/revocable_test.c
 create mode 100644 tools/testing/selftests/revocable/revocable_test.h
 create mode 100755 tools/testing/selftests/revocable/test-revocable.sh
 create mode 100644 tools/testing/selftests/revocable/test_modules/Makefile
 create mode 100644 
tools/testing/selftests/revocable/test_modules/revocable_test.c

diff --git a/MAINTAINERS b/MAINTAINERS
index 6ce7a5477f25..76816c741017 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -22416,6 +22416,7 @@ T:      git 
git://git.kernel.org/pub/scm/linux/kernel/git/driver-core/driver-core.git
 F:     drivers/base/revocable.c
 F:     drivers/base/revocable_test.c
 F:     include/linux/revocable.h
+F:     tools/testing/selftests/revocable/
 
 RFKILL
 M:     Johannes Berg <[email protected]>
diff --git a/tools/testing/selftests/revocable/Makefile 
b/tools/testing/selftests/revocable/Makefile
new file mode 100644
index 000000000000..a986ad50200a
--- /dev/null
+++ b/tools/testing/selftests/revocable/Makefile
@@ -0,0 +1,7 @@
+# SPDX-License-Identifier: GPL-2.0
+
+TEST_GEN_MODS_DIR := test_modules
+TEST_GEN_PROGS_EXTENDED := revocable_test
+TEST_PROGS := test-revocable.sh
+
+include ../lib.mk
diff --git a/tools/testing/selftests/revocable/revocable_test.c 
b/tools/testing/selftests/revocable/revocable_test.c
new file mode 100644
index 000000000000..2e90de210d9c
--- /dev/null
+++ b/tools/testing/selftests/revocable/revocable_test.c
@@ -0,0 +1,177 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright 2026 Google LLC
+ *
+ * A selftest for the revocable API.
+ *
+ * The test cases cover the following scenarios:
+ *
+ * - Basic: Verifies that a consumer can successfully access the resource.
+ *
+ * - Revocation: Verifies that after the provider revokes the resource,
+ *   the consumer correctly receives a NULL pointer on a subsequent access.
+ *
+ * - Try Access Macro: Same as "Revocation" but uses the macro level
+ *   helpers.
+ */
+
+#include <errno.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+#include "revocable_test.h"
+
+#include "../kselftest_harness.h"
+
+#define DEBUGFS_PATH "/sys/kernel/debug/revocable_test"
+
+FIXTURE(revocable_fixture) {
+       int fd;
+       char data[16];
+};
+
+FIXTURE_SETUP(revocable_fixture) {
+       int ret;
+
+       self->fd = open(DEBUGFS_PATH, O_RDWR);
+       ASSERT_NE(-1, self->fd)
+               TH_LOG("failed to open fd");
+}
+
+FIXTURE_TEARDOWN(revocable_fixture) {
+       close(self->fd);
+}
+
+/*
+ * ASSERT_* is only available in TEST or TEST_F block.  Use
+ * macro for the helper.
+ */
+#define READ_TEST_DATA_MSG(_offset, _msg)                              \
+       do {                                                            \
+               int ret;                                                \
+                                                                       \
+               ret = lseek(self->fd, _offset, SEEK_SET);               \
+               ASSERT_NE(-1, ret)                                      \
+                       TH_LOG("failed to lseek");                      \
+                                                                       \
+               ret = read(self->fd, self->data, sizeof(self->data)-1); \
+               ASSERT_NE(-1, ret)                                      \
+                       TH_LOG("failed to read test data" _msg);        \
+               self->data[ret] = '\0';                                 \
+       } while (0)
+
+#define READ_TEST_DATA(_offset)                                                
\
+       READ_TEST_DATA_MSG(_offset, "")
+
+#define READ_TEST_DATA_ERR(_offset)                                    \
+       do {                                                            \
+               int ret;                                                \
+                                                                       \
+               ret = lseek(self->fd, _offset, SEEK_SET);               \
+               ASSERT_NE(-1, ret)                                      \
+                       TH_LOG("failed to lseek");                      \
+                                                                       \
+               ret = read(self->fd, self->data, sizeof(self->data)-1); \
+               EXPECT_EQ(-1, ret);                                     \
+       } while (0)
+
+#define SIGNAL_RESOURCE_GONE()                                         \
+       do {                                                            \
+               int ret;                                                \
+                                                                       \
+               ret = write(self->fd, "", 0);                           \
+               ASSERT_NE(-1, ret)                                      \
+                       TH_LOG("failed to signal resource is gone");    \
+       } while (0)
+
+TEST_F(revocable_fixture, basic) {
+       READ_TEST_DATA(TEST_MAGIC_OFFSET_RAW);
+       EXPECT_STREQ(TEST_DATA, self->data);
+}
+
+TEST_F(revocable_fixture, revocation) {
+       const int offset = TEST_MAGIC_OFFSET_RAW;
+
+       READ_TEST_DATA(offset);
+       EXPECT_STREQ(TEST_DATA, self->data);
+
+       SIGNAL_RESOURCE_GONE();
+
+       READ_TEST_DATA_MSG(offset, " after resource gone");
+       EXPECT_STREQ(TEST_DATA_NULL, self->data);
+}
+
+TEST_F(revocable_fixture, try_access_macro1) {
+       const int offset = TEST_MAGIC_OFFSET_MACRO_TRY_ACCESS_WITH;
+
+       READ_TEST_DATA(offset);
+       EXPECT_STREQ(TEST_DATA, self->data);
+
+       SIGNAL_RESOURCE_GONE();
+
+       READ_TEST_DATA_MSG(offset, " after resource gone");
+       EXPECT_STREQ(TEST_DATA_NULL, self->data);
+}
+
+TEST_F(revocable_fixture, try_access_macro2) {
+       const int offset = TEST_MAGIC_OFFSET_MACRO_TRY_ACCESS_OR_RETURN_ERR;
+
+       READ_TEST_DATA(offset);
+       EXPECT_STREQ(TEST_DATA, self->data);
+
+       SIGNAL_RESOURCE_GONE();
+
+       READ_TEST_DATA_ERR(offset);
+       EXPECT_EQ(ENXIO, errno);
+}
+
+TEST_F(revocable_fixture, try_access_macro3) {
+       const int offset = TEST_MAGIC_OFFSET_MACRO_TRY_ACCESS_OR_RETURN;
+
+       READ_TEST_DATA(offset);
+       EXPECT_STREQ(TEST_DATA, self->data);
+
+       SIGNAL_RESOURCE_GONE();
+
+       READ_TEST_DATA_ERR(offset);
+       EXPECT_EQ(ENODEV, errno);
+}
+
+TEST_F(revocable_fixture, try_access_macro4) {
+       const int offset = TEST_MAGIC_OFFSET_MACRO_TRY_ACCESS_WITH_SCOPED;
+
+       READ_TEST_DATA(offset);
+       EXPECT_STREQ(TEST_DATA, self->data);
+
+       SIGNAL_RESOURCE_GONE();
+
+       READ_TEST_DATA_MSG(offset, " after resource gone");
+       EXPECT_STREQ(TEST_DATA_NULL, self->data);
+}
+
+TEST_F(revocable_fixture, try_access_macro5) {
+       const int offset =
+               TEST_MAGIC_OFFSET_MACRO_TRY_ACCESS_OR_RETURN_ERR_SCOPED;
+
+       READ_TEST_DATA(offset);
+       EXPECT_STREQ(TEST_DATA, self->data);
+
+       SIGNAL_RESOURCE_GONE();
+
+       READ_TEST_DATA_ERR(offset);
+       EXPECT_EQ(ENXIO, errno);
+}
+
+TEST_F(revocable_fixture, try_access_macro6) {
+       const int offset = TEST_MAGIC_OFFSET_MACRO_TRY_ACCESS_OR_RETURN_SCOPED;
+
+       READ_TEST_DATA(offset);
+       EXPECT_STREQ(TEST_DATA, self->data);
+
+       SIGNAL_RESOURCE_GONE();
+
+       READ_TEST_DATA_ERR(offset);
+       EXPECT_EQ(ENODEV, errno);
+}
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/revocable/revocable_test.h 
b/tools/testing/selftests/revocable/revocable_test.h
new file mode 100644
index 000000000000..270b456ef7d9
--- /dev/null
+++ b/tools/testing/selftests/revocable/revocable_test.h
@@ -0,0 +1,20 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright 2026 Google LLC
+ */
+
+#ifndef __REVOCABLE_TEST_H
+#define __REVOCABLE_TEST_H
+
+#define TEST_DATA      "12345678"
+#define TEST_DATA_NULL "(null)"
+
+#define TEST_MAGIC_OFFSET_RAW                                  0x0
+#define TEST_MAGIC_OFFSET_MACRO_TRY_ACCESS_WITH                        0x1
+#define TEST_MAGIC_OFFSET_MACRO_TRY_ACCESS_OR_RETURN_ERR       0x2
+#define TEST_MAGIC_OFFSET_MACRO_TRY_ACCESS_OR_RETURN           0x3
+#define TEST_MAGIC_OFFSET_MACRO_TRY_ACCESS_WITH_SCOPED         0x4
+#define TEST_MAGIC_OFFSET_MACRO_TRY_ACCESS_OR_RETURN_ERR_SCOPED        0x5
+#define TEST_MAGIC_OFFSET_MACRO_TRY_ACCESS_OR_RETURN_SCOPED    0x6
+
+#endif /* __REVOCABLE_TEST_H */
diff --git a/tools/testing/selftests/revocable/test-revocable.sh 
b/tools/testing/selftests/revocable/test-revocable.sh
new file mode 100755
index 000000000000..0cfc26a1c49a
--- /dev/null
+++ b/tools/testing/selftests/revocable/test-revocable.sh
@@ -0,0 +1,34 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+
+mod_name="revocable_test"
+ksft_fail=1
+ksft_skip=4
+
+if [ "$(id -u)" -ne 0 ]; then
+       echo "$0: Must be run as root"
+       exit "$ksft_skip"
+fi
+
+if ! which insmod > /dev/null 2>&1; then
+       echo "$0: Need insmod"
+       exit "$ksft_skip"
+fi
+
+if ! which rmmod > /dev/null 2>&1; then
+       echo "$0: Need rmmod"
+       exit "$ksft_skip"
+fi
+
+if ! mountpoint -q /sys/kernel/debug; then
+       mount -t debugfs none /sys/kernel/debug
+fi
+
+insmod test_modules/"${mod_name}".ko
+
+./revocable_test
+ret=$?
+
+rmmod "${mod_name}"
+
+exit "${ret}"
diff --git a/tools/testing/selftests/revocable/test_modules/Makefile 
b/tools/testing/selftests/revocable/test_modules/Makefile
new file mode 100644
index 000000000000..f29e4f909402
--- /dev/null
+++ b/tools/testing/selftests/revocable/test_modules/Makefile
@@ -0,0 +1,10 @@
+TESTMODS_DIR := $(realpath $(dir $(abspath $(lastword $(MAKEFILE_LIST)))))
+KDIR ?= /lib/modules/$(shell uname -r)/build
+
+obj-m += revocable_test.o
+
+all:
+       $(Q)$(MAKE) -C $(KDIR) M=$(TESTMODS_DIR)
+
+clean:
+       $(Q)$(MAKE) -C $(KDIR) M=$(TESTMODS_DIR) clean
diff --git a/tools/testing/selftests/revocable/test_modules/revocable_test.c 
b/tools/testing/selftests/revocable/test_modules/revocable_test.c
new file mode 100644
index 000000000000..b2914c7b4ef5
--- /dev/null
+++ b/tools/testing/selftests/revocable/test_modules/revocable_test.c
@@ -0,0 +1,234 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright 2026 Google LLC
+ *
+ * A kernel module for testing the revocable API.
+ */
+
+#include <linux/debugfs.h>
+#include <linux/module.h>
+#include <linux/revocable.h>
+#include <linux/slab.h>
+
+#include "../revocable_test.h"
+
+struct dentry *test_file;
+
+struct revocable_test_priv {
+       struct revocable *rp;
+       char res[16];
+};
+
+/*
+ * This creates a revocable provider.
+ */
+static int revocable_test_open(struct inode *inode, struct file *filp)
+{
+       struct revocable_test_priv *priv;
+
+       priv = kzalloc(sizeof(*priv), GFP_KERNEL);
+       if (!priv)
+               return -ENOMEM;
+
+       strscpy(priv->res, TEST_DATA);
+       priv->rp = revocable_alloc(&priv->res);
+       if (!priv->rp)
+               return -ENOMEM;
+
+       filp->private_data = priv;
+       return 0;
+}
+
+static int revocable_test_release(struct inode *inode,
+                                          struct file *filp)
+{
+       struct revocable_test_priv *priv = filp->private_data;
+       bool revoked = true;
+       void *res;
+
+       revocable_try_access_or_skip_scoped(priv->rp, res)
+               revoked = false;
+       if (!revoked)
+               revocable_revoke(priv->rp);
+
+       revocable_put(priv->rp);
+
+       kfree(priv);
+       return 0;
+}
+
+/*
+ * This revokes the resource.  Here is a side command channel.
+ *
+ * The test can't just close the file descriptor for signaling the
+ * resource is gone.  Subsequent file operations on the open file
+ * descriptor of debugfs return -EIO after calling debugfs_remove().
+ * See also debugfs_file_get().
+ */
+static ssize_t revocable_test_write(struct file *filp, const char __user *buf,
+                                   size_t count, loff_t *offset)
+{
+       size_t copied;
+       char data[64];
+       struct revocable_test_priv *priv = filp->private_data;
+       bool revoked = true;
+       void *res;
+
+       revocable_try_access_or_skip_scoped(priv->rp, res)
+               revoked = false;
+       if (revoked)
+               return -EINVAL;
+
+       copied = strncpy_from_user(data, buf, sizeof(data));
+       if (copied < 0)
+               return copied;
+
+       revocable_revoke(priv->rp);
+       return copied;
+}
+
+static void copy_resource_data(char *data, size_t len, char *res)
+{
+       if (!res)
+               strscpy(data, TEST_DATA_NULL, len);
+       else
+               strscpy(data, res, len);
+}
+
+static int call_revocable_try_access_or_return_err(struct revocable *rp,
+                                                  char *data, size_t len)
+{
+       char *res;
+
+       revocable_try_access_or_return_err(rp, res, -ENXIO);
+       copy_resource_data(data, len, res);
+       return 0;
+}
+
+static int call_revocable_try_access_or_return(struct revocable *rp,
+                                              char *data, size_t len)
+{
+       char *res;
+
+       revocable_try_access_or_return(rp, res);
+       copy_resource_data(data, len, res);
+       return 0;
+}
+
+static int call_revocable_try_access_or_return_err_scoped(struct revocable *rp,
+                                                         char *data,
+                                                         size_t len)
+{
+       char *res;
+
+       revocable_try_access_or_return_err_scoped(rp, res, -ENXIO)
+               copy_resource_data(data, len, res);
+       return 0;
+}
+
+static int call_revocable_try_access_or_return_scoped(struct revocable *rp,
+                                                     char *data, size_t len)
+{
+       char *res;
+
+       revocable_try_access_or_return_scoped(rp, res)
+               copy_resource_data(data, len, res);
+       return 0;
+}
+
+/*
+ * This creates a revocable consumer and returns the resource value.
+ */
+static ssize_t revocable_test_read(struct file *filp, char __user *buf,
+                                  size_t count, loff_t *offset)
+{
+       char *res;
+       char data[16];
+       size_t len;
+       int ret;
+       struct revocable_test_priv *priv = filp->private_data;
+
+       switch (*offset) {
+       case TEST_MAGIC_OFFSET_RAW:
+               {
+                       struct revocable_consumer rev;
+
+                       revocable_init(priv->rp, &rev);
+                       res = revocable_try_access(&rev);
+                       copy_resource_data(data, sizeof(data), res);
+                       revocable_withdraw_access(&rev);
+                       revocable_deinit(&rev);
+               }
+               break;
+       case TEST_MAGIC_OFFSET_MACRO_TRY_ACCESS_WITH:
+               {
+                       revocable_try_access_with(priv->rp, res);
+                       copy_resource_data(data, sizeof(data), res);
+               }
+               break;
+       case TEST_MAGIC_OFFSET_MACRO_TRY_ACCESS_OR_RETURN_ERR:
+               ret = call_revocable_try_access_or_return_err(priv->rp, data,
+                                                             sizeof(data));
+               if (ret)
+                       return ret;
+               break;
+       case TEST_MAGIC_OFFSET_MACRO_TRY_ACCESS_OR_RETURN:
+               ret = call_revocable_try_access_or_return(priv->rp, data,
+                                                         sizeof(data));
+               if (ret)
+                       return ret;
+               break;
+       case TEST_MAGIC_OFFSET_MACRO_TRY_ACCESS_WITH_SCOPED:
+               revocable_try_access_with_scoped(priv->rp, res)
+                       copy_resource_data(data, sizeof(data), res);
+               break;
+       case TEST_MAGIC_OFFSET_MACRO_TRY_ACCESS_OR_RETURN_ERR_SCOPED:
+               ret = call_revocable_try_access_or_return_err_scoped(
+                                       priv->rp, data, sizeof(data));
+               if (ret)
+                       return ret;
+               break;
+       case TEST_MAGIC_OFFSET_MACRO_TRY_ACCESS_OR_RETURN_SCOPED:
+               ret = call_revocable_try_access_or_return_scoped(
+                                       priv->rp, data, sizeof(data));
+               if (ret)
+                       return ret;
+               break;
+       default:
+               return 0;
+       }
+
+       len = min_t(size_t, strlen(data), count);
+       if (copy_to_user(buf, data, len))
+               return -EFAULT;
+
+       *offset = len;
+       return len;
+}
+
+static const struct file_operations revocable_test_fops = {
+       .open = revocable_test_open,
+       .release = revocable_test_release,
+       .write = revocable_test_write,
+       .read = revocable_test_read,
+       .llseek = default_llseek,
+};
+
+static int __init revocable_test_init(void)
+{
+       test_file = debugfs_create_file("revocable_test", 0600, NULL, NULL,
+                                       &revocable_test_fops);
+       return test_file ? 0 : -ENOMEM;
+}
+
+static void __exit revocable_test_exit(void)
+{
+       debugfs_remove(test_file);
+}
+
+module_init(revocable_test_init);
+module_exit(revocable_test_exit);
+
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR("Tzung-Bi Shih <[email protected]>");
+MODULE_DESCRIPTION("Revocable Kselftest");
-- 
2.53.0.310.g728cabbaf7-goog


Reply via email to