[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