Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 37 additions & 2 deletions exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ following will output a list of processes running in the container:
Name: "preserve-fds",
Usage: "Pass N additional file descriptors to the container (stdio + $LISTEN_FDS + N in total)",
},
cli.StringSliceFlag{
Name: "cgroup",
Usage: "run the process in an (existing) sub-cgroup(s). Format is [<controller>:]<cgroup>.",
},
},
Action: func(context *cli.Context) error {
if err := checkArgs(context, 1, minArgs); err != nil {
Expand All @@ -105,6 +109,32 @@ following will output a list of processes running in the container:
SkipArgReorder: true,
}

func getSubCgroupPaths(args []string) (map[string]string, error) {
if len(args) == 0 {
return nil, nil
}
paths := make(map[string]string, len(args))
for _, c := range args {
// Split into controller:path.
cs := strings.SplitN(c, ":", 3)
if len(cs) > 2 {
return nil, fmt.Errorf("invalid --cgroup argument: %s", c)
}
if len(cs) == 1 { // no controller: prefix
if len(args) != 1 {
return nil, fmt.Errorf("invalid --cgroup argument: %s (missing <controller>: prefix)", c)
}
paths[""] = c
} else {
// There may be a few comma-separated controllers.
for _, ctrl := range strings.Split(cs[0], ",") {
paths[ctrl] = cs[1]
}
}
}
return paths, nil
}

func execProcess(context *cli.Context) (int, error) {
container, err := getContainer(context)
if err != nil {
Expand All @@ -121,7 +151,6 @@ func execProcess(context *cli.Context) (int, error) {
if path == "" && len(context.Args()) == 1 {
return -1, errors.New("process args cannot be empty")
}
detach := context.Bool("detach")
state, err := container.State()
if err != nil {
return -1, err
Expand All @@ -132,16 +161,22 @@ func execProcess(context *cli.Context) (int, error) {
return -1, err
}

cgPaths, err := getSubCgroupPaths(context.StringSlice("cgroup"))
if err != nil {
return -1, err
}

r := &runner{
enableSubreaper: false,
shouldDestroy: false,
container: container,
consoleSocket: context.String("console-socket"),
detach: detach,
detach: context.Bool("detach"),
pidFile: context.String("pid-file"),
action: CT_ACT_RUN,
init: false,
preserveFDs: context.Int("preserve-fds"),
subCgroupPaths: cgPaths,
}
return r.run(p)
}
Expand Down
8 changes: 8 additions & 0 deletions libcontainer/cgroups/fs/fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@ var (

var errSubsystemDoesNotExist = errors.New("cgroup: subsystem does not exist")

func init() {
// If using cgroups-hybrid mode then add a "" controller indicating
// it should join the cgroups v2.
if cgroups.IsCgroup2HybridMode() {
subsystems = append(subsystems, &NameGroup{GroupName: "", Join: true})
}
}

type subsystem interface {
// Name returns the name of the subsystem.
Name() string
Expand Down
13 changes: 13 additions & 0 deletions libcontainer/cgroups/systemd/v1.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,19 @@ func initPaths(c *configs.Cgroup) (map[string]string, error) {
}
paths[s.Name()] = subsystemPath
}

// If systemd is using cgroups-hybrid mode then add the slice path of
// this container to the paths so the following process executed with
// "runc exec" joins that cgroup as well.
if cgroups.IsCgroup2HybridMode() {
// "" means cgroup-hybrid path
cgroupsHybridPath, err := getSubsystemPath(slice, unit, "")
if err != nil && cgroups.IsNotFound(err) {
return nil, err
}
paths[""] = cgroupsHybridPath
}

return paths, nil
}

Expand Down
25 changes: 23 additions & 2 deletions libcontainer/cgroups/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,14 @@ import (
const (
CgroupProcesses = "cgroup.procs"
unifiedMountpoint = "/sys/fs/cgroup"
hybridMountpoint = "/sys/fs/cgroup/unified"
)

var (
isUnifiedOnce sync.Once
isUnified bool
isHybridOnce sync.Once
isHybrid bool
)

// IsCgroup2UnifiedMode returns whether we are running in cgroup v2 unified mode.
Expand All @@ -47,6 +50,24 @@ func IsCgroup2UnifiedMode() bool {
return isUnified
}

// IsCgroup2HybridMode returns whether we are running in cgroup v2 hybrid mode.
func IsCgroup2HybridMode() bool {
isHybridOnce.Do(func() {
var st unix.Statfs_t
err := unix.Statfs(hybridMountpoint, &st)
if err != nil {
if os.IsNotExist(err) {
// ignore the "not found" error
isHybrid = false
return
}
panic(fmt.Sprintf("cannot statfs cgroup root: %s", err))
}
isHybrid = st.Type == unix.CGROUP2_SUPER_MAGIC
})
return isHybrid
}

type Mount struct {
Mountpoint string
Root string
Expand Down Expand Up @@ -352,7 +373,7 @@ func WriteCgroupProc(dir string, pid int) error {

file, err := OpenFile(dir, CgroupProcesses, os.O_WRONLY)
if err != nil {
return fmt.Errorf("failed to write %v to %v: %w", pid, CgroupProcesses, err)
return fmt.Errorf("failed to write %v: %w", pid, err)
}
defer file.Close()

Expand All @@ -369,7 +390,7 @@ func WriteCgroupProc(dir string, pid int) error {
continue
}

return fmt.Errorf("failed to write %v to %v: %w", pid, CgroupProcesses, err)
return fmt.Errorf("failed to write %v: %w", pid, err)
}
return err
}
Expand Down
10 changes: 10 additions & 0 deletions libcontainer/cgroups/v1_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,11 @@ func FindCgroupMountpoint(cgroupPath, subsystem string) (string, error) {
return "", errUnified
}

// If subsystem is empty, we look for the cgroupv2 hybrid path.
if len(subsystem) == 0 {
return hybridMountpoint, nil
}

// Avoid parsing mountinfo by trying the default path first, if possible.
if path := tryDefaultPath(cgroupPath, subsystem); path != "" {
return path, nil
Expand Down Expand Up @@ -223,6 +228,11 @@ func GetOwnCgroupPath(subsystem string) (string, error) {
return "", err
}

// If subsystem is empty, we look for the cgroupv2 hybrid path.
if len(subsystem) == 0 {
return hybridMountpoint, nil
}

return getCgroupPathHelper(subsystem, cgroup)
}

Expand Down
27 changes: 25 additions & 2 deletions libcontainer/container_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"net"
"os"
"os/exec"
"path"
"path/filepath"
"reflect"
"strconv"
Expand Down Expand Up @@ -561,7 +562,7 @@ func (c *linuxContainer) newSetnsProcess(p *Process, cmd *exec.Cmd, messageSockP
if err != nil {
return nil, err
}
return &setnsProcess{
proc := &setnsProcess{
cmd: cmd,
cgroupPaths: state.CgroupPaths,
rootlessCgroups: c.config.RootlessCgroups,
Expand All @@ -573,7 +574,29 @@ func (c *linuxContainer) newSetnsProcess(p *Process, cmd *exec.Cmd, messageSockP
process: p,
bootstrapData: data,
initProcessPid: state.InitProcessPid,
}, nil
}
if len(p.SubCgroupPaths) > 0 {
if add, ok := p.SubCgroupPaths[""]; ok {
// cgroup v1: using the same path for all controllers.
// cgroup v2: the only possible way.
for k := range proc.cgroupPaths {
proc.cgroupPaths[k] = path.Join(proc.cgroupPaths[k], add)
}
// cgroup v2: do not try to join init process's cgroup
// as a fallback (see (*setnsProcess).start).
proc.initProcessPid = 0
} else {
// Per-controller paths.
for ctrl, add := range p.SubCgroupPaths {
if val, ok := proc.cgroupPaths[ctrl]; ok {
proc.cgroupPaths[ctrl] = path.Join(val, add)
} else {
return nil, fmt.Errorf("unknown controller %s in SubCgroupPaths", ctrl)
}
}
}
}
return proc, nil
}

func (c *linuxContainer) newInitConfig(process *Process) *initConfig {
Expand Down
9 changes: 9 additions & 0 deletions libcontainer/process.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,15 @@ type Process struct {
ops processOperations

LogLevel string

// SubCgroupPaths specifies sub-cgroups to run the process in.
// Map keys are controller names, map values are paths (relative to
// container's top-level cgroup).
//
// If empty, the default top-level container's cgroup is used.
//
// For cgroup v2, the only key allowed is "".
SubCgroupPaths map[string]string
}

// Wait waits for the process to exit.
Expand Down
8 changes: 4 additions & 4 deletions libcontainer/process_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,12 +124,12 @@ func (p *setnsProcess) start() (retErr error) {
if err := p.execSetns(); err != nil {
return fmt.Errorf("error executing setns process: %w", err)
}
if len(p.cgroupPaths) > 0 {
if err := cgroups.EnterPid(p.cgroupPaths, p.pid()); err != nil && !p.rootlessCgroups {
// On cgroup v2 + nesting + domain controllers, EnterPid may fail with EBUSY.
for _, path := range p.cgroupPaths {
if err := cgroups.WriteCgroupProc(path, p.pid()); err != nil && !p.rootlessCgroups {
// On cgroup v2 + nesting + domain controllers, WriteCgroupProc may fail with EBUSY.
// https://github.com/opencontainers/runc/issues/2356#issuecomment-621277643
// Try to join the cgroup of InitProcessPid.
if cgroups.IsCgroup2UnifiedMode() {
if cgroups.IsCgroup2UnifiedMode() && p.initProcessPid != 0 {
initProcCgroupFile := fmt.Sprintf("/proc/%d/cgroup", p.initProcessPid)
initCg, initCgErr := cgroups.ParseCgroupFile(initProcCgroupFile)
if initCgErr == nil {
Expand Down
11 changes: 11 additions & 0 deletions man/runc-exec.8.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,17 @@ multiple times.
: Pass _N_ additional file descriptors to the container (**stdio** +
**$LISTEN_FDS** + _N_ in total). Default is **0**.

**--cgroup** _path_ | _controller_[,_controller_...]:_path_
: Execute a process in a sub-cgroup. If the specified cgroup does not exist, an
error is returned. Default is empty path, which means to use container's top
level cgroup.
: For cgroup v1 only, a particular _controller_ (or multiple comma-separated
controllers) can be specified, and the option can be used multiple times to set
different paths for different controllers.
: Note for cgroup v2, in case the process can't join the top level cgroup,
**runc exec** fallback is to try joining the cgroup of container's init.
This fallback can be disabled by using **--cgroup /**.

# EXIT STATUS

Exits with a status of _command_ (unless **-d** is used), or **255** if
Expand Down
21 changes: 21 additions & 0 deletions tests/integration/cgroups.bats
Original file line number Diff line number Diff line change
Expand Up @@ -282,3 +282,24 @@ function setup() {
[ "$status" -eq 0 ]
[ "$(wc -l <<<"$output")" -eq 1 ]
}

@test "runc exec (cgroup v1+hybrid joins correct cgroup)" {
requires root cgroups_hybrid

set_cgroups_path

runc run --pid-file pid.txt -d --console-socket "$CONSOLE_SOCKET" test_cgroups_group
[ "$status" -eq 0 ]

pid=$(cat pid.txt)
run_cgroup=$(tail -1 </proc/"$pid"/cgroup)
[[ "$run_cgroup" == *"runc-cgroups-integration-test"* ]]

runc exec test_cgroups_group cat /proc/self/cgroup
[ "$status" -eq 0 ]
exec_cgroup=${lines[-1]}
[[ $exec_cgroup == *"runc-cgroups-integration-test"* ]]

# check that the cgroups v2 path is the same for both processes
[[ "$run_cgroup" == "$exec_cgroup" ]]
}
Loading