[PATCH AUTOSEL 6.19-6.18] cgroup/cpuset: Don't fail cpuset.cpus change in v2

Sasha Levin
2026-02-14T01:06:52Z

From: Waiman Long <longman@redhat.com>

[ Upstream commit 6e6f13f6d5095f3a432da421e78f4d7d51ef39c8 ]

Commit fe8cd2736e75 ("cgroup/cpuset: Delay setting of CS_CPU_EXCLUSIVE
until valid partition") introduced a new check to disallow the setting
of a new cpuset.cpus.exclusive value that is a superset of a sibling's
cpuset.cpus value so that there will at least be one CPU left in the
sibling in case the cpuset becomes a valid partition root. This new
check does have the side effect of failing a cpuset.cpus change that
make it a subset of a sibling's cpuset.cpus.exclusive value.

With v2, users are supposed to be allowed to set whatever value they
want in cpuset.cpus without failure. To maintain this rule, the check
is now restricted to only when cpuset.cpus.exclusive is being changed
not when cpuset.cpus is changed.

The cgroup-v2.rst doc file is also updated to reflect this change.

Signed-off-by: Waiman Long <longman@redhat.com>
Reviewed-by: Chen Ridong <chenridong@huawei.com>
Signed-off-by: Tejun Heo <tj@kernel.org>
Signed-off-by: Sasha Levin <sashal@kernel.org>
---

LLM Generated explanations, may be completely bogus:

This is important. The `cpus_excl_conflict()` helper function was only
introduced in v6.18-rc1 (commit `8daab66eb329`). The current fix
**modifies this helper**. This means the fix as-is only applies cleanly
to v6.18+. For earlier stable trees (6.11.y through 6.17.y), the bug
exists but the code structure is different — the fix would need to be
adapted to the inline code in `validate_change()` directly.

### 8. SUMMARY

**What the bug is**: In cgroup v2, changing `cpuset.cpus` to a value
that happens to be a subset of a sibling's `cpuset.cpus.exclusive`
incorrectly fails with `-EINVAL`. This violates the cgroup v2 API
contract that `cpuset.cpus` can be set to any value without failure.

**Who is affected**: Any cgroup v2 user (containers, Kubernetes,
systemd) who has cpusets with `cpuset.cpus.exclusive` set and tries to
change `cpuset.cpus` on a sibling.

**Severity**: Medium-high. It's a regression that breaks expected
behavior — operations that should succeed are failing. While not a crash
or data corruption, it can break container orchestration workflows.

**Fix quality**: The fix is small, well-understood, and correctly
targeted. It adds a boolean guard (`xcpus_changed`) so the restrictive
check only applies when `cpuset.cpus.exclusive` is being changed, not
when `cpuset.cpus` is being changed.

**Risk**: Low. The change makes validation less restrictive in a
controlled way. It's authored by the cgroup/cpuset maintainer and
reviewed.

**Dependencies**: The fix as written depends on the
`cpus_excl_conflict()` helper introduced in v6.18-rc1 (`8daab66eb329`).
For stable trees before 6.18, the fix would need adaptation. However,
the underlying bug exists since v6.11.

### Decision

This is a clear regression fix for a user-visible behavioral change in
cgroup v2. It's small, well-scoped, fixes a real API contract violation
that affects container workloads, authored by the subsystem maintainer,
and reviewed. The only concern is the dependency on the helper
refactoring commit for clean backporting to pre-6.18 stable trees, but
that's a mechanical backporting issue, not a reason to reject. The fix
meets all stable kernel criteria.

**YES**

Documentation/admin-guide/cgroup-v2.rst | 8 +++----
kernel/cgroup/cpuset.c | 30 ++++++++++++-------------
2 files changed, 19 insertions(+), 19 deletions(-)

diff --git a/Documentation/admin-guide/cgroup-v2.rst b/Documentation/admin-guide/cgroup-v2.rst
index 7f5b59d95fce5..510df2461aff2 100644
--- a/Documentation/admin-guide/cgroup-v2.rst
+++ b/Documentation/admin-guide/cgroup-v2.rst
@@ -2561,10 +2561,10 @@ Cpuset Interface Files
Users can manually set it to a value that is different from
"cpuset.cpus". One constraint in setting it is that the list of
CPUs must be exclusive with respect to "cpuset.cpus.exclusive"
- of its sibling. If "cpuset.cpus.exclusive" of a sibling cgroup
- isn't set, its "cpuset.cpus" value, if set, cannot be a subset
- of it to leave at least one CPU available when the exclusive
- CPUs are taken away.
+ and "cpuset.cpus.exclusive.effective" of its siblings. Another
+ constraint is that it cannot be a superset of "cpuset.cpus"
+ of its sibling in order to leave at least one CPU available to
+ that sibling when the exclusive CPUs are taken away.

For a parent cgroup, any one of its exclusive CPUs can only
be distributed to at most one of its child cgroups. Having an
diff --git a/kernel/cgroup/cpuset.c b/kernel/cgroup/cpuset.c
index c06e2e96f79dc..dc3ac38c5d160 100644
--- a/kernel/cgroup/cpuset.c
+++ b/kernel/cgroup/cpuset.c
@@ -603,33 +603,31 @@ static inline bool cpusets_are_exclusive(struct cpuset *cs1, struct cpuset *cs2)

/**
* cpus_excl_conflict - Check if two cpusets have exclusive CPU conflicts
- * @cs1: first cpuset to check
- * @cs2: second cpuset to check
+ * @trial: the trial cpuset to be checked
+ * @sibling: a sibling cpuset to be checked against
+ * @xcpus_changed: set if exclusive_cpus has been set
*
* Returns: true if CPU exclusivity conflict exists, false otherwise
*
* Conflict detection rules:
* 1. If either cpuset is CPU exclusive, they must be mutually exclusive
* 2. exclusive_cpus masks cannot intersect between cpusets
- * 3. The allowed CPUs of one cpuset cannot be a subset of another's exclusive CPUs
+ * 3. The allowed CPUs of a sibling cpuset cannot be a subset of the new exclusive CPUs
*/
-static inline bool cpus_excl_conflict(struct cpuset *cs1, struct cpuset *cs2)
+static inline bool cpus_excl_conflict(struct cpuset *trial, struct cpuset *sibling,
+ bool xcpus_changed)
{
/* If either cpuset is exclusive, check if they are mutually exclusive */
- if (is_cpu_exclusive(cs1) || is_cpu_exclusive(cs2))
- return !cpusets_are_exclusive(cs1, cs2);
+ if (is_cpu_exclusive(trial) || is_cpu_exclusive(sibling))
+ return !cpusets_are_exclusive(trial, sibling);

/* Exclusive_cpus cannot intersect */
- if (cpumask_intersects(cs1->exclusive_cpus, cs2->exclusive_cpus))
+ if (cpumask_intersects(trial->exclusive_cpus, sibling->exclusive_cpus))
return true;

- /* The cpus_allowed of one cpuset cannot be a subset of another cpuset's exclusive_cpus */
- if (!cpumask_empty(cs1->cpus_allowed) &&
- cpumask_subset(cs1->cpus_allowed, cs2->exclusive_cpus))
- return true;
-
- if (!cpumask_empty(cs2->cpus_allowed) &&
- cpumask_subset(cs2->cpus_allowed, cs1->exclusive_cpus))
+ /* The cpus_allowed of a sibling cpuset cannot be a subset of the new exclusive_cpus */
+ if (xcpus_changed && !cpumask_empty(sibling->cpus_allowed) &&
+ cpumask_subset(sibling->cpus_allowed, trial->exclusive_cpus))
return true;

return false;
@@ -666,6 +664,7 @@ static int validate_change(struct cpuset *cur, struct cpuset *trial)
{
struct cgroup_subsys_state *css;
struct cpuset *c, *par;
+ bool xcpus_changed;
int ret = 0;

rcu_read_lock();
@@ -722,10 +721,11 @@ static int validate_change(struct cpuset *cur, struct cpuset *trial)
* overlap. exclusive_cpus cannot overlap with each other if set.
*/
ret = -EINVAL;
+ xcpus_changed = !cpumask_equal(cur->exclusive_cpus, trial->exclusive_cpus);
cpuset_for_each_child(c, css, par) {
if (c == cur)
continue;
- if (cpus_excl_conflict(trial, c))
+ if (cpus_excl_conflict(trial, c, xcpus_changed))
goto out;
if (mems_excl_conflict(trial, c))
goto out;
--
2.51.0