Linux cgroups development
 help / color / mirror / Atom feed
* [PATCH] selftests: cgroup: Add basic tests for rdma controller
@ 2026-04-30  8:43 Tao Cui
  2026-04-30 17:59 ` Michal Koutný
  0 siblings, 1 reply; 5+ messages in thread
From: Tao Cui @ 2026-04-30  8:43 UTC (permalink / raw)
  To: tj, hannes, mkoutny, cgroups; +Cc: Tao Cui

This commit adds (and wires in) new test program for checking basic rdma
controller functionality -- rdma.max file interface, writing invalid
values, device specific limits, and parent-child hierarchy.

Signed-off-by: Tao Cui <cuitao@kylinos.cn>
---
 tools/testing/selftests/cgroup/Makefile    |   2 +
 tools/testing/selftests/cgroup/test_rdma.c | 371 +++++++++++++++++++++
 2 files changed, 373 insertions(+)
 create mode 100644 tools/testing/selftests/cgroup/test_rdma.c

diff --git a/tools/testing/selftests/cgroup/Makefile b/tools/testing/selftests/cgroup/Makefile
index e01584c2189a..b8ef370eaa9d 100644
--- a/tools/testing/selftests/cgroup/Makefile
+++ b/tools/testing/selftests/cgroup/Makefile
@@ -16,6 +16,7 @@ TEST_GEN_PROGS += test_kill
 TEST_GEN_PROGS += test_kmem
 TEST_GEN_PROGS += test_memcontrol
 TEST_GEN_PROGS += test_pids
+TEST_GEN_PROGS += test_rdma
 TEST_GEN_PROGS += test_zswap
 
 LOCAL_HDRS += $(selfdir)/clone3/clone3_selftests.h $(selfdir)/pidfd/pidfd.h
@@ -32,4 +33,5 @@ $(OUTPUT)/test_kill: $(LIBCGROUP_O)
 $(OUTPUT)/test_kmem: $(LIBCGROUP_O)
 $(OUTPUT)/test_memcontrol: $(LIBCGROUP_O)
 $(OUTPUT)/test_pids: $(LIBCGROUP_O)
+$(OUTPUT)/test_rdma: $(LIBCGROUP_O)
 $(OUTPUT)/test_zswap: $(LIBCGROUP_O)
diff --git a/tools/testing/selftests/cgroup/test_rdma.c b/tools/testing/selftests/cgroup/test_rdma.c
new file mode 100644
index 000000000000..e204d21bb562
--- /dev/null
+++ b/tools/testing/selftests/cgroup/test_rdma.c
@@ -0,0 +1,371 @@
+// SPDX-License-Identifier: GPL-2.0
+#define _GNU_SOURCE
+
+#include <errno.h>
+#include <linux/limits.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "kselftest.h"
+#include "cgroup_util.h"
+
+static int rdmacg_read_max(const char *cgroup, char *buf, size_t len)
+{
+	return cg_read(cgroup, "rdma.max", buf, len);
+}
+
+static int rdmacg_write_max(const char *cgroup, const char *value)
+{
+	char *dup = strdup(value);
+	int ret;
+
+	if (!dup)
+		return -1;
+	ret = cg_write(cgroup, "rdma.max", dup);
+	free(dup);
+	return ret;
+}
+
+static char *rdmacg_get_first_device(const char *cgroup)
+{
+	char buf[PAGE_SIZE];
+	char *space;
+
+	if (rdmacg_read_max(cgroup, buf, sizeof(buf)))
+		return NULL;
+
+	if (buf[0] == '\0')
+		return NULL;
+
+	space = strchr(buf, ' ');
+	if (!space)
+		return NULL;
+
+	return strndup(buf, space - buf);
+}
+
+/*
+ * Check that a specific resource value for a device in rdma.max matches
+ * the expected string. Returns 0 on match, -1 otherwise.
+ */
+static int rdmacg_check_value(const char *cgroup, const char *device,
+			      const char *resource, const char *expected)
+{
+	char buf[PAGE_SIZE];
+	char pattern[256];
+	char *p;
+	size_t elen;
+
+	if (rdmacg_read_max(cgroup, buf, sizeof(buf)))
+		return -1;
+
+	/* Find device line */
+	snprintf(pattern, sizeof(pattern), "%s ", device);
+	p = strstr(buf, pattern);
+	if (!p)
+		return -1;
+
+	/* Find resource= within this line */
+	snprintf(pattern, sizeof(pattern), "%s=", resource);
+	p = strstr(p, pattern);
+	if (!p)
+		return -1;
+	p += strlen(pattern);
+
+	elen = strlen(expected);
+	if (strncmp(p, expected, elen))
+		return -1;
+
+	if (p[elen] != ' ' && p[elen] != '\n' && p[elen] != '\0')
+		return -1;
+
+	return 0;
+}
+
+/*
+ * Test: rdma.max file exists and is readable on child cgroup.
+ */
+static int test_rdmacg_max_read(const char *root)
+{
+	int ret = KSFT_FAIL;
+	char *cg;
+	char buf[PAGE_SIZE];
+
+	cg = cg_name(root, "rdmacg_test_1");
+	if (!cg)
+		return KSFT_FAIL;
+
+	if (cg_create(cg))
+		goto cleanup;
+
+	/* rdma.max should be readable on non-root cgroup */
+	if (rdmacg_read_max(cg, buf, sizeof(buf)))
+		goto cleanup;
+
+	ret = KSFT_PASS;
+
+cleanup:
+	cg_destroy(cg);
+	free(cg);
+	return ret;
+}
+
+/*
+ * Test: Writing a non-existent device name to rdma.max should fail.
+ * The kernel returns -ENOENT when the device is not registered.
+ */
+static int test_rdmacg_max_write_nonexistent(const char *root)
+{
+	int ret = KSFT_FAIL;
+	char *cg;
+
+	cg = cg_name(root, "rdmacg_test_2");
+	if (!cg)
+		return KSFT_FAIL;
+
+	if (cg_create(cg))
+		goto cleanup;
+
+	/* Write with a device that does not exist */
+	if (rdmacg_write_max(cg, "fake_device_xyz hca_handle=10") == 0)
+		goto cleanup;
+
+	/* File should still be readable */
+	char buf[PAGE_SIZE];
+	if (rdmacg_read_max(cg, buf, sizeof(buf)))
+		goto cleanup;
+
+	ret = KSFT_PASS;
+
+cleanup:
+	cg_destroy(cg);
+	free(cg);
+	return ret;
+}
+
+/*
+ * Test: Writing invalid format to rdma.max should fail.
+ */
+static int test_rdmacg_max_write_invalid(const char *root)
+{
+	int ret = KSFT_FAIL;
+	char *cg;
+
+	cg = cg_name(root, "rdmacg_test_3");
+	if (!cg)
+		return KSFT_FAIL;
+
+	if (cg_create(cg))
+		goto cleanup;
+
+	/* Bare number -- not a valid device entry */
+	if (rdmacg_write_max(cg, "42") == 0)
+		goto cleanup;
+
+	/* Missing value after '=' */
+	if (rdmacg_write_max(cg, "fake_dev hca_handle=") == 0)
+		goto cleanup;
+
+	/* Negative value */
+	if (rdmacg_write_max(cg, "fake_dev hca_handle=-1") == 0)
+		goto cleanup;
+
+	ret = KSFT_PASS;
+
+cleanup:
+	cg_destroy(cg);
+	free(cg);
+	return ret;
+}
+
+/*
+ * Test: Set and read back limits when an RDMA device is present.
+ * Skipped if no RDMA device is registered.
+ */
+static int test_rdmacg_max_device_limits(const char *root)
+{
+	int ret = KSFT_FAIL;
+	char *cg;
+	char *device = NULL;
+	char buf[256];
+
+	cg = cg_name(root, "rdmacg_test_4");
+	if (!cg)
+		return KSFT_FAIL;
+
+	if (cg_create(cg))
+		goto cleanup;
+
+	device = rdmacg_get_first_device(cg);
+	if (!device) {
+		ret = KSFT_SKIP;
+		goto cleanup;
+	}
+
+	snprintf(buf, sizeof(buf), "%s hca_handle=5 hca_object=100", device);
+	if (rdmacg_write_max(cg, buf))
+		goto cleanup;
+
+	if (rdmacg_check_value(cg, device, "hca_handle", "5"))
+		goto cleanup;
+	if (rdmacg_check_value(cg, device, "hca_object", "100"))
+		goto cleanup;
+
+	snprintf(buf, sizeof(buf), "%s hca_handle=20", device);
+	if (rdmacg_write_max(cg, buf))
+		goto cleanup;
+
+	if (rdmacg_check_value(cg, device, "hca_handle", "20"))
+		goto cleanup;
+	if (rdmacg_check_value(cg, device, "hca_object", "100"))
+		goto cleanup;
+
+	snprintf(buf, sizeof(buf), "%s hca_handle=max hca_object=max", device);
+	if (rdmacg_write_max(cg, buf))
+		goto cleanup;
+
+	if (rdmacg_check_value(cg, device, "hca_handle", "max"))
+		goto cleanup;
+	if (rdmacg_check_value(cg, device, "hca_object", "max"))
+		goto cleanup;
+
+	ret = KSFT_PASS;
+
+cleanup:
+	free(device);
+	cg_destroy(cg);
+	free(cg);
+	return ret;
+}
+
+/*
+ * Test: Hierarchy -- parent limits and child limits are independent.
+ * The child defaults to "max"; parent limits constrain at charge time.
+ */
+static int test_rdmacg_max_hierarchy(const char *root)
+{
+	int ret = KSFT_FAIL;
+	char *cg_parent = NULL, *cg_child = NULL;
+	char *device = NULL;
+	char buf[256];
+
+	cg_parent = cg_name(root, "rdmacg_parent");
+	cg_child = cg_name(cg_parent, "rdmacg_child");
+	if (!cg_parent || !cg_child)
+		goto cleanup;
+
+	if (cg_create(cg_parent))
+		goto cleanup;
+
+	if (cg_write(cg_parent, "cgroup.subtree_control", "+rdma"))
+		goto cleanup;
+
+	if (cg_create(cg_child))
+		goto cleanup;
+
+	device = rdmacg_get_first_device(cg_child);
+	if (!device) {
+		ret = KSFT_SKIP;
+		goto cleanup;
+	}
+
+	snprintf(buf, sizeof(buf), "%s hca_handle=10 hca_object=200", device);
+	if (rdmacg_write_max(cg_parent, buf))
+		goto cleanup;
+
+	if (rdmacg_check_value(cg_parent, device, "hca_handle", "10"))
+		goto cleanup;
+
+	if (rdmacg_check_value(cg_child, device, "hca_handle", "max"))
+		goto cleanup;
+
+	snprintf(buf, sizeof(buf), "%s hca_handle=3 hca_object=50", device);
+	if (rdmacg_write_max(cg_child, buf))
+		goto cleanup;
+
+	if (rdmacg_check_value(cg_child, device, "hca_handle", "3"))
+		goto cleanup;
+	if (rdmacg_check_value(cg_child, device, "hca_object", "50"))
+		goto cleanup;
+
+	if (rdmacg_check_value(cg_parent, device, "hca_handle", "10"))
+		goto cleanup;
+	if (rdmacg_check_value(cg_parent, device, "hca_object", "200"))
+		goto cleanup;
+
+	ret = KSFT_PASS;
+
+cleanup:
+	free(device);
+	if (cg_child)
+		cg_destroy(cg_child);
+	if (cg_parent)
+		cg_destroy(cg_parent);
+	free(cg_child);
+	free(cg_parent);
+	return ret;
+}
+
+#define T(x) { x, #x }
+struct rdmacg_test {
+	int (*fn)(const char *root);
+	const char *name;
+} tests[] = {
+	T(test_rdmacg_max_read),
+	T(test_rdmacg_max_write_nonexistent),
+	T(test_rdmacg_max_write_invalid),
+	T(test_rdmacg_max_device_limits),
+	T(test_rdmacg_max_hierarchy),
+};
+#undef T
+
+int main(int argc, char **argv)
+{
+	char root[PATH_MAX];
+	char orig_subtree[PAGE_SIZE] = {0};
+	bool rdma_was_enabled = false;
+
+	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", "rdma"))
+		ksft_exit_skip("rdma controller isn't available\n");
+
+	/* Save original subtree_control so we can restore it later */
+	if (cg_read(root, "cgroup.subtree_control", orig_subtree,
+		    sizeof(orig_subtree)))
+		orig_subtree[0] = '\0';
+
+	rdma_was_enabled = (strstr(orig_subtree, "rdma") != NULL);
+
+	/* Enable rdma controller if not already enabled */
+	if (!rdma_was_enabled) {
+		if (cg_write(root, "cgroup.subtree_control", "+rdma"))
+			ksft_exit_skip("Failed to enable rdma controller\n");
+	}
+
+	for (int 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;
+		}
+	}
+
+	/* Restore original subtree_control state */
+	if (!rdma_was_enabled)
+		cg_write(root, "cgroup.subtree_control", "-rdma");
+
+	ksft_finished();
+}
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 5+ messages in thread

* Re: [PATCH] selftests: cgroup: Add basic tests for rdma controller
  2026-04-30  8:43 [PATCH] selftests: cgroup: Add basic tests for rdma controller Tao Cui
@ 2026-04-30 17:59 ` Michal Koutný
  2026-05-06  8:35   ` Tao Cui
  0 siblings, 1 reply; 5+ messages in thread
From: Michal Koutný @ 2026-04-30 17:59 UTC (permalink / raw)
  To: Tao Cui; +Cc: tj, hannes, cgroups

[-- Attachment #1: Type: text/plain, Size: 1000 bytes --]

Hello.

On Thu, Apr 30, 2026 at 04:43:10PM +0800, Tao Cui <cuitao@kylinos.cn> wrote:
> +struct rdmacg_test {
> +	int (*fn)(const char *root);
> +	const char *name;
> +} tests[] = {
> +	T(test_rdmacg_max_read),
> +	T(test_rdmacg_max_write_nonexistent),
> +	T(test_rdmacg_max_write_invalid),
> +	T(test_rdmacg_max_device_limits),
> +	T(test_rdmacg_max_hierarchy),

IIUC, these are tests for proper parsing of the limits but not so useful
wrt RDMA controller (test_rdmacg_max_read has apparently little use).

I see that you try to work with a first found device -- if that's
available, it'd be good to have a test that checks whether respective
rdma.current-s respond to object allocations.


As I am looking at test_hugetlb_memcg.c that does only simple
testing of the .current would be sufficient, not sure how difficult
would be to implement a test for actual limit enforcement (but would be
nice too).

I.e. -- these would be good test to validate basic behavior of the
controller.

Thanks,
Michal

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 265 bytes --]

^ permalink raw reply	[flat|nested] 5+ messages in thread

* Re: [PATCH] selftests: cgroup: Add basic tests for rdma controller
  2026-04-30 17:59 ` Michal Koutný
@ 2026-05-06  8:35   ` Tao Cui
  2026-05-06 12:43     ` [PATCH v2] " Tao Cui
  0 siblings, 1 reply; 5+ messages in thread
From: Tao Cui @ 2026-05-06  8:35 UTC (permalink / raw)
  To: Michal Koutný; +Cc: tj, hannes, cgroups

Hello Michal,

Thank you for the review.

On Thu, Apr 30, 2026 at 04:43:10PM +0800, Michal Koutný wrote:
> IIUC, these are tests for proper parsing of the limits but not so useful
> wrt RDMA controller (test_rdmacg_max_read has apparently little use).

You are right. The initial test cases were modeled after the pids
controller selftests -- since both pids and rdma controllers track
non-reclaimable resources, I focused on the limit interface (rdma.max)
parsing and validation, similar to how test_pids.c verifies pids.max
defaults and limit enforcement via forking.

However, as you pointed out, this approach does not cover the essential
behavior of the rdma controller itself. I agree that testing whether
rdma.current actually responds to IB resource allocations and whether
rdma.max limits are properly enforced is more valuable.

> I see that you try to work with a first found device -- if that's
> available, it'd be good to have a test that checks whether respective
> rdma.current-s respond to object allocations.

> As I am looking at test_hugetlb_memcg.c that does only simple
> testing of the .current would be sufficient, not sure how difficult
> would be to implement a test for actual limit enforcement (but would be
> nice too).

I have reworked the tests in the next version to include:

  - test_rdmacg_current_response: verifies that rdma.current correctly
    tracks hca_handle and hca_object as IB resources (ibv_open_device,
    ibv_alloc_pd, ibv_dealloc_pd, ibv_close_device) are allocated and
    freed, similar to how test_hugetlb_memcg.c validates memory.current
    against hugetlb mmap/munmap operations.

  - test_rdmacg_limit_enforcement: verifies that exceeding the
    hca_object limit causes allocation failure (EAGAIN/ENOMEM) and that
    freeing a resource allows subsequent allocations to succeed.

These tests require libibverbs and will be skipped if no RDMA device
is available or the library is not present.

I will send the updated patch soon.

Thanks,
Tao Cui

^ permalink raw reply	[flat|nested] 5+ messages in thread

* [PATCH v2] selftests: cgroup: Add basic tests for rdma controller
  2026-05-06  8:35   ` Tao Cui
@ 2026-05-06 12:43     ` Tao Cui
  2026-05-07  1:43       ` [PATCH v3] " Tao Cui
  0 siblings, 1 reply; 5+ messages in thread
From: Tao Cui @ 2026-05-06 12:43 UTC (permalink / raw)
  To: cuitao; +Cc: cgroups, hannes, mkoutny, tj

This patch adds (and wires in) new test program for verifying rdma
controller behavior -- checking that rdma.current correctly tracks
resource allocation/deallocation and that rdma.max limits are enforced.

Signed-off-by: Tao Cui <cuitao@kylinos.cn>

---
Changes in v2:
- Replace rdma.max interface-only tests with tests that verify actual
  rdma.current behavior and limit enforcement
- Use libibverbs (conditionally compiled) to allocate real IB resources
- Skip gracefully when no RDMA device is present
---
 tools/testing/selftests/cgroup/Makefile    |   9 +
 tools/testing/selftests/cgroup/test_rdma.c | 327 +++++++++++++++++++++
 2 files changed, 336 insertions(+)
 create mode 100644 tools/testing/selftests/cgroup/test_rdma.c

diff --git a/tools/testing/selftests/cgroup/Makefile b/tools/testing/selftests/cgroup/Makefile
index e01584c2189a..df6abf633bab 100644
--- a/tools/testing/selftests/cgroup/Makefile
+++ b/tools/testing/selftests/cgroup/Makefile
@@ -16,6 +16,7 @@ TEST_GEN_PROGS += test_kill
 TEST_GEN_PROGS += test_kmem
 TEST_GEN_PROGS += test_memcontrol
 TEST_GEN_PROGS += test_pids
+TEST_GEN_PROGS += test_rdma
 TEST_GEN_PROGS += test_zswap
 
 LOCAL_HDRS += $(selfdir)/clone3/clone3_selftests.h $(selfdir)/pidfd/pidfd.h
@@ -32,4 +33,12 @@ $(OUTPUT)/test_kill: $(LIBCGROUP_O)
 $(OUTPUT)/test_kmem: $(LIBCGROUP_O)
 $(OUTPUT)/test_memcontrol: $(LIBCGROUP_O)
 $(OUTPUT)/test_pids: $(LIBCGROUP_O)
+$(OUTPUT)/test_rdma: $(LIBCGROUP_O)
 $(OUTPUT)/test_zswap: $(LIBCGROUP_O)
+
+# Conditionally enable rdma.current/limit tests when libibverbs is available
+IBVERBS_AVAILABLE := $(shell pkg-config --exists libibverbs 2>/dev/null && echo y)
+ifeq ($(IBVERBS_AVAILABLE),y)
+$(OUTPUT)/test_rdma: CFLAGS += -DHAVE_LIBIBVERBS
+$(OUTPUT)/test_rdma: LDLIBS += -libverbs
+endif
diff --git a/tools/testing/selftests/cgroup/test_rdma.c b/tools/testing/selftests/cgroup/test_rdma.c
new file mode 100644
index 000000000000..5c2719ed7e62
--- /dev/null
+++ b/tools/testing/selftests/cgroup/test_rdma.c
@@ -0,0 +1,327 @@
+// SPDX-License-Identifier: GPL-2.0
+#define _GNU_SOURCE
+
+#include <errno.h>
+#include <linux/limits.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "kselftest.h"
+#include "cgroup_util.h"
+
+#ifdef HAVE_LIBIBVERBS
+#include <infiniband/verbs.h>
+
+static char *rdmacg_get_first_device(const char *cgroup)
+{
+	char buf[PAGE_SIZE];
+	char *space;
+
+	if (cg_read(cgroup, "rdma.max", buf, sizeof(buf)))
+		return NULL;
+
+	if (buf[0] == '\0')
+		return NULL;
+
+	space = strchr(buf, ' ');
+	if (!space)
+		return NULL;
+
+	return strndup(buf, space - buf);
+}
+
+static long rdmacg_get_current_value(const char *cgroup, const char *device,
+				     const char *resource)
+{
+	char buf[PAGE_SIZE];
+	char pattern[256];
+	char *p;
+
+	if (cg_read(cgroup, "rdma.current", buf, sizeof(buf)))
+		return -1;
+
+	snprintf(pattern, sizeof(pattern), "%s ", device);
+	p = strstr(buf, pattern);
+	if (!p)
+		return -1;
+
+	snprintf(pattern, sizeof(pattern), "%s=", resource);
+	p = strstr(p, pattern);
+	if (!p)
+		return -1;
+	p += strlen(pattern);
+
+	return strtol(p, NULL, 10);
+}
+
+static struct ibv_context *rdmacg_open_device(const char *device_name)
+{
+	struct ibv_device **dev_list;
+	struct ibv_context *ctx = NULL;
+	int i;
+
+	dev_list = ibv_get_device_list(NULL);
+	if (!dev_list)
+		return NULL;
+
+	for (i = 0; dev_list[i]; i++) {
+		if (!strcmp(ibv_get_device_name(dev_list[i]), device_name)) {
+			ctx = ibv_open_device(dev_list[i]);
+			break;
+		}
+	}
+	ibv_free_device_list(dev_list);
+	return ctx;
+}
+
+static int rdmacg_current_fn(const char *cgroup, void *arg)
+{
+	const char *device_name = (const char *)arg;
+	struct ibv_context *ctx = NULL;
+	struct ibv_pd *pd = NULL;
+	long val;
+	int ret = EXIT_FAILURE;
+
+	ctx = rdmacg_open_device(device_name);
+	if (!ctx)
+		return EXIT_FAILURE;
+
+	val = rdmacg_get_current_value(cgroup, device_name, "hca_handle");
+	if (val != 1) {
+		ksft_print_msg("hca_handle should be 1 after open, got %ld\n", val);
+		goto cleanup;
+	}
+	val = rdmacg_get_current_value(cgroup, device_name, "hca_object");
+	if (val != 0) {
+		ksft_print_msg("hca_object should be 0 before alloc, got %ld\n", val);
+		goto cleanup;
+	}
+
+	pd = ibv_alloc_pd(ctx);
+	if (!pd) {
+		ksft_print_msg("ibv_alloc_pd failed: %s\n", strerror(errno));
+		goto cleanup;
+	}
+	val = rdmacg_get_current_value(cgroup, device_name, "hca_object");
+	if (val != 1) {
+		ksft_print_msg("hca_object should be 1 after alloc_pd, got %ld\n", val);
+		goto cleanup;
+	}
+
+	/* After ibv_dealloc_pd: hca_object should be 0 */
+	ibv_dealloc_pd(pd);
+	pd = NULL;
+	val = rdmacg_get_current_value(cgroup, device_name, "hca_object");
+	if (val != 0) {
+		ksft_print_msg("hca_object should be 0 after dealloc_pd, got %ld\n", val);
+		goto cleanup;
+	}
+
+	/* After ibv_close_device: hca_handle should be 0 */
+	ibv_close_device(ctx);
+	ctx = NULL;
+	val = rdmacg_get_current_value(cgroup, device_name, "hca_handle");
+	if (val != 0) {
+		ksft_print_msg("hca_handle should be 0 after close, got %ld\n", val);
+		goto cleanup;
+	}
+
+	ret = EXIT_SUCCESS;
+
+cleanup:
+	if (pd)
+		ibv_dealloc_pd(pd);
+	if (ctx)
+		ibv_close_device(ctx);
+	return ret;
+}
+
+/*
+ * Test: rdma.current responds to actual IB resource allocation and deallocation.
+ */
+static int test_rdmacg_current_response(const char *root)
+{
+	int ret = KSFT_FAIL;
+	char *cg;
+	char *device = NULL;
+
+	cg = cg_name(root, "rdmacg_test_1");
+	if (!cg)
+		return KSFT_FAIL;
+
+	if (cg_create(cg))
+		goto cleanup;
+
+	device = rdmacg_get_first_device(cg);
+	if (!device) {
+		ret = KSFT_SKIP;
+		goto cleanup;
+	}
+
+	if (!cg_run(cg, rdmacg_current_fn, device))
+		ret = KSFT_PASS;
+
+cleanup:
+	free(device);
+	cg_destroy(cg);
+	free(cg);
+	return ret;
+}
+
+static int rdmacg_limit_fn(const char *cgroup, void *arg)
+{
+	const char *device_name = (const char *)arg;
+	struct ibv_context *ctx = NULL;
+	struct ibv_pd *pd1 = NULL, *pd2 = NULL;
+	int ret = EXIT_FAILURE;
+
+	ctx = rdmacg_open_device(device_name);
+	if (!ctx)
+		return EXIT_FAILURE;
+
+	/* First PD allocation should succeed (within hca_object=1 limit) */
+	pd1 = ibv_alloc_pd(ctx);
+	if (!pd1) {
+		ksft_print_msg("first ibv_alloc_pd failed: %s\n", strerror(errno));
+		goto cleanup;
+	}
+
+	/* Second PD allocation should fail (exceeds hca_object=1 limit) */
+	pd2 = ibv_alloc_pd(ctx);
+	if (pd2) {
+		ksft_print_msg("second ibv_alloc_pd should have failed\n");
+		goto cleanup;
+	}
+
+	/* Free first PD, then try again -- should succeed */
+	ibv_dealloc_pd(pd1);
+	pd1 = NULL;
+
+	pd1 = ibv_alloc_pd(ctx);
+	if (!pd1) {
+		ksft_print_msg("ibv_alloc_pd after free failed: %s\n", strerror(errno));
+		goto cleanup;
+	}
+
+	ret = EXIT_SUCCESS;
+
+cleanup:
+	if (pd1)
+		ibv_dealloc_pd(pd1);
+	if (pd2)
+		ibv_dealloc_pd(pd2);
+	if (ctx)
+		ibv_close_device(ctx);
+	return ret;
+}
+
+/*
+ * Test: rdma.max limits are enforced -- exceeding hca_object limit causes
+ * allocation failure.
+ */
+static int test_rdmacg_limit_enforcement(const char *root)
+{
+	int ret = KSFT_FAIL;
+	char *cg;
+	char *device = NULL;
+	char buf[256];
+
+	cg = cg_name(root, "rdmacg_test_2");
+	if (!cg)
+		return KSFT_FAIL;
+
+	if (cg_create(cg))
+		goto cleanup;
+
+	device = rdmacg_get_first_device(cg);
+	if (!device) {
+		ret = KSFT_SKIP;
+		goto cleanup;
+	}
+
+	snprintf(buf, sizeof(buf), "%s hca_handle=max hca_object=1", device);
+	if (cg_write(cg, "rdma.max", buf)) {
+		ksft_print_msg("failed to set hca_object=1 limit\n");
+		goto cleanup;
+	}
+
+	if (!cg_run(cg, rdmacg_limit_fn, device))
+		ret = KSFT_PASS;
+
+cleanup:
+	free(device);
+	cg_destroy(cg);
+	free(cg);
+	return ret;
+}
+
+#define T(x) { x, #x }
+struct rdmacg_test {
+	int (*fn)(const char *root);
+	const char *name;
+} tests[] = {
+	T(test_rdmacg_current_response),
+	T(test_rdmacg_limit_enforcement),
+};
+#undef T
+
+int main(int argc, char **argv)
+{
+	char root[PATH_MAX];
+	char orig_subtree[PAGE_SIZE] = {0};
+	bool rdma_was_enabled = false;
+
+	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", "rdma"))
+		ksft_exit_skip("rdma controller isn't available\n");
+
+	/* Save original subtree_control so we can restore it later */
+	if (cg_read(root, "cgroup.subtree_control", orig_subtree,
+		    sizeof(orig_subtree)))
+		orig_subtree[0] = '\0';
+
+	rdma_was_enabled = (strstr(orig_subtree, "rdma") != NULL);
+
+	/* Enable rdma controller if not already enabled */
+	if (!rdma_was_enabled) {
+		if (cg_write(root, "cgroup.subtree_control", "+rdma"))
+			ksft_exit_skip("Failed to enable rdma controller\n");
+	}
+
+	for (int 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;
+		}
+	}
+
+	/* Restore original subtree_control state */
+	if (!rdma_was_enabled)
+		cg_write(root, "cgroup.subtree_control", "-rdma");
+
+	ksft_finished();
+}
+
+#else /* !HAVE_LIBIBVERBS */
+
+int main(int argc, char **argv)
+{
+	ksft_print_header();
+	ksft_exit_skip("test requires libibverbs\n");
+}
+
+#endif /* HAVE_LIBIBVERBS */
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 5+ messages in thread

* [PATCH v3] selftests: cgroup: Add basic tests for rdma controller
  2026-05-06 12:43     ` [PATCH v2] " Tao Cui
@ 2026-05-07  1:43       ` Tao Cui
  0 siblings, 0 replies; 5+ messages in thread
From: Tao Cui @ 2026-05-07  1:43 UTC (permalink / raw)
  To: cuitao; +Cc: cgroups, hannes, mkoutny, tj

This patch adds (and wires in) new test program for verifying rdma
controller behavior -- checking that rdma.current correctly tracks
resource allocation/deallocation and that rdma.max limits are enforced.

Signed-off-by: Tao Cui <cuitao@kylinos.cn>

---
Changes in v3:
- Add test_rdma to .gitignore to avoid untracked file after compilation.

Changes in v2:
- Replace rdma.max interface-only tests with tests that verify actual
  rdma.current behavior and limit enforcement
- Use libibverbs (conditionally compiled) to allocate real IB resources
- Skip gracefully when no RDMA device is present
---
 tools/testing/selftests/cgroup/.gitignore  |   1 +
 tools/testing/selftests/cgroup/Makefile    |   9 +
 tools/testing/selftests/cgroup/test_rdma.c | 327 +++++++++++++++++++++
 3 files changed, 337 insertions(+)
 create mode 100644 tools/testing/selftests/cgroup/test_rdma.c

diff --git a/tools/testing/selftests/cgroup/.gitignore b/tools/testing/selftests/cgroup/.gitignore
index 952e4448bf07..70e4ef310ebd 100644
--- a/tools/testing/selftests/cgroup/.gitignore
+++ b/tools/testing/selftests/cgroup/.gitignore
@@ -8,5 +8,6 @@ test_kill
 test_kmem
 test_memcontrol
 test_pids
+test_rdma
 test_zswap
 wait_inotify
diff --git a/tools/testing/selftests/cgroup/Makefile b/tools/testing/selftests/cgroup/Makefile
index e01584c2189a..df6abf633bab 100644
--- a/tools/testing/selftests/cgroup/Makefile
+++ b/tools/testing/selftests/cgroup/Makefile
@@ -16,6 +16,7 @@ TEST_GEN_PROGS += test_kill
 TEST_GEN_PROGS += test_kmem
 TEST_GEN_PROGS += test_memcontrol
 TEST_GEN_PROGS += test_pids
+TEST_GEN_PROGS += test_rdma
 TEST_GEN_PROGS += test_zswap
 
 LOCAL_HDRS += $(selfdir)/clone3/clone3_selftests.h $(selfdir)/pidfd/pidfd.h
@@ -32,4 +33,12 @@ $(OUTPUT)/test_kill: $(LIBCGROUP_O)
 $(OUTPUT)/test_kmem: $(LIBCGROUP_O)
 $(OUTPUT)/test_memcontrol: $(LIBCGROUP_O)
 $(OUTPUT)/test_pids: $(LIBCGROUP_O)
+$(OUTPUT)/test_rdma: $(LIBCGROUP_O)
 $(OUTPUT)/test_zswap: $(LIBCGROUP_O)
+
+# Conditionally enable rdma.current/limit tests when libibverbs is available
+IBVERBS_AVAILABLE := $(shell pkg-config --exists libibverbs 2>/dev/null && echo y)
+ifeq ($(IBVERBS_AVAILABLE),y)
+$(OUTPUT)/test_rdma: CFLAGS += -DHAVE_LIBIBVERBS
+$(OUTPUT)/test_rdma: LDLIBS += -libverbs
+endif
diff --git a/tools/testing/selftests/cgroup/test_rdma.c b/tools/testing/selftests/cgroup/test_rdma.c
new file mode 100644
index 000000000000..5c2719ed7e62
--- /dev/null
+++ b/tools/testing/selftests/cgroup/test_rdma.c
@@ -0,0 +1,327 @@
+// SPDX-License-Identifier: GPL-2.0
+#define _GNU_SOURCE
+
+#include <errno.h>
+#include <linux/limits.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "kselftest.h"
+#include "cgroup_util.h"
+
+#ifdef HAVE_LIBIBVERBS
+#include <infiniband/verbs.h>
+
+static char *rdmacg_get_first_device(const char *cgroup)
+{
+	char buf[PAGE_SIZE];
+	char *space;
+
+	if (cg_read(cgroup, "rdma.max", buf, sizeof(buf)))
+		return NULL;
+
+	if (buf[0] == '\0')
+		return NULL;
+
+	space = strchr(buf, ' ');
+	if (!space)
+		return NULL;
+
+	return strndup(buf, space - buf);
+}
+
+static long rdmacg_get_current_value(const char *cgroup, const char *device,
+				     const char *resource)
+{
+	char buf[PAGE_SIZE];
+	char pattern[256];
+	char *p;
+
+	if (cg_read(cgroup, "rdma.current", buf, sizeof(buf)))
+		return -1;
+
+	snprintf(pattern, sizeof(pattern), "%s ", device);
+	p = strstr(buf, pattern);
+	if (!p)
+		return -1;
+
+	snprintf(pattern, sizeof(pattern), "%s=", resource);
+	p = strstr(p, pattern);
+	if (!p)
+		return -1;
+	p += strlen(pattern);
+
+	return strtol(p, NULL, 10);
+}
+
+static struct ibv_context *rdmacg_open_device(const char *device_name)
+{
+	struct ibv_device **dev_list;
+	struct ibv_context *ctx = NULL;
+	int i;
+
+	dev_list = ibv_get_device_list(NULL);
+	if (!dev_list)
+		return NULL;
+
+	for (i = 0; dev_list[i]; i++) {
+		if (!strcmp(ibv_get_device_name(dev_list[i]), device_name)) {
+			ctx = ibv_open_device(dev_list[i]);
+			break;
+		}
+	}
+	ibv_free_device_list(dev_list);
+	return ctx;
+}
+
+static int rdmacg_current_fn(const char *cgroup, void *arg)
+{
+	const char *device_name = (const char *)arg;
+	struct ibv_context *ctx = NULL;
+	struct ibv_pd *pd = NULL;
+	long val;
+	int ret = EXIT_FAILURE;
+
+	ctx = rdmacg_open_device(device_name);
+	if (!ctx)
+		return EXIT_FAILURE;
+
+	val = rdmacg_get_current_value(cgroup, device_name, "hca_handle");
+	if (val != 1) {
+		ksft_print_msg("hca_handle should be 1 after open, got %ld\n", val);
+		goto cleanup;
+	}
+	val = rdmacg_get_current_value(cgroup, device_name, "hca_object");
+	if (val != 0) {
+		ksft_print_msg("hca_object should be 0 before alloc, got %ld\n", val);
+		goto cleanup;
+	}
+
+	pd = ibv_alloc_pd(ctx);
+	if (!pd) {
+		ksft_print_msg("ibv_alloc_pd failed: %s\n", strerror(errno));
+		goto cleanup;
+	}
+	val = rdmacg_get_current_value(cgroup, device_name, "hca_object");
+	if (val != 1) {
+		ksft_print_msg("hca_object should be 1 after alloc_pd, got %ld\n", val);
+		goto cleanup;
+	}
+
+	/* After ibv_dealloc_pd: hca_object should be 0 */
+	ibv_dealloc_pd(pd);
+	pd = NULL;
+	val = rdmacg_get_current_value(cgroup, device_name, "hca_object");
+	if (val != 0) {
+		ksft_print_msg("hca_object should be 0 after dealloc_pd, got %ld\n", val);
+		goto cleanup;
+	}
+
+	/* After ibv_close_device: hca_handle should be 0 */
+	ibv_close_device(ctx);
+	ctx = NULL;
+	val = rdmacg_get_current_value(cgroup, device_name, "hca_handle");
+	if (val != 0) {
+		ksft_print_msg("hca_handle should be 0 after close, got %ld\n", val);
+		goto cleanup;
+	}
+
+	ret = EXIT_SUCCESS;
+
+cleanup:
+	if (pd)
+		ibv_dealloc_pd(pd);
+	if (ctx)
+		ibv_close_device(ctx);
+	return ret;
+}
+
+/*
+ * Test: rdma.current responds to actual IB resource allocation and deallocation.
+ */
+static int test_rdmacg_current_response(const char *root)
+{
+	int ret = KSFT_FAIL;
+	char *cg;
+	char *device = NULL;
+
+	cg = cg_name(root, "rdmacg_test_1");
+	if (!cg)
+		return KSFT_FAIL;
+
+	if (cg_create(cg))
+		goto cleanup;
+
+	device = rdmacg_get_first_device(cg);
+	if (!device) {
+		ret = KSFT_SKIP;
+		goto cleanup;
+	}
+
+	if (!cg_run(cg, rdmacg_current_fn, device))
+		ret = KSFT_PASS;
+
+cleanup:
+	free(device);
+	cg_destroy(cg);
+	free(cg);
+	return ret;
+}
+
+static int rdmacg_limit_fn(const char *cgroup, void *arg)
+{
+	const char *device_name = (const char *)arg;
+	struct ibv_context *ctx = NULL;
+	struct ibv_pd *pd1 = NULL, *pd2 = NULL;
+	int ret = EXIT_FAILURE;
+
+	ctx = rdmacg_open_device(device_name);
+	if (!ctx)
+		return EXIT_FAILURE;
+
+	/* First PD allocation should succeed (within hca_object=1 limit) */
+	pd1 = ibv_alloc_pd(ctx);
+	if (!pd1) {
+		ksft_print_msg("first ibv_alloc_pd failed: %s\n", strerror(errno));
+		goto cleanup;
+	}
+
+	/* Second PD allocation should fail (exceeds hca_object=1 limit) */
+	pd2 = ibv_alloc_pd(ctx);
+	if (pd2) {
+		ksft_print_msg("second ibv_alloc_pd should have failed\n");
+		goto cleanup;
+	}
+
+	/* Free first PD, then try again -- should succeed */
+	ibv_dealloc_pd(pd1);
+	pd1 = NULL;
+
+	pd1 = ibv_alloc_pd(ctx);
+	if (!pd1) {
+		ksft_print_msg("ibv_alloc_pd after free failed: %s\n", strerror(errno));
+		goto cleanup;
+	}
+
+	ret = EXIT_SUCCESS;
+
+cleanup:
+	if (pd1)
+		ibv_dealloc_pd(pd1);
+	if (pd2)
+		ibv_dealloc_pd(pd2);
+	if (ctx)
+		ibv_close_device(ctx);
+	return ret;
+}
+
+/*
+ * Test: rdma.max limits are enforced -- exceeding hca_object limit causes
+ * allocation failure.
+ */
+static int test_rdmacg_limit_enforcement(const char *root)
+{
+	int ret = KSFT_FAIL;
+	char *cg;
+	char *device = NULL;
+	char buf[256];
+
+	cg = cg_name(root, "rdmacg_test_2");
+	if (!cg)
+		return KSFT_FAIL;
+
+	if (cg_create(cg))
+		goto cleanup;
+
+	device = rdmacg_get_first_device(cg);
+	if (!device) {
+		ret = KSFT_SKIP;
+		goto cleanup;
+	}
+
+	snprintf(buf, sizeof(buf), "%s hca_handle=max hca_object=1", device);
+	if (cg_write(cg, "rdma.max", buf)) {
+		ksft_print_msg("failed to set hca_object=1 limit\n");
+		goto cleanup;
+	}
+
+	if (!cg_run(cg, rdmacg_limit_fn, device))
+		ret = KSFT_PASS;
+
+cleanup:
+	free(device);
+	cg_destroy(cg);
+	free(cg);
+	return ret;
+}
+
+#define T(x) { x, #x }
+struct rdmacg_test {
+	int (*fn)(const char *root);
+	const char *name;
+} tests[] = {
+	T(test_rdmacg_current_response),
+	T(test_rdmacg_limit_enforcement),
+};
+#undef T
+
+int main(int argc, char **argv)
+{
+	char root[PATH_MAX];
+	char orig_subtree[PAGE_SIZE] = {0};
+	bool rdma_was_enabled = false;
+
+	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", "rdma"))
+		ksft_exit_skip("rdma controller isn't available\n");
+
+	/* Save original subtree_control so we can restore it later */
+	if (cg_read(root, "cgroup.subtree_control", orig_subtree,
+		    sizeof(orig_subtree)))
+		orig_subtree[0] = '\0';
+
+	rdma_was_enabled = (strstr(orig_subtree, "rdma") != NULL);
+
+	/* Enable rdma controller if not already enabled */
+	if (!rdma_was_enabled) {
+		if (cg_write(root, "cgroup.subtree_control", "+rdma"))
+			ksft_exit_skip("Failed to enable rdma controller\n");
+	}
+
+	for (int 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;
+		}
+	}
+
+	/* Restore original subtree_control state */
+	if (!rdma_was_enabled)
+		cg_write(root, "cgroup.subtree_control", "-rdma");
+
+	ksft_finished();
+}
+
+#else /* !HAVE_LIBIBVERBS */
+
+int main(int argc, char **argv)
+{
+	ksft_print_header();
+	ksft_exit_skip("test requires libibverbs\n");
+}
+
+#endif /* HAVE_LIBIBVERBS */
-- 
2.43.0


^ permalink raw reply related	[flat|nested] 5+ messages in thread

end of thread, other threads:[~2026-05-07  1:44 UTC | newest]

Thread overview: 5+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-04-30  8:43 [PATCH] selftests: cgroup: Add basic tests for rdma controller Tao Cui
2026-04-30 17:59 ` Michal Koutný
2026-05-06  8:35   ` Tao Cui
2026-05-06 12:43     ` [PATCH v2] " Tao Cui
2026-05-07  1:43       ` [PATCH v3] " Tao Cui

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox