Skip to content

Commit 9017018

Browse files
🤖 fix: cpu.max file not found error in init.scope cgroup (#27)
On systems running RKE2 with sysbox where /proc/self/cgroup reports 0::/init.scope, the code was failing because /sys/fs/cgroup/init.scope/cpu.max doesn't exist. The error occurred because cpuUsed() and cpuTotal() tried to read the CPU period directly from the missing file without falling back to parent cgroups. Changes: - Added cpuPeriod() helper method that follows the same pattern as cpuQuota() - The new method handles fs.ErrNotExist and strconv.ErrSyntax errors - Falls back to parent cgroup when cpu.max is missing (common in system-level cgroups like init.scope) - Updated cpuUsed() to use cpuPeriod() instead of directly reading - Updated cpuTotal() to use cpuPeriod() instead of directly reading - Added test case fsContainerCgroupV2InitScope to verify the fix The fix ensures consistent parent fallback behavior across all values read from cpu.max (quota, period, and usage calculations). Fixes issue where systems report: /sys/fs/cgroup/init.scope/cpu.max file does not exist --- 🤖 PR was written by Claude Sonnet 4.5 Thinking using [Coder Mux](https://github.com/coder/cmux) and reviewed by a human 👩
1 parent c7136b5 commit 9017018

File tree

2 files changed

+112
-5
lines changed

2 files changed

+112
-5
lines changed

cgroupv2.go

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ const (
2525
// Other memory stats - we are interested in total_inactive_file
2626
cgroupV2MemoryStat = "memory.stat"
2727

28+
// Default period for cpu.max as documented in the kernel docs.
29+
// The default period is 100000 microseconds (100ms).
30+
// Ref: https://docs.kernel.org/6.17/admin-guide/cgroup-v2.html#cpu-interface-files
31+
cgroupV2DefaultPeriodUs = 100000
32+
2833
// What is the maximum cgroup depth we support?
2934
// We only expect to see a depth of around 3-4 at max, but we
3035
// allow 10 to give us some headroom. If this limit is reached
@@ -66,18 +71,17 @@ func newCgroupV2Statter(fs afero.Fs, path string, depth int) (*cgroupV2Statter,
6671

6772
func (s cgroupV2Statter) cpuUsed() (used float64, err error) {
6873
cpuStatPath := filepath.Join(s.path, cgroupV2CPUStat)
69-
cpuMaxPath := filepath.Join(s.path, cgroupV2CPUMax)
7074

7175
usageUs, err := readInt64Prefix(s.fs, cpuStatPath, "usage_usec")
7276
if err != nil {
7377
return 0, xerrors.Errorf("get cgroupv2 cpu used: %w", err)
7478
}
75-
periodUs, err := readInt64SepIdx(s.fs, cpuMaxPath, " ", 1)
79+
periodUs, err := s.cpuPeriod()
7680
if err != nil {
7781
return 0, xerrors.Errorf("get cpu period: %w", err)
7882
}
7983

80-
return float64(usageUs) / float64(periodUs), nil
84+
return float64(usageUs) / periodUs, nil
8185
}
8286

8387
func (s cgroupV2Statter) cpuQuota() (float64, error) {
@@ -106,10 +110,35 @@ func (s cgroupV2Statter) cpuQuota() (float64, error) {
106110
return float64(quotaUs), nil
107111
}
108112

109-
func (s cgroupV2Statter) cpuTotal() (total float64, err error) {
113+
func (s cgroupV2Statter) cpuPeriod() (float64, error) {
110114
cpuMaxPath := filepath.Join(s.path, cgroupV2CPUMax)
111115

112116
periodUs, err := readInt64SepIdx(s.fs, cpuMaxPath, " ", 1)
117+
if err != nil {
118+
if !errors.Is(err, strconv.ErrSyntax) && !errors.Is(err, fs.ErrNotExist) {
119+
return 0, xerrors.Errorf("get cpu period: %w", err)
120+
}
121+
122+
// If the value is not a valid integer or the cpu.max file does
123+
// not exist, we call the parent to find its period. This can happen
124+
// in system-level cgroups like init.scope where cpu.max may not exist.
125+
if s.parent != nil {
126+
period, err := s.parent.cpuPeriod()
127+
if err != nil {
128+
return 0, xerrors.Errorf("get parent cpu period: %w", err)
129+
}
130+
return period, nil
131+
}
132+
133+
// No parent and no period found in the cgroup hierarchy.
134+
return cgroupV2DefaultPeriodUs, nil
135+
}
136+
137+
return float64(periodUs), nil
138+
}
139+
140+
func (s cgroupV2Statter) cpuTotal() (total float64, err error) {
141+
periodUs, err := s.cpuPeriod()
113142
if err != nil {
114143
return 0, xerrors.Errorf("get cpu period: %w", err)
115144
}
@@ -119,7 +148,7 @@ func (s cgroupV2Statter) cpuTotal() (total float64, err error) {
119148
return 0, xerrors.Errorf("get cpu quota: %w", err)
120149
}
121150

122-
return float64(quotaUs) / float64(periodUs), nil
151+
return quotaUs / periodUs, nil
123152
}
124153

125154
func (s cgroupV2Statter) memoryMaxBytes() (*float64, error) {

stat_internal_test.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,50 @@ func TestStatter(t *testing.T) {
396396
assert.Equal(t, "cores", cpu.Unit)
397397
})
398398

399+
t.Run("CPU/InitScopeFallback", func(t *testing.T) {
400+
t.Parallel()
401+
402+
// Test RKE2/sysbox scenario where /init.scope cgroup doesn't have
403+
// cpu.max but the root cgroup does. The period should be read from
404+
// the parent (root) cgroup.
405+
fs := initFS(t, fsContainerCgroupV2InitScope)
406+
fakeWait := func(time.Duration) {
407+
mungeFS(t, fs, filepath.Join(cgroupRootPath, "init.scope", cgroupV2CPUStat), "usage_usec 100000")
408+
}
409+
s, err := New(WithFS(fs), withWait(fakeWait), withIsCgroupV2(true))
410+
require.NoError(t, err)
411+
412+
cpu, err := s.ContainerCPU()
413+
require.NoError(t, err)
414+
415+
require.NotNil(t, cpu)
416+
assert.Equal(t, 1.0, cpu.Used)
417+
require.Nil(t, cpu.Total) // quota is "max" so no limit
418+
assert.Equal(t, "cores", cpu.Unit)
419+
})
420+
421+
t.Run("CPU/InitScopeDefaultPeriod", func(t *testing.T) {
422+
t.Parallel()
423+
424+
// Test scenario where cpu.max doesn't exist at any level in the
425+
// hierarchy. Per kernel docs, the default period is 100000us (100ms).
426+
fs := initFS(t, fsContainerCgroupV2InitScopeNoCPUMax)
427+
fakeWait := func(time.Duration) {
428+
mungeFS(t, fs, filepath.Join(cgroupRootPath, "init.scope", cgroupV2CPUStat), "usage_usec 100000")
429+
}
430+
s, err := New(WithFS(fs), withWait(fakeWait), withIsCgroupV2(true))
431+
require.NoError(t, err)
432+
433+
cpu, err := s.ContainerCPU()
434+
require.NoError(t, err)
435+
436+
require.NotNil(t, cpu)
437+
// With default period of 100000us, usage_usec 100000 = 1.0 core
438+
assert.Equal(t, 1.0, cpu.Used)
439+
require.Nil(t, cpu.Total) // no limit anywhere
440+
assert.Equal(t, "cores", cpu.Unit)
441+
})
442+
399443
t.Run("Memory/Limit", func(t *testing.T) {
400444
t.Parallel()
401445

@@ -727,6 +771,40 @@ proc /proc/sys proc ro,nosuid,nodev,noexec,relatime 0 0`,
727771
filepath.Join(cgroupRootPath, fsContainerCgroupV2KubernetesPath, cgroupV2MemoryStat): "inactive_file 268435456",
728772
filepath.Join(cgroupRootPath, fsContainerCgroupV2KubernetesPath, cgroupV2MemoryUsageBytes): "536870912",
729773
}
774+
// fsContainerCgroupV2InitScope simulates RKE2/sysbox environment where
775+
// the cgroup path is /init.scope and cpu.max does not exist at that level
776+
// but does exist at the root cgroup. This tests the parent fallback logic.
777+
fsContainerCgroupV2InitScope = map[string]string{
778+
procOneCgroup: "0::/",
779+
procSelfCgroup: "0::/init.scope",
780+
procMounts: `overlay / overlay rw,relatime,lowerdir=/some/path:/some/path,upperdir=/some/path:/some/path,workdir=/some/path:/some/path 0 0
781+
proc /proc/sys proc ro,nosuid,nodev,noexec,relatime 0 0
782+
sysboxfs /proc/sys sysboxfs rw,nosuid,nodev,noexec,relatime 0 0`,
783+
sysCgroupType: "domain",
784+
785+
// cpu.max purposefully missing at /init.scope level
786+
filepath.Join(cgroupRootPath, cgroupV2CPUMax): "max 100000",
787+
filepath.Join(cgroupRootPath, "init.scope", cgroupV2CPUStat): "usage_usec 0",
788+
filepath.Join(cgroupRootPath, "init.scope", cgroupV2MemoryMaxBytes): "max",
789+
filepath.Join(cgroupRootPath, "init.scope", cgroupV2MemoryStat): "inactive_file 268435456",
790+
filepath.Join(cgroupRootPath, "init.scope", cgroupV2MemoryUsageBytes): "536870912",
791+
}
792+
// fsContainerCgroupV2InitScopeNoCPUMax simulates a scenario where cpu.max
793+
// doesn't exist at any level in the hierarchy. Tests the default period fallback.
794+
fsContainerCgroupV2InitScopeNoCPUMax = map[string]string{
795+
procOneCgroup: "0::/",
796+
procSelfCgroup: "0::/init.scope",
797+
procMounts: `overlay / overlay rw,relatime,lowerdir=/some/path:/some/path,upperdir=/some/path:/some/path,workdir=/some/path:/some/path 0 0
798+
proc /proc/sys proc ro,nosuid,nodev,noexec,relatime 0 0
799+
sysboxfs /proc/sys sysboxfs rw,nosuid,nodev,noexec,relatime 0 0`,
800+
sysCgroupType: "domain",
801+
802+
// cpu.max purposefully missing at all levels to test default period
803+
filepath.Join(cgroupRootPath, "init.scope", cgroupV2CPUStat): "usage_usec 0",
804+
filepath.Join(cgroupRootPath, "init.scope", cgroupV2MemoryMaxBytes): "max",
805+
filepath.Join(cgroupRootPath, "init.scope", cgroupV2MemoryStat): "inactive_file 268435456",
806+
filepath.Join(cgroupRootPath, "init.scope", cgroupV2MemoryUsageBytes): "536870912",
807+
}
730808
fsContainerCgroupV1 = map[string]string{
731809
procOneCgroup: "0::/docker/aa86ac98959eeedeae0ecb6e0c9ddd8ae8b97a9d0fdccccf7ea7a474f4e0bb1f",
732810
procSelfCgroup: "0::/docker/aa86ac98959eeedeae0ecb6e0c9ddd8ae8b97a9d0fdccccf7ea7a474f4e0bb1f",

0 commit comments

Comments
 (0)