package main import ( "context" "encoding/json" "errors" "fmt" "os" "strconv" "strings" "github.com/opencontainers/runc/libcontainer" "github.com/opencontainers/runc/libcontainer/utils" "github.com/opencontainers/runtime-spec/specs-go" "github.com/urfave/cli/v3" ) var execCommand = &cli.Command{ Name: "exec", Usage: "execute new process inside the container", ArgsUsage: ` [command options] || -p process.json Where "" is the name for the instance of the container and "" is the command to be executed in the container. "" can't be empty unless a "-p" flag provided. EXAMPLE: For example, if the container is configured to run the linux ps command the following will output a list of processes running in the container: # runc exec ps`, // Stop parsing flags after the first positional argument (command). // This allows passing flags like -c to the command being executed. StopOnNthArg: mkPtr(1), // Disable comma as separator for slice flags. // This allows cgroup controller lists like "cpu,cpuacct:subcpu". DisableSliceFlagSeparator: true, Flags: []cli.Flag{ &cli.StringFlag{ Name: "console-socket", Usage: "path to an AF_UNIX socket which will receive a file descriptor referencing the master end of the console's pseudoterminal", }, &cli.StringFlag{ Name: "pidfd-socket", Usage: "path to an AF_UNIX socket which will receive a file descriptor referencing the exec process", }, &cli.StringFlag{ Name: "cwd", Usage: "current working directory in the container", }, &cli.StringSliceFlag{ Name: "env", Aliases: []string{"e"}, Usage: "set environment variables", }, &cli.BoolFlag{ Name: "tty", Aliases: []string{"t"}, Usage: "allocate a pseudo-TTY", }, &cli.StringFlag{ Name: "user", Aliases: []string{"u"}, Usage: "UID (format: [:])", }, &cli.Int64SliceFlag{ Name: "additional-gids", Aliases: []string{"g"}, Usage: "additional gids", }, &cli.StringFlag{ Name: "process", Aliases: []string{"p"}, Usage: "path to the process.json", }, &cli.BoolFlag{ Name: "detach", Aliases: []string{"d"}, Usage: "detach from the container's process", }, &cli.StringFlag{ Name: "pid-file", Value: "", Usage: "specify the file to write the process id to", }, &cli.StringFlag{ Name: "process-label", Usage: "set the asm process label for the process commonly used with selinux", }, &cli.StringFlag{ Name: "apparmor", Usage: "set the apparmor profile for the process", }, &cli.BoolFlag{ Name: "no-new-privs", Usage: "set the no new privileges value for the process", }, &cli.StringSliceFlag{ Name: "cap", Aliases: []string{"c"}, Value: []string{}, Usage: "add a capability to the bounding set for the process", }, &cli.IntFlag{ 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 [:].", }, &cli.BoolFlag{ Name: "ignore-paused", Usage: "allow exec in a paused container", }, }, Action: func(_ context.Context, cmd *cli.Command) error { if err := checkArgs(cmd, 1, minArgs); err != nil { return err } if err := revisePidFile(cmd); err != nil { return err } status, err := execProcess(cmd) if err == nil { os.Exit(status) } fatalWithCode(fmt.Errorf("exec failed: %w", err), 255) return nil // to satisfy the linter }, } // getSubCgroupPaths parses --cgroup arguments, which can either be // - a single "path" argument (for cgroup v2); // - one or more controller[,controller[,...]]:path arguments (for cgroup v1). // // Returns a controller to path map. For cgroup v2, it's a single entity map // with empty controller value. 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. if ctr, path, ok := strings.Cut(c, ":"); ok { // There may be a few comma-separated controllers. for ctrl := range strings.SplitSeq(ctr, ",") { if ctrl == "" { return nil, fmt.Errorf("invalid --cgroup argument: %s (empty prefix)", c) } if _, ok := paths[ctrl]; ok { return nil, fmt.Errorf("invalid --cgroup argument(s): controller %s specified multiple times", ctrl) } paths[ctrl] = path } } else { // No "controller:" prefix (cgroup v2, a single path). if len(args) != 1 { return nil, fmt.Errorf("invalid --cgroup argument: %s (missing : prefix)", c) } paths[""] = c } } return paths, nil } func execProcess(cmd *cli.Command) (int, error) { container, err := getContainer(cmd) if err != nil { return -1, err } status, err := container.Status() if err != nil { return -1, err } if status == libcontainer.Stopped { return -1, errors.New("cannot exec in a stopped container") } if status == libcontainer.Paused && !cmd.Bool("ignore-paused") { return -1, errors.New("cannot exec in a paused container (use --ignore-paused to override)") } p, err := getProcess(cmd, container) if err != nil { return -1, err } cgPaths, err := getSubCgroupPaths(cmd.StringSlice("cgroup")) if err != nil { return -1, err } r := &runner{ enableSubreaper: false, shouldDestroy: false, container: container, consoleSocket: cmd.String("console-socket"), pidfdSocket: cmd.String("pidfd-socket"), detach: cmd.Bool("detach"), pidFile: cmd.String("pid-file"), action: CT_ACT_RUN, init: false, preserveFDs: cmd.Int("preserve-fds"), subCgroupPaths: cgPaths, } return r.run(p) } func getProcess(cmd *cli.Command, c *libcontainer.Container) (*specs.Process, error) { if path := cmd.String("process"); path != "" { f, err := os.Open(path) if err != nil { return nil, err } defer f.Close() var p specs.Process if err := json.NewDecoder(f).Decode(&p); err != nil { return nil, err } return &p, validateProcessSpec(&p) } // Process from config.json and CLI flags. bundle, ok := utils.SearchLabels(c.Config().Labels, "bundle") if !ok { return nil, errors.New("bundle not found in labels") } if err := os.Chdir(bundle); err != nil { return nil, err } spec, err := loadSpec(specConfig) if err != nil { return nil, err } p := spec.Process args := cmd.Args().Slice() if len(args) < 2 { return nil, errors.New("exec args cannot be empty") } p.Args = args[1:] // Override the cwd, if passed. if cwd := cmd.String("cwd"); cwd != "" { p.Cwd = cwd } if ap := cmd.String("apparmor"); ap != "" { p.ApparmorProfile = ap } if l := cmd.String("process-label"); l != "" { p.SelinuxLabel = l } if caps := cmd.StringSlice("cap"); len(caps) > 0 { for _, c := range caps { p.Capabilities.Bounding = append(p.Capabilities.Bounding, c) p.Capabilities.Effective = append(p.Capabilities.Effective, c) p.Capabilities.Permitted = append(p.Capabilities.Permitted, c) // Since ambient capabilities can't be set without inherritable, // and runc exec --cap don't set inheritable, let's only set // ambient if we already have some inheritable bits set from spec. if p.Capabilities.Inheritable != nil { p.Capabilities.Ambient = append(p.Capabilities.Ambient, c) } } } // append the passed env variables p.Env = append(p.Env, cmd.StringSlice("env")...) // Always set tty to false, unless explicitly enabled from CLI. p.Terminal = cmd.Bool("tty") if cmd.IsSet("no-new-privs") { p.NoNewPrivileges = cmd.Bool("no-new-privs") } // Override the user, if passed. if user := cmd.String("user"); user != "" { uids, gids, ok := strings.Cut(user, ":") if ok { gid, err := strconv.Atoi(gids) if err != nil { return nil, fmt.Errorf("bad gid: %w", err) } p.User.GID = uint32(gid) } uid, err := strconv.Atoi(uids) if err != nil { return nil, fmt.Errorf("bad uid: %w", err) } p.User.UID = uint32(uid) } for _, gid := range cmd.Int64Slice("additional-gids") { if gid < 0 { return nil, fmt.Errorf("additional-gids must be a positive number %d", gid) } p.User.AdditionalGids = append(p.User.AdditionalGids, uint32(gid)) } return p, validateProcessSpec(p) }