From e21361b0e2ec2dac51e7df3914c131dbd2f1a326 Mon Sep 17 00:00:00 2001 From: Sohan Kunkerkar Date: Sat, 25 Apr 2026 02:09:13 -0400 Subject: [PATCH] systemd: add SetUnified for direct cgroupfs writes bypassing dbus Add SetUnified method on UnifiedManager that writes unified cgroup v2 resource values directly to cgroupfs without going through systemd's SetUnitProperties dbus path. When callers use Set() to update specific unified keys (e.g. memory.min, memory.low), the current implementation bundles those updates with all other resource properties into a single SetUnitProperties dbus call. This can cause systemd to reset unrelated cgroup properties on the unit. SetUnified avoids this by writing only the specified unified values via the existing fs2 manager path. This is needed by kubelet to clear stale MemoryQoS cgroup protection values during feature rollback without triggering systemd side effects on CPU and memory limit properties. Signed-off-by: Sohan Kunkerkar --- systemd/systemd_test.go | 77 +++++++++++++++++++++++++++++++++++++++++ systemd/v2.go | 13 +++++++ 2 files changed, 90 insertions(+) diff --git a/systemd/systemd_test.go b/systemd/systemd_test.go index 60a6c1f..4a54245 100644 --- a/systemd/systemd_test.go +++ b/systemd/systemd_test.go @@ -3,6 +3,7 @@ package systemd import ( "os" "reflect" + "strings" "testing" systemdDbus "github.com/coreos/go-systemd/v22/dbus" @@ -97,6 +98,82 @@ func TestUnitExistsIgnored(t *testing.T) { } } +func TestSetUnified(t *testing.T) { + if !IsRunningSystemd() { + t.Skip("Test requires systemd.") + } + if !cgroups.IsCgroup2UnifiedMode() { + t.Skip("Test requires cgroup v2.") + } + if os.Geteuid() != 0 { + t.Skip("Test requires root.") + } + + testCases := []struct { + name string + file string + setVal string + clearVal string + }{ + { + name: "memory.min", + file: "memory.min", + setVal: "4096", + clearVal: "0", + }, + { + name: "memory.low", + file: "memory.low", + setVal: "4096", + clearVal: "0", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + config := &cgroups.Cgroup{ + Parent: "system.slice", + Name: "system-runc_test_set_unified_" + tc.name + ".slice", + Resources: &cgroups.Resources{ + Unified: map[string]string{tc.file: tc.setVal}, + }, + } + m, err := NewUnifiedManager(config, "") + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = m.Destroy() }) + + if err := m.Apply(-1); err != nil { + t.Fatal(err) + } + if err := m.Set(config.Resources); err != nil { + t.Fatal(err) + } + + val, err := cgroups.ReadFile(m.Path(""), tc.file) + if err != nil { + t.Fatal(err) + } + if v := strings.TrimSpace(val); v != tc.setVal { + t.Fatalf("after Set: expected %s=%s, got %q", tc.file, tc.setVal, v) + } + + if err := m.SetUnified(map[string]string{tc.file: tc.clearVal}); err != nil { + t.Fatal(err) + } + + val, err = cgroups.ReadFile(m.Path(""), tc.file) + if err != nil { + t.Fatal(err) + } + if v := strings.TrimSpace(val); v != tc.clearVal { + t.Fatalf("after SetUnified: expected %s=%s, got %q", tc.file, tc.clearVal, v) + } + }) + } +} + func TestUnifiedResToSystemdProps(t *testing.T) { if !IsRunningSystemd() { t.Skip("Test requires systemd.") diff --git a/systemd/v2.go b/systemd/v2.go index f76c93e..4f7fce9 100644 --- a/systemd/v2.go +++ b/systemd/v2.go @@ -516,6 +516,19 @@ func (m *UnifiedManager) Set(r *cgroups.Resources) error { return m.fsMgr.Set(r) } +// SetUnified writes unified cgroup v2 resource values directly to cgroupfs, +// bypassing systemd's SetUnitProperties. This avoids the side effect where +// systemd may reset unrelated cgroup properties when processing a property +// update via dbus. +func (m *UnifiedManager) SetUnified(unified map[string]string) error { + for k, v := range unified { + if err := cgroups.WriteFileByLine(m.path, k, v); err != nil { + return fmt.Errorf("unable to set unified resource %q: %w", k, err) + } + } + return nil +} + func (m *UnifiedManager) GetPaths() map[string]string { paths := make(map[string]string, 1) paths[""] = m.path