* [PATCH 0/3] cgroup: dmem: add selftest helper, coverage, and VM runner
@ 2026-03-27 8:53 Albert Esteve
2026-03-27 8:53 ` [PATCH 1/3] cgroup: Add dmem_selftest module Albert Esteve
` (2 more replies)
0 siblings, 3 replies; 5+ messages in thread
From: Albert Esteve @ 2026-03-27 8:53 UTC (permalink / raw)
To: Tejun Heo, Johannes Weiner, Michal Koutný, Shuah Khan
Cc: linux-kernel, cgroups, linux-kselftest, Albert Esteve, mripard,
echanude
Hi all,
This small series adds practical test coverage for the dmem
cgroup controller.
The motivation came from following the recent dmem API discussion in
thread [1]. That discussion considered changing the dmem API and
adding a new knob. Currently there are no dedicated tests covering
dmem behaviour, which makes such changes riskier.
Adding selftests has an additional challenge: dmem charging paths
are driver-driven today, so regression testing harder unless a
suitable driver is present in the test environment.
This series addresses that by adding:
- a kernel-side selftest helper module to trigger charge/uncharge
from userspace in a controlled way,
- cgroup selftests covering dmem accounting and protection semantics
(including dmem.max enforcement and byte-granularity checks),
- a virtme-based VM runner for repeatable execution of the dmem tests.
The goal is to make dmem behavior easier to validate when evolving the API
and implementation, while keeping tests deterministic and driver-independent.
Thanks.
[1] - https://lore.kernel.org/all/aZoHfloupKvF2oSu@fedora/
Signed-off-by: Albert Esteve <aesteve@redhat.com>
---
Albert Esteve (3):
cgroup: Add dmem_selftest module
selftests: cgroup: Add dmem selftest coverage
selftests: cgroup: Add vmtest-dmem runner based on hid vmtest
init/Kconfig | 12 +
kernel/cgroup/Makefile | 1 +
kernel/cgroup/dmem_selftest.c | 192 ++++++++++
tools/testing/selftests/cgroup/.gitignore | 1 +
tools/testing/selftests/cgroup/Makefile | 2 +
tools/testing/selftests/cgroup/test_dmem.c | 487 ++++++++++++++++++++++++++
tools/testing/selftests/cgroup/vmtest-dmem.sh | 189 ++++++++++
7 files changed, 884 insertions(+)
---
base-commit: 80234b5ab240f52fa45d201e899e207b9265ef91
change-id: 20260318-kunit_cgroups-7fb0b9e64017
Best regards,
--
Albert Esteve <aesteve@redhat.com>
^ permalink raw reply [flat|nested] 5+ messages in thread
* [PATCH 1/3] cgroup: Add dmem_selftest module
2026-03-27 8:53 [PATCH 0/3] cgroup: dmem: add selftest helper, coverage, and VM runner Albert Esteve
@ 2026-03-27 8:53 ` Albert Esteve
2026-03-27 8:53 ` [PATCH 2/3] selftests: cgroup: Add dmem selftest coverage Albert Esteve
2026-03-27 8:53 ` [PATCH 3/3] selftests: cgroup: Add vmtest-dmem runner based on hid vmtest Albert Esteve
2 siblings, 0 replies; 5+ messages in thread
From: Albert Esteve @ 2026-03-27 8:53 UTC (permalink / raw)
To: Tejun Heo, Johannes Weiner, Michal Koutný, Shuah Khan
Cc: linux-kernel, cgroups, linux-kselftest, Albert Esteve, mripard,
echanude
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@redhat.com>
---
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@redhat.com>");
+MODULE_DESCRIPTION("Kselftest helper for cgroup dmem controller");
+MODULE_LICENSE("GPL");
--
2.52.0
^ permalink raw reply related [flat|nested] 5+ messages in thread
* [PATCH 2/3] selftests: cgroup: Add dmem selftest coverage
2026-03-27 8:53 [PATCH 0/3] cgroup: dmem: add selftest helper, coverage, and VM runner Albert Esteve
2026-03-27 8:53 ` [PATCH 1/3] cgroup: Add dmem_selftest module Albert Esteve
@ 2026-03-27 8:53 ` Albert Esteve
2026-03-27 13:15 ` Albert Esteve
2026-03-27 8:53 ` [PATCH 3/3] selftests: cgroup: Add vmtest-dmem runner based on hid vmtest Albert Esteve
2 siblings, 1 reply; 5+ messages in thread
From: Albert Esteve @ 2026-03-27 8:53 UTC (permalink / raw)
To: Tejun Heo, Johannes Weiner, Michal Koutný, Shuah Khan
Cc: linux-kernel, cgroups, linux-kselftest, Albert Esteve, mripard,
echanude
Currently, tools/testing/selftests/cgroup/ does not include
a dmem-specific test binary. This leaves dmem charge and
limit behavior largely unvalidated in kselftest coverage.
Add test_dmem and wire it into the cgroup selftests Makefile.
The new test exercises dmem controller behavior through the
dmem_selftest debugfs interface for the dmem_selftest region.
The test adds three complementary checks:
- test_dmem_max creates a nested hierarchy with per-leaf
dmem.max values and verifies that over-limit charges
fail while in-limit charges succeed with bounded rounding
in dmem.current.
- test_dmem_min and test_dmem_low verify that charging
from a cgroup with the corresponding protection knob
set updates dmem.current as expected.
- test_dmem_charge_byte_granularity validates accounting
bounds for non-page-aligned charge sizes and
uncharge-to-zero behavior.
This provides deterministic userspace coverage for dmem
accounting and hard-limit enforcement using a test helper
module, without requiring subsystem-specific production
drivers.
Signed-off-by: Albert Esteve <aesteve@redhat.com>
---
tools/testing/selftests/cgroup/.gitignore | 1 +
tools/testing/selftests/cgroup/Makefile | 2 +
tools/testing/selftests/cgroup/test_dmem.c | 487 +++++++++++++++++++++++++++++
3 files changed, 490 insertions(+)
diff --git a/tools/testing/selftests/cgroup/.gitignore b/tools/testing/selftests/cgroup/.gitignore
index 952e4448bf070..ea2322598217d 100644
--- a/tools/testing/selftests/cgroup/.gitignore
+++ b/tools/testing/selftests/cgroup/.gitignore
@@ -2,6 +2,7 @@
test_core
test_cpu
test_cpuset
+test_dmem
test_freezer
test_hugetlb_memcg
test_kill
diff --git a/tools/testing/selftests/cgroup/Makefile b/tools/testing/selftests/cgroup/Makefile
index e01584c2189ac..e1a5e9316620e 100644
--- a/tools/testing/selftests/cgroup/Makefile
+++ b/tools/testing/selftests/cgroup/Makefile
@@ -10,6 +10,7 @@ TEST_GEN_FILES := wait_inotify
TEST_GEN_PROGS = test_core
TEST_GEN_PROGS += test_cpu
TEST_GEN_PROGS += test_cpuset
+TEST_GEN_PROGS += test_dmem
TEST_GEN_PROGS += test_freezer
TEST_GEN_PROGS += test_hugetlb_memcg
TEST_GEN_PROGS += test_kill
@@ -26,6 +27,7 @@ include lib/libcgroup.mk
$(OUTPUT)/test_core: $(LIBCGROUP_O)
$(OUTPUT)/test_cpu: $(LIBCGROUP_O)
$(OUTPUT)/test_cpuset: $(LIBCGROUP_O)
+$(OUTPUT)/test_dmem: $(LIBCGROUP_O)
$(OUTPUT)/test_freezer: $(LIBCGROUP_O)
$(OUTPUT)/test_hugetlb_memcg: $(LIBCGROUP_O)
$(OUTPUT)/test_kill: $(LIBCGROUP_O)
diff --git a/tools/testing/selftests/cgroup/test_dmem.c b/tools/testing/selftests/cgroup/test_dmem.c
new file mode 100644
index 0000000000000..cdd5cb7206f16
--- /dev/null
+++ b/tools/testing/selftests/cgroup/test_dmem.c
@@ -0,0 +1,487 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Test the dmem (device memory) cgroup controller.
+ *
+ * Depends on dmem_selftest kernel module.
+ */
+
+#define _GNU_SOURCE
+
+#include <linux/limits.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "kselftest.h"
+#include "cgroup_util.h"
+
+/* kernel/cgroup/dmem_selftest.c */
+#define DM_SELFTEST_REGION "dmem_selftest"
+#define DM_SELFTEST_CHARGE "/sys/kernel/debug/dmem_selftest/charge"
+#define DM_SELFTEST_UNCHARGE "/sys/kernel/debug/dmem_selftest/uncharge"
+
+/*
+ * Parse the first line of dmem.capacity (root):
+ * "<name> <size_in_bytes>"
+ * Returns 1 if a region was found, 0 if capacity is empty, -1 on read error.
+ */
+static int parse_first_region(const char *root, char *name, size_t name_len,
+ unsigned long long *size_out)
+{
+ char buf[4096];
+ char nm[256];
+ unsigned long long sz;
+
+ if (cg_read(root, "dmem.capacity", buf, sizeof(buf)) < 0)
+ return -1;
+
+ if (sscanf(buf, "%255s %llu", nm, &sz) < 2)
+ return 0;
+
+ if (name_len <= strlen(nm))
+ return -1;
+
+ strcpy(name, nm);
+ *size_out = sz;
+ return 1;
+}
+
+/*
+ * Read the numeric limit for @region_name from a multiline
+ * dmem.{min,low,max} file. Returns bytes,
+ * or -1 if the line is "<name> max", or -2 if missing/err.
+ */
+static long long dmem_read_limit_for_region(const char *cgroup, const char *ctrl,
+ const char *region_name)
+{
+ char buf[4096];
+ char *line, *saveptr = NULL;
+ char fname[256];
+ char fval[64];
+
+ if (cg_read(cgroup, ctrl, buf, sizeof(buf)) < 0)
+ return -2;
+
+ for (line = strtok_r(buf, "\n", &saveptr); line;
+ line = strtok_r(NULL, "\n", &saveptr)) {
+ if (!line[0])
+ continue;
+ if (sscanf(line, "%255s %63s", fname, fval) != 2)
+ continue;
+ if (strcmp(fname, region_name))
+ continue;
+ if (!strcmp(fval, "max"))
+ return -1;
+ return strtoll(fval, NULL, 0);
+ }
+ return -2;
+}
+
+static long long dmem_read_limit(const char *cgroup, const char *ctrl)
+{
+ return dmem_read_limit_for_region(cgroup, ctrl, DM_SELFTEST_REGION);
+}
+
+static int dmem_write_limit(const char *cgroup, const char *ctrl,
+ const char *val)
+{
+ char wr[512];
+
+ snprintf(wr, sizeof(wr), "%s %s", DM_SELFTEST_REGION, val);
+ return cg_write(cgroup, ctrl, wr);
+}
+
+static int dmem_selftest_charge_bytes(unsigned long long bytes)
+{
+ char wr[32];
+
+ snprintf(wr, sizeof(wr), "%llu", bytes);
+ return write_text(DM_SELFTEST_CHARGE, wr, strlen(wr));
+}
+
+static int dmem_selftest_uncharge(void)
+{
+ return write_text(DM_SELFTEST_UNCHARGE, "\n", 1);
+}
+
+/*
+ * First, this test creates the following hierarchy:
+ * A
+ * A/B dmem.max=1M
+ * A/B/C dmem.max=75K
+ * A/B/D dmem.max=25K
+ * A/B/E dmem.max=8K
+ * A/B/F dmem.max=0
+ *
+ * Then for each leaf cgroup it tries to charge above dmem.max
+ * and expects the charge request to fail and dmem.current to
+ * remain unchanged.
+ *
+ * For leaves with non-zero dmem.max, it additionally charges a
+ * smaller amount and verifies accounting grows within one PAGE_SIZE
+ * rounding bound, then uncharges and verifies dmem.current returns
+ * to the previous value.
+ *
+ */
+static int test_dmem_max(const char *root)
+{
+ static const char * const leaf_max[] = { "75K", "25K", "8K", "0" };
+ static const unsigned long long fail_sz[] = {
+ (75ULL * 1024ULL) + 1ULL,
+ (25ULL * 1024ULL) + 1ULL,
+ (8ULL * 1024ULL) + 1ULL,
+ 1ULL
+ };
+ static const unsigned long long pass_sz[] = {
+ 4096ULL, 4096ULL, 4096ULL, 0ULL
+ };
+ char *parent[2] = {NULL};
+ char *children[4] = {NULL};
+ unsigned long long cap;
+ char region[256];
+ long long page_size;
+ long long cur_before, cur_after;
+ int ret = KSFT_FAIL;
+ int charged = 0;
+ int in_child = 0;
+ long long v;
+ int i;
+
+ if (access(DM_SELFTEST_CHARGE, W_OK) != 0)
+ return KSFT_SKIP;
+
+ if (parse_first_region(root, region, sizeof(region), &cap) != 1)
+ return KSFT_SKIP;
+ if (strcmp(region, DM_SELFTEST_REGION) != 0)
+ return KSFT_SKIP;
+
+ page_size = sysconf(_SC_PAGESIZE);
+ if (page_size <= 0)
+ goto cleanup;
+
+ parent[0] = cg_name(root, "dmem_prot_0");
+ parent[1] = cg_name(parent[0], "dmem_prot_1");
+ if (!parent[0] || !parent[1])
+ goto cleanup;
+
+ if (cg_create(parent[0]))
+ goto cleanup;
+
+ if (cg_write(parent[0], "cgroup.subtree_control", "+dmem"))
+ goto cleanup;
+
+ if (cg_create(parent[1]))
+ goto cleanup;
+
+ if (cg_write(parent[1], "cgroup.subtree_control", "+dmem"))
+ goto cleanup;
+
+ for (i = 0; i < 4; i++) {
+ children[i] = cg_name_indexed(parent[1], "dmem_child", i);
+ if (!children[i])
+ goto cleanup;
+ if (cg_create(children[i]))
+ goto cleanup;
+ }
+
+ if (dmem_write_limit(parent[1], "dmem.max", "1M"))
+ goto cleanup;
+ for (i = 0; i < 4; i++)
+ if (dmem_write_limit(children[i], "dmem.max", leaf_max[i]))
+ goto cleanup;
+
+ v = dmem_read_limit(parent[1], "dmem.max");
+ if (!values_close(v, 1024LL * 1024LL, 3))
+ goto cleanup;
+ v = dmem_read_limit(children[0], "dmem.max");
+ if (!values_close(v, 75LL * 1024LL, 3))
+ goto cleanup;
+ v = dmem_read_limit(children[1], "dmem.max");
+ if (!values_close(v, 25LL * 1024LL, 3))
+ goto cleanup;
+ v = dmem_read_limit(children[2], "dmem.max");
+ if (!values_close(v, 8LL * 1024LL, 3))
+ goto cleanup;
+ v = dmem_read_limit(children[3], "dmem.max");
+ if (v != 0)
+ goto cleanup;
+
+ for (i = 0; i < 4; i++) {
+ if (cg_enter_current(children[i]))
+ goto cleanup;
+ in_child = 1;
+
+ cur_before = dmem_read_limit(children[i], "dmem.current");
+ if (cur_before < 0)
+ goto cleanup;
+
+ if (dmem_selftest_charge_bytes(fail_sz[i]) == 0)
+ goto cleanup;
+
+ cur_after = dmem_read_limit(children[i], "dmem.current");
+ if (cur_after != cur_before)
+ goto cleanup;
+
+ if (pass_sz[i] > 0) {
+ if (dmem_selftest_charge_bytes(pass_sz[i]) < 0)
+ goto cleanup;
+ charged = 1;
+
+ cur_after = dmem_read_limit(children[i], "dmem.current");
+ if (cur_after < cur_before + (long long)pass_sz[i])
+ goto cleanup;
+ if (cur_after > cur_before + (long long)pass_sz[i] + page_size)
+ goto cleanup;
+
+ if (dmem_selftest_uncharge() < 0)
+ goto cleanup;
+ charged = 0;
+
+ cur_after = dmem_read_limit(children[i], "dmem.current");
+ if (cur_after != cur_before)
+ goto cleanup;
+ }
+
+ if (cg_enter_current(root))
+ goto cleanup;
+ in_child = 0;
+ }
+
+ ret = KSFT_PASS;
+
+cleanup:
+ if (charged)
+ dmem_selftest_uncharge();
+ if (in_child)
+ cg_enter_current(root);
+ for (i = 3; i >= 0; i--) {
+ if (!children[i])
+ continue;
+ cg_destroy(children[i]);
+ free(children[i]);
+ }
+ for (i = 1; i >= 0; i--) {
+ if (!parent[i])
+ continue;
+ cg_destroy(parent[i]);
+ free(parent[i]);
+ }
+ return ret;
+}
+
+/*
+ * This test sets dmem.min and dmem.low on a child cgroup, then charge
+ * from that context and verify dmem.current tracks the charged bytes
+ * (within one page rounding).
+ */
+static int test_dmem_charge_with_attr(const char *root, bool min)
+{
+ char region[256];
+ unsigned long long cap;
+ const unsigned long long charge_sz = 12345ULL;
+ const char *attribute = min ? "dmem.min" : "dmem.low";
+ int ret = KSFT_FAIL;
+ char *cg = NULL;
+ long long cur;
+ long long page_size;
+ int charged = 0;
+ int in_child = 0;
+
+ if (access(DM_SELFTEST_CHARGE, W_OK) != 0)
+ return KSFT_SKIP;
+
+ if (parse_first_region(root, region, sizeof(region), &cap) != 1)
+ return KSFT_SKIP;
+ if (strcmp(region, DM_SELFTEST_REGION) != 0)
+ return KSFT_SKIP;
+
+ page_size = sysconf(_SC_PAGESIZE);
+ if (page_size <= 0)
+ goto cleanup;
+
+ cg = cg_name(root, "test_dmem_attr");
+ if (!cg)
+ goto cleanup;
+
+ if (cg_create(cg))
+ goto cleanup;
+
+ if (cg_enter_current(cg))
+ goto cleanup;
+ in_child = 1;
+
+ if (dmem_write_limit(cg, attribute, "16K"))
+ goto cleanup;
+
+ if (dmem_selftest_charge_bytes(charge_sz) < 0)
+ goto cleanup;
+ charged = 1;
+
+ cur = dmem_read_limit(cg, "dmem.current");
+ if (cur < (long long)charge_sz)
+ goto cleanup;
+ if (cur > (long long)charge_sz + page_size)
+ goto cleanup;
+
+ if (dmem_selftest_uncharge() < 0)
+ goto cleanup;
+ charged = 0;
+
+ cur = dmem_read_limit(cg, "dmem.current");
+ if (cur != 0)
+ goto cleanup;
+
+ ret = KSFT_PASS;
+
+cleanup:
+ if (charged)
+ dmem_selftest_uncharge();
+ if (in_child)
+ cg_enter_current(root);
+ cg_destroy(cg);
+ free(cg);
+ return ret;
+}
+
+static int test_dmem_min(const char *root)
+{
+ return test_dmem_charge_with_attr(root, "dmem.min");
+}
+
+static int test_dmem_low(const char *root)
+{
+ return test_dmem_charge_with_attr(root, "dmem.low");
+}
+
+/*
+ * This test charges non-page-aligned byte sizes and verify dmem.current
+ * stays consistent: it must account at least the requested bytes and
+ * never exceed one kernel page of rounding overhead. Then uncharge must
+ * return usage to 0.
+ */
+static int test_dmem_charge_byte_granularity(const char *root)
+{
+ static const unsigned long long sizes[] = { 1ULL, 4095ULL, 4097ULL, 12345ULL };
+ char *cg = NULL;
+ unsigned long long cap;
+ char region[256];
+ long long cur;
+ long long page_size;
+ int ret = KSFT_FAIL;
+ int charged = 0;
+ int in_child = 0;
+ size_t i;
+
+ if (access(DM_SELFTEST_CHARGE, W_OK) != 0)
+ return KSFT_SKIP;
+
+ if (parse_first_region(root, region, sizeof(region), &cap) != 1)
+ return KSFT_SKIP;
+ if (strcmp(region, DM_SELFTEST_REGION) != 0)
+ return KSFT_SKIP;
+
+ page_size = sysconf(_SC_PAGESIZE);
+ if (page_size <= 0)
+ goto cleanup;
+
+ cg = cg_name(root, "dmem_dbg_byte_gran");
+ if (!cg)
+ goto cleanup;
+
+ if (cg_create(cg))
+ goto cleanup;
+
+ if (dmem_write_limit(cg, "dmem.max", "8M"))
+ goto cleanup;
+
+ if (cg_enter_current(cg))
+ goto cleanup;
+ in_child = 1;
+
+ for (i = 0; i < ARRAY_SIZE(sizes); i++) {
+ if (dmem_selftest_charge_bytes(sizes[i]) < 0)
+ goto cleanup;
+ charged = 1;
+
+ cur = dmem_read_limit(cg, "dmem.current");
+ if (cur < (long long)sizes[i])
+ goto cleanup;
+ if (cur > (long long)sizes[i] + page_size)
+ goto cleanup;
+
+ if (dmem_selftest_uncharge() < 0)
+ goto cleanup;
+ charged = 0;
+
+ cur = dmem_read_limit(cg, "dmem.current");
+ if (cur != 0)
+ goto cleanup;
+ }
+
+ ret = KSFT_PASS;
+
+cleanup:
+ if (charged)
+ dmem_selftest_uncharge();
+ if (in_child)
+ cg_enter_current(root);
+ if (cg) {
+ cg_destroy(cg);
+ free(cg);
+ }
+ return ret;
+}
+
+#define T(x) { x, #x }
+struct dmem_test {
+ int (*fn)(const char *root);
+ const char *name;
+} tests[] = {
+ T(test_dmem_max),
+ T(test_dmem_min),
+ T(test_dmem_low),
+ T(test_dmem_charge_byte_granularity),
+};
+#undef T
+
+int main(int argc, char **argv)
+{
+ char root[PATH_MAX];
+ int i;
+
+ ksft_print_header();
+ ksft_set_plan(ARRAY_SIZE(tests));
+
+ if (cg_find_unified_root(root, sizeof(root), NULL))
+ ksft_exit_skip("cgroup v2 isn't mounted\n");
+
+ if (cg_read_strstr(root, "cgroup.controllers", "dmem"))
+ ksft_exit_skip("dmem controller isn't available (CONFIG_CGROUP_DMEM?)\n");
+
+ if (cg_read_strstr(root, "cgroup.subtree_control", "dmem"))
+ if (cg_write(root, "cgroup.subtree_control", "+dmem"))
+ ksft_exit_skip("Failed to enable dmem controller\n");
+
+ for (i = 0; i < ARRAY_SIZE(tests); i++) {
+ switch (tests[i].fn(root)) {
+ case KSFT_PASS:
+ ksft_test_result_pass("%s\n", tests[i].name);
+ break;
+ case KSFT_SKIP:
+ ksft_test_result_skip(
+ "%s (need CONFIG_DMEM_SELFTEST, modprobe dmem_selftest)\n",
+ tests[i].name);
+ break;
+ default:
+ ksft_test_result_fail("%s\n", tests[i].name);
+ break;
+ }
+ }
+
+ ksft_finished();
+}
--
2.52.0
^ permalink raw reply related [flat|nested] 5+ messages in thread
* [PATCH 3/3] selftests: cgroup: Add vmtest-dmem runner based on hid vmtest
2026-03-27 8:53 [PATCH 0/3] cgroup: dmem: add selftest helper, coverage, and VM runner Albert Esteve
2026-03-27 8:53 ` [PATCH 1/3] cgroup: Add dmem_selftest module Albert Esteve
2026-03-27 8:53 ` [PATCH 2/3] selftests: cgroup: Add dmem selftest coverage Albert Esteve
@ 2026-03-27 8:53 ` Albert Esteve
2 siblings, 0 replies; 5+ messages in thread
From: Albert Esteve @ 2026-03-27 8:53 UTC (permalink / raw)
To: Tejun Heo, Johannes Weiner, Michal Koutný, Shuah Khan
Cc: linux-kernel, cgroups, linux-kselftest, Albert Esteve, mripard,
echanude
Currently, test_dmem relies on the dmem_selftest helper module
and a VM setup that may not have the helper preinstalled.
This makes automated coverage of dmem charge paths harder in
virtme-based runs.
Add tools/testing/selftests/cgroup/vmtest-dmem.sh, modeled
after the existing selftests vmtest runners
(notably tools/testing/selftests/hid/vmtest.sh),
to provide a repeatable VM workflow for dmem tests.
The script boots a virtme-ng guest, validates dmem
controller availability, ensures the dmem helper path is
present, and runs tools/testing/selftests/cgroup/test_dmem.
If the helper is not available as a loaded module, it
attempts module build/load for the running guest kernel
before executing the test binary.
The runner also supports interactive shell mode and
reuses the same verbosity and exit-code conventions
used by other vmtest scripts, so it integrates with existing
kselftest workflows.
Signed-off-by: Albert Esteve <aesteve@redhat.com>
---
tools/testing/selftests/cgroup/vmtest-dmem.sh | 189 ++++++++++++++++++++++++++
1 file changed, 189 insertions(+)
diff --git a/tools/testing/selftests/cgroup/vmtest-dmem.sh b/tools/testing/selftests/cgroup/vmtest-dmem.sh
new file mode 100755
index 0000000000000..e481d3b2cdf8f
--- /dev/null
+++ b/tools/testing/selftests/cgroup/vmtest-dmem.sh
@@ -0,0 +1,189 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+#
+# Copyright (c) 2026 Red Hat, Inc.
+#
+# Run cgroup test_dmem inside a virtme-ng VM.
+# Dependencies:
+# * virtme-ng
+# * busybox-static (used by virtme-ng)
+# * qemu (used by virtme-ng)
+
+set -euo pipefail
+
+readonly SCRIPT_DIR="$(cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
+readonly KERNEL_CHECKOUT="$(realpath "${SCRIPT_DIR}"/../../../../)"
+
+source "${SCRIPT_DIR}"/../kselftest/ktap_helpers.sh
+
+readonly SSH_GUEST_PORT="${SSH_GUEST_PORT:-22}"
+readonly WAIT_PERIOD=3
+readonly WAIT_PERIOD_MAX=80
+readonly WAIT_TOTAL=$((WAIT_PERIOD * WAIT_PERIOD_MAX))
+readonly QEMU_PIDFILE="$(mktemp /tmp/qemu_dmem_vmtest_XXXX.pid)"
+readonly QEMU_OPTS=" --pidfile ${QEMU_PIDFILE} "
+
+QEMU="qemu-system-$(uname -m)"
+VERBOSE=0
+SHELL_MODE=0
+GUEST_TREE="${GUEST_TREE:-$KERNEL_CHECKOUT}"
+
+usage() {
+ echo
+ echo "$0 [OPTIONS]"
+ echo " -q <qemu> QEMU binary/path (default: ${QEMU})"
+ echo " -s Start interactive shell in VM"
+ echo " -v Verbose output (use -vv for vng boot logs)"
+ echo
+}
+
+die() {
+ echo "$*" >&2
+ exit "${KSFT_FAIL}"
+}
+
+cleanup() {
+ if [[ -s "${QEMU_PIDFILE}" ]]; then
+ pkill -SIGTERM -F "${QEMU_PIDFILE}" >/dev/null 2>&1 || true
+ fi
+ rm -f "${QEMU_PIDFILE}"
+}
+
+vm_ssh() {
+ stdbuf -oL ssh -q \
+ -F "${HOME}/.cache/virtme-ng/.ssh/virtme-ng-ssh.conf" \
+ -l root "virtme-ng%${SSH_GUEST_PORT}" \
+ "$@"
+}
+
+check_deps() {
+ for dep in vng "${QEMU}" busybox pkill ssh; do
+ if ! command -v "${dep}" >/dev/null 2>&1; then
+ echo "skip: dependency ${dep} not found"
+ exit "${KSFT_SKIP}"
+ fi
+ done
+}
+
+vm_start() {
+ local logfile=/dev/null
+ local verbose_opt=""
+
+ if [[ "${VERBOSE}" -eq 2 ]]; then
+ verbose_opt="--verbose"
+ logfile=/dev/stdout
+ fi
+
+ vng \
+ --run \
+ ${verbose_opt} \
+ --qemu-opts="${QEMU_OPTS}" \
+ --qemu="$(command -v "${QEMU}")" \
+ --user root \
+ --ssh "${SSH_GUEST_PORT}" \
+ --rw &>"${logfile}" &
+
+ local vng_pid=$!
+ local elapsed=0
+
+ while [[ ! -s "${QEMU_PIDFILE}" ]]; do
+ kill -0 "${vng_pid}" 2>/dev/null || die "vng exited early; failed to boot VM"
+ [[ "${elapsed}" -ge "${WAIT_TOTAL}" ]] && die "timed out waiting for VM boot"
+ sleep 1
+ elapsed=$((elapsed + 1))
+ done
+}
+
+vm_wait_for_ssh() {
+ local i=0
+ while true; do
+ vm_ssh -- true && break
+ i=$((i + 1))
+ [[ "${i}" -gt "${WAIT_PERIOD_MAX}" ]] && die "timed out waiting for guest ssh"
+ sleep "${WAIT_PERIOD}"
+ done
+}
+
+check_guest_requirements() {
+ local cfg_ok
+ cfg_ok="$(vm_ssh -- "cfg=/boot/config-\$(uname -r); \
+ if [[ -r \"\$cfg\" ]]; then grep -Eq '^CONFIG_CGROUP_DMEM=(y|m)$' \"\$cfg\"; \
+ elif [[ -r /proc/config.gz ]]; then \
+ zgrep -Eq '^CONFIG_CGROUP_DMEM=(y|m)$' /proc/config.gz; \
+ else false; fi; echo \$?")"
+ [[ "${cfg_ok}" == "0" ]] || die "guest kernel missing CONFIG_CGROUP_DMEM"
+}
+
+setup_guest_dmem_helper() {
+ local kdir
+
+ vm_ssh -- "mountpoint -q /sys/kernel/debug || \
+ mount -t debugfs none /sys/kernel/debug" || true
+
+ # Already available (built-in or loaded).
+ if vm_ssh -- "[[ -e /sys/kernel/debug/dmem_selftest/charge ]]"; then
+ echo "dmem_selftest ready"
+ return 0
+ fi
+
+ # Fast path: try installed module.
+ vm_ssh -- "modprobe -q dmem_selftest 2>/dev/null || true"
+ if vm_ssh -- "[[ -e /sys/kernel/debug/dmem_selftest/charge ]]"; then
+ echo "dmem_selftest ready"
+ return 0
+ fi
+
+ # Fallback: build only this module against running guest kernel,
+ # then insert it.
+ kdir="$(vm_ssh -- "echo /lib/modules/\$(uname -r)/build")"
+ if vm_ssh -- "[[ -d '${kdir}' ]]"; then
+ echo "Building dmem_selftest.ko against running guest kernel..."
+ vm_ssh -- "make -C '${kdir}' \
+ M='${GUEST_TREE}/kernel/cgroup' \
+ CONFIG_DMEM_SELFTEST=m modules"
+ vm_ssh -- "insmod '${GUEST_TREE}/kernel/cgroup/dmem_selftest.ko' \
+ 2>/dev/null || modprobe -q dmem_selftest 2>/dev/null || true"
+ fi
+
+ if vm_ssh -- "[[ -e /sys/kernel/debug/dmem_selftest/charge ]]"; then
+ echo "dmem_selftest ready"
+ return 0
+ fi
+
+ die "dmem_selftest unavailable (modprobe/build+insmod failed)"
+}
+
+run_test() {
+ vm_ssh -- "cd '${GUEST_TREE}' && make -C tools/testing/selftests TARGETS=cgroup"
+ vm_ssh -- "cd '${GUEST_TREE}' && ./tools/testing/selftests/cgroup/test_dmem"
+}
+
+while getopts ":hvq:s" o; do
+ case "${o}" in
+ v) VERBOSE=$((VERBOSE + 1)) ;;
+ q) QEMU="${OPTARG}" ;;
+ s) SHELL_MODE=1 ;;
+ h|*) usage ;;
+ esac
+done
+
+trap cleanup EXIT
+
+check_deps
+echo "Booting virtme-ng VM..."
+vm_start
+vm_wait_for_ssh
+echo "VM is reachable via SSH."
+check_guest_requirements
+setup_guest_dmem_helper
+
+if [[ "${SHELL_MODE}" -eq 1 ]]; then
+ echo "Starting interactive shell in VM. Exit to stop VM."
+ vm_ssh -t -- "cd '${GUEST_TREE}' && exec bash --noprofile --norc"
+ exit "${KSFT_PASS}"
+fi
+
+echo "Running cgroup/test_dmem in VM..."
+run_test
+echo "PASS: test_dmem completed"
+exit "${KSFT_PASS}"
--
2.52.0
^ permalink raw reply related [flat|nested] 5+ messages in thread
* Re: [PATCH 2/3] selftests: cgroup: Add dmem selftest coverage
2026-03-27 8:53 ` [PATCH 2/3] selftests: cgroup: Add dmem selftest coverage Albert Esteve
@ 2026-03-27 13:15 ` Albert Esteve
0 siblings, 0 replies; 5+ messages in thread
From: Albert Esteve @ 2026-03-27 13:15 UTC (permalink / raw)
To: Tejun Heo, Johannes Weiner, Michal Koutný, Shuah Khan
Cc: linux-kernel, cgroups, linux-kselftest, mripard, echanude
On Fri, Mar 27, 2026 at 9:53 AM Albert Esteve <aesteve@redhat.com> wrote:
>
> Currently, tools/testing/selftests/cgroup/ does not include
> a dmem-specific test binary. This leaves dmem charge and
> limit behavior largely unvalidated in kselftest coverage.
>
> Add test_dmem and wire it into the cgroup selftests Makefile.
> The new test exercises dmem controller behavior through the
> dmem_selftest debugfs interface for the dmem_selftest region.
>
> The test adds three complementary checks:
> - test_dmem_max creates a nested hierarchy with per-leaf
> dmem.max values and verifies that over-limit charges
> fail while in-limit charges succeed with bounded rounding
> in dmem.current.
> - test_dmem_min and test_dmem_low verify that charging
> from a cgroup with the corresponding protection knob
> set updates dmem.current as expected.
> - test_dmem_charge_byte_granularity validates accounting
> bounds for non-page-aligned charge sizes and
> uncharge-to-zero behavior.
>
> This provides deterministic userspace coverage for dmem
> accounting and hard-limit enforcement using a test helper
> module, without requiring subsystem-specific production
> drivers.
>
> Signed-off-by: Albert Esteve <aesteve@redhat.com>
> ---
> tools/testing/selftests/cgroup/.gitignore | 1 +
> tools/testing/selftests/cgroup/Makefile | 2 +
> tools/testing/selftests/cgroup/test_dmem.c | 487 +++++++++++++++++++++++++++++
> 3 files changed, 490 insertions(+)
>
> diff --git a/tools/testing/selftests/cgroup/.gitignore b/tools/testing/selftests/cgroup/.gitignore
> index 952e4448bf070..ea2322598217d 100644
> --- a/tools/testing/selftests/cgroup/.gitignore
> +++ b/tools/testing/selftests/cgroup/.gitignore
> @@ -2,6 +2,7 @@
> test_core
> test_cpu
> test_cpuset
> +test_dmem
> test_freezer
> test_hugetlb_memcg
> test_kill
> diff --git a/tools/testing/selftests/cgroup/Makefile b/tools/testing/selftests/cgroup/Makefile
> index e01584c2189ac..e1a5e9316620e 100644
> --- a/tools/testing/selftests/cgroup/Makefile
> +++ b/tools/testing/selftests/cgroup/Makefile
> @@ -10,6 +10,7 @@ TEST_GEN_FILES := wait_inotify
> TEST_GEN_PROGS = test_core
> TEST_GEN_PROGS += test_cpu
> TEST_GEN_PROGS += test_cpuset
> +TEST_GEN_PROGS += test_dmem
> TEST_GEN_PROGS += test_freezer
> TEST_GEN_PROGS += test_hugetlb_memcg
> TEST_GEN_PROGS += test_kill
> @@ -26,6 +27,7 @@ include lib/libcgroup.mk
> $(OUTPUT)/test_core: $(LIBCGROUP_O)
> $(OUTPUT)/test_cpu: $(LIBCGROUP_O)
> $(OUTPUT)/test_cpuset: $(LIBCGROUP_O)
> +$(OUTPUT)/test_dmem: $(LIBCGROUP_O)
> $(OUTPUT)/test_freezer: $(LIBCGROUP_O)
> $(OUTPUT)/test_hugetlb_memcg: $(LIBCGROUP_O)
> $(OUTPUT)/test_kill: $(LIBCGROUP_O)
> diff --git a/tools/testing/selftests/cgroup/test_dmem.c b/tools/testing/selftests/cgroup/test_dmem.c
> new file mode 100644
> index 0000000000000..cdd5cb7206f16
> --- /dev/null
> +++ b/tools/testing/selftests/cgroup/test_dmem.c
> @@ -0,0 +1,487 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * Test the dmem (device memory) cgroup controller.
> + *
> + * Depends on dmem_selftest kernel module.
> + */
> +
> +#define _GNU_SOURCE
> +
> +#include <linux/limits.h>
> +#include <errno.h>
> +#include <fcntl.h>
> +#include <stdio.h>
> +#include <stdlib.h>
> +#include <string.h>
> +#include <sys/stat.h>
> +#include <sys/types.h>
> +#include <unistd.h>
> +
> +#include "kselftest.h"
> +#include "cgroup_util.h"
> +
> +/* kernel/cgroup/dmem_selftest.c */
> +#define DM_SELFTEST_REGION "dmem_selftest"
> +#define DM_SELFTEST_CHARGE "/sys/kernel/debug/dmem_selftest/charge"
> +#define DM_SELFTEST_UNCHARGE "/sys/kernel/debug/dmem_selftest/uncharge"
> +
> +/*
> + * Parse the first line of dmem.capacity (root):
> + * "<name> <size_in_bytes>"
> + * Returns 1 if a region was found, 0 if capacity is empty, -1 on read error.
> + */
> +static int parse_first_region(const char *root, char *name, size_t name_len,
> + unsigned long long *size_out)
> +{
> + char buf[4096];
> + char nm[256];
> + unsigned long long sz;
> +
> + if (cg_read(root, "dmem.capacity", buf, sizeof(buf)) < 0)
> + return -1;
> +
> + if (sscanf(buf, "%255s %llu", nm, &sz) < 2)
> + return 0;
> +
> + if (name_len <= strlen(nm))
> + return -1;
> +
> + strcpy(name, nm);
> + *size_out = sz;
> + return 1;
> +}
> +
> +/*
> + * Read the numeric limit for @region_name from a multiline
> + * dmem.{min,low,max} file. Returns bytes,
> + * or -1 if the line is "<name> max", or -2 if missing/err.
> + */
> +static long long dmem_read_limit_for_region(const char *cgroup, const char *ctrl,
> + const char *region_name)
> +{
> + char buf[4096];
> + char *line, *saveptr = NULL;
> + char fname[256];
> + char fval[64];
> +
> + if (cg_read(cgroup, ctrl, buf, sizeof(buf)) < 0)
> + return -2;
> +
> + for (line = strtok_r(buf, "\n", &saveptr); line;
> + line = strtok_r(NULL, "\n", &saveptr)) {
> + if (!line[0])
> + continue;
> + if (sscanf(line, "%255s %63s", fname, fval) != 2)
> + continue;
> + if (strcmp(fname, region_name))
> + continue;
> + if (!strcmp(fval, "max"))
> + return -1;
> + return strtoll(fval, NULL, 0);
> + }
> + return -2;
> +}
> +
> +static long long dmem_read_limit(const char *cgroup, const char *ctrl)
> +{
> + return dmem_read_limit_for_region(cgroup, ctrl, DM_SELFTEST_REGION);
> +}
> +
> +static int dmem_write_limit(const char *cgroup, const char *ctrl,
> + const char *val)
> +{
> + char wr[512];
> +
> + snprintf(wr, sizeof(wr), "%s %s", DM_SELFTEST_REGION, val);
> + return cg_write(cgroup, ctrl, wr);
> +}
> +
> +static int dmem_selftest_charge_bytes(unsigned long long bytes)
> +{
> + char wr[32];
> +
> + snprintf(wr, sizeof(wr), "%llu", bytes);
> + return write_text(DM_SELFTEST_CHARGE, wr, strlen(wr));
> +}
> +
> +static int dmem_selftest_uncharge(void)
> +{
> + return write_text(DM_SELFTEST_UNCHARGE, "\n", 1);
> +}
> +
> +/*
> + * First, this test creates the following hierarchy:
> + * A
> + * A/B dmem.max=1M
> + * A/B/C dmem.max=75K
> + * A/B/D dmem.max=25K
> + * A/B/E dmem.max=8K
> + * A/B/F dmem.max=0
> + *
> + * Then for each leaf cgroup it tries to charge above dmem.max
> + * and expects the charge request to fail and dmem.current to
> + * remain unchanged.
> + *
> + * For leaves with non-zero dmem.max, it additionally charges a
> + * smaller amount and verifies accounting grows within one PAGE_SIZE
> + * rounding bound, then uncharges and verifies dmem.current returns
> + * to the previous value.
> + *
> + */
> +static int test_dmem_max(const char *root)
> +{
> + static const char * const leaf_max[] = { "75K", "25K", "8K", "0" };
> + static const unsigned long long fail_sz[] = {
> + (75ULL * 1024ULL) + 1ULL,
> + (25ULL * 1024ULL) + 1ULL,
> + (8ULL * 1024ULL) + 1ULL,
> + 1ULL
> + };
> + static const unsigned long long pass_sz[] = {
> + 4096ULL, 4096ULL, 4096ULL, 0ULL
> + };
> + char *parent[2] = {NULL};
> + char *children[4] = {NULL};
> + unsigned long long cap;
> + char region[256];
> + long long page_size;
> + long long cur_before, cur_after;
> + int ret = KSFT_FAIL;
> + int charged = 0;
> + int in_child = 0;
> + long long v;
> + int i;
> +
> + if (access(DM_SELFTEST_CHARGE, W_OK) != 0)
> + return KSFT_SKIP;
> +
> + if (parse_first_region(root, region, sizeof(region), &cap) != 1)
> + return KSFT_SKIP;
> + if (strcmp(region, DM_SELFTEST_REGION) != 0)
> + return KSFT_SKIP;
> +
> + page_size = sysconf(_SC_PAGESIZE);
> + if (page_size <= 0)
> + goto cleanup;
> +
> + parent[0] = cg_name(root, "dmem_prot_0");
> + parent[1] = cg_name(parent[0], "dmem_prot_1");
> + if (!parent[0] || !parent[1])
> + goto cleanup;
> +
> + if (cg_create(parent[0]))
> + goto cleanup;
> +
> + if (cg_write(parent[0], "cgroup.subtree_control", "+dmem"))
> + goto cleanup;
> +
> + if (cg_create(parent[1]))
> + goto cleanup;
> +
> + if (cg_write(parent[1], "cgroup.subtree_control", "+dmem"))
> + goto cleanup;
> +
> + for (i = 0; i < 4; i++) {
> + children[i] = cg_name_indexed(parent[1], "dmem_child", i);
> + if (!children[i])
> + goto cleanup;
> + if (cg_create(children[i]))
> + goto cleanup;
> + }
> +
> + if (dmem_write_limit(parent[1], "dmem.max", "1M"))
> + goto cleanup;
> + for (i = 0; i < 4; i++)
> + if (dmem_write_limit(children[i], "dmem.max", leaf_max[i]))
> + goto cleanup;
> +
> + v = dmem_read_limit(parent[1], "dmem.max");
> + if (!values_close(v, 1024LL * 1024LL, 3))
> + goto cleanup;
> + v = dmem_read_limit(children[0], "dmem.max");
> + if (!values_close(v, 75LL * 1024LL, 3))
> + goto cleanup;
> + v = dmem_read_limit(children[1], "dmem.max");
> + if (!values_close(v, 25LL * 1024LL, 3))
> + goto cleanup;
> + v = dmem_read_limit(children[2], "dmem.max");
> + if (!values_close(v, 8LL * 1024LL, 3))
> + goto cleanup;
> + v = dmem_read_limit(children[3], "dmem.max");
> + if (v != 0)
> + goto cleanup;
> +
> + for (i = 0; i < 4; i++) {
> + if (cg_enter_current(children[i]))
> + goto cleanup;
> + in_child = 1;
> +
> + cur_before = dmem_read_limit(children[i], "dmem.current");
> + if (cur_before < 0)
> + goto cleanup;
> +
> + if (dmem_selftest_charge_bytes(fail_sz[i]) == 0)
This should be '>= 0', dmem_selftest_charge_bytes() returns the
written bytes on success.
I will fix it in the next iteration.
> + goto cleanup;
> +
> + cur_after = dmem_read_limit(children[i], "dmem.current");
> + if (cur_after != cur_before)
> + goto cleanup;
> +
> + if (pass_sz[i] > 0) {
> + if (dmem_selftest_charge_bytes(pass_sz[i]) < 0)
> + goto cleanup;
> + charged = 1;
> +
> + cur_after = dmem_read_limit(children[i], "dmem.current");
> + if (cur_after < cur_before + (long long)pass_sz[i])
> + goto cleanup;
> + if (cur_after > cur_before + (long long)pass_sz[i] + page_size)
> + goto cleanup;
> +
> + if (dmem_selftest_uncharge() < 0)
> + goto cleanup;
> + charged = 0;
> +
> + cur_after = dmem_read_limit(children[i], "dmem.current");
> + if (cur_after != cur_before)
> + goto cleanup;
> + }
> +
> + if (cg_enter_current(root))
> + goto cleanup;
> + in_child = 0;
> + }
> +
> + ret = KSFT_PASS;
> +
> +cleanup:
> + if (charged)
> + dmem_selftest_uncharge();
> + if (in_child)
> + cg_enter_current(root);
> + for (i = 3; i >= 0; i--) {
> + if (!children[i])
> + continue;
> + cg_destroy(children[i]);
> + free(children[i]);
> + }
> + for (i = 1; i >= 0; i--) {
> + if (!parent[i])
> + continue;
> + cg_destroy(parent[i]);
> + free(parent[i]);
> + }
> + return ret;
> +}
> +
> +/*
> + * This test sets dmem.min and dmem.low on a child cgroup, then charge
> + * from that context and verify dmem.current tracks the charged bytes
> + * (within one page rounding).
> + */
> +static int test_dmem_charge_with_attr(const char *root, bool min)
> +{
> + char region[256];
> + unsigned long long cap;
> + const unsigned long long charge_sz = 12345ULL;
> + const char *attribute = min ? "dmem.min" : "dmem.low";
> + int ret = KSFT_FAIL;
> + char *cg = NULL;
> + long long cur;
> + long long page_size;
> + int charged = 0;
> + int in_child = 0;
> +
> + if (access(DM_SELFTEST_CHARGE, W_OK) != 0)
> + return KSFT_SKIP;
> +
> + if (parse_first_region(root, region, sizeof(region), &cap) != 1)
> + return KSFT_SKIP;
> + if (strcmp(region, DM_SELFTEST_REGION) != 0)
> + return KSFT_SKIP;
> +
> + page_size = sysconf(_SC_PAGESIZE);
> + if (page_size <= 0)
> + goto cleanup;
> +
> + cg = cg_name(root, "test_dmem_attr");
> + if (!cg)
> + goto cleanup;
> +
> + if (cg_create(cg))
> + goto cleanup;
> +
> + if (cg_enter_current(cg))
> + goto cleanup;
> + in_child = 1;
> +
> + if (dmem_write_limit(cg, attribute, "16K"))
> + goto cleanup;
> +
> + if (dmem_selftest_charge_bytes(charge_sz) < 0)
> + goto cleanup;
> + charged = 1;
> +
> + cur = dmem_read_limit(cg, "dmem.current");
> + if (cur < (long long)charge_sz)
> + goto cleanup;
> + if (cur > (long long)charge_sz + page_size)
> + goto cleanup;
> +
> + if (dmem_selftest_uncharge() < 0)
> + goto cleanup;
> + charged = 0;
> +
> + cur = dmem_read_limit(cg, "dmem.current");
> + if (cur != 0)
> + goto cleanup;
> +
> + ret = KSFT_PASS;
> +
> +cleanup:
> + if (charged)
> + dmem_selftest_uncharge();
> + if (in_child)
> + cg_enter_current(root);
> + cg_destroy(cg);
> + free(cg);
> + return ret;
> +}
> +
> +static int test_dmem_min(const char *root)
> +{
> + return test_dmem_charge_with_attr(root, "dmem.min");
> +}
> +
> +static int test_dmem_low(const char *root)
> +{
> + return test_dmem_charge_with_attr(root, "dmem.low");
> +}
> +
> +/*
> + * This test charges non-page-aligned byte sizes and verify dmem.current
> + * stays consistent: it must account at least the requested bytes and
> + * never exceed one kernel page of rounding overhead. Then uncharge must
> + * return usage to 0.
> + */
> +static int test_dmem_charge_byte_granularity(const char *root)
> +{
> + static const unsigned long long sizes[] = { 1ULL, 4095ULL, 4097ULL, 12345ULL };
> + char *cg = NULL;
> + unsigned long long cap;
> + char region[256];
> + long long cur;
> + long long page_size;
> + int ret = KSFT_FAIL;
> + int charged = 0;
> + int in_child = 0;
> + size_t i;
> +
> + if (access(DM_SELFTEST_CHARGE, W_OK) != 0)
> + return KSFT_SKIP;
> +
> + if (parse_first_region(root, region, sizeof(region), &cap) != 1)
> + return KSFT_SKIP;
> + if (strcmp(region, DM_SELFTEST_REGION) != 0)
> + return KSFT_SKIP;
> +
> + page_size = sysconf(_SC_PAGESIZE);
> + if (page_size <= 0)
> + goto cleanup;
> +
> + cg = cg_name(root, "dmem_dbg_byte_gran");
> + if (!cg)
> + goto cleanup;
> +
> + if (cg_create(cg))
> + goto cleanup;
> +
> + if (dmem_write_limit(cg, "dmem.max", "8M"))
> + goto cleanup;
> +
> + if (cg_enter_current(cg))
> + goto cleanup;
> + in_child = 1;
> +
> + for (i = 0; i < ARRAY_SIZE(sizes); i++) {
> + if (dmem_selftest_charge_bytes(sizes[i]) < 0)
> + goto cleanup;
> + charged = 1;
> +
> + cur = dmem_read_limit(cg, "dmem.current");
> + if (cur < (long long)sizes[i])
> + goto cleanup;
> + if (cur > (long long)sizes[i] + page_size)
> + goto cleanup;
> +
> + if (dmem_selftest_uncharge() < 0)
> + goto cleanup;
> + charged = 0;
> +
> + cur = dmem_read_limit(cg, "dmem.current");
> + if (cur != 0)
> + goto cleanup;
> + }
> +
> + ret = KSFT_PASS;
> +
> +cleanup:
> + if (charged)
> + dmem_selftest_uncharge();
> + if (in_child)
> + cg_enter_current(root);
> + if (cg) {
> + cg_destroy(cg);
> + free(cg);
> + }
> + return ret;
> +}
> +
> +#define T(x) { x, #x }
> +struct dmem_test {
> + int (*fn)(const char *root);
> + const char *name;
> +} tests[] = {
> + T(test_dmem_max),
> + T(test_dmem_min),
> + T(test_dmem_low),
> + T(test_dmem_charge_byte_granularity),
> +};
> +#undef T
> +
> +int main(int argc, char **argv)
> +{
> + char root[PATH_MAX];
> + int i;
> +
> + ksft_print_header();
> + ksft_set_plan(ARRAY_SIZE(tests));
> +
> + if (cg_find_unified_root(root, sizeof(root), NULL))
> + ksft_exit_skip("cgroup v2 isn't mounted\n");
> +
> + if (cg_read_strstr(root, "cgroup.controllers", "dmem"))
> + ksft_exit_skip("dmem controller isn't available (CONFIG_CGROUP_DMEM?)\n");
> +
> + if (cg_read_strstr(root, "cgroup.subtree_control", "dmem"))
> + if (cg_write(root, "cgroup.subtree_control", "+dmem"))
> + ksft_exit_skip("Failed to enable dmem controller\n");
> +
> + for (i = 0; i < ARRAY_SIZE(tests); i++) {
> + switch (tests[i].fn(root)) {
> + case KSFT_PASS:
> + ksft_test_result_pass("%s\n", tests[i].name);
> + break;
> + case KSFT_SKIP:
> + ksft_test_result_skip(
> + "%s (need CONFIG_DMEM_SELFTEST, modprobe dmem_selftest)\n",
> + tests[i].name);
> + break;
> + default:
> + ksft_test_result_fail("%s\n", tests[i].name);
> + break;
> + }
> + }
> +
> + ksft_finished();
> +}
>
> --
> 2.52.0
>
^ permalink raw reply [flat|nested] 5+ messages in thread
end of thread, other threads:[~2026-03-27 13:15 UTC | newest]
Thread overview: 5+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-03-27 8:53 [PATCH 0/3] cgroup: dmem: add selftest helper, coverage, and VM runner Albert Esteve
2026-03-27 8:53 ` [PATCH 1/3] cgroup: Add dmem_selftest module Albert Esteve
2026-03-27 8:53 ` [PATCH 2/3] selftests: cgroup: Add dmem selftest coverage Albert Esteve
2026-03-27 13:15 ` Albert Esteve
2026-03-27 8:53 ` [PATCH 3/3] selftests: cgroup: Add vmtest-dmem runner based on hid vmtest Albert Esteve
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox