All of lore.kernel.org
 help / color / mirror / Atom feed
From: Samir Mulani <samir@linux.ibm.com>
To: ltp@lists.linux.it
Cc: Samir Mulani <samir@linux.ibm.com>
Subject: [LTP] [PATCH 2/2] cgroup: Add invalid burst test
Date: Mon, 28 Sep 2026 07:17:35 +0200	[thread overview]
Message-ID: <20260928051740.591819-3-samir@linux.ibm.com> (raw)
In-Reply-To: <20260928051740.591819-1-samir@linux.ibm.com>

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


-- 
Mailing list info: https://lists.linux.it/listinfo/ltp

      parent reply	other threads:[~2026-09-28  5:20 UTC|newest]

Thread overview: 4+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-28  5:17 [LTP] [PATCH 0/2] cgroup: Add tests for cgroupv2 cpu.max.burst Samir Mulani
2026-09-28  5:17 ` [LTP] [PATCH 1/2] cgroup: Add cgroup_burst01 test Samir Mulani
2026-09-29  7:14   ` [LTP] " linuxtestproject.agent
2026-09-28  5:17 ` Samir Mulani [this message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20260928051740.591819-3-samir@linux.ibm.com \
    --to=samir@linux.ibm.com \
    --cc=ltp@lists.linux.it \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.