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 }