Repository: containerd/cgroups

Issue:
Unable to parse "max 0" as a uint from Cgroup file "/sys/fs/cgroup/.../hugetlb.<size>.events"
This line:
https://github.com/containerd/cgroups/blame/f1e92d84a45c1c1ae283505f5b680156687ef2f0/cgroup2/utils.go#L426
fails as the events file has a different structure `max <num>`, resulting in this log being written to stdout:
```
time="2025-11-10T13:33:16Z" level=error msg="unable to parse \"max 0\" as a uint from Cgroup file \"/sys/fs/cgroup/system.slice/docker-9b73966a7d4f6e4aaf14dac40d40fdfd25f4531882698e8ddd56a3666b4dc95e.scope/hugetlb.2MB.events\""
```

File for example:
```
cat /sys/fs/cgroup/system.slice/docker-9b73966a7d4f6e4aaf14dac40d40fdfd25f4531882698e8ddd56a3666b4dc95e.scope/hugetlb.1GB.events
max 0
```


The file structure:
https://docs.kernel.org/admin-guide/cgroup-v2.html#hugetlb-interface-files:~:text=cgroup%20except%20root.-,hugetlb.%3Chugepagesize%3E.events,-A%20read%2Donly

Hints from issue comments:
@voidbar: @akhilerm / @AkihiroSuda - is it possible to release a new version to fix downstream users from having this annoying log?
`level=error msg="unable to parse \"max 0\" as a uint from Cgroup file \"sys/fs/cgroup/wiz-disk-scanner/hugetlb.2MB.events\"`@AkihiroSuda: done
https://github.com/containerd/cgroups/releases/tag/v3.1.2@rpardini: Hitting this with `containerd github.com/containerd/containerd/v2 v2.2.0 1c4457e00facac03ce1d75f7b6777a7a851e5c41` so I guess it also needs a bump+release...?

Solution patch:
diff --git a/cgroup2/utils.go b/cgroup2/utils.go
index 4bdeac9..a48213d 100644
--- a/cgroup2/utils.go
+++ b/cgroup2/utils.go
@@ -293,6 +293,31 @@ func getStatFileContentUint64(filePath string) uint64 {
 	return res
 }
 
+// getKVStatsFileContentUint64 gets uint64 parsed content of key-value cgroup stat file
+func getKVStatsFileContentUint64(filePath string, propertyName string) uint64 {
+	f, err := os.Open(filePath)
+	if err != nil {
+		return 0
+	}
+	defer f.Close()
+
+	s := bufio.NewScanner(f)
+	for s.Scan() {
+		name, value, err := parseKV(s.Text())
+		if name == propertyName {
+			if err != nil {
+				log.L.WithError(err).Errorf("unable to parse %q as a uint from Cgroup file %q", propertyName, filePath)
+				return 0
+			}
+			return value
+		}
+	}
+	if err = s.Err(); err != nil {
+		log.L.WithError(err).Errorf("error reading Cgroup file %q for property %q", filePath, propertyName)
+	}
+	return 0
+}
+
 func readIoStats(path string) []*stats.IOEntry {
 	// more details on the io.stat file format: https://www.kernel.org/doc/Documentation/cgroup-v2.txt
 	var usage []*stats.IOEntry
@@ -423,7 +448,7 @@ func readHugeTlbStats(path string) []*stats.HugeTlbStat {
 			Max:      getStatFileContentUint64(filepath.Join(path, "hugetlb."+pagesize+".max")),
 			Current:  getStatFileContentUint64(filepath.Join(path, "hugetlb."+pagesize+".current")),
 			Pagesize: pagesize,
-			Failcnt:  getStatFileContentUint64(filepath.Join(path, "hugetlb."+pagesize+".events")),
+			Failcnt:  getKVStatsFileContentUint64(filepath.Join(path, "hugetlb."+pagesize+".events"), "max"),
 		}
 	}
 	return usage



Test patch:
diff --git a/cgroup2/utils_test.go b/cgroup2/utils_test.go
index f481fb4..0a2028c 100644
--- a/cgroup2/utils_test.go
+++ b/cgroup2/utils_test.go
@@ -139,3 +139,101 @@ func BenchmarkGetStatFileContentUint64(b *testing.B) {
 		_ = getStatFileContentUint64("/proc/self/loginuid")
 	}
 }
+
+func TestGetKVStatsFileContentUint64(t *testing.T) {
+	testCases := []struct {
+		name         string
+		fileContent  string
+		propertyName string
+		expected     uint64
+	}{
+		{
+			name: "valid property found",
+			fileContent: `some_key 123
+another_key 456
+third_key 789`,
+			propertyName: "another_key",
+			expected:     456,
+		},
+		{
+			name: "property at beginning",
+			fileContent: `first_key 999
+second_key 888`,
+			propertyName: "first_key",
+			expected:     999,
+		},
+		{
+			name: "property at end",
+			fileContent: `first_key 111
+second_key 222
+third_key 333`,
+			propertyName: "third_key",
+			expected:     333,
+		},
+		{
+			name: "property not found",
+			fileContent: `key1 100
+key2 200`,
+			propertyName: "missing_key",
+			expected:     0,
+		},
+		{
+			name: "invalid format - single field",
+			fileContent: `invalid_line
+valid_key 42`,
+			propertyName: "valid_key",
+			expected:     42,
+		},
+		{
+			name: "invalid format - three fields",
+			fileContent: `invalid key 1 2
+valid_key 100`,
+			propertyName: "valid_key",
+			expected:     100,
+		},
+		{
+			name: "invalid number format",
+			fileContent: `key1 not_a_number
+key2 500`,
+			propertyName: "key1",
+			expected:     0,
+		},
+		{
+			name:         "empty file",
+			fileContent:  "",
+			propertyName: "any_key",
+			expected:     0,
+		},
+		{
+			name:         "large number",
+			fileContent:  `max 18446744073709551615`,
+			propertyName: "max",
+			expected:     18446744073709551615,
+		},
+		{
+			name: "zero value",
+			fileContent: `zero 0
+non_zero 1`,
+			propertyName: "zero",
+			expected:     0,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			tmpDir := t.TempDir()
+			filePath := filepath.Join(tmpDir, "test.stat")
+
+			err := os.WriteFile(filePath, []byte(tc.fileContent), 0o644)
+			assert.NoError(t, err)
+
+			result := getKVStatsFileContentUint64(filePath, tc.propertyName)
+			assert.Equal(t, tc.expected, result)
+		})
+	}
+
+	t.Run("file not found", func(t *testing.T) {
+		result := getKVStatsFileContentUint64("/nonexistent/path/file.stat", "any_key")
+		assert.Equal(t, uint64(0), result)
+	})
+}