From f13e11318f7a4f9d7f39556584f367da6cb0d5a9 Mon Sep 17 00:00:00 2001 From: cui fliter Date: Fri, 11 Sep 2026 05:31:03 +0800 Subject: [PATCH 1/2] Fix cgroup v2 memory limit traversal at hierarchy root (#133471) Related to #130092 Fixes https://github.com/dotnet/runtime/issues/133470 Fix managed cgroup v2 memory limit traversal when the current cgroup is exactly the hierarchy mount root. The previous `do/while` loop moved to the parent directory before checking whether the mount root had been reached. This could walk outside the cgroup hierarchy and eventually dereference a null path. The traversal now stops before reading or moving above the hierarchy mount root. Path equality uses ordinal string comparison. Signed-off-by: cuishuang --- .../Interop/Linux/cgroups/Interop.cgroups.cs | 32 ++++++++++++++++--- .../tests/Tests/Interop/cgroupsTests.cs | 30 +++++++++++++++++ 2 files changed, 57 insertions(+), 5 deletions(-) diff --git a/src/libraries/Common/src/Interop/Linux/cgroups/Interop.cgroups.cs b/src/libraries/Common/src/Interop/Linux/cgroups/Interop.cgroups.cs index 973d4c63573e70..8bb0fc3a2d445a 100644 --- a/src/libraries/Common/src/Interop/Linux/cgroups/Interop.cgroups.cs +++ b/src/libraries/Common/src/Interop/Linux/cgroups/Interop.cgroups.cs @@ -116,16 +116,24 @@ internal static bool TryGetMemoryLimitV1(out ulong limit) /// The read limit, or 0 if it couldn't be read. /// true if the limit was read successfully; otherwise, false. internal static bool TryGetMemoryLimitV2(out ulong limit) + { + return TryGetMemoryLimitV2(s_cgroupMemoryPath, s_cgroupMemoryHierarchyMountPath, out limit); + } + + /// Tries to read the memory limit from a cgroup v2 path hierarchy. + /// The current cgroup memory path. + /// The cgroup memory hierarchy mount path. + /// The read limit, or 0 if it couldn't be read. + /// true if the limit was read successfully; otherwise, false. + internal static bool TryGetMemoryLimitV2(string? currentCGroupMemoryPath, string? cgroupMemoryHierarchyMountPath, out ulong limit) { bool foundAnyLimit = false; ulong minLimit = ulong.MaxValue; - string? currentCGroupMemoryPath = s_cgroupMemoryPath; - string? cgroupMemoryHierarchyMountPath = s_cgroupMemoryHierarchyMountPath; if (currentCGroupMemoryPath != null && cgroupMemoryHierarchyMountPath != null) { // Iterate over the directory hierarchy representing the cgroup hierarchy until reaching the - // mount directory. The mount directory doesn't contain the memory.max. - do + // mount directory. The mount directory can contain memory.max when it is not the global root. + while (currentCGroupMemoryPath != null && IsPathAtOrBelowMount(currentCGroupMemoryPath, cgroupMemoryHierarchyMountPath)) { if (TryReadMemoryValueFromFile(currentCGroupMemoryPath + "/memory.max", out ulong currentLevelLimit)) { @@ -135,9 +143,13 @@ internal static bool TryGetMemoryLimitV2(out ulong limit) minLimit = currentLevelLimit; } } + if (string.Equals(currentCGroupMemoryPath, cgroupMemoryHierarchyMountPath, StringComparison.Ordinal)) + { + break; + } + currentCGroupMemoryPath = Path.GetDirectoryName(currentCGroupMemoryPath); } - while (currentCGroupMemoryPath!.Length != cgroupMemoryHierarchyMountPath.Length); } limit = minLimit; @@ -145,6 +157,16 @@ internal static bool TryGetMemoryLimitV2(out ulong limit) return foundAnyLimit; } + private static bool IsPathAtOrBelowMount(string path, string mount) + { + if (string.Equals(path, mount, StringComparison.Ordinal)) + { + return true; + } + + return mount == "/" || path.StartsWith(mount + "/", StringComparison.Ordinal); + } + /// Tries to parse a memory limit from the specified file. /// The path to the file to parse. /// The parsed result, or 0 if it couldn't be parsed. diff --git a/src/libraries/Common/tests/Tests/Interop/cgroupsTests.cs b/src/libraries/Common/tests/Tests/Interop/cgroupsTests.cs index 3ad0b3cee4d150..0d05a6b11b9d70 100644 --- a/src/libraries/Common/tests/Tests/Interop/cgroupsTests.cs +++ b/src/libraries/Common/tests/Tests/Interop/cgroupsTests.cs @@ -34,11 +34,41 @@ public void ValidateTryReadMemoryValue(bool expectedResult, string valueText, ul [Theory] [InlineData("/sys/fs/cgroup/cpu/my_cgroup", "/docker/1234", "/sys/fs/cgroup/cpu", "/docker/1234/my_cgroup")] [InlineData("/sys/fs/cgroup/cpu/my_cgroup", "/", "/sys/fs/cgroup/cpu", "/my_cgroup")] + [InlineData("/sys/fs/cgroup", "/some/container", "/sys/fs/cgroup", "/some/container")] public void ValidateFindCGroupPath(string expectedResult, string hierarchyRoot, string hierarchyMount, string cgroupPathRelativeToMount) { Assert.Equal(expectedResult, Interop.cgroups.FindCGroupPath(hierarchyRoot, hierarchyMount, cgroupPathRelativeToMount)); } + [Fact] + public void ValidateTryGetMemoryLimitV2AtHierarchyMount() + { + string testRoot = GetTestFilePath(); + string hierarchyMount = Path.Combine(testRoot, "mount"); + Directory.CreateDirectory(hierarchyMount); + File.WriteAllText(Path.Combine(hierarchyMount, "memory.max"), "1"); + + Assert.True(Interop.cgroups.TryGetMemoryLimitV2(hierarchyMount, hierarchyMount, out ulong limit)); + Assert.Equal(1UL, limit); + } + + [Fact] + public void ValidateTryGetMemoryLimitV2StopsAtHierarchyMount() + { + string testRoot = GetTestFilePath(); + string hierarchyMount = Path.Combine(testRoot, "mount"); + string parentCGroup = Path.Combine(hierarchyMount, "a"); + string currentCGroup = Path.Combine(parentCGroup, "b"); + Directory.CreateDirectory(currentCGroup); + File.WriteAllText(Path.Combine(hierarchyMount, "memory.max"), "20"); + File.WriteAllText(Path.Combine(parentCGroup, "memory.max"), "20"); + File.WriteAllText(Path.Combine(currentCGroup, "memory.max"), "10"); + File.WriteAllText(Path.Combine(testRoot, "memory.max"), "2"); + + Assert.True(Interop.cgroups.TryGetMemoryLimitV2(currentCGroup, hierarchyMount, out ulong limit)); + Assert.Equal(10UL, limit); + } + [Theory] [InlineData(true, 2, "0 0 0:0 / /foo ignore ignore - cgroup2 cgroup2 ignore", "ignore", "/", "/foo")] [InlineData(true, 2, "0 0 0:0 / /foo ignore ignore - cgroup2 cgroup2 ignore", "memory", "/", "/foo")] From 76beb9bda38845917778e0431867b4e7c9789521 Mon Sep 17 00:00:00 2001 From: Jan Vorlicek Date: Fri, 11 Sep 2026 14:32:47 +0200 Subject: [PATCH 2/2] Reflect PR feedback --- .../src/Interop/Linux/cgroups/Interop.cgroups.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/libraries/Common/src/Interop/Linux/cgroups/Interop.cgroups.cs b/src/libraries/Common/src/Interop/Linux/cgroups/Interop.cgroups.cs index 8bb0fc3a2d445a..513ed5306fa5a7 100644 --- a/src/libraries/Common/src/Interop/Linux/cgroups/Interop.cgroups.cs +++ b/src/libraries/Common/src/Interop/Linux/cgroups/Interop.cgroups.cs @@ -143,7 +143,7 @@ internal static bool TryGetMemoryLimitV2(string? currentCGroupMemoryPath, string minLimit = currentLevelLimit; } } - if (string.Equals(currentCGroupMemoryPath, cgroupMemoryHierarchyMountPath, StringComparison.Ordinal)) + if (currentCGroupMemoryPath == cgroupMemoryHierarchyMountPath) { break; } @@ -159,12 +159,14 @@ internal static bool TryGetMemoryLimitV2(string? currentCGroupMemoryPath, string private static bool IsPathAtOrBelowMount(string path, string mount) { - if (string.Equals(path, mount, StringComparison.Ordinal)) + if (!path.StartsWith(mount, StringComparison.Ordinal)) { - return true; + return false; } - return mount == "/" || path.StartsWith(mount + "/", StringComparison.Ordinal); + return path.Length == mount.Length || + mount == "/" || + path[mount.Length] == '/'; } /// Tries to parse a memory limit from the specified file.