[Devel] [PATCH RHEL10 COMMIT] kselftest/cgroup/freezer: test unfreezable process reporting

Konstantin Khorenko khorenko at virtuozzo.com
Fri Jun 19 15:54:17 MSK 2026


The commit is pushed to "branch-rh10-6.12.0-211.16.1.12.x.vz10-ovz" and will appear at git at bitbucket.org:openvz/vzkernel.git
after rh10-6.12.0-211.16.1.12.4.vz10
------>
commit 48b42165045dd624a96c04e8964f19f91257dd60
Author: Pavel Tikhomirov <ptikhomirov at virtuozzo.com>
Date:   Tue Jun 16 13:54:27 2026 +0200

    kselftest/cgroup/freezer: test unfreezable process reporting
    
    Add a selftest for the "cgroup-v2/freezer: Print information about
    unfreezable process" change: a process stuck uninterruptibly in the
    kernel can not be frozen, and once the freeze exceeds the timeout the
    kernel must log a warning naming the guilty process and dumping its
    stack.
    
    The test uses a helper module (cg_freezer_hang) that parks a task in an
    uninterruptible in-kernel sleep, so the cgroup can never freeze. It
    shrinks the freeze_cgroup_timeout sysctl to keep the test fast, triggers
    the warning by reading cgroup.events, and verifies the warning line and
    the captured stack trace via /dev/kmsg.
    
    Note: requires kernel-devel package to build the helper modules.
    
    https://virtuozzo.atlassian.net/browse/VSTOR-119676
    Feature: cgroup/freeze: enhance logging
    Signed-off-by: Pavel Tikhomirov <ptikhomirov at virtuozzo.com>
---
 tools/testing/selftests/cgroup/config              |   1 +
 tools/testing/selftests/cgroup/test_freezer.c      | 207 +++++++++++++++++++++
 .../testing/selftests/cgroup/test_modules/Makefile |   3 +-
 .../cgroup/test_modules/cg_freezer_hang.c          |  86 +++++++++
 4 files changed, 296 insertions(+), 1 deletion(-)

diff --git a/tools/testing/selftests/cgroup/config b/tools/testing/selftests/cgroup/config
index c87a6df7edb53..619c56ae4bc99 100644
--- a/tools/testing/selftests/cgroup/config
+++ b/tools/testing/selftests/cgroup/config
@@ -10,3 +10,4 @@ CONFIG_TRANSPARENT_HUGEPAGE=y
 CONFIG_CACHESTAT_SYSCALL=y
 CONFIG_MODULES=y
 CONFIG_MODULE_UNLOAD=y
+CONFIG_STACKTRACE=y
diff --git a/tools/testing/selftests/cgroup/test_freezer.c b/tools/testing/selftests/cgroup/test_freezer.c
index 0adc5b6c4ac78..a28b68950db6e 100644
--- a/tools/testing/selftests/cgroup/test_freezer.c
+++ b/tools/testing/selftests/cgroup/test_freezer.c
@@ -10,6 +10,8 @@
 #include <stdlib.h>
 #include <string.h>
 #include <sys/wait.h>
+#include <fcntl.h>
+#include <signal.h>
 
 #include "../kselftest.h"
 #include "cgroup_util.h"
@@ -21,6 +23,8 @@
 #define debug(args...)
 #endif
 
+#define FREEZE_TIMEOUT_SYSCTL	"/proc/sys/kernel/freeze_cgroup_timeout"
+
 /*
  * Directory containing the test binary. The helper modules live in a
  * test_modules/ subdirectory next to it (rsync'd there by TEST_GEN_MODS_DIR),
@@ -76,6 +80,105 @@ static void mod_unload(const char *mod)
 		debug("Failed to unload module %s\n", mod);
 }
 
+/*
+ * Read/write the freezer timeout sysctl (the unit is jiffies). Used to
+ * shrink the default 30s timeout so the unfreezable test runs quickly.
+ */
+static int read_freeze_timeout(void)
+{
+	int v = -1;
+	FILE *f;
+
+	f = fopen(FREEZE_TIMEOUT_SYSCTL, "r");
+	if (!f)
+		return -1;
+	if (fscanf(f, "%d", &v) != 1)
+		v = -1;
+	fclose(f);
+	return v;
+}
+
+static void write_freeze_timeout(int v)
+{
+	FILE *f;
+
+	f = fopen(FREEZE_TIMEOUT_SYSCTL, "w");
+	if (!f)
+		return;
+	fprintf(f, "%d", v);
+	fclose(f);
+}
+
+/*
+ * Open /dev/kmsg positioned past the last existing record, so that later
+ * reads return only messages logged from now on. This avoids matching stale
+ * warnings from earlier runs (pids get reused) and scanning the whole buffer.
+ * Returns the fd, or -1 on error.
+ */
+static int kmsg_open_tail(void)
+{
+	int fd;
+
+	fd = open("/dev/kmsg", O_RDONLY | O_NONBLOCK);
+	if (fd < 0)
+		return -1;
+	lseek(fd, 0, SEEK_END);
+	return fd;
+}
+
+/*
+ * Verify the freeze-timeout warning for @pid in the kernel log read from
+ * @kfd (see kmsg_open_tail()): the kernel must name @pid as the unfreezable
+ * process and then dump its stack, which has to point back into the
+ * cg_freezer_hang helper module. Returns 0 if both are found.
+ */
+static int dmesg_check_unfreezable(int kfd, int pid)
+{
+	int found_warn = 0, found_stack = 0;
+	char rec[8192];
+	char needle[128];
+	ssize_t n;
+
+	snprintf(needle, sizeof(needle),
+		 "due to unfreezable process %d:", pid);
+
+	/* Each read() returns one record: "<prefix>;<message>\n". */
+	while ((n = read(kfd, rec, sizeof(rec) - 1)) > 0) {
+		char *msg;
+
+		rec[n] = '\0';
+		msg = strchr(rec, ';');
+		msg = msg ? msg + 1 : rec;
+
+		if (!found_warn) {
+			if (strstr(msg, needle))
+				found_warn = 1;
+			continue;
+		}
+		/*
+		 * The stack trace is printed right after the warning, one
+		 * "[<addr>] symbol" frame per record. Stop at the first
+		 * record that is no longer a stack frame.
+		 */
+		if (!strstr(msg, "[<"))
+			break;
+		if (strstr(msg, "cg_freezer_hang")) {
+			found_stack = 1;
+			break;
+		}
+	}
+
+	if (!found_warn) {
+		debug("No freeze-timeout warning for pid %d in dmesg\n", pid);
+		return -1;
+	}
+	if (!found_stack) {
+		debug("No cg_freezer_hang frame in stack of pid %d\n", pid);
+		return -1;
+	}
+	return 0;
+}
+
 /*
  * Check if the cgroup is frozen by looking at the cgroup.events::frozen value.
  */
@@ -859,6 +962,109 @@ static int test_cgfreezer_vfork(const char *root)
 	return ret;
 }
 
+/*
+ * Open /proc/cg_freezer_hang and write to it. The write blocks deep in the
+ * kernel in an uninterruptible sleep and only returns once the helper module
+ * is unloaded, so this task can never be frozen by the cgroup freezer.
+ */
+static int unfreezable_fn(const char *cgroup, void *arg)
+{
+	int fd;
+
+	fd = open("/proc/cg_freezer_hang", O_WRONLY);
+	if (fd < 0)
+		return EXIT_FAILURE;
+
+	/* Blocks in the kernel until cg_freezer_hang is unloaded. */
+	if (write(fd, "x", 1) != 1) {
+		close(fd);
+		return EXIT_FAILURE;
+	}
+
+	close(fd);
+	return EXIT_SUCCESS;
+}
+
+/*
+ * Test that a cgroup holding a process which is stuck uninterruptibly in the
+ * kernel fails to freeze, and that the kernel logs a freeze-timeout warning
+ * naming the guilty process.
+ */
+static int test_cgfreezer_unfreezable(const char *root)
+{
+	int ret = KSFT_FAIL;
+	char *cgroup = NULL;
+	int saved_timeout = -1;
+	int pid = -1;
+	int kfd = -1;
+
+	if (mod_load("cg_freezer_hang"))
+		return KSFT_SKIP;
+
+	cgroup = cg_name(root, "cg_test_unfreezable");
+	if (!cgroup)
+		goto cleanup;
+
+	if (cg_create(cgroup))
+		goto cleanup;
+
+	/*
+	 * Shrink the freeze timeout (jiffies) so we don't wait the 30s
+	 * default. A small value is short for any reasonable HZ.
+	 */
+	saved_timeout = read_freeze_timeout();
+	write_freeze_timeout(50);
+
+	pid = cg_run_nowait(cgroup, unfreezable_fn, NULL);
+	if (pid < 0)
+		goto cleanup;
+
+	if (cg_wait_for_proc_count(cgroup, 1))
+		goto cleanup;
+
+	/* Let the task actually enter the in-kernel hang. */
+	sleep(1);
+
+	/* Start freezing. It can never complete: the task is unfreezable. */
+	if (cg_freeze_nowait(cgroup, true))
+		goto cleanup;
+
+	/* Wait well past the (shrunk) timeout. */
+	sleep(3);
+
+	/* Capture only kernel log messages emitted from here on. */
+	kfd = kmsg_open_tail();
+
+	/*
+	 * Reading cgroup.events runs check_freeze_timeout() in the kernel,
+	 * which emits the warning. The cgroup must still report not frozen.
+	 */
+	if (cg_check_frozen(cgroup, false))
+		goto cleanup;
+
+	/* The kernel must name the guilty process and dump its stack. */
+	if (kfd >= 0 && dmesg_check_unfreezable(kfd, pid) == 0)
+		ret = KSFT_PASS;
+
+cleanup:
+	if (kfd >= 0)
+		close(kfd);
+	/* Thaw first, then unload the module to release the parked task. */
+	if (cgroup)
+		cg_freeze_nowait(cgroup, false);
+	if (saved_timeout >= 0)
+		write_freeze_timeout(saved_timeout);
+	mod_unload("cg_freezer_hang");
+	if (pid > 0) {
+		kill(pid, SIGKILL);
+		waitpid(pid, NULL, 0);
+	}
+	if (cgroup)
+		cg_destroy(cgroup);
+	free(cgroup);
+	return ret;
+}
+
 /*
  * Read the pid of the spare kernel thread spawned by cg_freezer_kthread.
  */
@@ -940,6 +1146,7 @@ struct cgfreezer_test {
 	T(test_cgfreezer_stopped),
 	T(test_cgfreezer_ptraced),
 	T(test_cgfreezer_vfork),
+	T(test_cgfreezer_unfreezable),
 	T(test_cgfreezer_kthread),
 };
 #undef T
diff --git a/tools/testing/selftests/cgroup/test_modules/Makefile b/tools/testing/selftests/cgroup/test_modules/Makefile
index dd401a4345d4b..3f39eeda3a92b 100644
--- a/tools/testing/selftests/cgroup/test_modules/Makefile
+++ b/tools/testing/selftests/cgroup/test_modules/Makefile
@@ -1,7 +1,8 @@
 TESTMODS_DIR := $(realpath $(dir $(abspath $(lastword $(MAKEFILE_LIST)))))
 KDIR ?= /lib/modules/$(shell uname -r)/build
 
-obj-m += cg_freezer_kthread.o
+obj-m += cg_freezer_hang.o \
+	cg_freezer_kthread.o
 
 # Ensure that KDIR exists, otherwise skip the compilation
 modules:
diff --git a/tools/testing/selftests/cgroup/test_modules/cg_freezer_hang.c b/tools/testing/selftests/cgroup/test_modules/cg_freezer_hang.c
new file mode 100644
index 0000000000000..0f54d7d4cd4be
--- /dev/null
+++ b/tools/testing/selftests/cgroup/test_modules/cg_freezer_hang.c
@@ -0,0 +1,86 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Helper module for the cgroup-v2 freezer selftest
+ * "test_cgfreezer_unfreezable".
+ *
+ * It exposes /proc/cg_freezer_hang. A task that writes to (or reads from)
+ * this file gets parked deep in the kernel in an uninterruptible sleep and
+ * never returns to userspace until the module is unloaded. Such a task can
+ * not be frozen by the cgroup freezer (the freeze is only acted upon on the
+ * way back to userspace), so a freezer cgroup holding it stays unfrozen and
+ * eventually trips the freeze-timeout warning.
+ *
+ * The sleep is done in short uninterruptible slices rather than one long
+ * sleep or a busy loop, so that:
+ *   - the CPU is not spun -> no soft/hard lockups;
+ *   - the task voluntarily context-switches every slice, advancing its
+ *     switch count -> the hung-task detector never sees 120s of "no
+ *     progress" and stays quiet;
+ *   - on rmmod we just raise a stop flag and the parked tasks drain within
+ *     one slice, so cleanup is reliable.
+ */
+#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
+
+#include <linux/module.h>
+#include <linux/kernel.h>
+#include <linux/sched.h>
+#include <linux/proc_fs.h>
+
+/* uninterruptible sleep slice (1 second) */
+#define HANG_SLICE HZ
+
+static bool stop;
+
+static void cg_freezer_hang(void)
+{
+	pr_info("pid %d parked in kernel\n", task_pid_nr(current));
+
+	while (!READ_ONCE(stop))
+		schedule_timeout_uninterruptible(HANG_SLICE);
+
+	pr_info("pid %d released\n", task_pid_nr(current));
+}
+
+static ssize_t cg_freezer_hang_write(struct file *file, const char __user *buf,
+				     size_t count, loff_t *ppos)
+{
+	cg_freezer_hang();
+	return count;
+}
+
+static ssize_t cg_freezer_hang_read(struct file *file, char __user *buf,
+				    size_t count, loff_t *ppos)
+{
+	cg_freezer_hang();
+	return 0;
+}
+
+static const struct proc_ops cg_freezer_hang_ops = {
+	.proc_write = cg_freezer_hang_write,
+	.proc_read = cg_freezer_hang_read,
+};
+
+static int __init cg_freezer_hang_init(void)
+{
+	if (!proc_create("cg_freezer_hang", 0666, NULL, &cg_freezer_hang_ops))
+		return -ENOMEM;
+	return 0;
+}
+
+static void __exit cg_freezer_hang_exit(void)
+{
+	/*
+	 * Raise 'stop' first, then remove the entry. remove_proc_entry()
+	 * blocks until all in-flight ->proc_write/->proc_read calls return,
+	 * which only happens once 'stop' is set -- so it also drains the
+	 * parked tasks for us.
+	 */
+	WRITE_ONCE(stop, true);
+	remove_proc_entry("cg_freezer_hang", NULL);
+}
+
+module_init(cg_freezer_hang_init);
+module_exit(cg_freezer_hang_exit);
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR("Pavel Tikhomirov <ptikhomirov at virtuozzo.com>");
+MODULE_DESCRIPTION("cgroup freezer test: park a task unfreezably in kernel");


More information about the Devel mailing list