[PATCH RFC 5/5] selftests/dmabuf-heaps: Add dma-buf memcg accounting tests
Albert Esteve
aesteve at redhat.com
Tue May 12 09:10:47 UTC 2026
Add tests for the new charge_pid_fd field in struct
dma_heap_allocation_data.
When the charge_pid_fd feature is absent (unpatched kernel),
the probe in pidfd_alloc_supported() detects this and the
tests are skipped gracefully.
Add vmtest.sh similar to other subsystem suites, to orchestrate
building the selftests (optionally with a freshly compiled kernel)
inside a virtme-ng VM, so the tests can be run without modifying
the host system. Add a config fragment with required Kconfig symbols.
Also add test_memcg_dmabuf() to the existing test_memcontrol suite
to verify end-to-end cross-cgroup accounting: a parent process opens
a pidfd for a child in a separate cgroup, allocates a dma-buf via
DMA_HEAP_IOCTL_ALLOC with that pidfd, and asserts that memory.stat
dmabuf in the child's cgroup reflects the allocation. If the dmabuf
key is missing (unpatched kernel) or /dev/dma_heap/system is absent,
the test is skipped.
Assisted-by: Claude:claude-sonnet-4-6 Cursor
Signed-off-by: Albert Esteve <aesteve at redhat.com>
---
tools/testing/selftests/cgroup/Makefile | 2 +-
tools/testing/selftests/cgroup/test_memcontrol.c | 143 +++++++++++++-
tools/testing/selftests/dmabuf-heaps/config | 1 +
tools/testing/selftests/dmabuf-heaps/dmabuf-heap.c | 126 ++++++++++++-
tools/testing/selftests/dmabuf-heaps/vmtest.sh | 205 +++++++++++++++++++++
5 files changed, 473 insertions(+), 4 deletions(-)
diff --git a/tools/testing/selftests/cgroup/Makefile b/tools/testing/selftests/cgroup/Makefile
index e01584c2189ac..9edfc9f1de5c4 100644
--- a/tools/testing/selftests/cgroup/Makefile
+++ b/tools/testing/selftests/cgroup/Makefile
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: GPL-2.0
-CFLAGS += -Wall -pthread
+CFLAGS += -Wall -pthread $(KHDR_INCLUDES)
all: ${HELPER_PROGS}
diff --git a/tools/testing/selftests/cgroup/test_memcontrol.c b/tools/testing/selftests/cgroup/test_memcontrol.c
index b43da9bc20c49..b6a228407530f 100644
--- a/tools/testing/selftests/cgroup/test_memcontrol.c
+++ b/tools/testing/selftests/cgroup/test_memcontrol.c
@@ -19,9 +19,17 @@
#include <errno.h>
#include <sys/mman.h>
+#include <linux/dma-heap.h>
+#include <signal.h>
+#include <sys/ioctl.h>
+
+#include "../pidfd/pidfd.h"
#include "kselftest.h"
#include "cgroup_util.h"
+#define DMA_HEAP_SYSTEM "/dev/dma_heap/system"
+#define ONE_MEG (1024 * 1024)
+
#define MEMCG_SOCKSTAT_WAIT_RETRIES 30
static bool has_localevents;
@@ -1762,6 +1770,125 @@ static int test_memcg_inotify_delete_dir(const char *root)
return ret;
}
+static int memcg_dmabuf_child(const char *cgroup, void *arg)
+{
+ pause();
+ return 0;
+}
+
+/*
+ * This test allocates a dma-buf via DMA_HEAP_IOCTL_ALLOC with a pidfd
+ * pointing to a child process in a separate cgroup, then checks that
+ * memory.stat[dmabuf] in the child's cgroup rises by the allocation size
+ * and returns to zero after the buffer fd is closed.
+ */
+static int test_memcg_dmabuf(const char *root)
+{
+ char *parent = NULL, *child_cg = NULL;
+ int ret = KSFT_FAIL;
+ int heap_fd = -1, dmabuf_fd = -1, pidfd = -1;
+ pid_t child_pid;
+ int child_status;
+ long dmabuf_stat;
+ struct dma_heap_allocation_data alloc = {
+ .len = ONE_MEG,
+ .fd_flags = O_RDWR | O_CLOEXEC,
+ };
+
+ if (access(DMA_HEAP_SYSTEM, R_OK | W_OK)) {
+ ret = KSFT_SKIP;
+ goto cleanup;
+ }
+
+ parent = cg_name(root, "dmabuf_memcg_test");
+ if (!parent)
+ goto cleanup;
+
+ if (cg_create(parent))
+ goto cleanup_parent;
+
+ if (cg_write(parent, "cgroup.subtree_control", "+memory"))
+ goto cleanup_parent;
+
+ child_cg = cg_name(parent, "child");
+ if (!child_cg)
+ goto cleanup_parent;
+
+ if (cg_create(child_cg))
+ goto cleanup_parent;
+
+ child_pid = cg_run_nowait(child_cg, memcg_dmabuf_child, NULL);
+ if (child_pid < 0)
+ goto cleanup_child;
+
+ if (cg_wait_for_proc_count(child_cg, 1))
+ goto cleanup_kill;
+
+ pidfd = sys_pidfd_open(child_pid, 0);
+ if (pidfd < 0) {
+ ret = KSFT_SKIP;
+ goto cleanup_kill;
+ }
+
+ heap_fd = open(DMA_HEAP_SYSTEM, O_RDWR);
+ if (heap_fd < 0) {
+ ret = KSFT_SKIP;
+ goto cleanup_pidfd;
+ }
+
+ alloc.charge_pid_fd = (__u32)pidfd;
+ if (ioctl(heap_fd, DMA_HEAP_IOCTL_ALLOC, &alloc) < 0)
+ goto cleanup_heap;
+ dmabuf_fd = (int)alloc.fd;
+
+ dmabuf_stat = cg_read_key_long(child_cg, "memory.stat", "dmabuf ");
+ if (dmabuf_stat == -1) {
+ ret = KSFT_SKIP;
+ goto cleanup_dmabuf;
+ }
+ if (dmabuf_stat != ONE_MEG)
+ dmabuf_stat = cg_read_key_long_poll(child_cg, "memory.stat",
+ "dmabuf ", ONE_MEG,
+ 15, 200000);
+ if (dmabuf_stat != ONE_MEG) {
+ fprintf(stderr, "Expected dmabuf stat %d, got %ld\n",
+ ONE_MEG, dmabuf_stat);
+ goto cleanup_dmabuf;
+ }
+
+ close(dmabuf_fd);
+ dmabuf_fd = -1;
+
+ dmabuf_stat = cg_read_key_long_poll(child_cg, "memory.stat",
+ "dmabuf ", 0, 15, 200000);
+ if (dmabuf_stat != 0) {
+ fprintf(stderr, "Expected dmabuf stat 0 after close, got %ld\n",
+ dmabuf_stat);
+ goto cleanup_heap;
+ }
+
+ ret = KSFT_PASS;
+
+cleanup_dmabuf:
+ if (dmabuf_fd >= 0)
+ close(dmabuf_fd);
+cleanup_heap:
+ close(heap_fd);
+cleanup_pidfd:
+ close(pidfd);
+cleanup_kill:
+ kill(child_pid, SIGTERM);
+ waitpid(child_pid, &child_status, 0);
+cleanup_child:
+ cg_destroy(child_cg);
+ free(child_cg);
+cleanup_parent:
+ cg_destroy(parent);
+ free(parent);
+cleanup:
+ return ret;
+}
+
#define T(x) { x, #x }
struct memcg_test {
int (*fn)(const char *root);
@@ -1783,16 +1910,26 @@ struct memcg_test {
T(test_memcg_oom_group_score_events),
T(test_memcg_inotify_delete_file),
T(test_memcg_inotify_delete_dir),
+ T(test_memcg_dmabuf),
};
#undef T
int main(int argc, char **argv)
{
char root[PATH_MAX];
- int i, proc_status;
+ int i, proc_status, plan;
+ const char *filter = NULL;
+
+ if (argc > 1)
+ filter = argv[1];
+
+ plan = 0;
+ for (i = 0; i < ARRAY_SIZE(tests); i++)
+ if (!filter || !strcmp(tests[i].name, filter))
+ plan++;
ksft_print_header();
- ksft_set_plan(ARRAY_SIZE(tests));
+ ksft_set_plan(plan);
if (cg_find_unified_root(root, sizeof(root), NULL))
ksft_exit_skip("cgroup v2 isn't mounted\n");
@@ -1818,6 +1955,8 @@ int main(int argc, char **argv)
has_localevents = proc_status;
for (i = 0; i < ARRAY_SIZE(tests); i++) {
+ if (filter && strcmp(tests[i].name, filter))
+ continue;
switch (tests[i].fn(root)) {
case KSFT_PASS:
ksft_test_result_pass("%s\n", tests[i].name);
diff --git a/tools/testing/selftests/dmabuf-heaps/config b/tools/testing/selftests/dmabuf-heaps/config
index be091f1cdfa04..94c8f33b71a28 100644
--- a/tools/testing/selftests/dmabuf-heaps/config
+++ b/tools/testing/selftests/dmabuf-heaps/config
@@ -1,3 +1,4 @@
+CONFIG_MEMCG=y
CONFIG_DMABUF_HEAPS=y
CONFIG_DMABUF_HEAPS_SYSTEM=y
CONFIG_DRM_VGEM=y
diff --git a/tools/testing/selftests/dmabuf-heaps/dmabuf-heap.c b/tools/testing/selftests/dmabuf-heaps/dmabuf-heap.c
index fc9694fc4e89e..904332b17698a 100644
--- a/tools/testing/selftests/dmabuf-heaps/dmabuf-heap.c
+++ b/tools/testing/selftests/dmabuf-heaps/dmabuf-heap.c
@@ -3,6 +3,7 @@
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
+#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
@@ -10,11 +11,14 @@
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
+#include <sys/syscall.h>
#include <sys/types.h>
+#include <sys/wait.h>
#include <linux/dma-buf.h>
#include <linux/dma-heap.h>
#include <drm/drm.h>
+#include "../pidfd/pidfd.h"
#include "kselftest.h"
#define DEVPATH "/dev/dma_heap"
@@ -320,6 +324,8 @@ static int dmabuf_heap_alloc_newer(int fd, size_t len, unsigned int flags,
__u32 fd;
__u32 fd_flags;
__u64 heap_flags;
+ __u32 charge_pid_fd;
+ __u32 __padding;
__u64 garbage1;
__u64 garbage2;
__u64 garbage3;
@@ -328,6 +334,8 @@ static int dmabuf_heap_alloc_newer(int fd, size_t len, unsigned int flags,
.fd = 0,
.fd_flags = O_RDWR | O_CLOEXEC,
.heap_flags = flags,
+ .charge_pid_fd = 0,
+ .__padding = 0,
.garbage1 = 0xffffffff,
.garbage2 = 0x88888888,
.garbage3 = 0x11111111,
@@ -390,6 +398,120 @@ static void test_alloc_errors(char *heap_name)
close(heap_fd);
}
+static int dmabuf_heap_alloc_pidfd(int fd, size_t len, unsigned int heap_flags,
+ unsigned int charge_pid_fd, int *dmabuf_fd)
+{
+ struct dma_heap_allocation_data data = {
+ .len = len,
+ .fd = 0,
+ .fd_flags = O_RDWR | O_CLOEXEC,
+ .heap_flags = heap_flags,
+ .charge_pid_fd = charge_pid_fd,
+ };
+ int ret;
+
+ if (!dmabuf_fd)
+ return -EINVAL;
+
+ ret = ioctl(fd, DMA_HEAP_IOCTL_ALLOC, &data);
+ if (ret < 0)
+ return ret;
+ *dmabuf_fd = (int)data.fd;
+ return ret;
+}
+
+/*
+ * Probe whether the kernel honours charge_pid_fd in DMA_HEAP_IOCTL_ALLOC.
+ */
+static bool pidfd_alloc_supported(int heap_fd)
+{
+ int devnull_fd, dmabuf_fd = -1, ret;
+
+ devnull_fd = open("/dev/null", O_RDONLY);
+ if (devnull_fd < 0)
+ return false;
+
+ ret = dmabuf_heap_alloc_pidfd(heap_fd, ONE_MEG, 0, devnull_fd, &dmabuf_fd);
+ if (dmabuf_fd >= 0) {
+ close(dmabuf_fd);
+ dmabuf_fd = -1;
+ }
+ close(devnull_fd);
+ return ret < 0;
+}
+
+/*
+ * Test: allocate charging the calling process's own cgroup via a self pidfd.
+ */
+static void test_alloc_pidfd_self(char *heap_name)
+{
+ int heap_fd = -1, pidfd = -1, dmabuf_fd = -1, ret;
+
+ heap_fd = dmabuf_heap_open(heap_name);
+
+ if (!pidfd_alloc_supported(heap_fd)) {
+ ksft_test_result_skip("charge_pid_fd not supported by this kernel\n");
+ goto out;
+ }
+
+ pidfd = sys_pidfd_open(getpid(), 0);
+ if (pidfd < 0) {
+ ksft_test_result_skip("pidfd_open not available\n");
+ goto out;
+ }
+
+ ret = dmabuf_heap_alloc_pidfd(heap_fd, ONE_MEG, 0, pidfd, &dmabuf_fd);
+ ksft_test_result(!ret, "Allocation with self pidfd %d\n", ret);
+ if (dmabuf_fd >= 0)
+ close(dmabuf_fd);
+ close(pidfd);
+out:
+ close(heap_fd);
+}
+
+/*
+ * Test: allocate charging a child process's cgroup via a child pidfd.
+ */
+static void test_alloc_pidfd_child(char *heap_name)
+{
+ int heap_fd = -1, pidfd = -1, dmabuf_fd = -1;
+ pid_t child_pid;
+ int status, ret;
+
+ heap_fd = dmabuf_heap_open(heap_name);
+
+ if (!pidfd_alloc_supported(heap_fd)) {
+ ksft_test_result_skip("charge_pid_fd not supported by this kernel\n");
+ goto out;
+ }
+
+ child_pid = fork();
+ if (child_pid == 0) {
+ pause();
+ _exit(0);
+ }
+ if (child_pid < 0)
+ ksft_exit_fail_msg("fork failed: %s\n", strerror(errno));
+
+ pidfd = sys_pidfd_open(child_pid, 0);
+ if (pidfd < 0) {
+ kill(child_pid, SIGTERM);
+ waitpid(child_pid, &status, 0);
+ ksft_test_result_skip("pidfd_open for child failed\n");
+ goto out;
+ }
+
+ ret = dmabuf_heap_alloc_pidfd(heap_fd, ONE_MEG, 0, pidfd, &dmabuf_fd);
+ ksft_test_result(!ret, "Allocation with child pidfd %d\n", ret);
+ if (dmabuf_fd >= 0)
+ close(dmabuf_fd);
+ close(pidfd);
+ kill(child_pid, SIGTERM);
+ waitpid(child_pid, &status, 0);
+out:
+ close(heap_fd);
+}
+
static int numer_of_heaps(void)
{
DIR *d = opendir(DEVPATH);
@@ -420,7 +542,7 @@ int main(void)
return KSFT_SKIP;
}
- ksft_set_plan(11 * numer_of_heaps());
+ ksft_set_plan(13 * numer_of_heaps());
while ((dir = readdir(d))) {
if (!strncmp(dir->d_name, ".", 2))
@@ -435,6 +557,8 @@ int main(void)
test_alloc_zeroed(dir->d_name, ONE_MEG);
test_alloc_compat(dir->d_name);
test_alloc_errors(dir->d_name);
+ test_alloc_pidfd_self(dir->d_name);
+ test_alloc_pidfd_child(dir->d_name);
}
closedir(d);
diff --git a/tools/testing/selftests/dmabuf-heaps/vmtest.sh b/tools/testing/selftests/dmabuf-heaps/vmtest.sh
new file mode 100755
index 0000000000000..6f1a878384127
--- /dev/null
+++ b/tools/testing/selftests/dmabuf-heaps/vmtest.sh
@@ -0,0 +1,205 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+#
+# Copyright (c) 2026 Red Hat
+#
+# Dependencies:
+# * virtme-ng
+# * qemu (used by virtme-ng)
+
+readonly SCRIPT_DIR="$(cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
+readonly KERNEL_CHECKOUT=$(realpath "${SCRIPT_DIR}"/../../../../)
+readonly CGROUP_DIR="${KERNEL_CHECKOUT}/tools/testing/selftests/cgroup"
+
+source "${SCRIPT_DIR}"/../kselftest/ktap_helpers.sh
+
+readonly DMABUF_HEAP_TEST="${SCRIPT_DIR}"/dmabuf-heap
+readonly MEMCONTROL_TEST="${CGROUP_DIR}"/test_memcontrol
+readonly TMP_DIR=$(mktemp -d /tmp/dmabuf-vmtest.XXXXXXXX)
+
+VERBOSE=false
+BUILD=false
+BUILD_HOST=""
+BUILD_HOST_PODMAN_CONTAINER_NAME=""
+
+usage() {
+ echo
+ echo "$0 [OPTIONS]"
+ echo
+ echo "Options"
+ echo " -b: build the kernel from the current source tree and use it for the VM"
+ echo " -H: hostname for remote build host (used with -b)"
+ echo " -p: podman container name for remote build host (used with -b)"
+ echo " Example: -H beefyserver -p vng"
+
+ echo " -v: enable verbose vng/qemu output"
+ echo
+
+ exit 1
+}
+
+die() {
+ echo "$*" >&2
+ exit "${KSFT_FAIL}"
+}
+
+cleanup() {
+ rm -rf "${TMP_DIR}"
+}
+
+check_deps() {
+ for dep in vng make; do
+ if [[ ! -x $(command -v "${dep}") ]]; then
+ echo -e "skip: dependency ${dep} not found!\n"
+ exit "${KSFT_SKIP}"
+ fi
+ done
+
+ if [[ ! -x "${DMABUF_HEAP_TEST}" ]]; then
+ printf "skip: %s not found!" "${DMABUF_HEAP_TEST}"
+ printf " Please build the kselftest dmabuf-heaps target (or use -b).\n"
+ exit "${KSFT_SKIP}"
+ fi
+
+ if [[ ! -x "${MEMCONTROL_TEST}" ]]; then
+ printf "skip: %s not found!" "${MEMCONTROL_TEST}"
+ printf " Please build the kselftest cgroup target (or use -b).\n"
+ exit "${KSFT_SKIP}"
+ fi
+}
+
+check_vng() {
+ local tested_versions=("1.36" "1.37")
+ local version
+ local ok=0
+
+ version="$(vng --version)"
+ for tv in "${tested_versions[@]}"; do
+ if [[ "${version}" == *"${tv}"* ]]; then
+ ok=1
+ break
+ fi
+ done
+
+ if [[ "${ok}" -eq 0 ]]; then
+ printf "warning: vng version '%s' has not been tested and may " "${version}" >&2
+ printf "not function properly.\n\tThe following versions have been tested: " >&2
+ echo "${tested_versions[@]}" >&2
+ fi
+}
+
+build_selftests() {
+ make -C "${KERNEL_CHECKOUT}" headers_install \
+ INSTALL_HDR_PATH="${TMP_DIR}/usr" -j"$(nproc)"
+
+ local khdr="-isystem ${TMP_DIR}/usr/include"
+
+ if ! make -C "${SCRIPT_DIR}" KHDR_INCLUDES="${khdr}" -j"$(nproc)"; then
+ die "failed to build dmabuf-heaps selftests"
+ fi
+
+ if ! make -C "${CGROUP_DIR}" KHDR_INCLUDES="${khdr}" \
+ "${MEMCONTROL_TEST}" -j"$(nproc)"; then
+ die "failed to build cgroup/test_memcontrol selftest"
+ fi
+}
+
+handle_build() {
+ if ! ${BUILD}; 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
+
+ local vng_args=("-v" "--config" "${SCRIPT_DIR}/config" "--build")
+
+ if [[ -n "${BUILD_HOST}" ]]; then
+ vng_args+=("--build-host" "${BUILD_HOST}")
+ fi
+
+ if [[ -n "${BUILD_HOST_PODMAN_CONTAINER_NAME}" ]]; then
+ vng_args+=("--build-host-exec-prefix" \
+ "podman exec -ti ${BUILD_HOST_PODMAN_CONTAINER_NAME}")
+ fi
+
+ if ! vng "${vng_args[@]}"; then
+ die "failed to build kernel from source tree (${KERNEL_CHECKOUT})"
+ fi
+
+ build_selftests
+
+ popd &>/dev/null
+}
+
+make_runner() {
+ # virtme-ng shares the host filesystem, so TMP_DIR is accessible
+ # inside the VM at the same absolute path.
+ cat > "${TMP_DIR}/run_tests.sh" <<-EOF
+ #!/bin/sh
+ set -u
+ PASS=0; FAIL=0; SKIP=0; N=0
+
+ run() {
+ name="\$1"; shift
+ N=\$((N+1))
+ "\$@"; rc=\$?
+ if [ \$rc -eq 0 ]; then echo "ok \$N \$name"; PASS=\$((PASS+1))
+ elif [ \$rc -eq 4 ]; then echo "ok \$N \$name # SKIP"; SKIP=\$((SKIP+1))
+ else echo "not ok \$N \$name"; FAIL=\$((FAIL+1))
+ fi
+ }
+
+ run "dmabuf-heap charge_pid_fd ioctl" ${DMABUF_HEAP_TEST}
+ run "memcontrol dma-buf memcg" ${MEMCONTROL_TEST} test_memcg_dmabuf
+ echo "# PASS=\$PASS SKIP=\$SKIP FAIL=\$FAIL"
+ [ \$FAIL -eq 0 ]
+ EOF
+ chmod +x "${TMP_DIR}/run_tests.sh"
+}
+
+run_vm() {
+ local verbose_opt=""
+ local kernel_opt=""
+
+ ${VERBOSE} && verbose_opt="--verbose"
+
+ # If we are running from within the kernel source tree, use the kernel
+ # source tree as the kernel to boot, otherwise use the running kernel.
+ if [[ "$(realpath "$(pwd)")" == "${KERNEL_CHECKOUT}"* ]]; then
+ kernel_opt="${KERNEL_CHECKOUT}"
+ fi
+
+ vng --run ${kernel_opt} ${verbose_opt} --user root --memory 512M \
+ --exec "${TMP_DIR}/run_tests.sh"
+}
+
+while getopts :hvbH:p: o
+do
+ case $o in
+ v) VERBOSE=true;;
+ b) BUILD=true;;
+ H) BUILD_HOST=$OPTARG;;
+ p) BUILD_HOST_PODMAN_CONTAINER_NAME=$OPTARG;;
+ h|*) usage;;
+ esac
+done
+shift $((OPTIND-1))
+
+trap cleanup EXIT
+
+check_vng
+handle_build
+check_deps
+make_runner
+
+echo "Booting VM and running tests..."
+run_vm
--
2.53.0
More information about the Linux-security-module-archive
mailing list