[LTP] [PATCH 2/2] cgroup: Add invalid burst test

Samir Mulani samir@linux.ibm.com
Mon Sep 28 07:17:35 CEST 2026


I added cpu_max_burst_invalid01 to test that the cpu.max.burst
interface in cgroupv2 correctly rejects invalid input values. The
kernel's tg_set_bandwidth() in kernel/sched/core.c rejects bad burst
values with EINVAL and must not change the previously stored burst
value.

Test scenarios:
  TC1 - Writing burst = quota+1 (exceeds quota) and burst = "-1"
        (negative value) must both be rejected with EINVAL and the
        burst value must remain unchanged.
  TC2 - Writing a value where burst+quota exceeds the kernel MAX_BW
        and writing "abc" (garbage string) must both be rejected with
        EINVAL.
  TC3 - Writing a value that overflows the nanosecond conversion and
        writing "100xyz" (trailing garbage) must both be rejected with
        EINVAL.
  TC4 - After five consecutive rejected writes the burst value must
        remain unchanged.
  TC5 - With unlimited quota, burst > quota is accepted; after
        re-enabling a finite quota the same burst value must be
        rejected with EINVAL.

Requires CONFIG_CFS_BANDWIDTH=y and cgroupv2 cpu controller.

Signed-off-by: Samir Mulani <samir@linux.ibm.com>
---
 runtest/controllers                           |   1 +
 .../kernel/controllers/cgroup/.gitignore      |   1 +
 .../cgroup/cpu_max_burst_invalid01.c          | 417 ++++++++++++++++++
 3 files changed, 419 insertions(+)
 create mode 100644 testcases/kernel/controllers/cgroup/cpu_max_burst_invalid01.c

diff --git a/runtest/controllers b/runtest/controllers
index aa93ea5af..b0c7ca3c4 100644
--- a/runtest/controllers
+++ b/runtest/controllers
@@ -4,6 +4,7 @@ cgroup_core02	cgroup_core02
 cgroup_core03	cgroup_core03
 cgroup		cgroup_regression_test.sh
 cpu_max_burst01  cpu_max_burst01
+cpu_max_burst_invalid01  cpu_max_burst_invalid01
 memcg_regression	memcg_regression_test.sh
 memcg_test_3	memcg_test_3
 memcg_failcnt memcg_failcnt.sh
diff --git a/testcases/kernel/controllers/cgroup/.gitignore b/testcases/kernel/controllers/cgroup/.gitignore
index 2506388fd..0cc5ead44 100644
--- a/testcases/kernel/controllers/cgroup/.gitignore
+++ b/testcases/kernel/controllers/cgroup/.gitignore
@@ -4,3 +4,4 @@
 /cgroup_core02
 /cgroup_core03
 /cpu_max_burst01
+/cpu_max_burst_invalid01
diff --git a/testcases/kernel/controllers/cgroup/cpu_max_burst_invalid01.c b/testcases/kernel/controllers/cgroup/cpu_max_burst_invalid01.c
new file mode 100644
index 000000000..cca92e178
--- /dev/null
+++ b/testcases/kernel/controllers/cgroup/cpu_max_burst_invalid01.c
@@ -0,0 +1,417 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (c) 2026 Samir Mulani, IBM Corporation
+ */
+
+/*\
+ * Negative / invalid-input tests for the cgroupv2 cpu.max.burst interface.
+ *
+ * The kernel's tg_set_bandwidth() (kernel/sched/core.c) imposes several
+ * hard constraints on the burst_us value written to cpu.max.burst.  Every
+ * invalid write must be rejected with EINVAL and must not modify the
+ * previously stored burst value.
+ *
+ * Kernel validation constraints from tg_set_bandwidth():
+ *   (A) burst_us > (U64_MAX / NSEC_PER_USEC)    -> EINVAL (nsec overflow)
+ *   (B) burst_us > quota_us                      -> EINVAL (exceeds quota)
+ *   (C) burst_us + quota_us > MAX_BW             -> EINVAL (sum overflow)
+ *   (D) Non-numeric / garbage string             -> EINVAL (parse error)
+ *   (E) Negative integer written as string       -> EINVAL (parse error)
+ *
+ * Test scenarios:
+ *   TC 1 - Boundary and sign violations: burst = quota+1 (B) and burst = "-1"
+ *          (E) must both be rejected with EINVAL, value unchanged.
+ *   TC 2 - Overflow and garbage: burst+quota > MAX_BW (C) and burst = "abc"
+ *          (D) must both be rejected with EINVAL, value unchanged.
+ *   TC 3 - Numeric overflow variants: burst overflows nsec translation (A)
+ *          and trailing garbage "100xyz" (D) must both be rejected.
+ *   TC 4 - Burst stable after a sequence of rejected writes.
+ *   TC 5 - Burst vs quota transition: unlimited -> finite quota.
+ *
+ * Requires: CONFIG_CFS_BANDWIDTH=y, cgroup v2 cpu controller.
+ */
+
+#define _GNU_SOURCE
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <stdint.h>
+#include <sys/types.h>
+
+#include "tst_test.h"
+#include "tst_safe_file_at.h"
+
+#define PERIOD_US	100000ULL
+#define QUOTA_US	 20000ULL
+#define BURST_US	 10000ULL
+
+/* MAX_BW_US: kernel max_bw_runtime_us = (1 << BW_SHIFT) * USEC_PER_SEC */
+#define MAX_BW_US	(((uint64_t)1 << 20) * 1000000ULL)
+
+static struct tst_cg_group *cg_child;
+
+static int cg_dirfd(const struct tst_cg_group *cg)
+{
+	int fd = tst_cg_group_unified_dir_fd(cg);
+
+	if (fd < 0)
+		tst_brk(TBROK, "tst_cg_group_unified_dir_fd returned %d", fd);
+	return fd;
+}
+
+static unsigned long long cg_read_ull(const struct tst_cg_group *cg,
+				      const char *fname)
+{
+	char buf[64];
+	char *endp;
+	unsigned long long val;
+
+	SAFE_FILE_READAT(cg_dirfd(cg), fname, buf, sizeof(buf) - 1);
+
+	val = strtoull(buf, &endp, 10);
+	if (endp == buf)
+		tst_brk(TBROK, "strtoull failed on '%s' from %s", buf, fname);
+	return val;
+}
+
+/*
+ * cg_write_str() - write @str to @fname; returns errno on failure.
+ * Uses raw write(2) so callers can inspect the error without aborting.
+ */
+static int cg_write_str(const struct tst_cg_group *cg,
+			const char *fname, const char *str)
+{
+	int fd, saved = 0;
+	ssize_t n;
+
+	fd = SAFE_OPENAT(cg_dirfd(cg), fname, O_WRONLY);
+	n  = write(fd, str, strlen(str));
+	if (n < 0)
+		saved = errno;
+	SAFE_CLOSE(fd);
+	return saved;
+}
+
+static void cg_write_str_safe(const struct tst_cg_group *cg,
+			      const char *fname, const char *str)
+{
+	SAFE_FILE_PRINTFAT(cg_dirfd(cg), fname, "%s", str);
+}
+
+static void set_cpu_max(const struct tst_cg_group *cg,
+			unsigned long long quota_us,
+			unsigned long long period_us)
+{
+	char buf[64];
+
+	snprintf(buf, sizeof(buf), "%llu %llu", quota_us, period_us);
+	cg_write_str_safe(cg, "cpu.max", buf);
+}
+
+static unsigned long long read_burst(const struct tst_cg_group *cg)
+{
+	return cg_read_ull(cg, "cpu.max.burst");
+}
+
+/* try_write_burst() - attempt a burst write; returns errno on failure. */
+static int try_write_burst(const struct tst_cg_group *cg,
+			   unsigned long long burst_us)
+{
+	char buf[32];
+
+	snprintf(buf, sizeof(buf), "%llu", burst_us);
+	return cg_write_str(cg, "cpu.max.burst", buf);
+}
+
+/* try_write_burst_str() - write a raw string; returns errno on failure. */
+static int try_write_burst_str(const struct tst_cg_group *cg, const char *str)
+{
+	return cg_write_str(cg, "cpu.max.burst", str);
+}
+
+/*
+ * check_rejected() - verify the write returned EINVAL and that the stored
+ * burst value is unchanged (rejected writes must not corrupt kernel state).
+ */
+static void check_rejected(const char *label, int err,
+			    unsigned long long expected_burst)
+{
+	unsigned long long actual_burst;
+
+	if (!err) {
+		tst_res(TFAIL, "%s: write succeeded unexpectedly (expected EINVAL)",
+			label);
+		return;
+	}
+
+	if (err != EINVAL) {
+		tst_res(TFAIL, "%s: got errno=%d (%s), expected EINVAL",
+			label, err, tst_strerrno(err));
+		return;
+	}
+
+	actual_burst = read_burst(cg_child);
+	if (actual_burst != expected_burst) {
+		tst_res(TFAIL, "%s: burst corrupted after rejected write (expected %llu us, got %llu us)",
+			label, expected_burst, actual_burst);
+		return;
+	}
+
+	tst_res(TPASS, "%s: write rejected (errno=EINVAL), burst unchanged (%llu us)",
+		label, actual_burst);
+}
+
+/*
+ * tc1_boundary_and_negative - burst > quota and negative integer rejected.
+ *
+ * quota+1 is the smallest violation; "-1" exercises the unsigned parser
+ * boundary.  Both writes must return EINVAL and leave burst unchanged.
+ */
+static void tc1_boundary_and_negative(void)
+{
+	int err;
+
+	cg_child = tst_cg_group_mk(tst_cg, "inv_tc1");
+	set_cpu_max(cg_child, QUOTA_US, PERIOD_US);
+	cg_write_str_safe(cg_child, "cpu.max.burst", "10000");
+
+	err = try_write_burst(cg_child, QUOTA_US + 1);
+	check_rejected("burst=quota+1", err, BURST_US);
+
+	err = try_write_burst_str(cg_child, "-1");
+	check_rejected("burst=\"-1\"", err, BURST_US);
+
+	cg_child = tst_cg_group_rm(cg_child);
+}
+
+/*
+ * tc2_overflow_and_garbage - sum overflow and non-numeric string rejected.
+ *
+ * Uses burst <= quota so only the sum-overflow check fires; "abc" exercises
+ * the non-numeric parse path.  Both must return EINVAL.
+ */
+static void tc2_overflow_and_garbage(void)
+{
+	const unsigned long long quota_large = MAX_BW_US / 2 + 1;
+	const unsigned long long burst_large = MAX_BW_US / 2;
+	char buf[64];
+	int fd, err;
+	ssize_t n;
+
+	cg_child = tst_cg_group_mk(tst_cg, "inv_tc2");
+
+	snprintf(buf, sizeof(buf), "%llu %llu", quota_large, PERIOD_US);
+	fd = SAFE_OPENAT(cg_dirfd(cg_child), "cpu.max", O_WRONLY);
+	n  = write(fd, buf, strlen(buf));
+	SAFE_CLOSE(fd);
+	if (n < 0) {
+		tst_res(TCONF,
+			"%s: cannot set quota=%llu us (exceeds kernel MAX_BW), skipping",
+			__func__, quota_large);
+	} else {
+		err = try_write_burst(cg_child, burst_large);
+		if (!err) {
+			/*
+			 * burst_large <= quota_large so the burst>quota check
+			 * passed; the sum check did not fire.  This kernel's
+			 * MAX_BW is larger than our compile-time constant.
+			 */
+			tst_res(TCONF,
+				"%s: burst=%llu accepted (kernel MAX_BW exceeds compile-time estimate), skipping",
+				__func__, burst_large);
+			/* Clear burst before reducing quota to avoid EINVAL. */
+			cg_write_str_safe(cg_child, "cpu.max.burst", "0");
+		} else {
+			check_rejected("burst+quota>MAX_BW", err, 0ULL);
+		}
+	}
+
+	/* Reset to a small quota; burst must be 0 at this point. */
+	set_cpu_max(cg_child, QUOTA_US, PERIOD_US);
+
+	err = try_write_burst_str(cg_child, "abc");
+	check_rejected("burst=\"abc\"", err, 0ULL);
+
+	cg_child = tst_cg_group_rm(cg_child);
+}
+
+/*
+ * tc3_nsec_overflow_and_trailing_garbage - nsec overflow and trailing
+ * garbage rejected.
+ *
+ * A value above U64_MAX/1000 overflows the internal ns conversion.
+ * "100xyz" must be rejected entirely, not silently truncated to 100.
+ */
+static void tc3_nsec_overflow_and_trailing_garbage(void)
+{
+	const unsigned long long overflow_val = (UINT64_MAX / 1000ULL) + 1ULL;
+	int err;
+
+	cg_child = tst_cg_group_mk(tst_cg, "inv_tc3");
+	set_cpu_max(cg_child, QUOTA_US, PERIOD_US);
+
+	err = try_write_burst(cg_child, overflow_val);
+	check_rejected("burst>U64_MAX/NSEC_PER_USEC", err, 0ULL);
+
+	err = try_write_burst_str(cg_child, "100xyz");
+	check_rejected("burst=\"100xyz\"", err, 0ULL);
+
+	cg_child = tst_cg_group_rm(cg_child);
+}
+
+/*
+ * tc4_stable_after_rejections - burst value stable after repeated rejected
+ * writes.
+ *
+ * Repeated invalid writes must not partially corrupt the stored burst value.
+ */
+static void tc4_stable_after_rejections(void)
+{
+	struct {
+		unsigned long long val;
+		const char        *str;
+		const char        *label;
+	} bad[] = {
+		{ QUOTA_US + 1,  NULL,   "quota+1"    },
+		{ QUOTA_US * 5,  NULL,   "quota*5"    },
+		{ UINT64_MAX,    NULL,   "UINT64_MAX" },
+		{ 0,             "bad",  "\"bad\""    },
+		{ 0,             "-999", "\"-999\""   },
+	};
+	unsigned long long burst_after;
+	unsigned int i;
+	int err;
+
+	cg_child = tst_cg_group_mk(tst_cg, "inv_tc4");
+	set_cpu_max(cg_child, QUOTA_US, PERIOD_US);
+	cg_write_str_safe(cg_child, "cpu.max.burst", "10000");
+
+	for (i = 0; i < ARRAY_SIZE(bad); i++) {
+		if (bad[i].str)
+			err = try_write_burst_str(cg_child, bad[i].str);
+		else
+			err = try_write_burst(cg_child, bad[i].val);
+
+		if (!err) {
+			tst_res(TFAIL,
+				"%s: bad write [%s] unexpectedly succeeded (expected EINVAL)",
+				__func__, bad[i].label);
+			continue;
+		}
+		if (err != EINVAL) {
+			tst_res(TFAIL,
+				"%s: bad write [%s] got errno=%d (%s), expected EINVAL",
+				__func__, bad[i].label, err, tst_strerrno(err));
+		}
+	}
+
+	burst_after = read_burst(cg_child);
+	if (burst_after == BURST_US)
+		tst_res(TPASS,
+			"%s: burst remains %llu us after %zu rejected writes",
+			__func__, BURST_US, ARRAY_SIZE(bad));
+	else
+		tst_res(TFAIL,
+			"%s: burst corrupted: expected %llu us, got %llu us",
+			__func__, BURST_US, burst_after);
+
+	cg_child = tst_cg_group_rm(cg_child);
+}
+
+/*
+ * tc5_quota_transition - burst > quota rejected after re-enabling finite quota.
+ *
+ * With unlimited quota the burst > quota check is skipped; re-enabling
+ * a finite quota must then cause the same burst value to be rejected.
+ */
+static void tc5_quota_transition(void)
+{
+	int err;
+	unsigned long long burst_after;
+
+	cg_child = tst_cg_group_mk(tst_cg, "inv_tc5");
+
+	set_cpu_max(cg_child, QUOTA_US, PERIOD_US);
+	cg_write_str_safe(cg_child, "cpu.max", "max 100000");
+
+	err = try_write_burst(cg_child, QUOTA_US + 1);
+	burst_after = read_burst(cg_child);
+
+	if (err != 0) {
+		tst_res(TCONF,
+			"%s: burst=%llu rejected with unlimited quota (errno=%d) -- kernel enforces stricter check, skip",
+			__func__, QUOTA_US + 1, err);
+		cg_child = tst_cg_group_rm(cg_child);
+		return;
+	}
+
+	if (burst_after != QUOTA_US + 1) {
+		tst_res(TFAIL,
+			"%s: burst readback mismatch: expected %llu, got %llu",
+			__func__, QUOTA_US + 1, burst_after);
+		cg_child = tst_cg_group_rm(cg_child);
+		return;
+	}
+
+	tst_res(TPASS,
+		"%s: burst=%llu accepted with unlimited quota",
+		__func__, QUOTA_US + 1);
+
+	cg_write_str_safe(cg_child, "cpu.max.burst", "0");
+	set_cpu_max(cg_child, QUOTA_US, PERIOD_US);
+
+	err = try_write_burst(cg_child, QUOTA_US + 1);
+	check_rejected("burst>quota after re-enable", err, 0ULL);
+
+	cg_child = tst_cg_group_rm(cg_child);
+}
+
+static void (*const tc_funcs[])(void) = {
+	tc1_boundary_and_negative,
+	tc2_overflow_and_garbage,
+	tc3_nsec_overflow_and_trailing_garbage,
+	tc4_stable_after_rejections,
+	tc5_quota_transition,
+};
+
+static void run_test(unsigned int n)
+{
+	tc_funcs[n]();
+}
+
+static void setup(void)
+{
+	int dirfd;
+
+	dirfd = tst_cg_group_unified_dir_fd(tst_cg);
+	if (dirfd < 0)
+		tst_brk(TBROK, "tst_cg_group_unified_dir_fd failed");
+
+	if (faccessat(dirfd, "cpu.max.burst", F_OK, 0) != 0)
+		tst_brk(TCONF, "cpu.max.burst not present -- kernel requires CONFIG_CFS_BANDWIDTH=y");
+}
+
+static void cleanup(void)
+{
+	if (cg_child)
+		cg_child = tst_cg_group_rm(cg_child);
+}
+
+static struct tst_test test = {
+	.tcnt		= ARRAY_SIZE(tc_funcs),
+	.test		= run_test,
+	.setup		= setup,
+	.cleanup	= cleanup,
+	.needs_root	= 1,
+	.timeout	= 30,
+	.taint_check	= TST_TAINT_W | TST_TAINT_D,
+	.needs_kconfigs = (const char *[]) {
+		"CONFIG_CFS_BANDWIDTH",
+		NULL
+	},
+	.needs_cgroup_ver    = TST_CG_V2,
+	.needs_cgroup_ctrls  = (const char *const []) { "cpu", NULL },
+};
-- 
2.53.0



More information about the ltp mailing list