github.com/apptainer/singularity@v3.1.1+incompatible/internal/pkg/runtime/engines/singularity/process.go (about)

     1  // Copyright (c) 2018, Sylabs Inc. All rights reserved.
     2  // This software is licensed under a 3-clause BSD license. Please consult the
     3  // LICENSE.md file distributed with the sources of this project regarding your
     4  // rights to use or distribute this software.
     5  
     6  package singularity
     7  
     8  import (
     9  	"encoding/json"
    10  	"fmt"
    11  	"net"
    12  	"os"
    13  	"os/exec"
    14  	"os/signal"
    15  	"reflect"
    16  	"strings"
    17  	"syscall"
    18  	"unsafe"
    19  
    20  	"github.com/sylabs/singularity/internal/pkg/security"
    21  
    22  	"github.com/sylabs/singularity/internal/pkg/util/mainthread"
    23  	"github.com/sylabs/singularity/internal/pkg/util/user"
    24  
    25  	specs "github.com/opencontainers/runtime-spec/specs-go"
    26  	"github.com/sylabs/singularity/internal/pkg/instance"
    27  	"github.com/sylabs/singularity/internal/pkg/sylog"
    28  	"golang.org/x/crypto/ssh/terminal"
    29  )
    30  
    31  func (engine *EngineOperations) checkExec() error {
    32  	shell := engine.EngineConfig.GetShell()
    33  
    34  	if shell == "" {
    35  		shell = "/bin/sh"
    36  	}
    37  
    38  	args := engine.EngineConfig.OciConfig.Process.Args
    39  	env := engine.EngineConfig.OciConfig.Process.Env
    40  
    41  	// match old behavior of searching path
    42  	oldpath := os.Getenv("PATH")
    43  	defer func() {
    44  		os.Setenv("PATH", oldpath)
    45  		engine.EngineConfig.OciConfig.Process.Args = args
    46  		engine.EngineConfig.OciConfig.Process.Env = env
    47  	}()
    48  
    49  	for _, keyval := range env {
    50  		if strings.HasPrefix(keyval, "PATH=") {
    51  			os.Setenv("PATH", keyval[5:])
    52  			break
    53  		}
    54  	}
    55  
    56  	// If args[0] is an absolute path, exec.LookPath() looks for
    57  	// this file directly instead of within PATH
    58  	if _, err := exec.LookPath(args[0]); err == nil {
    59  		return nil
    60  	}
    61  
    62  	// If args[0] isn't executable (either via PATH or absolute path),
    63  	// look for alternative approaches to handling it
    64  	switch args[0] {
    65  	case "/.singularity.d/actions/exec":
    66  		if p, err := exec.LookPath("/.exec"); err == nil {
    67  			args[0] = p
    68  			return nil
    69  		}
    70  		if p, err := exec.LookPath(args[1]); err == nil {
    71  			sylog.Warningf("container does not have %s, calling %s directly", args[0], args[1])
    72  			args[1] = p
    73  			args = args[1:]
    74  			return nil
    75  		}
    76  		return fmt.Errorf("no executable %s found", args[1])
    77  	case "/.singularity.d/actions/shell":
    78  		if p, err := exec.LookPath("/.shell"); err == nil {
    79  			args[0] = p
    80  			return nil
    81  		}
    82  		if p, err := exec.LookPath(shell); err == nil {
    83  			sylog.Warningf("container does not have %s, calling %s directly", args[0], shell)
    84  			args[0] = p
    85  			return nil
    86  		}
    87  		return fmt.Errorf("no %s found inside container", shell)
    88  	case "/.singularity.d/actions/run":
    89  		if p, err := exec.LookPath("/.run"); err == nil {
    90  			args[0] = p
    91  			return nil
    92  		}
    93  		if p, err := exec.LookPath("/singularity"); err == nil {
    94  			args[0] = p
    95  			return nil
    96  		}
    97  		return fmt.Errorf("no run driver found inside container")
    98  	case "/.singularity.d/actions/start":
    99  		if _, err := exec.LookPath(shell); err != nil {
   100  			return fmt.Errorf("no %s found inside container, can't run instance", shell)
   101  		}
   102  		args = []string{shell, "-c", `echo "instance start script not found"`}
   103  		return nil
   104  	case "/.singularity.d/actions/test":
   105  		if p, err := exec.LookPath("/.test"); err == nil {
   106  			args[0] = p
   107  			return nil
   108  		}
   109  		return fmt.Errorf("no test driver found inside container")
   110  	}
   111  
   112  	return fmt.Errorf("no %s found inside container", args[0])
   113  }
   114  
   115  // StartProcess starts the process
   116  func (engine *EngineOperations) StartProcess(masterConn net.Conn) error {
   117  	isInstance := engine.EngineConfig.GetInstance()
   118  	bootInstance := isInstance && engine.EngineConfig.GetBootInstance()
   119  	shimProcess := false
   120  
   121  	if err := os.Chdir(engine.EngineConfig.OciConfig.Process.Cwd); err != nil {
   122  		if err := os.Chdir(engine.EngineConfig.GetHomeDest()); err != nil {
   123  			os.Chdir("/")
   124  		}
   125  	}
   126  
   127  	if err := engine.checkExec(); err != nil {
   128  		return err
   129  	}
   130  
   131  	if engine.EngineConfig.File.MountDev == "minimal" || engine.EngineConfig.GetContain() {
   132  		// If on a terminal, reopen /dev/console so /proc/self/fd/[0-2
   133  		//   will point to /dev/console.  This is needed so that tty and
   134  		//   ttyname() on el6 will return the correct answer.  Newer
   135  		//   ttyname() functions might work because they will search
   136  		//   /dev if the value of /proc/self/fd/X doesn't exist, but
   137  		//   they won't work if another /dev/pts/X is allocated in its
   138  		//   place.  Also, programs that don't use ttyname() and instead
   139  		//   directly do readlink() on /proc/self/fd/X need this.
   140  		for fd := 0; fd <= 2; fd++ {
   141  			if !terminal.IsTerminal(fd) {
   142  				continue
   143  			}
   144  			consfile, err := os.OpenFile("/dev/console", os.O_RDWR, 0600)
   145  			if err != nil {
   146  				sylog.Debugf("Could not open minimal /dev/console, skipping replacing tty descriptors")
   147  				break
   148  			}
   149  			sylog.Debugf("Replacing tty descriptors with /dev/console")
   150  			consfd := int(consfile.Fd())
   151  			for ; fd <= 2; fd++ {
   152  				if !terminal.IsTerminal(fd) {
   153  					continue
   154  				}
   155  				syscall.Close(fd)
   156  				syscall.Dup3(consfd, fd, 0)
   157  			}
   158  			consfile.Close()
   159  			break
   160  		}
   161  	}
   162  
   163  	args := engine.EngineConfig.OciConfig.Process.Args
   164  	env := engine.EngineConfig.OciConfig.Process.Env
   165  
   166  	if engine.EngineConfig.OciConfig.Linux != nil {
   167  		namespaces := engine.EngineConfig.OciConfig.Linux.Namespaces
   168  		for _, ns := range namespaces {
   169  			if ns.Type == specs.PIDNamespace {
   170  				if !engine.EngineConfig.GetNoInit() {
   171  					shimProcess = true
   172  				}
   173  				break
   174  			}
   175  		}
   176  	}
   177  
   178  	for _, img := range engine.EngineConfig.GetImageList() {
   179  		if err := syscall.Close(int(img.Fd)); err != nil {
   180  			return fmt.Errorf("failed to close file descriptor for %s", img.Path)
   181  		}
   182  	}
   183  
   184  	for _, fd := range engine.EngineConfig.GetOpenFd() {
   185  		if err := syscall.Close(fd); err != nil {
   186  			return fmt.Errorf("aborting failed to close file descriptor: %s", err)
   187  		}
   188  	}
   189  
   190  	if err := security.Configure(&engine.EngineConfig.OciConfig.Spec); err != nil {
   191  		return fmt.Errorf("failed to apply security configuration: %s", err)
   192  	}
   193  
   194  	if (!isInstance && !shimProcess) || bootInstance || engine.EngineConfig.GetInstanceJoin() {
   195  		err := syscall.Exec(args[0], args, env)
   196  		return fmt.Errorf("exec %s failed: %s", args[0], err)
   197  	}
   198  
   199  	// Spawn and wait container process, signal handler
   200  	cmd := exec.Command(args[0], args[1:]...)
   201  	cmd.Stdout = os.Stdout
   202  	cmd.Stderr = os.Stderr
   203  	cmd.Stdin = os.Stdin
   204  	cmd.Env = env
   205  
   206  	var status syscall.WaitStatus
   207  	errChan := make(chan error, 1)
   208  	signals := make(chan os.Signal, 1)
   209  
   210  	if err := cmd.Start(); err != nil {
   211  		return fmt.Errorf("exec %s failed: %s", args[0], err)
   212  	}
   213  
   214  	go func() {
   215  		errChan <- cmd.Wait()
   216  	}()
   217  
   218  	// Modify argv argument and program name shown in /proc/self/comm
   219  	name := "sinit"
   220  
   221  	argv0str := (*reflect.StringHeader)(unsafe.Pointer(&os.Args[0]))
   222  	argv0 := (*[1 << 30]byte)(unsafe.Pointer(argv0str.Data))[:argv0str.Len]
   223  	progname := make([]byte, argv0str.Len)
   224  
   225  	if len(name) > argv0str.Len {
   226  		return fmt.Errorf("program name too short")
   227  	}
   228  
   229  	copy(progname, name)
   230  	copy(argv0, progname)
   231  
   232  	ptr := unsafe.Pointer(&progname[0])
   233  	if _, _, err := syscall.Syscall(syscall.SYS_PRCTL, syscall.PR_SET_NAME, uintptr(ptr), 0); err != 0 {
   234  		return syscall.Errno(err)
   235  	}
   236  
   237  	// Manage all signals
   238  	signal.Notify(signals)
   239  
   240  	masterConn.Close()
   241  
   242  	for {
   243  		select {
   244  		case s := <-signals:
   245  			sylog.Debugf("Received signal %s", s.String())
   246  			switch s {
   247  			case syscall.SIGCHLD:
   248  				for {
   249  					wpid, err := syscall.Wait4(-1, &status, syscall.WNOHANG, nil)
   250  					if wpid <= 0 || err != nil {
   251  						break
   252  					}
   253  				}
   254  			default:
   255  				signal := s.(syscall.Signal)
   256  				if isInstance {
   257  					if err := syscall.Kill(-1, signal); err == syscall.ESRCH {
   258  						sylog.Debugf("No child process, exiting ...")
   259  						os.Exit(128 + int(signal))
   260  					}
   261  				} else {
   262  					// kill ourself with SIGKILL whatever signal was received
   263  					syscall.Kill(syscall.Gettid(), syscall.SIGKILL)
   264  				}
   265  			}
   266  		case err := <-errChan:
   267  			if e, ok := err.(*exec.ExitError); ok {
   268  				if status, ok := e.Sys().(syscall.WaitStatus); ok {
   269  					if status.Signaled() {
   270  						syscall.Kill(syscall.Gettid(), syscall.SIGKILL)
   271  					}
   272  					os.Exit(status.ExitStatus())
   273  				}
   274  				return fmt.Errorf("command exit with error: %s", err)
   275  			} else if e, ok := err.(*os.SyscallError); ok {
   276  				// handle possible race with Wait4 call above by ignoring ECHILD
   277  				// error because child process was already catched
   278  				if e.Err.(syscall.Errno) != syscall.ECHILD {
   279  					sylog.Fatalf("error while waiting container process: %s", e.Error())
   280  				}
   281  			}
   282  			if !isInstance {
   283  				os.Exit(0)
   284  			}
   285  		}
   286  	}
   287  }
   288  
   289  // PostStartProcess will execute code in master context after execution of container
   290  // process, typically to write instance state/config files or execute post start OCI hook
   291  func (engine *EngineOperations) PostStartProcess(pid int) error {
   292  	sylog.Debugf("Post start process")
   293  
   294  	if engine.EngineConfig.GetInstance() {
   295  		uid := os.Getuid()
   296  		gid := os.Getgid()
   297  		name := engine.CommonConfig.ContainerID
   298  		privileged := true
   299  
   300  		if err := os.Chdir("/"); err != nil {
   301  			return fmt.Errorf("failed to change directory to /: %s", err)
   302  		}
   303  
   304  		if engine.EngineConfig.OciConfig.Linux != nil {
   305  			for _, ns := range engine.EngineConfig.OciConfig.Linux.Namespaces {
   306  				if ns.Type == specs.UserNamespace {
   307  					privileged = false
   308  					break
   309  				}
   310  			}
   311  		}
   312  
   313  		file, err := instance.Add(name, privileged, instance.SingSubDir)
   314  		if err != nil {
   315  			return err
   316  		}
   317  
   318  		file.Config, err = json.Marshal(engine.CommonConfig)
   319  		if err != nil {
   320  			return err
   321  		}
   322  
   323  		pw, err := user.GetPwUID(uint32(uid))
   324  		if err != nil {
   325  			return err
   326  		}
   327  		file.User = pw.Name
   328  		file.Pid = pid
   329  		file.PPid = os.Getpid()
   330  		file.Image = engine.EngineConfig.GetImage()
   331  
   332  		if privileged {
   333  			var err error
   334  
   335  			mainthread.Execute(func() {
   336  				if err = syscall.Setresuid(0, 0, uid); err != nil {
   337  					err = fmt.Errorf("failed to escalate uid privileges")
   338  					return
   339  				}
   340  				if err = syscall.Setresgid(0, 0, gid); err != nil {
   341  					err = fmt.Errorf("failed to escalate gid privileges")
   342  					return
   343  				}
   344  				if err = file.Update(); err != nil {
   345  					return
   346  				}
   347  				if err = file.MountNamespaces(); err != nil {
   348  					return
   349  				}
   350  				if err = syscall.Setresgid(gid, gid, 0); err != nil {
   351  					err = fmt.Errorf("failed to escalate gid privileges")
   352  					return
   353  				}
   354  				if err := syscall.Setresuid(uid, uid, 0); err != nil {
   355  					err = fmt.Errorf("failed to escalate uid privileges")
   356  					return
   357  				}
   358  			})
   359  
   360  			return err
   361  		}
   362  
   363  		if err := file.Update(); err != nil {
   364  			return err
   365  		}
   366  		return file.MountNamespaces()
   367  	}
   368  	return nil
   369  }