1
//go:build !remote && (linux || freebsd)
17
"github.com/containers/common/pkg/config"
18
"github.com/containers/common/pkg/detach"
19
"github.com/containers/common/pkg/resize"
20
"github.com/containers/podman/v5/libpod/define"
21
"github.com/containers/podman/v5/pkg/errorhandling"
22
"github.com/containers/podman/v5/pkg/lookup"
23
spec "github.com/opencontainers/runtime-spec/specs-go"
24
"github.com/sirupsen/logrus"
25
"golang.org/x/sys/unix"
28
// ExecContainer executes a command in a running container
29
func (r *ConmonOCIRuntime) ExecContainer(c *Container, sessionID string, options *ExecOptions, streams *define.AttachStreams, newSize *resize.TerminalSize) (int, chan error, error) {
31
return -1, nil, fmt.Errorf("must provide an ExecOptions struct to ExecContainer: %w", define.ErrInvalidArg)
33
if len(options.Cmd) == 0 {
34
return -1, nil, fmt.Errorf("must provide a command to execute: %w", define.ErrInvalidArg)
38
return -1, nil, fmt.Errorf("must provide a session ID for exec: %w", define.ErrEmptyID)
41
// TODO: Should we default this to false?
42
// Or maybe make streams mandatory?
45
attachStdin = streams.AttachInput
49
if logrus.GetLevel() != logrus.DebugLevel && r.supportsJSON {
50
ociLog = c.execOCILog(sessionID)
53
execCmd, pipes, err := r.startExec(c, sessionID, options, attachStdin, ociLog)
58
// Only close sync pipe. Start and attach are consumed in the attach
61
if pipes.syncPipe != nil && !pipes.syncClosed {
62
errorhandling.CloseQuiet(pipes.syncPipe)
63
pipes.syncClosed = true
67
// TODO Only create if !detach
68
// Attach to the container before starting it
69
attachChan := make(chan error)
71
// attachToExec is responsible for closing pipes
72
attachChan <- c.attachToExec(streams, options.DetachKeys, sessionID, pipes.startPipe, pipes.attachPipe, newSize)
76
if err := execCmd.Wait(); err != nil {
77
return -1, nil, fmt.Errorf("cannot run conmon: %w", err)
80
pid, err := readConmonPipeData(r.name, pipes.syncPipe, ociLog)
82
return pid, attachChan, err
85
// ExecContainerHTTP executes a new command in an existing container and
86
// forwards its standard streams over an attach
87
func (r *ConmonOCIRuntime) ExecContainerHTTP(ctr *Container, sessionID string, options *ExecOptions, req *http.Request, w http.ResponseWriter,
88
streams *HTTPAttachStreams, cancel <-chan bool, hijackDone chan<- bool, holdConnOpen <-chan bool, newSize *resize.TerminalSize) (int, chan error, error) {
90
if !streams.Stdin && !streams.Stdout && !streams.Stderr {
91
return -1, nil, fmt.Errorf("must provide at least one stream to attach to: %w", define.ErrInvalidArg)
96
return -1, nil, fmt.Errorf("must provide exec options to ExecContainerHTTP: %w", define.ErrInvalidArg)
99
detachString := config.DefaultDetachKeys
100
if options.DetachKeys != nil {
101
detachString = *options.DetachKeys
103
detachKeys, err := processDetachKeys(detachString)
108
// TODO: Should we default this to false?
109
// Or maybe make streams mandatory?
112
attachStdin = streams.Stdin
116
if logrus.GetLevel() != logrus.DebugLevel && r.supportsJSON {
117
ociLog = ctr.execOCILog(sessionID)
120
execCmd, pipes, err := r.startExec(ctr, sessionID, options, attachStdin, ociLog)
125
// Only close sync pipe. Start and attach are consumed in the attach
128
if pipes.syncPipe != nil && !pipes.syncClosed {
129
errorhandling.CloseQuiet(pipes.syncPipe)
130
pipes.syncClosed = true
134
attachChan := make(chan error)
135
conmonPipeDataChan := make(chan conmonPipeData)
137
// attachToExec is responsible for closing pipes
138
attachChan <- attachExecHTTP(ctr, sessionID, req, w, streams, pipes, detachKeys, options.Terminal, cancel, hijackDone, holdConnOpen, execCmd, conmonPipeDataChan, ociLog, newSize, r.name)
142
// NOTE: the channel is needed to communicate conmon's data. In case
143
// of an error, the error will be written on the hijacked http
144
// connection such that remote clients will receive the error.
145
pipeData := <-conmonPipeDataChan
147
return pipeData.pid, attachChan, pipeData.err
150
// conmonPipeData contains the data when reading from conmon's pipe.
151
type conmonPipeData struct {
156
// ExecContainerDetached executes a command in a running container, but does
158
func (r *ConmonOCIRuntime) ExecContainerDetached(ctr *Container, sessionID string, options *ExecOptions, stdin bool) (int, error) {
160
return -1, fmt.Errorf("must provide exec options to ExecContainerHTTP: %w", define.ErrInvalidArg)
164
if logrus.GetLevel() != logrus.DebugLevel && r.supportsJSON {
165
ociLog = ctr.execOCILog(sessionID)
168
execCmd, pipes, err := r.startExec(ctr, sessionID, options, stdin, ociLog)
177
// Wait for Conmon to tell us we're ready to attach.
178
// We aren't actually *going* to attach, but this means that we're good
180
if _, err := readConmonPipeData(r.name, pipes.attachPipe, ""); err != nil {
184
// Start the exec session
185
if err := writeConmonPipeData(pipes.startPipe); err != nil {
189
// Wait for conmon to succeed, when return.
190
if err := execCmd.Wait(); err != nil {
191
return -1, fmt.Errorf("cannot run conmon: %w", err)
194
pid, err := readConmonPipeData(r.name, pipes.syncPipe, ociLog)
199
// ExecAttachResize resizes the TTY of the given exec session.
200
func (r *ConmonOCIRuntime) ExecAttachResize(ctr *Container, sessionID string, newSize resize.TerminalSize) error {
201
controlFile, err := openControlFile(ctr, ctr.execBundlePath(sessionID))
205
defer controlFile.Close()
207
if _, err = fmt.Fprintf(controlFile, "%d %d %d\n", 1, newSize.Height, newSize.Width); err != nil {
208
return fmt.Errorf("failed to write to ctl file to resize terminal: %w", err)
214
// ExecStopContainer stops a given exec session in a running container.
215
func (r *ConmonOCIRuntime) ExecStopContainer(ctr *Container, sessionID string, timeout uint) error {
216
pid, err := ctr.getExecSessionPID(sessionID)
221
logrus.Debugf("Going to stop container %s exec session %s", ctr.ID(), sessionID)
223
// Is the session dead?
224
// Ping the PID with signal 0 to see if it still exists.
225
if err := unix.Kill(pid, 0); err != nil {
226
if err == unix.ESRCH {
229
return fmt.Errorf("pinging container %s exec session %s PID %d with signal 0: %w", ctr.ID(), sessionID, pid, err)
233
// Use SIGTERM by default, then SIGSTOP after timeout.
234
logrus.Debugf("Killing exec session %s (PID %d) of container %s with SIGTERM", sessionID, pid, ctr.ID())
235
if err := unix.Kill(pid, unix.SIGTERM); err != nil {
236
if err == unix.ESRCH {
239
return fmt.Errorf("killing container %s exec session %s PID %d with SIGTERM: %w", ctr.ID(), sessionID, pid, err)
242
// Wait for the PID to stop
243
if err := waitPidStop(pid, time.Duration(timeout)*time.Second); err != nil {
244
logrus.Infof("Timed out waiting for container %s exec session %s to stop, resorting to SIGKILL: %v", ctr.ID(), sessionID, err)
246
// No error, container is dead
251
// SIGTERM did not work. On to SIGKILL.
252
logrus.Debugf("Killing exec session %s (PID %d) of container %s with SIGKILL", sessionID, pid, ctr.ID())
253
if err := unix.Kill(pid, unix.SIGTERM); err != nil {
254
if err == unix.ESRCH {
257
return fmt.Errorf("killing container %s exec session %s PID %d with SIGKILL: %w", ctr.ID(), sessionID, pid, err)
260
// Wait for the PID to stop
261
if err := waitPidStop(pid, killContainerTimeout); err != nil {
262
return fmt.Errorf("timed out waiting for container %s exec session %s PID %d to stop after SIGKILL: %w", ctr.ID(), sessionID, pid, err)
268
// ExecUpdateStatus checks if the given exec session is still running.
269
func (r *ConmonOCIRuntime) ExecUpdateStatus(ctr *Container, sessionID string) (bool, error) {
270
pid, err := ctr.getExecSessionPID(sessionID)
275
logrus.Debugf("Checking status of container %s exec session %s", ctr.ID(), sessionID)
277
// Is the session dead?
278
// Ping the PID with signal 0 to see if it still exists.
279
if err := unix.Kill(pid, 0); err != nil {
280
if err == unix.ESRCH {
283
return false, fmt.Errorf("pinging container %s exec session %s PID %d with signal 0: %w", ctr.ID(), sessionID, pid, err)
289
// ExecAttachSocketPath is the path to a container's exec session attach socket.
290
func (r *ConmonOCIRuntime) ExecAttachSocketPath(ctr *Container, sessionID string) (string, error) {
291
// We don't even use container, so don't validity check it
293
return "", fmt.Errorf("must provide a valid session ID to get attach socket path: %w", define.ErrInvalidArg)
296
return filepath.Join(ctr.execBundlePath(sessionID), "attach"), nil
299
// This contains pipes used by the exec API.
300
type execPipes struct {
309
func (p *execPipes) cleanup() {
310
if p.syncPipe != nil && !p.syncClosed {
311
errorhandling.CloseQuiet(p.syncPipe)
314
if p.startPipe != nil && !p.startClosed {
315
errorhandling.CloseQuiet(p.startPipe)
318
if p.attachPipe != nil && !p.attachClosed {
319
errorhandling.CloseQuiet(p.attachPipe)
320
p.attachClosed = true
324
// Start an exec session's conmon parent from the given options.
325
func (r *ConmonOCIRuntime) startExec(c *Container, sessionID string, options *ExecOptions, attachStdin bool, ociLog string) (_ *exec.Cmd, _ *execPipes, deferredErr error) {
326
pipes := new(execPipes)
329
return nil, nil, fmt.Errorf("must provide an ExecOptions struct to ExecContainer: %w", define.ErrInvalidArg)
331
if len(options.Cmd) == 0 {
332
return nil, nil, fmt.Errorf("must provide a command to execute: %w", define.ErrInvalidArg)
336
return nil, nil, fmt.Errorf("must provide a session ID for exec: %w", define.ErrEmptyID)
339
// create sync pipe to receive the pid
340
parentSyncPipe, childSyncPipe, err := newPipe()
342
return nil, nil, fmt.Errorf("creating socket pair: %w", err)
344
pipes.syncPipe = parentSyncPipe
347
if deferredErr != nil {
352
// create start pipe to set the cgroup before running
353
// attachToExec is responsible for closing parentStartPipe
354
childStartPipe, parentStartPipe, err := newPipe()
356
return nil, nil, fmt.Errorf("creating socket pair: %w", err)
358
pipes.startPipe = parentStartPipe
360
// create the attach pipe to allow attach socket to be created before
361
// $RUNTIME exec starts running. This is to make sure we can capture all output
362
// from the process through that socket, rather than half reading the log, half attaching to the socket
363
// attachToExec is responsible for closing parentAttachPipe
364
parentAttachPipe, childAttachPipe, err := newPipe()
366
return nil, nil, fmt.Errorf("creating socket pair: %w", err)
368
pipes.attachPipe = parentAttachPipe
370
childrenClosed := false
373
errorhandling.CloseQuiet(childSyncPipe)
374
errorhandling.CloseQuiet(childAttachPipe)
375
errorhandling.CloseQuiet(childStartPipe)
379
finalEnv := make([]string, 0, len(options.Env))
380
for k, v := range options.Env {
381
finalEnv = append(finalEnv, fmt.Sprintf("%s=%s", k, v))
384
processFile, err := c.prepareProcessExec(options, finalEnv, sessionID)
388
defer processFile.Close()
390
args, err := r.sharedConmonArgs(c, sessionID, c.execBundlePath(sessionID), c.execPidPath(sessionID), c.execLogPath(sessionID), c.execExitFileDir(sessionID), c.execPersistDir(sessionID), ociLog, define.NoLogging, c.config.LogTag)
395
preserveFDs, filesToClose, extraFiles, err := getPreserveFdExtraFiles(options.PreserveFD, options.PreserveFDs)
401
args = append(args, formatRuntimeOpts("--preserve-fds", strconv.FormatUint(uint64(preserveFDs), 10))...)
404
if options.Terminal {
405
args = append(args, "-t")
409
args = append(args, "-i")
412
// Append container ID and command
413
args = append(args, "-e")
414
// TODO make this optional when we can detach
415
args = append(args, "--exec-attach")
416
args = append(args, "--exec-process-spec", processFile.Name())
418
if len(options.ExitCommand) > 0 {
419
args = append(args, "--exit-command", options.ExitCommand[0])
420
for _, arg := range options.ExitCommand[1:] {
421
args = append(args, []string{"--exit-command-arg", arg}...)
423
if options.ExitCommandDelay > 0 {
424
args = append(args, []string{"--exit-delay", strconv.FormatUint(uint64(options.ExitCommandDelay), 10)}...)
428
logrus.WithFields(logrus.Fields{
430
}).Debugf("running conmon: %s", r.conmonPath)
431
execCmd := exec.Command(r.conmonPath, args...)
433
// TODO: This is commented because it doesn't make much sense in HTTP
434
// attach, and I'm not certain it does for non-HTTP attach as well.
435
// if streams != nil {
436
// // Don't add the InputStream to the execCmd. Instead, the data should be passed
437
// // through CopyDetachable
438
// if streams.AttachOutput {
439
// execCmd.Stdout = options.Streams.OutputStream
441
// if streams.AttachError {
442
// execCmd.Stderr = options.Streams.ErrorStream
446
conmonEnv, err := r.configureConmonEnv()
448
return nil, nil, fmt.Errorf("configuring conmon env: %w", err)
451
execCmd.ExtraFiles = extraFiles
453
// we don't want to step on users fds they asked to preserve
454
// Since 0-2 are used for stdio, start the fds we pass in at preserveFDs+3
455
execCmd.Env = r.conmonEnv
456
execCmd.Env = append(execCmd.Env, fmt.Sprintf("_OCI_SYNCPIPE=%d", preserveFDs+3), fmt.Sprintf("_OCI_STARTPIPE=%d", preserveFDs+4), fmt.Sprintf("_OCI_ATTACHPIPE=%d", preserveFDs+5))
457
execCmd.Env = append(execCmd.Env, conmonEnv...)
459
execCmd.ExtraFiles = append(execCmd.ExtraFiles, childSyncPipe, childStartPipe, childAttachPipe)
460
execCmd.Dir = c.execBundlePath(sessionID)
461
execCmd.SysProcAttr = &syscall.SysProcAttr{
465
err = execCmd.Start()
467
// We don't need children pipes on the parent side
468
errorhandling.CloseQuiet(childSyncPipe)
469
errorhandling.CloseQuiet(childAttachPipe)
470
errorhandling.CloseQuiet(childStartPipe)
471
childrenClosed = true
474
return nil, nil, fmt.Errorf("cannot start container %s: %w", c.ID(), err)
476
if err := r.moveConmonToCgroupAndSignal(c, execCmd, parentStartPipe); err != nil {
480
// These fds were passed down to the runtime. Close them
482
for _, f := range filesToClose {
483
errorhandling.CloseQuiet(f)
486
return execCmd, pipes, nil
489
// Attach to a container over HTTP
490
func attachExecHTTP(c *Container, sessionID string, r *http.Request, w http.ResponseWriter, streams *HTTPAttachStreams, pipes *execPipes, detachKeys []byte, isTerminal bool, cancel <-chan bool, hijackDone chan<- bool, holdConnOpen <-chan bool, execCmd *exec.Cmd, conmonPipeDataChan chan<- conmonPipeData, ociLog string, newSize *resize.TerminalSize, runtimeName string) (deferredErr error) {
491
// NOTE: As you may notice, the attach code is quite complex.
492
// Many things happen concurrently and yet are interdependent.
493
// If you ever change this function, make sure to write to the
494
// conmonPipeDataChan in case of an error.
496
if pipes == nil || pipes.startPipe == nil || pipes.attachPipe == nil {
497
err := fmt.Errorf("must provide a start and attach pipe to finish an exec attach: %w", define.ErrInvalidArg)
498
conmonPipeDataChan <- conmonPipeData{-1, err}
503
if !pipes.startClosed {
504
errorhandling.CloseQuiet(pipes.startPipe)
505
pipes.startClosed = true
507
if !pipes.attachClosed {
508
errorhandling.CloseQuiet(pipes.attachPipe)
509
pipes.attachClosed = true
513
logrus.Debugf("Attaching to container %s exec session %s", c.ID(), sessionID)
515
// set up the socket path, such that it is the correct length and location for exec
516
sockPath, err := c.execAttachSocketPath(sessionID)
518
conmonPipeDataChan <- conmonPipeData{-1, err}
522
// 2: read from attachFd that the parent process has set up the console socket
523
if _, err := readConmonPipeData(runtimeName, pipes.attachPipe, ""); err != nil {
524
conmonPipeDataChan <- conmonPipeData{-1, err}
528
// resize before we start the container process
530
err = c.ociRuntime.ExecAttachResize(c, sessionID, *newSize)
532
logrus.Warnf("Resize failed: %v", err)
537
conn, err := openUnixSocket(sockPath)
539
conmonPipeDataChan <- conmonPipeData{-1, err}
540
return fmt.Errorf("failed to connect to container's attach socket: %v: %w", sockPath, err)
543
if err := conn.Close(); err != nil {
544
logrus.Errorf("Unable to close socket: %q", err)
552
attachStdout = streams.Stdout
553
attachStderr = streams.Stderr
554
attachStdin = streams.Stdin
558
hijacker, ok := w.(http.Hijacker)
560
conmonPipeDataChan <- conmonPipeData{-1, err}
561
return errors.New("unable to hijack connection")
564
httpCon, httpBuf, err := hijacker.Hijack()
566
conmonPipeDataChan <- conmonPipeData{-1, err}
567
return fmt.Errorf("hijacking connection: %w", err)
572
// Write a header to let the client know what happened
573
writeHijackHeader(r, httpBuf, isTerminal)
575
// Force a flush after the header is written.
576
if err := httpBuf.Flush(); err != nil {
577
conmonPipeDataChan <- conmonPipeData{-1, err}
578
return fmt.Errorf("flushing HTTP hijack header: %w", err)
582
// Wait for conmon to succeed, when return.
583
if err := execCmd.Wait(); err != nil {
584
conmonPipeDataChan <- conmonPipeData{-1, err}
586
pid, err := readConmonPipeData(runtimeName, pipes.syncPipe, ociLog)
588
hijackWriteError(err, c.ID(), isTerminal, httpBuf)
589
conmonPipeDataChan <- conmonPipeData{pid, err}
591
conmonPipeDataChan <- conmonPipeData{pid, err}
594
// We need to hold the connection open until the complete exec
595
// function has finished. This channel will be closed in a defer
596
// in that function, so we can wait for it here.
597
// Can't be a defer, because this would block the function from
600
hijackWriteErrorAndClose(deferredErr, c.ID(), isTerminal, httpCon, httpBuf)
603
stdoutChan := make(chan error)
604
stdinChan := make(chan error)
606
// Next, STDIN. Avoid entirely if attachStdin unset.
609
logrus.Debugf("Beginning STDIN copy")
610
_, err := detach.Copy(conn, httpBuf, detachKeys)
611
logrus.Debugf("STDIN copy completed")
616
// 4: send start message to child
617
if err := writeConmonPipeData(pipes.startPipe); err != nil {
621
// Handle STDOUT/STDERR *after* start message is sent
625
// Hack: return immediately if attachStdout not set to
627
// Basically, when terminal is set, STDERR goes nowhere.
628
// Everything does over STDOUT.
629
// Therefore, if not attaching STDOUT - we'll never copy
630
// anything from here.
631
logrus.Debugf("Performing terminal HTTP attach for container %s", c.ID())
633
err = httpAttachTerminalCopy(conn, httpBuf, c.ID())
636
logrus.Debugf("Performing non-terminal HTTP attach for container %s", c.ID())
637
err = httpAttachNonTerminalCopy(conn, httpBuf, c.ID(), attachStdin, attachStdout, attachStderr)
640
logrus.Debugf("STDOUT/ERR copy completed")
645
case err := <-stdoutChan:
651
case err := <-stdinChan:
655
// copy stdin is done, close it
656
if connErr := socketCloseWrite(conn); connErr != nil {
657
logrus.Errorf("Unable to close conn: %v", connErr)
665
// prepareProcessExec returns the path of the process.json used in runc exec -p
666
// caller is responsible to close the returned *os.File if needed.
667
func (c *Container) prepareProcessExec(options *ExecOptions, env []string, sessionID string) (*os.File, error) {
668
f, err := os.CreateTemp(c.execBundlePath(sessionID), "exec-process-")
672
pspec := new(spec.Process)
673
if err := JSONDeepCopy(c.config.Spec.Process, pspec); err != nil {
676
pspec.SelinuxLabel = c.config.ProcessLabel
677
pspec.Args = options.Cmd
679
// We need to default this to false else it will inherit terminal as true
680
// from the container.
681
pspec.Terminal = false
682
if options.Terminal {
683
pspec.Terminal = true
686
pspec.Env = append(pspec.Env, env...)
689
// Add secret envs if they exist
690
manager, err := c.runtime.SecretsManager()
694
for name, secr := range c.config.EnvSecrets {
695
_, data, err := manager.LookupSecretData(secr.Name)
699
pspec.Env = append(pspec.Env, fmt.Sprintf("%s=%s", name, string(data)))
702
if options.Cwd != "" {
703
pspec.Cwd = options.Cwd
706
var addGroups []string
709
// if the user is empty, we should inherit the user that the container is currently running with
712
logrus.Debugf("Set user to %s", c.config.User)
714
addGroups = c.config.Groups
717
overrides := c.getUserOverrides()
718
execUser, err := lookup.GetUserGroupInfo(c.state.Mountpoint, user, overrides)
723
if len(addGroups) > 0 {
724
sgids, err = lookup.GetContainerGroups(addGroups, c.state.Mountpoint, overrides)
726
return nil, fmt.Errorf("looking up supplemental groups for container %s exec session %s: %w", c.ID(), sessionID, err)
730
// If user was set, look it up in the container to get a UID to use on
732
if user != "" || len(sgids) > 0 {
734
for _, sgid := range execUser.Sgids {
735
sgids = append(sgids, uint32(sgid))
738
processUser := spec.User{
739
UID: uint32(execUser.Uid),
740
GID: uint32(execUser.Gid),
741
AdditionalGids: sgids,
744
pspec.User = processUser
747
if c.config.Umask != "" {
748
umask, err := c.umask()
752
pspec.User.Umask = &umask
755
if err := c.setProcessCapabilitiesExec(options, user, execUser, pspec); err != nil {
760
for _, s := range pspec.Env {
761
if strings.HasPrefix(s, "HOME=") {
767
pspec.Env = append(pspec.Env, fmt.Sprintf("HOME=%s", execUser.Home))
770
processJSON, err := json.Marshal(pspec)
775
if err := os.WriteFile(f.Name(), processJSON, 0644); err != nil {