[PATCH 09/11] selftests: cgroup: add a vmtest script for memcg

From: Eric Chanudet

Date: Fri Aug 21 2026 - 15:01:58 EST


Following dmem's example[1], add a vmtest-memcg.sh to provide a
repeatable VM workflow using virtme-ng for memcg tests.

Boot a VM using virtme-ng with cma=64M in its kargs and enable
memory_cma_accounting on cgroupfs, then run the cgroup tests inside the
guest.

Provide -b to build the kernel first, -q to use a different qemu binary,
-s to start a shell in the VM, and -v for verbose boot.

[1] https://lore.kernel.org/all/20260706-kunit_cgroups-v5-3-6c42c8753468@xxxxxxxxxx/

Signed-off-by: Eric Chanudet <echanude@xxxxxxxxxx>
---
tools/testing/selftests/cgroup/vmtest-memcg.sh | 196 +++++++++++++++++++++++++
1 file changed, 196 insertions(+)

diff --git a/tools/testing/selftests/cgroup/vmtest-memcg.sh b/tools/testing/selftests/cgroup/vmtest-memcg.sh
new file mode 100755
index 000000000000..b19b96731784
--- /dev/null
+++ b/tools/testing/selftests/cgroup/vmtest-memcg.sh
@@ -0,0 +1,196 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+#
+# Copyright (c) 2026 Red Hat, Inc.
+#
+# Run cgroup memory controller selftests 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_memcg_vmtest_XXXX.pid)"
+readonly QEMU_OPTS=" --pidfile ${QEMU_PIDFILE} "
+
+BUILD=0
+QEMU="qemu-system-$(uname -m)"
+VERBOSE=0
+SHELL_MODE=0
+GUEST_TREE="${GUEST_TREE:-$KERNEL_CHECKOUT}"
+
+usage() {
+ cat <<EOF
+$0 [OPTIONS]
+ -b Build kernel from source tree before booting
+ -q <qemu> QEMU binary/path (default: $QEMU)
+ -s Start interactive shell in VM
+ -v Verbose output (vng boot logs on stdout)
+EOF
+ 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 [[ -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
+}
+
+handle_build() {
+ if [[ ! "$BUILD" -eq 1 ]]; then
+ return
+ fi
+
+ if [[ ! -d "$KERNEL_CHECKOUT" ]]; then
+ echo "-b requires $0 to be 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 1 ]]; 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")" \
+ --user root \
+ --ssh "$SSH_GUEST_PORT" \
+ --append "cma=64M" \
+ --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() {
+ vm_ssh -- "grep -q memory /sys/fs/cgroup/cgroup.controllers" \
+ || die "memory controller not available (CONFIG_MEMCG?)"
+ vm_ssh -- "[[ -e /dev/dma_heap/default_cma_region ]]" \
+ || die "CMA heap not available (CONFIG_DMABUF_HEAPS_CMA? cma= cmdline?)"
+}
+
+setup_guest_cma_accounting() {
+ vm_ssh -- "mount -o remount,memory_cma_accounting /sys/fs/cgroup" \
+ || die "failed to enable memory_cma_accounting"
+ vm_ssh -- "echo +memory > /sys/fs/cgroup/cgroup.subtree_control" \
+ || die "failed to enable memory controller"
+}
+
+run_test() {
+ vm_ssh -- "cd '$GUEST_TREE' && make -C tools/testing/selftests TARGETS=cgroup"
+ vm_ssh -- "cd '$GUEST_TREE' && make -C tools/testing/selftests TARGETS=cgroup run_tests"
+}
+
+while getopts ":hvq:sb" opt; do
+ case "$opt" in
+ v) VERBOSE=1 ;;
+ q) QEMU="${OPTARG}" ;;
+ b) BUILD=1 ;;
+ s) SHELL_MODE=1 ;;
+ h|*) usage ;;
+ esac
+done
+
+trap cleanup EXIT
+
+check_deps
+handle_build
+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_cma_accounting
+
+echo "Running cgroup selftests in VM..."
+run_test
+echo "PASS: cgroup selftests completed"
+exit "$KSFT_PASS"

--
2.53.0