github.com/opencontainers/runc@v1.2.0-rc.1.0.20240520010911-492dc558cdd6/exec.go (about)

     1  package main
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"fmt"
     7  	"os"
     8  	"strconv"
     9  	"strings"
    10  
    11  	"github.com/opencontainers/runc/libcontainer"
    12  	"github.com/opencontainers/runc/libcontainer/utils"
    13  	"github.com/opencontainers/runtime-spec/specs-go"
    14  	"github.com/urfave/cli"
    15  )
    16  
    17  var execCommand = cli.Command{
    18  	Name:  "exec",
    19  	Usage: "execute new process inside the container",
    20  	ArgsUsage: `<container-id> <command> [command options]  || -p process.json <container-id>
    21  
    22  Where "<container-id>" is the name for the instance of the container and
    23  "<command>" is the command to be executed in the container.
    24  "<command>" can't be empty unless a "-p" flag provided.
    25  
    26  EXAMPLE:
    27  For example, if the container is configured to run the linux ps command the
    28  following will output a list of processes running in the container:
    29  
    30         # runc exec <container-id> ps`,
    31  	Flags: []cli.Flag{
    32  		cli.StringFlag{
    33  			Name:  "console-socket",
    34  			Usage: "path to an AF_UNIX socket which will receive a file descriptor referencing the master end of the console's pseudoterminal",
    35  		},
    36  		cli.StringFlag{
    37  			Name:  "pidfd-socket",
    38  			Usage: "path to an AF_UNIX socket which will receive a file descriptor referencing the exec process",
    39  		},
    40  		cli.StringFlag{
    41  			Name:  "cwd",
    42  			Usage: "current working directory in the container",
    43  		},
    44  		cli.StringSliceFlag{
    45  			Name:  "env, e",
    46  			Usage: "set environment variables",
    47  		},
    48  		cli.BoolFlag{
    49  			Name:  "tty, t",
    50  			Usage: "allocate a pseudo-TTY",
    51  		},
    52  		cli.StringFlag{
    53  			Name:  "user, u",
    54  			Usage: "UID (format: <uid>[:<gid>])",
    55  		},
    56  		cli.Int64SliceFlag{
    57  			Name:  "additional-gids, g",
    58  			Usage: "additional gids",
    59  		},
    60  		cli.StringFlag{
    61  			Name:  "process, p",
    62  			Usage: "path to the process.json",
    63  		},
    64  		cli.BoolFlag{
    65  			Name:  "detach,d",
    66  			Usage: "detach from the container's process",
    67  		},
    68  		cli.StringFlag{
    69  			Name:  "pid-file",
    70  			Value: "",
    71  			Usage: "specify the file to write the process id to",
    72  		},
    73  		cli.StringFlag{
    74  			Name:  "process-label",
    75  			Usage: "set the asm process label for the process commonly used with selinux",
    76  		},
    77  		cli.StringFlag{
    78  			Name:  "apparmor",
    79  			Usage: "set the apparmor profile for the process",
    80  		},
    81  		cli.BoolFlag{
    82  			Name:  "no-new-privs",
    83  			Usage: "set the no new privileges value for the process",
    84  		},
    85  		cli.StringSliceFlag{
    86  			Name:  "cap, c",
    87  			Value: &cli.StringSlice{},
    88  			Usage: "add a capability to the bounding set for the process",
    89  		},
    90  		cli.IntFlag{
    91  			Name:  "preserve-fds",
    92  			Usage: "Pass N additional file descriptors to the container (stdio + $LISTEN_FDS + N in total)",
    93  		},
    94  		cli.StringSliceFlag{
    95  			Name:  "cgroup",
    96  			Usage: "run the process in an (existing) sub-cgroup(s). Format is [<controller>:]<cgroup>.",
    97  		},
    98  		cli.BoolFlag{
    99  			Name:  "ignore-paused",
   100  			Usage: "allow exec in a paused container",
   101  		},
   102  	},
   103  	Action: func(context *cli.Context) error {
   104  		if err := checkArgs(context, 1, minArgs); err != nil {
   105  			return err
   106  		}
   107  		if err := revisePidFile(context); err != nil {
   108  			return err
   109  		}
   110  		status, err := execProcess(context)
   111  		if err == nil {
   112  			os.Exit(status)
   113  		}
   114  		fatalWithCode(fmt.Errorf("exec failed: %w", err), 255)
   115  		return nil // to satisfy the linter
   116  	},
   117  	SkipArgReorder: true,
   118  }
   119  
   120  func getSubCgroupPaths(args []string) (map[string]string, error) {
   121  	if len(args) == 0 {
   122  		return nil, nil
   123  	}
   124  	paths := make(map[string]string, len(args))
   125  	for _, c := range args {
   126  		// Split into controller:path.
   127  		cs := strings.SplitN(c, ":", 3)
   128  		if len(cs) > 2 {
   129  			return nil, fmt.Errorf("invalid --cgroup argument: %s", c)
   130  		}
   131  		if len(cs) == 1 { // no controller: prefix
   132  			if len(args) != 1 {
   133  				return nil, fmt.Errorf("invalid --cgroup argument: %s (missing <controller>: prefix)", c)
   134  			}
   135  			paths[""] = c
   136  		} else {
   137  			// There may be a few comma-separated controllers.
   138  			for _, ctrl := range strings.Split(cs[0], ",") {
   139  				paths[ctrl] = cs[1]
   140  			}
   141  		}
   142  	}
   143  	return paths, nil
   144  }
   145  
   146  func execProcess(context *cli.Context) (int, error) {
   147  	container, err := getContainer(context)
   148  	if err != nil {
   149  		return -1, err
   150  	}
   151  	status, err := container.Status()
   152  	if err != nil {
   153  		return -1, err
   154  	}
   155  	if status == libcontainer.Stopped {
   156  		return -1, errors.New("cannot exec in a stopped container")
   157  	}
   158  	if status == libcontainer.Paused && !context.Bool("ignore-paused") {
   159  		return -1, errors.New("cannot exec in a paused container (use --ignore-paused to override)")
   160  	}
   161  	path := context.String("process")
   162  	if path == "" && len(context.Args()) == 1 {
   163  		return -1, errors.New("process args cannot be empty")
   164  	}
   165  	state, err := container.State()
   166  	if err != nil {
   167  		return -1, err
   168  	}
   169  	bundle, ok := utils.SearchLabels(state.Config.Labels, "bundle")
   170  	if !ok {
   171  		return -1, errors.New("bundle not found in labels")
   172  	}
   173  	p, err := getProcess(context, bundle)
   174  	if err != nil {
   175  		return -1, err
   176  	}
   177  
   178  	cgPaths, err := getSubCgroupPaths(context.StringSlice("cgroup"))
   179  	if err != nil {
   180  		return -1, err
   181  	}
   182  
   183  	r := &runner{
   184  		enableSubreaper: false,
   185  		shouldDestroy:   false,
   186  		container:       container,
   187  		consoleSocket:   context.String("console-socket"),
   188  		pidfdSocket:     context.String("pidfd-socket"),
   189  		detach:          context.Bool("detach"),
   190  		pidFile:         context.String("pid-file"),
   191  		action:          CT_ACT_RUN,
   192  		init:            false,
   193  		preserveFDs:     context.Int("preserve-fds"),
   194  		subCgroupPaths:  cgPaths,
   195  	}
   196  	return r.run(p)
   197  }
   198  
   199  func getProcess(context *cli.Context, bundle string) (*specs.Process, error) {
   200  	if path := context.String("process"); path != "" {
   201  		f, err := os.Open(path)
   202  		if err != nil {
   203  			return nil, err
   204  		}
   205  		defer f.Close()
   206  		var p specs.Process
   207  		if err := json.NewDecoder(f).Decode(&p); err != nil {
   208  			return nil, err
   209  		}
   210  		return &p, validateProcessSpec(&p)
   211  	}
   212  	// process via cli flags
   213  	if err := os.Chdir(bundle); err != nil {
   214  		return nil, err
   215  	}
   216  	spec, err := loadSpec(specConfig)
   217  	if err != nil {
   218  		return nil, err
   219  	}
   220  	p := spec.Process
   221  	p.Args = context.Args()[1:]
   222  	// override the cwd, if passed
   223  	if context.String("cwd") != "" {
   224  		p.Cwd = context.String("cwd")
   225  	}
   226  	if ap := context.String("apparmor"); ap != "" {
   227  		p.ApparmorProfile = ap
   228  	}
   229  	if l := context.String("process-label"); l != "" {
   230  		p.SelinuxLabel = l
   231  	}
   232  	if caps := context.StringSlice("cap"); len(caps) > 0 {
   233  		for _, c := range caps {
   234  			p.Capabilities.Bounding = append(p.Capabilities.Bounding, c)
   235  			p.Capabilities.Effective = append(p.Capabilities.Effective, c)
   236  			p.Capabilities.Permitted = append(p.Capabilities.Permitted, c)
   237  			p.Capabilities.Ambient = append(p.Capabilities.Ambient, c)
   238  		}
   239  	}
   240  	// append the passed env variables
   241  	p.Env = append(p.Env, context.StringSlice("env")...)
   242  
   243  	// set the tty
   244  	p.Terminal = false
   245  	if context.IsSet("tty") {
   246  		p.Terminal = context.Bool("tty")
   247  	}
   248  	if context.IsSet("no-new-privs") {
   249  		p.NoNewPrivileges = context.Bool("no-new-privs")
   250  	}
   251  	// override the user, if passed
   252  	if context.String("user") != "" {
   253  		u := strings.SplitN(context.String("user"), ":", 2)
   254  		if len(u) > 1 {
   255  			gid, err := strconv.Atoi(u[1])
   256  			if err != nil {
   257  				return nil, fmt.Errorf("parsing %s as int for gid failed: %w", u[1], err)
   258  			}
   259  			p.User.GID = uint32(gid)
   260  		}
   261  		uid, err := strconv.Atoi(u[0])
   262  		if err != nil {
   263  			return nil, fmt.Errorf("parsing %s as int for uid failed: %w", u[0], err)
   264  		}
   265  		p.User.UID = uint32(uid)
   266  	}
   267  	for _, gid := range context.Int64Slice("additional-gids") {
   268  		if gid < 0 {
   269  			return nil, fmt.Errorf("additional-gids must be a positive number %d", gid)
   270  		}
   271  		p.User.AdditionalGids = append(p.User.AdditionalGids, uint32(gid))
   272  	}
   273  	return p, validateProcessSpec(p)
   274  }