Merge pull request #5117 from kolyshkin/1.4-fix-exec

[1.4] libct: prepareCgroupFD: fall back to container init cgroup
This commit is contained in:
lfbzhm
2026-02-12 09:53:53 +08:00
committed by GitHub
2 changed files with 119 additions and 26 deletions
+84 -26
View File
@@ -19,10 +19,11 @@ import (
"syscall"
"time"
"github.com/opencontainers/runtime-spec/specs-go"
"github.com/sirupsen/logrus"
"golang.org/x/sys/unix"
"github.com/opencontainers/runtime-spec/specs-go"
"github.com/opencontainers/cgroups"
"github.com/opencontainers/cgroups/fs2"
"github.com/opencontainers/runc/libcontainer/configs"
@@ -274,32 +275,66 @@ func (p *setnsProcess) addIntoCgroupV1() error {
return nil
}
// initProcessCgroupPath returns container init's cgroup path,
// as read from /proc/PID/cgroup. Only works for cgroup v2.
// Returns empty string if the path can not be obtained.
//
// This is used by runc exec in these cases:
//
// 1. On cgroup v2 + nesting + domain controllers, adding to initial cgroup
// may fail with EBUSY (https://github.com/opencontainers/runc/issues/2356);
//
// 2. A container init process with no cgroupns and /sys/fs/cgroup rw access
// may move itself to any other cgroup, and the original cgroup will disappear.
func (p *setnsProcess) initProcessCgroupPath() string {
if p.initProcessPid == 0 || !cgroups.IsCgroup2UnifiedMode() {
return ""
}
cg, err := cgroups.ParseCgroupFile("/proc/" + strconv.Itoa(p.initProcessPid) + "/cgroup")
if err != nil {
return ""
}
cgroup, ok := cg[""]
if !ok {
return ""
}
return fs2.UnifiedMountpoint + cgroup
}
func (p *setnsProcess) addIntoCgroupV2() error {
sub := p.process.SubCgroupPaths[""]
err := p.manager.AddPid(sub, p.pid())
if err != nil && !p.rootlessCgroups {
// On cgroup v2 + nesting + domain controllers, adding to initial cgroup may fail with EBUSY.
// https://github.com/opencontainers/runc/issues/2356#issuecomment-621277643
// Try to join the cgroup of InitProcessPid, unless sub-cgroup is explicitly set.
if p.initProcessPid != 0 && sub == "" {
initProcCgroupFile := fmt.Sprintf("/proc/%d/cgroup", p.initProcessPid)
initCg, initCgErr := cgroups.ParseCgroupFile(initProcCgroupFile)
if initCgErr == nil {
if initCgPath, ok := initCg[""]; ok {
initCgDirpath := filepath.Join(fs2.UnifiedMountpoint, initCgPath)
logrus.Debugf("adding pid %d to cgroup failed (%v), attempting to join %s",
p.pid(), err, initCgDirpath)
// NOTE: initCgDirPath is not guaranteed to exist because we didn't pause the container.
err = cgroups.WriteCgroupProc(initCgDirpath, p.pid())
}
}
}
if err != nil {
return fmt.Errorf("error adding pid %d to cgroups: %w", p.pid(), err)
}
if err == nil {
return nil
}
// Failed to join the configured cgroup. Fall back to container init's cgroup
// unless sub-cgroup is explicitly requested.
var path string
if sub != "" {
goto fail
}
path = p.initProcessCgroupPath()
if path == "" {
goto fail
}
logrus.Debugf("adding pid %d to configured cgroup failed (%v), will join container init cgroup %q", p.pid(), err, path)
// NOTE: path is not guaranteed to exist because we didn't pause the container.
err = cgroups.WriteCgroupProc(path, p.pid())
if err != nil {
goto fail
}
return nil
fail:
if p.rootlessCgroups {
// Ignore cgroup join errors when rootless.
return nil
}
return fmt.Errorf("error adding pid %d to cgroups: %w", p.pid(), err)
}
func (p *setnsProcess) addIntoCgroup() error {
@@ -318,6 +353,8 @@ func (p *setnsProcess) addIntoCgroup() error {
// to join cgroup early, in p.cmd.Start. Returns an *os.File which
// must be closed by the caller after p.Cmd.Start return.
func (p *setnsProcess) prepareCgroupFD() (*os.File, error) {
const openFlags = unix.O_PATH | unix.O_DIRECTORY | unix.O_CLOEXEC
if !cgroups.IsCgroup2UnifiedMode() {
return nil, nil
}
@@ -335,14 +372,28 @@ func (p *setnsProcess) prepareCgroupFD() (*os.File, error) {
return nil, fmt.Errorf("bad sub cgroup path: %s", sub)
}
fd, err := cgroups.OpenFile(base, sub, unix.O_PATH|unix.O_DIRECTORY|unix.O_CLOEXEC)
fd, err := cgroups.OpenFile(base, sub, openFlags)
if err == nil {
goto success
}
// Failed to open the configured cgroup. Fall back to container init's cgroup
// unless sub-cgroup is explicitly requested. The fallback logic should be
// the same as in addIntoCgroupV2.
if sub != "" {
goto fail
}
cgroup = p.initProcessCgroupPath()
if cgroup == "" {
goto fail
}
logrus.Debugf("failed to open configured cgroup (%v), will open container init cgroup %q", err, cgroup)
// NOTE: path is not guaranteed to exist because we didn't pause the container.
fd, err = cgroups.OpenFile(cgroup, "", openFlags)
if err != nil {
if p.rootlessCgroups {
return nil, nil
}
return nil, fmt.Errorf("can't open cgroup: %w", err)
goto fail
}
success:
logrus.Debugf("using CLONE_INTO_CGROUP %q", cgroup)
if p.cmd.SysProcAttr == nil {
p.cmd.SysProcAttr = &syscall.SysProcAttr{}
@@ -351,6 +402,13 @@ func (p *setnsProcess) prepareCgroupFD() (*os.File, error) {
p.cmd.SysProcAttr.CgroupFD = int(fd.Fd())
return fd, nil
fail:
// Ignore cgroup join error for rootless.
if p.rootlessCgroups {
return nil, nil
}
return nil, fmt.Errorf("can't open cgroup: %w", err)
}
// startWithCgroupFD starts a process via clone3 with CLONE_INTO_CGROUP,
+35
View File
@@ -335,6 +335,41 @@ function check_exec_debug() {
[ "$status" -eq 0 ]
}
# https://github.com/opencontainers/runc/issues/5089
@test "runc exec [init changes cgroup]" {
requires root cgroups_v2
NEW_CGROUP_REL=/runc-tst-$$
NEW_CGROUP=/sys/fs/cgroup$NEW_CGROUP_REL
mkdir $NEW_CGROUP
# The container is placed into a $CGROUP_V2_PATH cgroup.
set_cgroups_path
# And upon the start it moves itself into $NEW_CGROUP.
set_cgroup_mount_writable
update_config ' .linux.namespaces -= [{"type": "cgroup"}]
| .process.args = ["sh", "-c", "echo 1 > '$NEW_CGROUP'/cgroup.procs && exec sleep 1h"]'
runc run -d --console-socket "$CONSOLE_SOCKET" test_busybox
[ $status -eq 0 ]
testcontainer test_busybox running
sleep 1
# Remove the original container cgroup. If systemd cgroup manager is used by runc,
# the cgroup might have already be deleted by systemd, so we ignore rmdir errors.
rmdir "$CGROUP_V2_PATH" || true
test -d "$CGROUP_V2_PATH" && false
# Test that runc exec is able to fallback to container's init cgroup
# even if the original cgroup is gone.
runc exec test_busybox cat /proc/self/cgroup
[ $status -eq 0 ]
[ "$output" = "0::$NEW_CGROUP_REL" ]
# Cleanup.
runc delete -f test_busybox
rmdir "$NEW_CGROUP"
}
@test "runc exec [execve error]" {
cat <<EOF >rootfs/run.sh
#!/mmnnttbb foo bar