8343191: Cgroup v1 subsystem fails to set subsystem path

Co-authored-by: Severin Gehwolf <sgehwolf@openjdk.org>
Reviewed-by: sgehwolf, mbaesken
This commit is contained in:
Sergey Chernyshev 2025-03-05 10:32:36 +00:00 committed by Dmitry Chuyko
parent 75f028b46b
commit de29ef3bf3
9 changed files with 488 additions and 42 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2024, Red Hat, Inc.
* Copyright (c) 2024, 2025, Red Hat, Inc.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -49,12 +49,18 @@ int CgroupUtil::processor_count(CgroupCpuController* cpu_ctrl, int host_cpus) {
}
void CgroupUtil::adjust_controller(CgroupMemoryController* mem) {
assert(mem->cgroup_path() != nullptr, "invariant");
if (strstr(mem->cgroup_path(), "../") != nullptr) {
log_warning(os, container)("Cgroup memory controller path at '%s' seems to have moved to '%s', detected limits won't be accurate",
mem->mount_point(), mem->cgroup_path());
mem->set_subsystem_path("/");
return;
}
if (!mem->needs_hierarchy_adjustment()) {
// nothing to do
return;
}
log_trace(os, container)("Adjusting controller path for memory: %s", mem->subsystem_path());
assert(mem->cgroup_path() != nullptr, "invariant");
char* orig = os::strdup(mem->cgroup_path());
char* cg_path = os::strdup(orig);
char* last_slash;
@ -62,7 +68,8 @@ void CgroupUtil::adjust_controller(CgroupMemoryController* mem) {
julong phys_mem = os::Linux::physical_memory();
char* limit_cg_path = nullptr;
jlong limit = mem->read_memory_limit_in_bytes(phys_mem);
jlong lowest_limit = phys_mem;
jlong lowest_limit = limit < 0 ? phys_mem : limit;
julong orig_limit = ((julong)lowest_limit) != phys_mem ? lowest_limit : phys_mem;
while ((last_slash = strrchr(cg_path, '/')) != cg_path) {
*last_slash = '\0'; // strip path
// update to shortened path and try again
@ -83,7 +90,7 @@ void CgroupUtil::adjust_controller(CgroupMemoryController* mem) {
limit_cg_path = os::strdup("/");
}
assert(lowest_limit >= 0, "limit must be positive");
if ((julong)lowest_limit != phys_mem) {
if ((julong)lowest_limit != orig_limit) {
// we've found a lower limit anywhere in the hierarchy,
// set the path to the limit path
assert(limit_cg_path != nullptr, "limit path must be set");
@ -93,6 +100,7 @@ void CgroupUtil::adjust_controller(CgroupMemoryController* mem) {
mem->subsystem_path(),
lowest_limit);
} else {
log_trace(os, container)("Lowest limit was: " JLONG_FORMAT, lowest_limit);
log_trace(os, container)("No lower limit found for memory in hierarchy %s, "
"adjusting to original path %s",
mem->mount_point(), orig);
@ -104,19 +112,26 @@ void CgroupUtil::adjust_controller(CgroupMemoryController* mem) {
}
void CgroupUtil::adjust_controller(CgroupCpuController* cpu) {
assert(cpu->cgroup_path() != nullptr, "invariant");
if (strstr(cpu->cgroup_path(), "../") != nullptr) {
log_warning(os, container)("Cgroup cpu controller path at '%s' seems to have moved to '%s', detected limits won't be accurate",
cpu->mount_point(), cpu->cgroup_path());
cpu->set_subsystem_path("/");
return;
}
if (!cpu->needs_hierarchy_adjustment()) {
// nothing to do
return;
}
log_trace(os, container)("Adjusting controller path for cpu: %s", cpu->subsystem_path());
assert(cpu->cgroup_path() != nullptr, "invariant");
char* orig = os::strdup(cpu->cgroup_path());
char* cg_path = os::strdup(orig);
char* last_slash;
assert(cg_path[0] == '/', "cgroup path must start with '/'");
int host_cpus = os::Linux::active_processor_count();
int cpus = CgroupUtil::processor_count(cpu, host_cpus);
int lowest_limit = host_cpus;
int lowest_limit = cpus < host_cpus ? cpus: host_cpus;
int orig_limit = lowest_limit != host_cpus ? lowest_limit : host_cpus;
char* limit_cg_path = nullptr;
while ((last_slash = strrchr(cg_path, '/')) != cg_path) {
*last_slash = '\0'; // strip path
@ -138,7 +153,7 @@ void CgroupUtil::adjust_controller(CgroupCpuController* cpu) {
limit_cg_path = os::strdup(cg_path);
}
assert(lowest_limit >= 0, "limit must be positive");
if (lowest_limit != host_cpus) {
if (lowest_limit != orig_limit) {
// we've found a lower limit anywhere in the hierarchy,
// set the path to the limit path
assert(limit_cg_path != nullptr, "limit path must be set");
@ -148,6 +163,7 @@ void CgroupUtil::adjust_controller(CgroupCpuController* cpu) {
cpu->subsystem_path(),
lowest_limit);
} else {
log_trace(os, container)("Lowest limit was: %d", lowest_limit);
log_trace(os, container)("No lower limit found for cpu in hierarchy %s, "
"adjusting to original path %s",
cpu->mount_point(), orig);

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019, 2024, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -37,6 +37,47 @@
/*
* Set directory to subsystem specific files based
* on the contents of the mountinfo and cgroup files.
*
* The method determines whether it runs in
* - host mode
* - container mode
*
* In the host mode, _root is equal to "/" and
* the subsystem path is equal to the _mount_point path
* joined with cgroup_path.
*
* In the container mode, it can be two possibilities:
* - private namespace (cgroupns=private)
* - host namespace (cgroupns=host, default mode in cgroup V1 hosts)
*
* Private namespace is equivalent to the host mode, i.e.
* the subsystem path is set by concatenating
* _mount_point and cgroup_path.
*
* In the host namespace, _root is equal to host's cgroup path
* of the control group to which the containerized process
* belongs to at the moment of creation. The mountinfo and
* cgroup files are mirrored from the host, while the subsystem
* specific files are mapped directly at _mount_point, i.e.
* at /sys/fs/cgroup/<controller>/, the subsystem path is
* then set equal to _mount_point.
*
* A special case of the subsystem path is when a cgroup path
* includes a subgroup, when a containerized process was associated
* with an existing cgroup, that is different from cgroup
* in which the process has been created.
* Here, the _root is equal to the host's initial cgroup path,
* cgroup_path will be equal to host's new cgroup path.
* As host cgroup hierarchies are not accessible in the container,
* it needs to be determined which part of cgroup path
* is accessible inside container, i.e. mapped under
* /sys/fs/cgroup/<controller>/<subgroup>.
* In Docker default setup, host's cgroup path can be
* of the form: /docker/<CONTAINER_ID>/<subgroup>,
* from which only <subgroup> is mapped.
* The method trims cgroup path from left, until the subgroup
* component is found. The subsystem path will be set to
* the _mount_point joined with the subgroup path.
*/
void CgroupV1Controller::set_subsystem_path(const char* cgroup_path) {
if (_cgroup_path != nullptr) {
@ -49,28 +90,36 @@ void CgroupV1Controller::set_subsystem_path(const char* cgroup_path) {
_cgroup_path = os::strdup(cgroup_path);
stringStream ss;
if (_root != nullptr && cgroup_path != nullptr) {
ss.print_raw(_mount_point);
if (strcmp(_root, "/") == 0) {
ss.print_raw(_mount_point);
// host processes and containers with cgroupns=private
if (strcmp(cgroup_path,"/") != 0) {
ss.print_raw(cgroup_path);
}
_path = os::strdup(ss.base());
} else {
if (strcmp(_root, cgroup_path) == 0) {
ss.print_raw(_mount_point);
_path = os::strdup(ss.base());
} else {
char *p = strstr((char*)cgroup_path, _root);
if (p != nullptr && p == _root) {
if (strlen(cgroup_path) > strlen(_root)) {
ss.print_raw(_mount_point);
const char* cg_path_sub = cgroup_path + strlen(_root);
ss.print_raw(cg_path_sub);
_path = os::strdup(ss.base());
// containers with cgroupns=host, default setting is _root==cgroup_path
if (strcmp(_root, cgroup_path) != 0) {
if (*cgroup_path != '\0' && strcmp(cgroup_path, "/") != 0) {
// When moved to a subgroup, between subgroups, the path suffix will change.
const char *suffix = cgroup_path;
while (suffix != nullptr) {
stringStream pp;
pp.print_raw(_mount_point);
pp.print_raw(suffix);
if (os::file_exists(pp.base())) {
ss.print_raw(suffix);
if (suffix != cgroup_path) {
log_trace(os, container)("set_subsystem_path: cgroup v1 path reduced to: %s.", suffix);
}
break;
}
log_trace(os, container)("set_subsystem_path: skipped non-existent directory: %s.", suffix);
suffix = strchr(suffix + 1, '/');
}
}
}
}
_path = os::strdup(ss.base());
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2020, 2022, Red Hat Inc.
* Copyright (c) 2020, 2025, Red Hat Inc.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -292,6 +292,10 @@ jlong memory_swap_limit_value(CgroupV2Controller* ctrl) {
}
void CgroupV2Controller::set_subsystem_path(const char* cgroup_path) {
if (_cgroup_path != nullptr) {
os::free(_cgroup_path);
}
_cgroup_path = os::strdup(cgroup_path);
if (_path != nullptr) {
os::free(_path);
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2018, 2022, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -25,6 +25,9 @@
package jdk.internal.platform.cgroupv1;
import java.lang.System.Logger.Level;
import java.nio.file.Path;
import java.nio.file.Files;
import jdk.internal.platform.CgroupSubsystem;
import jdk.internal.platform.CgroupSubsystemController;
@ -44,27 +47,36 @@ public class CgroupV1SubsystemController implements CgroupSubsystemController {
public void setPath(String cgroupPath) {
if (root != null && cgroupPath != null) {
String path = mountPoint;
if (root.equals("/")) {
// host processes and containers with cgroupns=private
if (!cgroupPath.equals("/")) {
path = mountPoint + cgroupPath;
path += cgroupPath;
}
else {
path = mountPoint;
}
}
else {
if (root.equals(cgroupPath)) {
path = mountPoint;
}
else {
if (cgroupPath.startsWith(root)) {
if (cgroupPath.length() > root.length()) {
String cgroupSubstr = cgroupPath.substring(root.length());
path = mountPoint + cgroupSubstr;
} else {
// containers with cgroupns=host, default setting is _root==cgroup_path
if (!cgroupPath.equals(root)) {
if (!cgroupPath.equals("") && !cgroupPath.equals("/")) {
// When moved to a subgroup, between subgroups, the path suffix will change.
Path cgp = Path.of(cgroupPath);
int nameCount = cgp.getNameCount();
for (int i=0; i < nameCount; i++) {
Path dir = Path.of(mountPoint, cgp.toString());
if (Files.isDirectory(dir)) {
path = dir.toString();
if (i > 0) {
System.getLogger("jdk.internal.platform").log(Level.DEBUG, String.format(
"Cgroup v1 path reduced to: %s.", cgp));
}
break;
}
int currentNameCount = cgp.getNameCount();
cgp = (currentNameCount > 1) ? cgp.subpath(1, currentNameCount) : Path.of("");
}
}
}
}
this.path = path;
}
}

View File

@ -25,6 +25,7 @@
#include "runtime/os.hpp"
#include "cgroupSubsystem_linux.hpp"
#include "cgroupUtil_linux.hpp"
#include "cgroupV1Subsystem_linux.hpp"
#include "cgroupV2Subsystem_linux.hpp"
#include "unittest.hpp"
@ -432,9 +433,16 @@ TEST(cgroupTest, set_cgroupv1_subsystem_path) {
"/user.slice/user-1000.slice/user@1000.service", // cgroup_path
"/sys/fs/cgroup/mem" // expected_path
};
int length = 2;
TestCase container_moving_cgroup = {
"/sys/fs/cgroup/cpu,cpuacct", // mount_path
"/system.slice/garden.service/garden/good/2f57368b-0eda-4e52-64d8-af5c", // root_path
"/system.slice/garden.service/garden/bad/2f57368b-0eda-4e52-64d8-af5c", // cgroup_path
"/sys/fs/cgroup/cpu,cpuacct" // expected_path
};
int length = 3;
TestCase* testCases[] = { &host,
&container_engine };
&container_engine,
&container_moving_cgroup };
for (int i = 0; i < length; i++) {
CgroupV1Controller* ctrl = new CgroupV1Controller( (char*)testCases[i]->root_path,
(char*)testCases[i]->mount_path,
@ -444,6 +452,72 @@ TEST(cgroupTest, set_cgroupv1_subsystem_path) {
}
}
TEST(cgroupTest, set_cgroupv1_subsystem_path_adjusted) {
TestCase memory = {
"/sys/fs/cgroup/memory", // mount_path
"/", // root_path
"../test1", // cgroup_path
"/sys/fs/cgroup/memory" // expected_path
};
TestCase cpu = {
"/sys/fs/cgroup/cpu", // mount_path
"/", // root_path
"../../test2", // cgroup_path
"/sys/fs/cgroup/cpu" // expected_path
};
CgroupCpuController* ccc = new CgroupV1CpuController(CgroupV1Controller((char*)cpu.root_path,
(char*)cpu.mount_path,
true /* read-only mount */));
ccc->set_subsystem_path((char*)cpu.cgroup_path);
EXPECT_TRUE(ccc->needs_hierarchy_adjustment());
CgroupUtil::adjust_controller(ccc);
ASSERT_STREQ(cpu.expected_path, ccc->subsystem_path());
EXPECT_FALSE(ccc->needs_hierarchy_adjustment());
CgroupMemoryController* cmc = new CgroupV1MemoryController(CgroupV1Controller((char*)memory.root_path,
(char*)memory.mount_path,
true /* read-only mount */));
cmc->set_subsystem_path((char*)memory.cgroup_path);
EXPECT_TRUE(cmc->needs_hierarchy_adjustment());
CgroupUtil::adjust_controller(cmc);
ASSERT_STREQ(memory.expected_path, cmc->subsystem_path());
EXPECT_FALSE(cmc->needs_hierarchy_adjustment());
}
TEST(cgroupTest, set_cgroupv2_subsystem_path_adjusted) {
TestCase memory = {
"/sys/fs/cgroup", // mount_path
"/", // root_path
"../test1", // cgroup_path
"/sys/fs/cgroup" // expected_path
};
TestCase cpu = {
"/sys/fs/cgroup", // mount_path
"/", // root_path
"../../test2", // cgroup_path
"/sys/fs/cgroup" // expected_path
};
CgroupCpuController* ccc = new CgroupV2CpuController(CgroupV2Controller((char*)cpu.mount_path,
(char*)cpu.cgroup_path,
true /* read-only mount */));
EXPECT_TRUE(ccc->needs_hierarchy_adjustment());
CgroupUtil::adjust_controller(ccc);
ASSERT_STREQ(cpu.expected_path, ccc->subsystem_path());
EXPECT_FALSE(ccc->needs_hierarchy_adjustment());
CgroupMemoryController* cmc = new CgroupV2MemoryController(CgroupV2Controller((char*)memory.mount_path,
(char*)memory.cgroup_path,
true /* read-only mount */));
EXPECT_TRUE(cmc->needs_hierarchy_adjustment());
CgroupUtil::adjust_controller(cmc);
ASSERT_STREQ(memory.expected_path, cmc->subsystem_path());
EXPECT_FALSE(cmc->needs_hierarchy_adjustment());
}
TEST(cgroupTest, set_cgroupv2_subsystem_path) {
TestCase at_mount_root = {
"/sys/fs/cgroup", // mount_path

View File

@ -0,0 +1,126 @@
/*
* Copyright (C) 2025, BELLSOFT. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
import jdk.test.lib.containers.docker.Common;
import jdk.test.lib.containers.docker.DockerTestUtils;
import jdk.test.lib.containers.docker.DockerRunOptions;
import jdk.test.lib.process.OutputAnalyzer;
import jdk.test.lib.process.ProcessTools;
import jdk.internal.platform.Metrics;
import java.util.ArrayList;
import jtreg.SkippedException;
/*
* @test
* @bug 8343191
* @requires os.family == "linux"
* @modules java.base/jdk.internal.platform
* @library /test/lib
* @build jdk.test.whitebox.WhiteBox
* @run driver jdk.test.lib.helpers.ClassFileInstaller -jar whitebox.jar jdk.test.whitebox.WhiteBox
* @run main TestMemoryWithSubgroups
*/
public class TestMemoryWithSubgroups {
private static final String imageName = Common.imageName("subgroup");
public static void main(String[] args) throws Exception {
Metrics metrics = Metrics.systemMetrics();
if (metrics == null) {
System.out.println("Cgroup not configured.");
return;
}
if (!DockerTestUtils.canTestDocker()) {
System.out.println("Unable to run docker tests.");
return;
}
Common.prepareWhiteBox();
DockerTestUtils.buildJdkContainerImage(imageName);
if ("cgroupv1".equals(metrics.getProvider())) {
try {
testMemoryLimitSubgroupV1("200m", "100m", "104857600", false);
testMemoryLimitSubgroupV1("1g", "500m", "524288000", false);
testMemoryLimitSubgroupV1("200m", "100m", "104857600", true);
testMemoryLimitSubgroupV1("1g", "500m", "524288000", true);
} finally {
DockerTestUtils.removeDockerImage(imageName);
}
} else if ("cgroupv2".equals(metrics.getProvider())) {
try {
testMemoryLimitSubgroupV2("200m", "100m", "104857600", false);
testMemoryLimitSubgroupV2("1g", "500m", "524288000", false);
testMemoryLimitSubgroupV2("200m", "100m", "104857600", true);
testMemoryLimitSubgroupV2("1g", "500m", "524288000", true);
} finally {
DockerTestUtils.removeDockerImage(imageName);
}
} else {
throw new SkippedException("Metrics are from neither cgroup v1 nor v2, skipped for now.");
}
}
private static void testMemoryLimitSubgroupV1(String containerMemorySize, String valueToSet, String expectedValue, boolean privateNamespace)
throws Exception {
Common.logNewTestCase("Cgroup V1 subgroup memory limit: " + valueToSet);
DockerRunOptions opts = new DockerRunOptions(imageName, "sh", "-c");
opts.javaOpts = new ArrayList<>();
opts.appendTestJavaOptions = false;
opts.addDockerOpts("--privileged")
.addDockerOpts("--cgroupns=" + (privateNamespace ? "private" : "host"))
.addDockerOpts("--memory", containerMemorySize);
opts.addClassOptions("mkdir -p /sys/fs/cgroup/memory/test ; " +
"echo " + valueToSet + " > /sys/fs/cgroup/memory/test/memory.limit_in_bytes ; " +
"echo $$ > /sys/fs/cgroup/memory/test/cgroup.procs ; " +
"/jdk/bin/java -Xlog:os+container=trace -version");
Common.run(opts)
.shouldMatch("Lowest limit was:.*" + expectedValue);
}
private static void testMemoryLimitSubgroupV2(String containerMemorySize, String valueToSet, String expectedValue, boolean privateNamespace)
throws Exception {
Common.logNewTestCase("Cgroup V2 subgroup memory limit: " + valueToSet);
DockerRunOptions opts = new DockerRunOptions(imageName, "sh", "-c");
opts.javaOpts = new ArrayList<>();
opts.appendTestJavaOptions = false;
opts.addDockerOpts("--privileged")
.addDockerOpts("--cgroupns=" + (privateNamespace ? "private" : "host"))
.addDockerOpts("--memory", containerMemorySize);
opts.addClassOptions("mkdir -p /sys/fs/cgroup/memory/test ; " +
"echo $$ > /sys/fs/cgroup/memory/test/cgroup.procs ; " +
"echo '+memory' > /sys/fs/cgroup/cgroup.subtree_control ; " +
"echo '+memory' > /sys/fs/cgroup/memory/cgroup.subtree_control ; " +
"echo " + valueToSet + " > /sys/fs/cgroup/memory/test/memory.max ; " +
"/jdk/bin/java -Xlog:os+container=trace -version");
Common.run(opts)
.shouldMatch("Lowest limit was:.*" + expectedValue);
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2022, Red Hat, Inc.
* Copyright (c) 2022, 2025, Red Hat, Inc.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -64,6 +64,9 @@ public class CgroupV1SubsystemControllerTest {
assertEquals(expectedPath, ctrl.path());
}
/*
* Less common cases: Containers
*/
@Test
public void testCgPathSubstring() {
String root = "/foo/bar/baz";
@ -71,8 +74,18 @@ public class CgroupV1SubsystemControllerTest {
CgroupV1SubsystemController ctrl = new CgroupV1SubsystemController(root, mountPoint);
String cgroupPath = "/foo/bar/baz/some";
ctrl.setPath(cgroupPath);
String expectedPath = mountPoint + "/some";
String expectedPath = mountPoint;
assertEquals(expectedPath, ctrl.path());
}
@Test
public void testCgPathToMovedPath() {
String root = "/system.slice/garden.service/garden/good/2f57368b-0eda-4e52-64d8-af5c";
String mountPoint = "/sys/fs/cgroup/cpu,cpuacct";
CgroupV1SubsystemController ctrl = new CgroupV1SubsystemController(root, mountPoint);
String cgroupPath = "/system.slice/garden.service/garden/bad/2f57368b-0eda-4e52-64d8-af5c";
ctrl.setPath(cgroupPath);
String expectedPath = mountPoint;
assertEquals(expectedPath, ctrl.path());
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2020, 2022, Red Hat Inc.
* Copyright (c) 2020, 2025, Red Hat Inc.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -44,6 +44,7 @@ import jdk.internal.platform.CgroupSubsystemFactory;
import jdk.internal.platform.CgroupSubsystemFactory.CgroupTypeResult;
import jdk.internal.platform.CgroupV1MetricsImpl;
import jdk.internal.platform.cgroupv1.CgroupV1Subsystem;
import jdk.internal.platform.cgroupv1.CgroupV1SubsystemController;
import jdk.internal.platform.Metrics;
import jdk.test.lib.Utils;
import jdk.test.lib.util.FileUtils;
@ -75,8 +76,10 @@ public class TestCgroupSubsystemFactory {
private Path cgroupv1MntInfoDoubleControllers;
private Path cgroupv1MntInfoDoubleControllers2;
private Path cgroupv1MntInfoColonsHierarchy;
private Path cgroupv1MntInfoNonTrivialRoot;
private Path cgroupv1SelfCgroup;
private Path cgroupv1SelfColons;
private Path cgroupv1SelfNonTrivialRoot;
private Path cgroupv2SelfCgroup;
private Path cgroupv1SelfCgroupJoinCtrl;
private Path cgroupv1CgroupsOnlyCPUCtrl;
@ -175,6 +178,7 @@ public class TestCgroupSubsystemFactory {
"42 30 0:38 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime shared:14 - cgroup none rw,seclabel,cpuset\n" +
"43 30 0:39 / /sys/fs/cgroup/blkio rw,nosuid,nodev,noexec,relatime shared:15 - cgroup none rw,seclabel,blkio\n" +
"44 30 0:40 / /sys/fs/cgroup/freezer rw,nosuid,nodev,noexec,relatime shared:16 - cgroup none rw,seclabel,freezer\n";
private String mntInfoNonTrivialRoot = "2207 2196 0:43 /system.slice/garden.service/garden/good/2f57368b-0eda-4e52-64d8-af5c /sys/fs/cgroup/cpu,cpuacct ro,nosuid,nodev,noexec,relatime master:25 - cgroup cgroup rw,cpu,cpuacct\n";
private String cgroupsNonZeroHierarchy =
"#subsys_name hierarchy num_cgroups enabled\n" +
"cpuset 9 1 1\n" +
@ -230,6 +234,7 @@ public class TestCgroupSubsystemFactory {
"2:cpu,cpuacct:/\n" +
"1:name=systemd:/user.slice/user-1000.slice/user@1000.service/apps.slice/apps-org.gnome.Terminal.slice/vte-spawn-3c00b338-5b65-439f-8e97-135e183d135d.scope\n" +
"0::/user.slice/user-1000.slice/user@1000.service/apps.slice/apps-org.gnome.Terminal.slice/vte-spawn-3c00b338-5b65-439f-8e97-135e183d135d.scope\n";
private String cgroupv1SelfNTRoot = "11:cpu,cpuacct:/system.slice/garden.service/garden/bad/2f57368b-0eda-4e52-64d8-af5c\n";
private String cgroupv2SelfCgroupContent = "0::/user.slice/user-1000.slice/session-2.scope";
// We have a mix of V1 and V2 controllers, but none of the V1 controllers
@ -294,12 +299,18 @@ public class TestCgroupSubsystemFactory {
cgroupv1MntInfoColonsHierarchy = Paths.get(existingDirectory.toString(), "mountinfo_colons");
Files.writeString(cgroupv1MntInfoColonsHierarchy, mntInfoColons);
cgroupv1MntInfoNonTrivialRoot = Paths.get(existingDirectory.toString(), "mountinfo_nt_root");
Files.writeString(cgroupv1MntInfoNonTrivialRoot, mntInfoNonTrivialRoot);
cgroupv1SelfCgroup = Paths.get(existingDirectory.toString(), "self_cgroup_cgv1");
Files.writeString(cgroupv1SelfCgroup, cgroupv1SelfCgroupContent);
cgroupv1SelfColons = Paths.get(existingDirectory.toString(), "self_colons_cgv1");
Files.writeString(cgroupv1SelfColons, cgroupv1SelfColonsContent);
cgroupv1SelfNonTrivialRoot = Paths.get(existingDirectory.toString(), "self_nt_root_cgv1");
Files.writeString(cgroupv1SelfNonTrivialRoot, cgroupv1SelfNTRoot);
cgroupv2SelfCgroup = Paths.get(existingDirectory.toString(), "self_cgroup_cgv2");
Files.writeString(cgroupv2SelfCgroup, cgroupv2SelfCgroupContent);
@ -449,6 +460,27 @@ public class TestCgroupSubsystemFactory {
assertEquals(memoryInfo.getMountRoot(), memoryInfo.getCgroupPath());
}
@Test
public void testMountPrefixCgroupsV1() throws IOException {
String cgroups = cgroupv1CgInfoNonZeroHierarchy.toString();
String mountInfo = cgroupv1MntInfoNonTrivialRoot.toString();
String selfCgroup = cgroupv1SelfNonTrivialRoot.toString();
Optional<CgroupTypeResult> result = CgroupSubsystemFactory.determineType(mountInfo, cgroups, selfCgroup);
assertTrue("Expected non-empty cgroup result", result.isPresent());
CgroupTypeResult res = result.get();
CgroupInfo cpuInfo = res.getInfos().get("cpu");
assertEquals(cpuInfo.getCgroupPath(), "/system.slice/garden.service/garden/bad/2f57368b-0eda-4e52-64d8-af5c");
String expectedMountPoint = "/sys/fs/cgroup/cpu,cpuacct";
assertEquals(expectedMountPoint, cpuInfo.getMountPoint());
CgroupV1SubsystemController cgroupv1MemoryController = new CgroupV1SubsystemController(cpuInfo.getMountRoot(), cpuInfo.getMountPoint());
cgroupv1MemoryController.setPath(cpuInfo.getCgroupPath());
String actualPath = cgroupv1MemoryController.path();
assertNotNull(actualPath);
String expectedPath = expectedMountPoint;
assertEquals("Should be equal to the mount point path", expectedPath, actualPath);
}
@Test
public void testZeroHierarchyCgroupsV1() throws IOException {
String cgroups = cgroupv1CgInfoZeroHierarchy.toString();

View File

@ -0,0 +1,120 @@
/*
* Copyright (c) 2025, BELLSOFT. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
import jdk.internal.platform.Metrics;
import jdk.test.lib.Utils;
import jdk.test.lib.containers.docker.Common;
import jdk.test.lib.containers.docker.DockerfileConfig;
import jdk.test.lib.containers.docker.DockerRunOptions;
import jdk.test.lib.containers.docker.DockerTestUtils;
import java.util.ArrayList;
import jtreg.SkippedException;
/*
* @test
* @bug 8343191
* @key cgroups
* @summary Cgroup v1 subsystem fails to set subsystem path
* @requires container.support
* @library /test/lib
* @modules java.base/jdk.internal.platform
* @build MetricsMemoryTester
* @run main TestDockerMemoryMetricsSubgroup
*/
public class TestDockerMemoryMetricsSubgroup {
private static final String imageName =
DockerfileConfig.getBaseImageName() + ":" +
DockerfileConfig.getBaseImageVersion();
public static void main(String[] args) throws Exception {
Metrics metrics = Metrics.systemMetrics();
if (metrics == null) {
System.out.println("Cgroup not configured.");
return;
}
if (!DockerTestUtils.canTestDocker()) {
System.out.println("Unable to run docker tests.");
return;
}
if ("cgroupv1".equals(metrics.getProvider())) {
testMemoryLimitSubgroupV1("200m", "400m", false);
testMemoryLimitSubgroupV1("500m", "1G", false);
testMemoryLimitSubgroupV1("200m", "400m", true);
testMemoryLimitSubgroupV1("500m", "1G", true);
} else if ("cgroupv2".equals(metrics.getProvider())) {
testMemoryLimitSubgroupV2("200m", "400m", false);
testMemoryLimitSubgroupV2("500m", "1G", false);
testMemoryLimitSubgroupV2("200m", "400m", true);
testMemoryLimitSubgroupV2("500m", "1G", true);
} else {
throw new SkippedException("Metrics are from neither cgroup v1 nor v2, skipped for now.");
}
}
private static void testMemoryLimitSubgroupV1(String innerSize, String outerGroupMemorySize, boolean privateNamespace) throws Exception {
Common.logNewTestCase("testMemoryLimitSubgroup, innerSize = " + innerSize);
DockerRunOptions opts =
new DockerRunOptions(imageName, "sh", "-c");
opts.javaOpts = new ArrayList<>();
opts.appendTestJavaOptions = false;
opts.addDockerOpts("--volume", Utils.TEST_CLASSES + ":/test-classes/")
.addDockerOpts("--volume", Utils.TEST_JDK + ":/jdk")
.addDockerOpts("--privileged")
.addDockerOpts("--cgroupns=" + (privateNamespace ? "private" : "host"))
.addDockerOpts("--memory", outerGroupMemorySize);
opts.addClassOptions("mkdir -p /sys/fs/cgroup/memory/test ; " +
"echo " + innerSize + " > /sys/fs/cgroup/memory/test/memory.limit_in_bytes ; " +
"echo $$ > /sys/fs/cgroup/memory/test/cgroup.procs ; " +
"/jdk/bin/java -cp /test-classes/ " +
"--add-exports java.base/jdk.internal.platform=ALL-UNNAMED " +
"MetricsMemoryTester memory " + innerSize);
DockerTestUtils.dockerRunJava(opts).shouldHaveExitValue(0).shouldContain("TEST PASSED!!!");
}
private static void testMemoryLimitSubgroupV2(String innerSize, String outerGroupMemorySize, boolean privateNamespace) throws Exception {
Common.logNewTestCase("testMemoryLimitSubgroup, innerSize = " + innerSize);
DockerRunOptions opts =
new DockerRunOptions(imageName, "sh", "-c");
opts.javaOpts = new ArrayList<>();
opts.appendTestJavaOptions = false;
opts.addDockerOpts("--volume", Utils.TEST_CLASSES + ":/test-classes/")
.addDockerOpts("--volume", Utils.TEST_JDK + ":/jdk")
.addDockerOpts("--privileged")
.addDockerOpts("--cgroupns=" + (privateNamespace ? "private" : "host"))
.addDockerOpts("--memory", outerGroupMemorySize);
opts.addClassOptions("mkdir -p /sys/fs/cgroup/memory/test ; " +
"echo $$ > /sys/fs/cgroup/memory/test/cgroup.procs ; " +
"echo '+memory' > /sys/fs/cgroup/cgroup.subtree_control ; " +
"echo '+memory' > /sys/fs/cgroup/memory/cgroup.subtree_control ; " +
"echo " + innerSize + " > /sys/fs/cgroup/memory/test/memory.max ; " +
"/jdk/bin/java -cp /test-classes/ " +
"--add-exports java.base/jdk.internal.platform=ALL-UNNAMED " +
"MetricsMemoryTester memory " + innerSize);
DockerTestUtils.dockerRunJava(opts).shouldHaveExitValue(0).shouldContain("TEST PASSED!!!");
}
}