[PATCH 08/11] selftests: cgroup: add memcg cma tests
From: Eric Chanudet
Date: Fri Aug 21 2026 - 15:01:29 EST
Add test_cma_memcg with two simple tests to exercise CMA memcg
accounting using the dma-buf CMA heap
(/dev/dma_heap/default_cma_region).
test_cma_charge allocates a 4MB buffer in the default CMA area, verifies
memory.cma.reserved.current increased by that amount, then frees the
buffer and verifies that the counter returns to its original value.
test_cma_limit sets memory.cma.reserved.max to 2MB, attempts a 4MB
allocation in the default CMA area and verifies it fails as expected.
Skip the test if memory_cma_accounting is not enabled on the cgroupfs.
Signed-off-by: Eric Chanudet <echanude@xxxxxxxxxx>
---
tools/testing/selftests/cgroup/.gitignore | 1 +
tools/testing/selftests/cgroup/Makefile | 2 +
tools/testing/selftests/cgroup/test_cma_memcg.c | 193 ++++++++++++++++++++++++
3 files changed, 196 insertions(+)
diff --git a/tools/testing/selftests/cgroup/.gitignore b/tools/testing/selftests/cgroup/.gitignore
index 952e4448bf07..b9a4298d4944 100644
--- a/tools/testing/selftests/cgroup/.gitignore
+++ b/tools/testing/selftests/cgroup/.gitignore
@@ -1,5 +1,6 @@
# SPDX-License-Identifier: GPL-2.0-only
test_core
+test_cma_memcg
test_cpu
test_cpuset
test_freezer
diff --git a/tools/testing/selftests/cgroup/Makefile b/tools/testing/selftests/cgroup/Makefile
index e01584c2189a..601d930160a4 100644
--- a/tools/testing/selftests/cgroup/Makefile
+++ b/tools/testing/selftests/cgroup/Makefile
@@ -8,6 +8,7 @@ TEST_PROGS := test_stress.sh test_cpuset_prs.sh test_cpuset_v1_hp.sh
TEST_GEN_FILES := wait_inotify
# Keep the lists lexicographically sorted
TEST_GEN_PROGS = test_core
+TEST_GEN_PROGS += test_cma_memcg
TEST_GEN_PROGS += test_cpu
TEST_GEN_PROGS += test_cpuset
TEST_GEN_PROGS += test_freezer
@@ -24,6 +25,7 @@ include ../lib.mk
include lib/libcgroup.mk
$(OUTPUT)/test_core: $(LIBCGROUP_O)
+$(OUTPUT)/test_cma_memcg: $(LIBCGROUP_O)
$(OUTPUT)/test_cpu: $(LIBCGROUP_O)
$(OUTPUT)/test_cpuset: $(LIBCGROUP_O)
$(OUTPUT)/test_freezer: $(LIBCGROUP_O)
diff --git a/tools/testing/selftests/cgroup/test_cma_memcg.c b/tools/testing/selftests/cgroup/test_cma_memcg.c
new file mode 100644
index 000000000000..b5b5ea140518
--- /dev/null
+++ b/tools/testing/selftests/cgroup/test_cma_memcg.c
@@ -0,0 +1,193 @@
+// SPDX-License-Identifier: GPL-2.0
+#define _GNU_SOURCE
+
+#include <linux/limits.h>
+#include <sys/mman.h>
+#include <stdio.h>
+#include <string.h>
+#include <fcntl.h>
+#include <linux/dma-heap.h>
+#include <sys/ioctl.h>
+#include "kselftest.h"
+#include "cgroup_util.h"
+
+#define CMA_HEAP_DEFAULT_PATH "/dev/dma_heap/default_cma_region"
+#define CMA_RESERVED_CURRENT "memory.cma.reserved.current"
+#define CMA_RESERVED_MAX "memory.cma.reserved.max"
+
+static int cma_heap_alloc(int heap_fd, size_t size)
+{
+ struct dma_heap_allocation_data data = {
+ .len = size,
+ .fd_flags = O_RDWR | O_CLOEXEC,
+ };
+
+ if (ioctl(heap_fd, DMA_HEAP_IOCTL_ALLOC, &data) < 0)
+ return -1;
+
+ return data.fd;
+}
+
+static int test_cma_charge(const char *root)
+{
+ int heap_fd = -1, buf_fd = -1;
+ int rc = KSFT_FAIL, in_cg = 0;
+ size_t size = 4UL << 20;
+ long before, after;
+ char *cg;
+
+ cg = cg_name(root, "memcg_cma_charge");
+ if (!cg)
+ goto cleanup;
+
+ if (cg_create(cg))
+ goto cleanup;
+
+ if (cg_enter_current(cg))
+ goto cleanup;
+ in_cg = 1;
+
+ heap_fd = open(CMA_HEAP_DEFAULT_PATH, O_RDONLY | O_CLOEXEC);
+ if (heap_fd < 0) {
+ ksft_print_msg("cannot open CMA heap (%m)\n");
+ goto cleanup;
+ }
+
+ before = cg_read_long(cg, CMA_RESERVED_CURRENT);
+
+ buf_fd = cma_heap_alloc(heap_fd, size);
+ if (buf_fd < 0) {
+ ksft_print_msg("cannot allocate in CMA heap (%m)\n");
+ goto cleanup;
+ }
+
+ after = cg_read_long(cg, CMA_RESERVED_CURRENT);
+
+ if (after - before < (long)size) {
+ ksft_print_msg(
+ "invalid CMA counter after charge: before=%ld after=%ld\n",
+ before, after);
+ goto cleanup;
+ }
+
+ close(buf_fd);
+ buf_fd = -1;
+
+ after = cg_read_long(cg, CMA_RESERVED_CURRENT);
+ if (!values_close(before, after, 3)) {
+ ksft_print_msg(
+ "invalid CMA counter after uncharge: before=%ld after=%ld\n",
+ before, after);
+ goto cleanup;
+ }
+
+ rc = KSFT_PASS;
+
+cleanup:
+ if (buf_fd >= 0)
+ close(buf_fd);
+ if (heap_fd >= 0)
+ close(heap_fd);
+
+ if (in_cg)
+ cg_enter_current(root);
+ cg_destroy(cg);
+ free(cg);
+
+ return rc;
+}
+
+static int test_cma_limit(const char *root)
+{
+ int heap_fd = -1, buf_fd = -1;
+ int rc = KSFT_FAIL, in_cg = 0;
+ size_t size = 4UL << 20;
+ char *cg;
+
+ cg = cg_name(root, "memcg_cma_limit");
+ if (!cg)
+ goto cleanup;
+
+ if (cg_create(cg))
+ goto cleanup;
+
+ if (cg_enter_current(cg))
+ goto cleanup;
+ in_cg = 1;
+
+ if (cg_write(cg, CMA_RESERVED_MAX, "2M")) {
+ ksft_print_msg("cannot set CMA limit (%m)\n");
+ goto cleanup;
+ }
+
+ heap_fd = open(CMA_HEAP_DEFAULT_PATH, O_RDONLY | O_CLOEXEC);
+ if (heap_fd < 0) {
+ ksft_print_msg("cannot open CMA heap (%m)\n");
+ goto cleanup;
+ }
+
+ buf_fd = cma_heap_alloc(heap_fd, size);
+ if (buf_fd >= 0) {
+ ksft_print_msg("CMA limit not enforced.\n");
+ close(buf_fd);
+ } else
+ rc = KSFT_PASS;
+
+cleanup:
+ if (heap_fd >= 0)
+ close(heap_fd);
+
+ if (in_cg)
+ cg_enter_current(root);
+ cg_destroy(cg);
+ free(cg);
+
+ return rc;
+}
+
+#define T(x) { x, #x }
+struct cma_memcg_tests {
+ int (*fn)(const char *root);
+ const char *name;
+} tests[] = {
+ T(test_cma_charge),
+ T(test_cma_limit),
+};
+#undef T
+
+int main(int argc, char *argv[])
+{
+ char root[PATH_MAX];
+ int has_memory_cma_acc, i;
+
+ ksft_print_header();
+ ksft_set_plan(ARRAY_SIZE(tests));
+
+ has_memory_cma_acc = proc_mount_contains("memory_cma_accounting");
+ if (has_memory_cma_acc < 0)
+ ksft_exit_skip("cannot query cgroup mount option.\n");
+ else if (!has_memory_cma_acc)
+ ksft_exit_skip("CMA accounting is disabled.\n");
+
+ if (cg_find_unified_root(root, sizeof(root), NULL))
+ ksft_exit_skip("cgroupfs is not available.\n");
+
+ if (cg_read_strstr(root, "cgroup.controllers", "memory"))
+ ksft_exit_skip("memory controller is not available (CONFIG_CGROUP_MEMCG?)\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\n", tests[i].name);
+ break;
+ default:
+ ksft_test_result_fail("%s\n", tests[i].name);
+ break;
+ }
+ }
+
+ ksft_finished();
+}
--
2.53.0