[PATCH 1/3] cgroup: Add dmem_selftest module

From: Albert Esteve

Date: Fri Mar 27 2026 - 04:59:58 EST


Currently, dmem charging is driver-driven through direct
calls to dmem_cgroup_try_charge(), so cgroup selftests
do not have a generic way to trigger charge and uncharge
paths from userspace.

This limits any selftest coverage to configuration/readout
checks unless a specific driver exposing charge hooks is
present in the test environment.

Add kernel/cgroup/dmem_selftest.c as a helper module
(CONFIG_DMEM_SELFTEST) that registers a synthetic dmem region
(dmem_selftest) and exposes debugfs control files:
/sys/kernel/debug/dmem_selftest/charge
/sys/kernel/debug/dmem_selftest/uncharge

Writing a size to charge triggers dmem_cgroup_try_charge() for
the calling task's cgroup (the module calls kstrtou64()).
Writing to uncharge releases the outstanding charge via
dmem_cgroup_uncharge(). Only a single outstanding charge
is supported.

This provides a deterministic, driver-independent mechanism
for exercising dmem accounting paths in selftests.

Signed-off-by: Albert Esteve <aesteve@xxxxxxxxxx>
---
init/Kconfig | 12 +++
kernel/cgroup/Makefile | 1 +
kernel/cgroup/dmem_selftest.c | 192 ++++++++++++++++++++++++++++++++++++++++++
3 files changed, 205 insertions(+)

diff --git a/init/Kconfig b/init/Kconfig
index 444ce811ea674..060ba8ca49333 100644
--- a/init/Kconfig
+++ b/init/Kconfig
@@ -1238,6 +1238,18 @@ config CGROUP_DMEM
As an example, it allows you to restrict VRAM usage for applications
in the DRM subsystem.

+config DMEM_SELFTEST
+ tristate "dmem cgroup selftest helper module"
+ depends on CGROUP_DMEM && DEBUG_FS
+ default n
+ help
+ Builds a small loadable module that registers a dmem region named
+ "dmem_selftest" and exposes debugfs files under
+ /sys/kernel/debug/dmem_selftest/ so kselftests can trigger
+ dmem charge/uncharge operations from userspace.
+
+ Say N unless you run dmem selftests or develop the dmem controller.
+
config CGROUP_FREEZER
bool "Freezer controller"
help
diff --git a/kernel/cgroup/Makefile b/kernel/cgroup/Makefile
index ede31601a363a..febc36e60f9f9 100644
--- a/kernel/cgroup/Makefile
+++ b/kernel/cgroup/Makefile
@@ -8,4 +8,5 @@ obj-$(CONFIG_CPUSETS) += cpuset.o
obj-$(CONFIG_CPUSETS_V1) += cpuset-v1.o
obj-$(CONFIG_CGROUP_MISC) += misc.o
obj-$(CONFIG_CGROUP_DMEM) += dmem.o
+obj-$(CONFIG_DMEM_SELFTEST) += dmem_selftest.o
obj-$(CONFIG_CGROUP_DEBUG) += debug.o
diff --git a/kernel/cgroup/dmem_selftest.c b/kernel/cgroup/dmem_selftest.c
new file mode 100644
index 0000000000000..09df70f718969
--- /dev/null
+++ b/kernel/cgroup/dmem_selftest.c
@@ -0,0 +1,192 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Kselftest helper for the dmem cgroup controller.
+ *
+ * Registers a dmem region and debugfs files so tests can trigger charges
+ * from the calling task's cgroup.
+ *
+ */
+
+#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
+
+#include <linux/cgroup_dmem.h>
+#include <linux/debugfs.h>
+#include <linux/fs.h>
+#include <linux/init.h>
+#include <linux/kernel.h>
+#include <linux/mutex.h>
+#include <linux/module.h>
+#include <linux/string.h>
+#include <linux/uaccess.h>
+
+#include "../../tools/testing/selftests/kselftest_module.h"
+
+#define DM_SELFTEST_REGION_NAME "dmem_selftest"
+#define DM_SELFTEST_REGION_SIZE (256ULL * 1024 * 1024)
+
+KSTM_MODULE_GLOBALS();
+
+static struct dmem_cgroup_region *selftest_region;
+static struct dentry *dbg_dir;
+
+static struct dmem_cgroup_pool_state *charged_pool;
+static u64 charged_size;
+static DEFINE_MUTEX(charge_lock);
+
+static ssize_t dmem_selftest_charge_write(struct file *file, const char __user *user_buf,
+ size_t count, loff_t *ppos)
+{
+ struct dmem_cgroup_pool_state *pool = NULL, *limit = NULL;
+ u64 size;
+ char buf[32];
+ int ret;
+
+ if (!selftest_region)
+ return -ENODEV;
+
+ if (count == 0 || count >= sizeof(buf))
+ return -EINVAL;
+
+ if (copy_from_user(buf, user_buf, count))
+ return -EFAULT;
+ buf[count] = '\0';
+
+ ret = kstrtou64(strim(buf), 0, &size);
+ if (ret)
+ return ret;
+ if (!size)
+ return -EINVAL;
+
+ mutex_lock(&charge_lock);
+ if (charged_pool) {
+ mutex_unlock(&charge_lock);
+ return -EBUSY;
+ }
+
+ ret = dmem_cgroup_try_charge(selftest_region, size, &pool, &limit);
+ if (ret == -EAGAIN && limit)
+ dmem_cgroup_pool_state_put(limit);
+ if (ret) {
+ mutex_unlock(&charge_lock);
+ return ret;
+ }
+
+ charged_pool = pool;
+ charged_size = size;
+ mutex_unlock(&charge_lock);
+
+ return count;
+}
+
+static ssize_t dmem_selftest_uncharge_write(struct file *file, const char __user *user_buf,
+ size_t count, loff_t *ppos)
+{
+ if (!count)
+ return -EINVAL;
+
+ mutex_lock(&charge_lock);
+ if (!charged_pool) {
+ mutex_unlock(&charge_lock);
+ return -EINVAL;
+ }
+
+ dmem_cgroup_uncharge(charged_pool, charged_size);
+ charged_pool = NULL;
+ charged_size = 0;
+ mutex_unlock(&charge_lock);
+
+ return count;
+}
+
+static const struct file_operations dmem_selftest_charge_fops = {
+ .write = dmem_selftest_charge_write,
+ .llseek = noop_llseek,
+};
+
+static const struct file_operations dmem_selftest_uncharge_fops = {
+ .write = dmem_selftest_uncharge_write,
+ .llseek = noop_llseek,
+};
+
+static int __init dmem_selftest_register(void)
+{
+ selftest_region = dmem_cgroup_register_region(
+ DM_SELFTEST_REGION_SIZE, DM_SELFTEST_REGION_NAME);
+ if (IS_ERR(selftest_region))
+ return PTR_ERR(selftest_region);
+ if (!selftest_region)
+ return -EINVAL;
+
+ dbg_dir = debugfs_create_dir("dmem_selftest", NULL);
+ if (!dbg_dir) {
+ dmem_cgroup_unregister_region(selftest_region);
+ selftest_region = NULL;
+ return -ENOMEM;
+ }
+
+ debugfs_create_file("charge", 0200, dbg_dir, NULL, &dmem_selftest_charge_fops);
+ debugfs_create_file("uncharge", 0200, dbg_dir, NULL, &dmem_selftest_uncharge_fops);
+
+ pr_info("region '%s' registered; debugfs at dmem_selftest/{charge,uncharge}\n",
+ DM_SELFTEST_REGION_NAME);
+ return 0;
+}
+
+static void dmem_selftest_remove(void)
+{
+ debugfs_remove_recursive(dbg_dir);
+ dbg_dir = NULL;
+
+ if (selftest_region) {
+ dmem_cgroup_unregister_region(selftest_region);
+ selftest_region = NULL;
+ }
+}
+
+static void __init selftest(void)
+{
+ KSTM_CHECK_ZERO(!selftest_region);
+ KSTM_CHECK_ZERO(!dbg_dir);
+}
+
+static int __init dmem_selftest_init(void)
+{
+ int report_rc;
+ int err;
+
+ err = dmem_selftest_register();
+ if (err)
+ return err;
+
+ pr_info("loaded.\n");
+ add_taint(TAINT_TEST, LOCKDEP_STILL_OK);
+ selftest();
+ report_rc = kstm_report(total_tests, failed_tests, skipped_tests);
+ if (report_rc) {
+ dmem_selftest_remove();
+ return report_rc;
+ }
+
+ return 0;
+}
+
+static void __exit dmem_selftest_exit(void)
+{
+ pr_info("unloaded.\n");
+
+ mutex_lock(&charge_lock);
+ if (charged_pool) {
+ dmem_cgroup_uncharge(charged_pool, charged_size);
+ charged_pool = NULL;
+ }
+ mutex_unlock(&charge_lock);
+
+ dmem_selftest_remove();
+}
+
+module_init(dmem_selftest_init);
+module_exit(dmem_selftest_exit);
+
+MODULE_AUTHOR("Albert Esteve <aesteve@xxxxxxxxxx>");
+MODULE_DESCRIPTION("Kselftest helper for cgroup dmem controller");
+MODULE_LICENSE("GPL");

--
2.52.0