* [LTP] [PATCH 1/2] cgroup: Add cgroup_burst01 test
2026-09-28 5:17 [LTP] [PATCH 0/2] cgroup: Add tests for cgroupv2 cpu.max.burst Samir Mulani
@ 2026-09-28 5:17 ` Samir Mulani
2026-09-29 7:14 ` [LTP] " linuxtestproject.agent
2026-09-28 5:17 ` [LTP] [PATCH 2/2] cgroup: Add invalid burst test Samir Mulani
1 sibling, 1 reply; 4+ messages in thread
From: Samir Mulani @ 2026-09-28 5:17 UTC (permalink / raw)
To: ltp; +Cc: Samir Mulani
I added cpu_max_burst01 to test the cpu.max.burst interface in
cgroupv2. This interface controls the burst budget for CFS bandwidth
throttling. When a cgroup uses less than its quota in a period, the
kernel saves up to (quota + burst) microseconds; a later busy period
can use that saved credit to run beyond the normal quota.
Test scenarios:
TC1 - cpu.max.burst defaults to 0; writing a non-zero value and
then clearing it back to 0 works correctly.
TC2 - Writing burst = quota/2 and burst = quota both read back the
correct value.
TC3 - A worker underruns to build up burst credit, then overloads
to consume it; cpu.stat nr_bursts and burst_usec counters
must increase. Also checks that burst can be updated while
tasks are running.
TC4 - The burst value is preserved after quota is removed
(cpu.max set to "max").
TC5 - The burst value is preserved after the period is changed.
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 +
.../controllers/cgroup/cpu_max_burst01.c | 650 ++++++++++++++++++
3 files changed, 652 insertions(+)
create mode 100644 testcases/kernel/controllers/cgroup/cpu_max_burst01.c
diff --git a/runtest/controllers b/runtest/controllers
index 93c52c439..aa93ea5af 100644
--- a/runtest/controllers
+++ b/runtest/controllers
@@ -3,6 +3,7 @@ cgroup_core01 cgroup_core01
cgroup_core02 cgroup_core02
cgroup_core03 cgroup_core03
cgroup cgroup_regression_test.sh
+cpu_max_burst01 cpu_max_burst01
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 9f1d1ada9..2506388fd 100644
--- a/testcases/kernel/controllers/cgroup/.gitignore
+++ b/testcases/kernel/controllers/cgroup/.gitignore
@@ -3,3 +3,4 @@
/cgroup_core01
/cgroup_core02
/cgroup_core03
+/cpu_max_burst01
diff --git a/testcases/kernel/controllers/cgroup/cpu_max_burst01.c b/testcases/kernel/controllers/cgroup/cpu_max_burst01.c
new file mode 100644
index 000000000..afb8befa6
--- /dev/null
+++ b/testcases/kernel/controllers/cgroup/cpu_max_burst01.c
@@ -0,0 +1,650 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (c) 2026 Samir Mulani, IBM Corporation.
+ */
+
+/*\
+ * Functional tests for the cgroupv2 cpu.max.burst interface.
+ *
+ * cpu.max.burst controls the burst budget for CFS bandwidth throttling.
+ * When a cgroup uses less than its quota in a period, the kernel retains
+ * up to (quota + burst) microseconds in the runtime pool. A later busy
+ * period may draw from that retained credit, temporarily running beyond
+ * the nominal quota rate.
+ *
+ * Kernel accounting (kernel/sched/fair.c __refill_cfs_bandwidth_runtime):
+ *
+ * At every period boundary:
+ * runtime += quota
+ * surplus = runtime_snap - runtime
+ * if (surplus > 0):
+ * burst_time += surplus
+ * nr_burst++
+ * runtime = min(runtime, quota + burst)
+ * runtime_snap = runtime
+ *
+ * Test scenarios:
+ * TC 1 - Default value and write-zero: cpu.max.burst defaults to 0;
+ * setting burst to non-zero then back to 0 is accepted.
+ * TC 2 - Write/readback, burst == quota boundary accepted, and
+ * burst > quota rejected with EINVAL.
+ * TC 3 - Functional burst accounting: a worker underruns to fill the
+ * pool, then overloads to trigger burst accounting; nr_bursts
+ * and burst_usec in cpu.stat must both increase.
+ * TC 4 - Burst preserved after quota cleared (cpu.max = "max").
+ * TC 5 - Burst preserved after period change.
+ *
+ * Requires: CONFIG_CFS_BANDWIDTH=y, cgroup v2 cpu controller.
+ */
+
+#define _GNU_SOURCE
+#include <limits.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <time.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+
+#include "tst_test.h"
+#include "tst_safe_clocks.h"
+#include "tst_safe_file_at.h"
+
+#define PERIOD_US 100000ULL
+#define QUOTA_US 20000ULL
+#define BURST_US 10000ULL
+
+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;
+}
+
+static void cg_write_ull(const struct tst_cg_group *cg,
+ const char *fname, unsigned long long val)
+{
+ SAFE_FILE_PRINTFAT(cg_dirfd(cg), fname, "%llu", val);
+}
+
+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);
+}
+
+/*
+ * cg_write_str() - attempt a write that may legitimately fail.
+ * Returns 0 on success, errno on failure.
+ */
+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 set_cpu_max(const struct tst_cg_group *cg,
+ unsigned long long quota_us,
+ unsigned long long period_us)
+{
+ char buf[64];
+
+ if (quota_us == ULLONG_MAX)
+ snprintf(buf, sizeof(buf), "max %llu", period_us);
+ else
+ 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");
+}
+
+static void write_burst(const struct tst_cg_group *cg,
+ unsigned long long burst_us)
+{
+ cg_write_ull(cg, "cpu.max.burst", burst_us);
+}
+
+/*
+ * try_write_burst() - attempt a burst write that may legitimately fail.
+ * Returns 0 on success, 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);
+}
+
+struct burst_stat {
+ unsigned long long nr_bursts;
+ unsigned long long burst_usec;
+};
+
+static int read_burst_stat(const struct tst_cg_group *cg,
+ struct burst_stat *s)
+{
+ char buf[4096];
+ char *line, *save;
+ ssize_t n;
+ int fd;
+ int got_bursts = 0, got_usec = 0;
+
+ memset(s, 0, sizeof(*s));
+
+ fd = SAFE_OPENAT(cg_dirfd(cg), "cpu.stat", O_RDONLY);
+ n = SAFE_READ(0, fd, buf, sizeof(buf) - 1);
+ SAFE_CLOSE(fd);
+
+ if (n <= 0)
+ return -1;
+ buf[n] = '\0';
+
+ for (line = strtok_r(buf, "\n", &save); line;
+ line = strtok_r(NULL, "\n", &save)) {
+ if (sscanf(line, "nr_bursts %llu", &s->nr_bursts) == 1)
+ got_bursts = 1;
+ if (sscanf(line, "burst_usec %llu", &s->burst_usec) == 1)
+ got_usec = 1;
+ }
+
+ return (got_bursts && got_usec) ? 0 : -1;
+}
+
+enum worker_cmd {
+ CMD_UNDERRUN,
+ CMD_OVERLOAD,
+ CMD_EXIT,
+};
+
+enum worker_ack { ACK_DONE };
+
+static int cmd_pipe[2] = { -1, -1 };
+static int ack_pipe[2] = { -1, -1 };
+static pid_t worker_pid = -1;
+
+static long long monotonic_us(void)
+{
+ struct timespec ts;
+
+ SAFE_CLOCK_GETTIME(CLOCK_MONOTONIC, &ts);
+ return (long long)ts.tv_sec * 1000000LL + ts.tv_nsec / 1000LL;
+}
+
+static long long process_cpu_us(void)
+{
+ struct timespec ts;
+
+ SAFE_CLOCK_GETTIME(CLOCK_PROCESS_CPUTIME_ID, &ts);
+ return (long long)ts.tv_sec * 1000000LL + ts.tv_nsec / 1000LL;
+}
+
+static void burn_cpu_for(long long dur_us)
+{
+ long long start = process_cpu_us();
+ volatile unsigned long v = 1;
+
+ while (process_cpu_us() - start < dur_us)
+ v = v * 1103515245UL + 12345UL;
+}
+
+/*
+ * worker_main() - command loop run by the forked child inside the cgroup.
+ *
+ * CMD_UNDERRUN:
+ * Runs 5 periods. In each period the worker burns 1 ms of CPU time then
+ * sleeps for the remainder of the period via clock_nanosleep(CLOCK_MONOTONIC).
+ * clock_nanosleep() genuinely blocks — the process goes off-CPU and consumes
+ * no quota while sleeping — so the cgroup uses ~1 ms out of 20 ms quota each
+ * period. After 2 periods the runtime pool reaches the (quota + burst = 30 ms)
+ * ceiling. nr_bursts stays 0 during this phase because the pool is never
+ * fully drained.
+ *
+ * CMD_OVERLOAD:
+ * Burns 4 x QUOTA_US = 80 ms of process CPU time to drain the pool
+ * across multiple period boundaries and trigger burst accounting.
+ */
+static void worker_main(void)
+{
+ enum worker_cmd cmd;
+ enum worker_ack ack = ACK_DONE;
+ ssize_t n;
+
+ SAFE_CLOSE(cmd_pipe[1]);
+ SAFE_CLOSE(ack_pipe[0]);
+
+ for (;;) {
+ n = SAFE_READ(1, cmd_pipe[0], &cmd, sizeof(cmd));
+ if (n != (ssize_t)sizeof(cmd))
+ break;
+
+ switch (cmd) {
+ case CMD_UNDERRUN:
+ {
+ int i;
+
+ for (i = 0; i < 5; i++) {
+ struct timespec deadline;
+ long long now_us;
+
+ /*
+ * Burn 1 ms of CPU quota, then
+ * genuinely sleep for the rest of the
+ * period. clock_nanosleep() blocks the
+ * process off-CPU so no quota is used
+ * while waiting.
+ */
+ burn_cpu_for(1000LL);
+
+ now_us = monotonic_us();
+ deadline.tv_sec =
+ (now_us + (long long)PERIOD_US)
+ / 1000000LL;
+ deadline.tv_nsec =
+ ((now_us + (long long)PERIOD_US)
+ % 1000000LL) * 1000LL;
+ while (clock_nanosleep(CLOCK_MONOTONIC,
+ TIMER_ABSTIME, &deadline,
+ NULL) == EINTR)
+ ;
+ }
+ }
+ break;
+
+ case CMD_OVERLOAD:
+ burn_cpu_for(4LL * (long long)QUOTA_US);
+ break;
+
+ case CMD_EXIT:
+ SAFE_CLOSE(cmd_pipe[0]);
+ SAFE_CLOSE(ack_pipe[1]);
+ exit(0);
+
+ default:
+ break;
+ }
+
+ SAFE_WRITE(1, ack_pipe[1], &ack, sizeof(ack));
+ }
+
+ exit(1);
+}
+
+static void worker_start(const struct tst_cg_group *cg)
+{
+ SAFE_PIPE(cmd_pipe);
+ SAFE_PIPE(ack_pipe);
+
+ worker_pid = SAFE_FORK();
+ if (!worker_pid) {
+ SAFE_CG_PRINTF(cg, "cgroup.procs", "%d", getpid());
+ worker_main();
+ exit(1);
+ }
+
+ SAFE_CLOSE(cmd_pipe[0]);
+ SAFE_CLOSE(ack_pipe[1]);
+}
+
+static void worker_send(enum worker_cmd cmd)
+{
+ enum worker_ack ack;
+
+ SAFE_WRITE(1, cmd_pipe[1], &cmd, sizeof(cmd));
+ if (cmd != CMD_EXIT)
+ SAFE_READ(1, ack_pipe[0], &ack, sizeof(ack));
+}
+
+static void worker_stop(void)
+{
+ if (worker_pid < 0)
+ return;
+
+ worker_send(CMD_EXIT);
+ SAFE_WAITPID(worker_pid, NULL, 0);
+ worker_pid = -1;
+
+ if (cmd_pipe[1] >= 0) {
+ SAFE_CLOSE(cmd_pipe[1]);
+ cmd_pipe[1] = -1;
+ }
+ if (ack_pipe[0] >= 0) {
+ SAFE_CLOSE(ack_pipe[0]);
+ ack_pipe[0] = -1;
+ }
+}
+
+/*
+ * tc1_default_and_zero - default value is 0; clearing back to 0 works.
+ *
+ * A freshly created cgroup must read 0 from cpu.max.burst. After writing
+ * a non-zero value, writing 0 must restore it to 0.
+ */
+static void tc1_default_and_zero(void)
+{
+ unsigned long long burst;
+
+ cg_child = tst_cg_group_mk(tst_cg, "burst_tc1");
+
+ burst = read_burst(cg_child);
+ if (burst != 0)
+ tst_res(TFAIL, "%s: expected default 0, got %llu", __func__, burst);
+ else
+ tst_res(TPASS, "%s: cpu.max.burst defaults to 0", __func__);
+
+ set_cpu_max(cg_child, QUOTA_US, PERIOD_US);
+ write_burst(cg_child, BURST_US);
+ write_burst(cg_child, 0);
+
+ burst = read_burst(cg_child);
+ if (burst == 0)
+ tst_res(TPASS, "%s: burst cleared back to 0", __func__);
+ else
+ tst_res(TFAIL, "%s: expected 0 after clear, got %llu us", __func__, burst);
+
+ cg_child = tst_cg_group_rm(cg_child);
+}
+
+/*
+ * tc2_write_readback_and_boundary - valid readback, burst == quota accepted,
+ * burst > quota rejected.
+ *
+ * Verifies write/readback correctness for burst = quota/2, confirms
+ * burst == quota is the upper valid boundary, and confirms burst > quota
+ * is rejected with EINVAL without corrupting the stored value.
+ */
+static void tc2_write_readback_and_boundary(void)
+{
+ const unsigned long long bad_burst = QUOTA_US + 1;
+ unsigned long long burst;
+ int err;
+
+ cg_child = tst_cg_group_mk(tst_cg, "burst_tc2");
+
+ set_cpu_max(cg_child, QUOTA_US, PERIOD_US);
+ write_burst(cg_child, BURST_US);
+
+ burst = read_burst(cg_child);
+ if (burst == BURST_US)
+ tst_res(TPASS, "tc2_write_readback: cpu.max.burst write/readback %llu us", burst);
+ else
+ tst_res(TFAIL, "tc2_write_readback: expected %llu us, got %llu us",
+ BURST_US, burst);
+
+ write_burst(cg_child, QUOTA_US);
+
+ burst = read_burst(cg_child);
+ if (burst == QUOTA_US)
+ tst_res(TPASS, "tc2_burst_equals_quota: burst == quota (%llu us) accepted", burst);
+ else
+ tst_res(TFAIL, "tc2_burst_equals_quota: expected %llu us, got %llu us",
+ QUOTA_US, burst);
+
+ /*
+ * Restore a known pre-seed value before the rejection test so that
+ * silent corruption of the stored burst is detectable.
+ */
+ write_burst(cg_child, BURST_US);
+
+ err = try_write_burst(cg_child, bad_burst);
+ burst = read_burst(cg_child);
+
+ if (!err) {
+ tst_res(TFAIL,
+ "tc2_burst_exceeds_quota: burst=%llu (quota+1) write succeeded, expected EINVAL",
+ bad_burst);
+ } else {
+ if (err != EINVAL) {
+ tst_res(TWARN,
+ "tc2_burst_exceeds_quota: got errno=%d (%s), expected EINVAL",
+ err, tst_strerrno(err));
+ }
+
+ if (burst != BURST_US) {
+ tst_res(TFAIL,
+ "tc2_burst_exceeds_quota: burst corrupted after rejected write (expected %llu us, got %llu us)",
+ BURST_US, burst);
+ } else {
+ tst_res(TPASS,
+ "tc2_burst_exceeds_quota: burst=%llu rejected (errno=%d), stored value unchanged (%llu us)",
+ bad_burst, err, burst);
+ }
+ }
+
+ cg_child = tst_cg_group_rm(cg_child);
+}
+
+/*
+ * tc3_functional_burst_accounting - verify nr_bursts and burst_usec increase.
+ *
+ * Configuration: quota=20ms burst=10ms period=100ms
+ * Pool ceiling: quota + burst = 30ms
+ *
+ * Phase A (underrun) - worker burns ~1 ms CPU per period over 5 periods:
+ * CPU usage ~1 ms/period << 20 ms quota. After 2 periods the pool reaches
+ * the 30 ms ceiling. nr_bursts stays 0: the pool fills but is never drained
+ * below runtime_snap, so the surplus at refill is never positive.
+ *
+ * Phase B (overload) - worker burns 4 x QUOTA_US = 80 ms process CPU time:
+ * Under 20 ms quota this spans >= 4 wall-clock periods.
+ * Period B1: pool=30ms, task consumes 30ms -> pool->0, throttled.
+ * Period B2 refill:
+ * runtime += 20ms -> 20ms
+ * surplus = snap(30) - 20 = +10ms > 0
+ * nr_burst++ , burst_time += 10ms
+ * runtime = min(20, 30) = 20ms
+ *
+ * The 'before' snapshot is taken after setup but BEFORE the worker starts,
+ * so it captures the zero baseline. Any burst activity from the underrun
+ * phase (there should be none) is already counted in 'before'. After the
+ * overload phase both counters must be strictly greater than 'before'.
+ */
+static void tc3_functional_burst_accounting(void)
+{
+ struct burst_stat before, after;
+ int stat_ok;
+
+ cg_child = tst_cg_group_mk(tst_cg, "burst_tc3");
+
+ set_cpu_max(cg_child, QUOTA_US, PERIOD_US);
+ write_burst(cg_child, BURST_US);
+
+ /*
+ * Snapshot BEFORE the worker starts. cpu.stat counters start at 0
+ * on a freshly created cgroup; capturing here gives a clean baseline
+ * that is unaffected by any scheduling activity inside the worker.
+ */
+ stat_ok = read_burst_stat(cg_child, &before);
+ if (stat_ok < 0) {
+ tst_res(TCONF,
+ "%s: nr_bursts/burst_usec absent from cpu.stat -- kernel may lack CONFIG_CFS_BANDWIDTH",
+ __func__);
+ cg_child = tst_cg_group_rm(cg_child);
+ return;
+ }
+
+ worker_start(cg_child);
+ worker_send(CMD_UNDERRUN);
+ worker_send(CMD_OVERLOAD);
+ worker_stop();
+
+ read_burst_stat(cg_child, &after);
+
+ tst_res(TINFO, "%s: nr_bursts %llu->%llu burst_usec %llu->%llu",
+ __func__,
+ before.nr_bursts, after.nr_bursts,
+ before.burst_usec, after.burst_usec);
+
+ if (after.nr_bursts > before.nr_bursts &&
+ after.burst_usec > before.burst_usec) {
+ tst_res(TPASS,
+ "%s: nr_bursts +%llu burst_usec +%llu us",
+ __func__,
+ after.nr_bursts - before.nr_bursts,
+ after.burst_usec - before.burst_usec);
+ } else {
+ tst_res(TFAIL,
+ "%s: burst accounting did not increase (nr_bursts %llu->%llu burst_usec %llu->%llu)",
+ __func__,
+ before.nr_bursts, after.nr_bursts,
+ before.burst_usec, after.burst_usec);
+ }
+
+ cg_child = tst_cg_group_rm(cg_child);
+}
+
+/*
+ * tc4_preserved_after_quota_clear - burst value survives quota removal.
+ *
+ * Setting cpu.max to "max <period>" (no quota) must not reset the stored
+ * burst value.
+ */
+static void tc4_preserved_after_quota_clear(void)
+{
+ unsigned long long burst_after;
+
+ cg_child = tst_cg_group_mk(tst_cg, "burst_tc4");
+
+ set_cpu_max(cg_child, QUOTA_US, PERIOD_US);
+ write_burst(cg_child, BURST_US);
+ set_cpu_max(cg_child, ULLONG_MAX, PERIOD_US);
+
+ burst_after = read_burst(cg_child);
+ if (burst_after == BURST_US)
+ tst_res(TPASS, "%s: burst preserved (%llu us) after quota cleared",
+ __func__, burst_after);
+ else
+ tst_res(TFAIL, "%s: expected %llu us after quota clear, got %llu us",
+ __func__, BURST_US, burst_after);
+
+ cg_child = tst_cg_group_rm(cg_child);
+}
+
+/*
+ * tc5_preserved_after_period_change - burst value survives period update.
+ *
+ * Updating only the period in cpu.max must not silently clear the burst value.
+ */
+static void tc5_preserved_after_period_change(void)
+{
+ const unsigned long long new_period = 200000ULL;
+ unsigned long long burst_after;
+
+ cg_child = tst_cg_group_mk(tst_cg, "burst_tc5");
+
+ set_cpu_max(cg_child, QUOTA_US, PERIOD_US);
+ write_burst(cg_child, BURST_US);
+ set_cpu_max(cg_child, QUOTA_US, new_period);
+
+ burst_after = read_burst(cg_child);
+ if (burst_after == BURST_US)
+ tst_res(TPASS, "%s: burst preserved (%llu us) after period changed to %llu us",
+ __func__, burst_after, new_period);
+ else
+ tst_res(TFAIL, "%s: expected %llu us after period change, got %llu us",
+ __func__, BURST_US, burst_after);
+
+ cg_child = tst_cg_group_rm(cg_child);
+}
+
+static void (*const tc_funcs[])(void) = {
+ tc1_default_and_zero,
+ tc2_write_readback_and_boundary,
+ tc3_functional_burst_accounting,
+ tc4_preserved_after_quota_clear,
+ tc5_preserved_after_period_change,
+};
+
+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");
+
+ if (faccessat(dirfd, "cpu.stat", F_OK, 0) != 0)
+ tst_brk(TCONF, "cpu.stat not present");
+
+ tst_res(TINFO, "quota=%llu us burst=%llu us period=%llu us",
+ QUOTA_US, BURST_US, PERIOD_US);
+}
+
+static void cleanup(void)
+{
+ if (worker_pid >= 0)
+ worker_stop();
+
+ if (cmd_pipe[1] >= 0) {
+ SAFE_CLOSE(cmd_pipe[1]);
+ cmd_pipe[1] = -1;
+ }
+ if (ack_pipe[0] >= 0) {
+ SAFE_CLOSE(ack_pipe[0]);
+ ack_pipe[0] = -1;
+ }
+
+ 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,
+ .forks_child = 1,
+ .timeout = 60,
+ .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
^ permalink raw reply related [flat|nested] 4+ messages in thread* [LTP] [PATCH 2/2] cgroup: Add invalid burst test
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-28 5:17 ` Samir Mulani
1 sibling, 0 replies; 4+ messages in thread
From: Samir Mulani @ 2026-09-28 5:17 UTC (permalink / raw)
To: ltp; +Cc: Samir Mulani
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
^ permalink raw reply related [flat|nested] 4+ messages in thread