* [PATCH v2 0/4] cgroup: dmem: add selftest helper, coverage, and VM runner
@ 2026-04-21 7:19 Albert Esteve
2026-04-21 7:19 ` [PATCH v2 1/4] cgroup: Add dmem_selftest module Albert Esteve
` (3 more replies)
0 siblings, 4 replies; 5+ messages in thread
From: Albert Esteve @ 2026-04-21 7:19 UTC (permalink / raw)
To: Tejun Heo, Johannes Weiner, Michal Koutný, Shuah Khan
Cc: linux-kernel, cgroups, linux-kselftest, Albert Esteve, mripard,
Eric Chanudet
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 is 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>
---
Changes in v2:
- Fix debugfs_create_dir() error check
- Fix module teardown race: call dmem_selftest_remove() before
uncharging so debugfs files are torn down
- Use IS_ERR_OR_NULL() in selftest() sanity check
- Add CONFIG_CGROUP_DMEM=y to the cgroup selftest config
- Replace config-file parsing in check_guest_requirements() with
a direct check of /sys/fs/cgroup/cgroup.controllers
- Add new patch 4 (from Eric Chanudet): vmtest-dmem.sh -b flag
to configure and build a local kernel tree
- Link to v1: https://lore.kernel.org/r/20260327-kunit_cgroups-v1-0-971b3c739a00@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
Eric Chanudet (1):
selftests: cgroup: handle vmtest-dmem -b to test locally built kernel
init/Kconfig | 12 +
kernel/cgroup/Makefile | 1 +
kernel/cgroup/dmem_selftest.c | 198 +++++++++++
tools/testing/selftests/cgroup/.gitignore | 1 +
tools/testing/selftests/cgroup/Makefile | 2 +
tools/testing/selftests/cgroup/config | 1 +
tools/testing/selftests/cgroup/test_dmem.c | 490 ++++++++++++++++++++++++++
tools/testing/selftests/cgroup/vmtest-dmem.sh | 229 ++++++++++++
8 files changed, 934 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 v2 1/4] cgroup: Add dmem_selftest module
2026-04-21 7:19 [PATCH v2 0/4] cgroup: dmem: add selftest helper, coverage, and VM runner Albert Esteve
@ 2026-04-21 7:19 ` Albert Esteve
2026-04-21 7:19 ` [PATCH v2 2/4] selftests: cgroup: Add dmem selftest coverage Albert Esteve
` (2 subsequent siblings)
3 siblings, 0 replies; 5+ messages in thread
From: Albert Esteve @ 2026-04-21 7:19 UTC (permalink / raw)
To: Tejun Heo, Johannes Weiner, Michal Koutný, Shuah Khan
Cc: linux-kernel, cgroups, linux-kselftest, Albert Esteve, mripard
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 | 198 ++++++++++++++++++++++++++++++++++++++++++
3 files changed, 211 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..cf7274eb02f71
--- /dev/null
+++ b/kernel/cgroup/dmem_selftest.c
@@ -0,0 +1,198 @@
+// 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)
+{
+ int ret = 0;
+ 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 (IS_ERR(dbg_dir)) {
+ ret = PTR_ERR(dbg_dir);
+ goto dbgfs_error;
+ }
+
+ 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 ret;
+
+dbgfs_error:
+ dmem_cgroup_unregister_region(selftest_region);
+ dbg_dir = NULL;
+ selftest_region = NULL;
+ return ret;
+}
+
+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(IS_ERR_OR_NULL(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");
+
+ dmem_selftest_remove();
+
+ mutex_lock(&charge_lock);
+ if (charged_pool) {
+ dmem_cgroup_uncharge(charged_pool, charged_size);
+ charged_pool = NULL;
+ }
+ mutex_unlock(&charge_lock);
+}
+
+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 v2 2/4] selftests: cgroup: Add dmem selftest coverage
2026-04-21 7:19 [PATCH v2 0/4] cgroup: dmem: add selftest helper, coverage, and VM runner Albert Esteve
2026-04-21 7:19 ` [PATCH v2 1/4] cgroup: Add dmem_selftest module Albert Esteve
@ 2026-04-21 7:19 ` Albert Esteve
2026-04-21 7:19 ` [PATCH v2 3/4] selftests: cgroup: Add vmtest-dmem runner based on hid vmtest Albert Esteve
2026-04-21 7:19 ` [PATCH v2 4/4] selftests: cgroup: handle vmtest-dmem -b to test locally built kernel Albert Esteve
3 siblings, 0 replies; 5+ messages in thread
From: Albert Esteve @ 2026-04-21 7:19 UTC (permalink / raw)
To: Tejun Heo, Johannes Weiner, Michal Koutný, Shuah Khan
Cc: linux-kernel, cgroups, linux-kselftest, Albert Esteve, mripard
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/config | 1 +
tools/testing/selftests/cgroup/test_dmem.c | 490 +++++++++++++++++++++++++++++
4 files changed, 494 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/config b/tools/testing/selftests/cgroup/config
index 39f979690dd3b..2ee0488c3d65f 100644
--- a/tools/testing/selftests/cgroup/config
+++ b/tools/testing/selftests/cgroup/config
@@ -1,5 +1,6 @@
CONFIG_CGROUPS=y
CONFIG_CGROUP_CPUACCT=y
+CONFIG_CGROUP_DMEM=y
CONFIG_CGROUP_FREEZER=y
CONFIG_CGROUP_SCHED=y
CONFIG_MEMCG=y
diff --git a/tools/testing/selftests/cgroup/test_dmem.c b/tools/testing/selftests/cgroup/test_dmem.c
new file mode 100644
index 0000000000000..ae1e19bafe0a8
--- /dev/null
+++ b/tools/testing/selftests/cgroup/test_dmem.c
@@ -0,0 +1,490 @@
+// 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");
+ if (!parent[0])
+ goto cleanup;
+
+ parent[1] = cg_name(parent[0], "dmem_prot_1");
+ if (!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, true);
+}
+
+static int test_dmem_low(const char *root)
+{
+ return test_dmem_charge_with_attr(root, false);
+}
+
+/*
+ * 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 v2 3/4] selftests: cgroup: Add vmtest-dmem runner based on hid vmtest
2026-04-21 7:19 [PATCH v2 0/4] cgroup: dmem: add selftest helper, coverage, and VM runner Albert Esteve
2026-04-21 7:19 ` [PATCH v2 1/4] cgroup: Add dmem_selftest module Albert Esteve
2026-04-21 7:19 ` [PATCH v2 2/4] selftests: cgroup: Add dmem selftest coverage Albert Esteve
@ 2026-04-21 7:19 ` Albert Esteve
2026-04-21 7:19 ` [PATCH v2 4/4] selftests: cgroup: handle vmtest-dmem -b to test locally built kernel Albert Esteve
3 siblings, 0 replies; 5+ messages in thread
From: Albert Esteve @ 2026-04-21 7:19 UTC (permalink / raw)
To: Tejun Heo, Johannes Weiner, Michal Koutný, Shuah Khan
Cc: linux-kernel, cgroups, linux-kselftest, Albert Esteve, mripard
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 | 197 ++++++++++++++++++++++++++
1 file changed, 197 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..3174f22b06361
--- /dev/null
+++ b/tools/testing/selftests/cgroup/vmtest-dmem.sh
@@ -0,0 +1,197 @@
+#!/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
+
+ exit 1
+}
+
+die() {
+ echo "$*" >&2
+ exit "${KSFT_FAIL}"
+}
+
+cleanup() {
+ if [[ -s "${QEMU_PIDFILE}" ]]; then
+ pkill -SIGTERM -F "${QEMU_PIDFILE}" >/dev/null 2>&1 || true
+ fi
+
+ # If failure occurred during or before qemu start up, then we need
+ # to clean this up ourselves.
+ if [[ -e "${QEMU_PIDFILE}" ]]; then
+ rm -f "${QEMU_PIDFILE}"
+ fi
+}
+
+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 -- " \
+ grep -q dmem /sys/fs/cgroup/cgroup.controllers && \
+ grep -q memory /sys/fs/cgroup/cgroup.controllers;
+ 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."
+
+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
+
+check_guest_requirements
+setup_guest_dmem_helper
+
+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
* [PATCH v2 4/4] selftests: cgroup: handle vmtest-dmem -b to test locally built kernel
2026-04-21 7:19 [PATCH v2 0/4] cgroup: dmem: add selftest helper, coverage, and VM runner Albert Esteve
` (2 preceding siblings ...)
2026-04-21 7:19 ` [PATCH v2 3/4] selftests: cgroup: Add vmtest-dmem runner based on hid vmtest Albert Esteve
@ 2026-04-21 7:19 ` Albert Esteve
3 siblings, 0 replies; 5+ messages in thread
From: Albert Esteve @ 2026-04-21 7:19 UTC (permalink / raw)
To: Tejun Heo, Johannes Weiner, Michal Koutný, Shuah Khan
Cc: linux-kernel, cgroups, linux-kselftest, Albert Esteve, mripard,
Eric Chanudet
From: Eric Chanudet <echanude@redhat.com>
Currently vmtest-dmem.sh relies on the host's running kernel or a
pre-built one when booting the virtme-ng VM, with no option to
configure and build a local kernel tree directly.
This adds friction to the development cycle: the user must manually
run vng --kconfig with the correct config fragment, build the kernel,
and pass the result to the script.
Add a -b flag that automates this workflow. When set, handle_build()
configures the kernel using vng --kconfig with the selftest config
fragment, builds it with make -j$(nproc), and vm_start() passes the
local tree to vng --run so the VM boots the freshly built kernel.
Signed-off-by: Eric Chanudet <echanude@redhat.com>
Signed-off-by: Albert Esteve <aesteve@redhat.com>
---
tools/testing/selftests/cgroup/vmtest-dmem.sh | 34 ++++++++++++++++++++++++++-
1 file changed, 33 insertions(+), 1 deletion(-)
diff --git a/tools/testing/selftests/cgroup/vmtest-dmem.sh b/tools/testing/selftests/cgroup/vmtest-dmem.sh
index 3174f22b06361..a5f1e529e1aa0 100755
--- a/tools/testing/selftests/cgroup/vmtest-dmem.sh
+++ b/tools/testing/selftests/cgroup/vmtest-dmem.sh
@@ -23,6 +23,7 @@ readonly WAIT_TOTAL=$((WAIT_PERIOD * WAIT_PERIOD_MAX))
readonly QEMU_PIDFILE="$(mktemp /tmp/qemu_dmem_vmtest_XXXX.pid)"
readonly QEMU_OPTS=" --pidfile ${QEMU_PIDFILE} "
+BUILD=0
QEMU="qemu-system-$(uname -m)"
VERBOSE=0
SHELL_MODE=0
@@ -72,17 +73,46 @@ check_deps() {
done
}
+handle_build() {
+ if [[ ! "${BUILD}" -eq 1 ]]; then
+ return
+ fi
+
+ if [[ ! -d "${KERNEL_CHECKOUT}" ]]; then
+ echo "-b requires vmtest.sh called from the kernel source tree" >&2
+ exit 1
+ fi
+
+ pushd "${KERNEL_CHECKOUT}" &>/dev/null
+
+ if ! vng --kconfig --config "${SCRIPT_DIR}"/config; then
+ die "failed to generate .config for kernel source tree (${KERNEL_CHECKOUT})"
+ fi
+
+ if ! make -j"$(nproc)"; then
+ die "failed to build kernel from source tree (${KERNEL_CHECKOUT})"
+ fi
+
+ popd &>/dev/null
+}
+
vm_start() {
local logfile=/dev/null
local verbose_opt=""
+ local kernel_opt=""
if [[ "${VERBOSE}" -eq 2 ]]; then
verbose_opt="--verbose"
logfile=/dev/stdout
fi
+ if [[ "${BUILD}" -eq 1 ]]; then
+ kernel_opt="${KERNEL_CHECKOUT}"
+ fi
+
vng \
--run \
+ "$kernel_opt" \
${verbose_opt} \
--qemu-opts="${QEMU_OPTS}" \
--qemu="$(command -v "${QEMU}")" \
@@ -165,10 +195,11 @@ run_test() {
vm_ssh -- "cd '${GUEST_TREE}' && ./tools/testing/selftests/cgroup/test_dmem"
}
-while getopts ":hvq:s" o; do
+while getopts ":hvq:sb" o; do
case "${o}" in
v) VERBOSE=$((VERBOSE + 1)) ;;
q) QEMU="${OPTARG}" ;;
+ b) BUILD=1 ;;
s) SHELL_MODE=1 ;;
h|*) usage ;;
esac
@@ -177,6 +208,7 @@ done
trap cleanup EXIT
check_deps
+handle_build
echo "Booting virtme-ng VM..."
vm_start
vm_wait_for_ssh
--
2.52.0
^ permalink raw reply related [flat|nested] 5+ messages in thread
end of thread, other threads:[~2026-04-21 7:20 UTC | newest]
Thread overview: 5+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-04-21 7:19 [PATCH v2 0/4] cgroup: dmem: add selftest helper, coverage, and VM runner Albert Esteve
2026-04-21 7:19 ` [PATCH v2 1/4] cgroup: Add dmem_selftest module Albert Esteve
2026-04-21 7:19 ` [PATCH v2 2/4] selftests: cgroup: Add dmem selftest coverage Albert Esteve
2026-04-21 7:19 ` [PATCH v2 3/4] selftests: cgroup: Add vmtest-dmem runner based on hid vmtest Albert Esteve
2026-04-21 7:19 ` [PATCH v2 4/4] selftests: cgroup: handle vmtest-dmem -b to test locally built kernel Albert Esteve
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox