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 }