github.com/apptainer/singularity@v3.1.1+incompatible/cmd/internal/cli/actions_linux.go (about)

     1  // Copyright (c) 2019, 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 cli
     7  
     8  import (
     9  	"encoding/json"
    10  	"fmt"
    11  	"io/ioutil"
    12  	"os"
    13  	"path/filepath"
    14  	"strconv"
    15  	"strings"
    16  	"syscall"
    17  	"time"
    18  
    19  	"github.com/opencontainers/runtime-tools/generate"
    20  	"github.com/sylabs/singularity/internal/pkg/util/nvidiautils"
    21  	"github.com/sylabs/singularity/pkg/image"
    22  	"github.com/sylabs/singularity/pkg/image/unpacker"
    23  
    24  	"github.com/spf13/cobra"
    25  	"github.com/sylabs/singularity/internal/pkg/buildcfg"
    26  	"github.com/sylabs/singularity/internal/pkg/instance"
    27  	"github.com/sylabs/singularity/internal/pkg/runtime/engines/config"
    28  	"github.com/sylabs/singularity/internal/pkg/runtime/engines/config/oci"
    29  	singularityConfig "github.com/sylabs/singularity/internal/pkg/runtime/engines/singularity/config"
    30  	"github.com/sylabs/singularity/internal/pkg/security"
    31  	"github.com/sylabs/singularity/internal/pkg/sylog"
    32  	"github.com/sylabs/singularity/internal/pkg/util/env"
    33  	"github.com/sylabs/singularity/internal/pkg/util/exec"
    34  	"github.com/sylabs/singularity/internal/pkg/util/fs"
    35  	"github.com/sylabs/singularity/internal/pkg/util/user"
    36  )
    37  
    38  func convertImage(filename string, unsquashfsPath string) (string, error) {
    39  	img, err := image.Init(filename, false)
    40  	if err != nil {
    41  		return "", fmt.Errorf("could not open image %s: %s", filename, err)
    42  	}
    43  	defer img.File.Close()
    44  
    45  	// squashfs only
    46  	if img.Partitions[0].Type != image.SQUASHFS {
    47  		return "", fmt.Errorf("not a squashfs root filesystem")
    48  	}
    49  
    50  	// create a reader for rootfs partition
    51  	reader, err := image.NewPartitionReader(img, "", 0)
    52  	if err != nil {
    53  		return "", fmt.Errorf("could not extract root filesystem: %s", err)
    54  	}
    55  	s := unpacker.NewSquashfs()
    56  	if !s.HasUnsquashfs() && unsquashfsPath != "" {
    57  		s.UnsquashfsPath = unsquashfsPath
    58  	}
    59  
    60  	// keep compatibility with v2
    61  	tmpdir := os.Getenv("SINGULARITY_LOCALCACHEDIR")
    62  	if tmpdir == "" {
    63  		tmpdir = os.Getenv("SINGULARITY_CACHEDIR")
    64  	}
    65  
    66  	// create temporary sandbox
    67  	dir, err := ioutil.TempDir(tmpdir, "rootfs-")
    68  	if err != nil {
    69  		return "", fmt.Errorf("could not create temporary sandbox: %s", err)
    70  	}
    71  
    72  	// extract root filesystem
    73  	if err := s.ExtractAll(reader, dir); err != nil {
    74  		os.RemoveAll(dir)
    75  		return "", fmt.Errorf("root filesystem extraction failed: %s", err)
    76  	}
    77  
    78  	return dir, err
    79  }
    80  
    81  // TODO: Let's stick this in another file so that that CLI is just CLI
    82  func execStarter(cobraCmd *cobra.Command, image string, args []string, name string) {
    83  	targetUID := 0
    84  	targetGID := make([]int, 0)
    85  
    86  	procname := ""
    87  
    88  	uid := uint32(os.Getuid())
    89  	gid := uint32(os.Getgid())
    90  
    91  	// Are we running from a privileged account?
    92  	isPrivileged := uid == 0
    93  	checkPrivileges := func(cond bool, desc string, fn func()) {
    94  		if !cond {
    95  			return
    96  		}
    97  
    98  		if !isPrivileged {
    99  			sylog.Fatalf("%s requires root privileges", desc)
   100  		}
   101  
   102  		fn()
   103  	}
   104  
   105  	syscall.Umask(0022)
   106  
   107  	starter := buildcfg.LIBEXECDIR + "/singularity/bin/starter-suid"
   108  
   109  	engineConfig := singularityConfig.NewConfig()
   110  
   111  	configurationFile := buildcfg.SYSCONFDIR + "/singularity/singularity.conf"
   112  	if err := config.Parser(configurationFile, engineConfig.File); err != nil {
   113  		sylog.Fatalf("Unable to parse singularity.conf file: %s", err)
   114  	}
   115  
   116  	ociConfig := &oci.Config{}
   117  	generator := generate.Generator{Config: &ociConfig.Spec}
   118  
   119  	engineConfig.OciConfig = ociConfig
   120  
   121  	generator.SetProcessArgs(args)
   122  
   123  	uidParam := security.GetParam(Security, "uid")
   124  	gidParam := security.GetParam(Security, "gid")
   125  
   126  	// handle target UID/GID for root user
   127  	checkPrivileges(uidParam != "", "uid security feature", func() {
   128  		u, err := strconv.ParseUint(uidParam, 10, 32)
   129  		if err != nil {
   130  			sylog.Fatalf("failed to parse provided UID")
   131  		}
   132  		targetUID = int(u)
   133  		uid = uint32(targetUID)
   134  
   135  		engineConfig.SetTargetUID(targetUID)
   136  	})
   137  
   138  	checkPrivileges(gidParam != "", "gid security feature", func() {
   139  		gids := strings.Split(gidParam, ":")
   140  		for _, id := range gids {
   141  			g, err := strconv.ParseUint(id, 10, 32)
   142  			if err != nil {
   143  				sylog.Fatalf("failed to parse provided GID")
   144  			}
   145  			targetGID = append(targetGID, int(g))
   146  		}
   147  		if len(gids) > 0 {
   148  			gid = uint32(targetGID[0])
   149  		}
   150  
   151  		engineConfig.SetTargetGID(targetGID)
   152  	})
   153  
   154  	if strings.HasPrefix(image, "instance://") {
   155  		instanceName := instance.ExtractName(image)
   156  		file, err := instance.Get(instanceName, instance.SingSubDir)
   157  		if err != nil {
   158  			sylog.Fatalf("%s", err)
   159  		}
   160  		if !file.Privileged {
   161  			UserNamespace = true
   162  		}
   163  		generator.AddProcessEnv("SINGULARITY_CONTAINER", file.Image)
   164  		generator.AddProcessEnv("SINGULARITY_NAME", filepath.Base(file.Image))
   165  		engineConfig.SetImage(image)
   166  		engineConfig.SetInstanceJoin(true)
   167  	} else {
   168  		abspath, err := filepath.Abs(image)
   169  		generator.AddProcessEnv("SINGULARITY_CONTAINER", abspath)
   170  		generator.AddProcessEnv("SINGULARITY_NAME", filepath.Base(abspath))
   171  		if err != nil {
   172  			sylog.Fatalf("Failed to determine image absolute path for %s: %s", image, err)
   173  		}
   174  		engineConfig.SetImage(abspath)
   175  	}
   176  
   177  	if !NoNvidia && (Nvidia || engineConfig.File.AlwaysUseNv) {
   178  		userPath := os.Getenv("USER_PATH")
   179  
   180  		if engineConfig.File.AlwaysUseNv {
   181  			sylog.Verbosef("'always use nv = yes' found in singularity.conf")
   182  			sylog.Verbosef("binding nvidia files into container")
   183  		}
   184  
   185  		libs, bins, err := nvidiautils.GetNvidiaPath(buildcfg.SINGULARITY_CONFDIR, userPath)
   186  		if err != nil {
   187  			sylog.Infof("Unable to capture nvidia bind points: %v", err)
   188  		} else {
   189  			if len(bins) == 0 {
   190  				sylog.Infof("Could not find any NVIDIA binaries on this host!")
   191  			} else {
   192  				if IsWritable {
   193  					sylog.Warningf("NVIDIA binaries may not be bound with --writable")
   194  				}
   195  				for _, binary := range bins {
   196  					usrBinBinary := filepath.Join("/usr/bin", filepath.Base(binary))
   197  					bind := strings.Join([]string{binary, usrBinBinary}, ":")
   198  					BindPaths = append(BindPaths, bind)
   199  				}
   200  			}
   201  			if len(libs) == 0 {
   202  				sylog.Warningf("Could not find any NVIDIA libraries on this host!")
   203  				sylog.Warningf("You may need to edit %v/nvliblist.conf", buildcfg.SINGULARITY_CONFDIR)
   204  			} else {
   205  				ContainLibsPath = append(ContainLibsPath, libs...)
   206  			}
   207  		}
   208  	}
   209  
   210  	engineConfig.SetBindPath(BindPaths)
   211  	engineConfig.SetNetwork(Network)
   212  	engineConfig.SetDNS(DNS)
   213  	engineConfig.SetNetworkArgs(NetworkArgs)
   214  	engineConfig.SetOverlayImage(OverlayPath)
   215  	engineConfig.SetWritableImage(IsWritable)
   216  	engineConfig.SetNoHome(NoHome)
   217  	engineConfig.SetNv(Nvidia)
   218  	engineConfig.SetAddCaps(AddCaps)
   219  	engineConfig.SetDropCaps(DropCaps)
   220  
   221  	checkPrivileges(AllowSUID, "--allow-setuid", func() {
   222  		engineConfig.SetAllowSUID(AllowSUID)
   223  	})
   224  
   225  	checkPrivileges(KeepPrivs, "--keep-privs", func() {
   226  		engineConfig.SetKeepPrivs(KeepPrivs)
   227  	})
   228  
   229  	engineConfig.SetNoPrivs(NoPrivs)
   230  	engineConfig.SetSecurity(Security)
   231  	engineConfig.SetShell(ShellPath)
   232  	engineConfig.SetLibrariesPath(ContainLibsPath)
   233  
   234  	if ShellPath != "" {
   235  		generator.AddProcessEnv("SINGULARITY_SHELL", ShellPath)
   236  	}
   237  
   238  	checkPrivileges(CgroupsPath != "", "--apply-cgroups", func() {
   239  		engineConfig.SetCgroupsPath(CgroupsPath)
   240  	})
   241  
   242  	if IsWritable && IsWritableTmpfs {
   243  		sylog.Warningf("Disabling --writable-tmpfs flag, mutually exclusive with --writable")
   244  		engineConfig.SetWritableTmpfs(false)
   245  	} else {
   246  		engineConfig.SetWritableTmpfs(IsWritableTmpfs)
   247  	}
   248  
   249  	homeFlag := cobraCmd.Flag("home")
   250  	engineConfig.SetCustomHome(homeFlag.Changed)
   251  
   252  	// set home directory for the targeted UID if it exists on host system
   253  	if !homeFlag.Changed && targetUID != 0 {
   254  		if targetUID > 500 {
   255  			if pwd, err := user.GetPwUID(uint32(targetUID)); err == nil {
   256  				sylog.Debugf("Target UID requested, set home directory to %s", pwd.Dir)
   257  				HomePath = pwd.Dir
   258  				engineConfig.SetCustomHome(true)
   259  			} else {
   260  				sylog.Verbosef("Home directory for UID %d not found, home won't be mounted", targetUID)
   261  				engineConfig.SetNoHome(true)
   262  				HomePath = "/"
   263  			}
   264  		} else {
   265  			sylog.Verbosef("System UID %d requested, home won't be mounted", targetUID)
   266  			engineConfig.SetNoHome(true)
   267  			HomePath = "/"
   268  		}
   269  	}
   270  
   271  	if Hostname != "" {
   272  		UtsNamespace = true
   273  		engineConfig.SetHostname(Hostname)
   274  	}
   275  
   276  	checkPrivileges(IsBoot, "--boot", func() {})
   277  
   278  	if IsContained || IsContainAll || IsBoot {
   279  		engineConfig.SetContain(true)
   280  
   281  		if IsContainAll {
   282  			PidNamespace = true
   283  			IpcNamespace = true
   284  			IsCleanEnv = true
   285  		}
   286  	}
   287  
   288  	engineConfig.SetScratchDir(ScratchPath)
   289  	engineConfig.SetWorkdir(WorkdirPath)
   290  
   291  	homeSlice := strings.Split(HomePath, ":")
   292  
   293  	if len(homeSlice) > 2 || len(homeSlice) == 0 {
   294  		sylog.Fatalf("home argument has incorrect number of elements: %v", len(homeSlice))
   295  	}
   296  
   297  	engineConfig.SetHomeSource(homeSlice[0])
   298  	if len(homeSlice) == 1 {
   299  		engineConfig.SetHomeDest(homeSlice[0])
   300  	} else {
   301  		engineConfig.SetHomeDest(homeSlice[1])
   302  	}
   303  
   304  	if !engineConfig.File.AllowSetuid || IsFakeroot {
   305  		UserNamespace = true
   306  	}
   307  
   308  	/* if name submitted, run as instance */
   309  	if name != "" {
   310  		PidNamespace = true
   311  		IpcNamespace = true
   312  		engineConfig.SetInstance(true)
   313  		engineConfig.SetBootInstance(IsBoot)
   314  
   315  		_, err := instance.Get(name, instance.SingSubDir)
   316  		if err == nil {
   317  			sylog.Fatalf("instance %s already exists", name)
   318  		}
   319  
   320  		if IsBoot {
   321  			UtsNamespace = true
   322  			NetNamespace = true
   323  			if Hostname == "" {
   324  				engineConfig.SetHostname(name)
   325  			}
   326  			if !KeepPrivs {
   327  				engineConfig.SetDropCaps("CAP_SYS_BOOT,CAP_SYS_RAWIO")
   328  			}
   329  			generator.SetProcessArgs([]string{"/sbin/init"})
   330  		}
   331  		pwd, err := user.GetPwUID(uint32(os.Getuid()))
   332  		if err != nil {
   333  			sylog.Fatalf("failed to retrieve user information for UID %d: %s", os.Getuid(), err)
   334  		}
   335  		procname = instance.ProcName(name, pwd.Name)
   336  	} else {
   337  		generator.SetProcessArgs(args)
   338  		procname = "Singularity runtime parent"
   339  	}
   340  
   341  	if NetNamespace {
   342  		generator.AddOrReplaceLinuxNamespace("network", "")
   343  	}
   344  	if UtsNamespace {
   345  		generator.AddOrReplaceLinuxNamespace("uts", "")
   346  	}
   347  	if PidNamespace {
   348  		generator.AddOrReplaceLinuxNamespace("pid", "")
   349  		engineConfig.SetNoInit(NoInit)
   350  	}
   351  	if IpcNamespace {
   352  		generator.AddOrReplaceLinuxNamespace("ipc", "")
   353  	}
   354  	if !UserNamespace {
   355  		if _, err := os.Stat(starter); os.IsNotExist(err) {
   356  			sylog.Verbosef("starter-suid not found, using user namespace")
   357  			UserNamespace = true
   358  		}
   359  	}
   360  
   361  	if UserNamespace {
   362  		generator.AddOrReplaceLinuxNamespace("user", "")
   363  		starter = buildcfg.LIBEXECDIR + "/singularity/bin/starter"
   364  
   365  		if IsFakeroot {
   366  			generator.AddLinuxUIDMapping(uid, 0, 1)
   367  			generator.AddLinuxGIDMapping(gid, 0, 1)
   368  		} else {
   369  			generator.AddLinuxUIDMapping(uid, uid, 1)
   370  			generator.AddLinuxGIDMapping(gid, gid, 1)
   371  		}
   372  	}
   373  
   374  	// Copy and cache environment
   375  	environment := os.Environ()
   376  
   377  	// Clean environment
   378  	env.SetContainerEnv(&generator, environment, IsCleanEnv, engineConfig.GetHomeDest())
   379  
   380  	// force to use getwd syscall
   381  	os.Unsetenv("PWD")
   382  
   383  	if pwd, err := os.Getwd(); err == nil {
   384  		if PwdPath != "" {
   385  			generator.SetProcessCwd(PwdPath)
   386  		} else {
   387  			if engineConfig.GetContain() {
   388  				generator.SetProcessCwd(engineConfig.GetHomeDest())
   389  			} else {
   390  				generator.SetProcessCwd(pwd)
   391  			}
   392  		}
   393  	} else {
   394  		sylog.Warningf("can't determine current working directory: %s", err)
   395  	}
   396  
   397  	Env := []string{sylog.GetEnvVar()}
   398  
   399  	generator.AddProcessEnv("SINGULARITY_APPNAME", AppName)
   400  
   401  	// convert image file to sandbox if image contains
   402  	// a squashfs filesystem
   403  	if UserNamespace && fs.IsFile(image) {
   404  		unsquashfsPath := ""
   405  		if engineConfig.File.MksquashfsPath != "" {
   406  			d := filepath.Dir(engineConfig.File.MksquashfsPath)
   407  			unsquashfsPath = filepath.Join(d, "unsquashfs")
   408  		}
   409  		sylog.Verbosef("User namespace requested, convert image %s to sandbox", image)
   410  		sylog.Infof("Convert SIF file to sandbox...")
   411  		dir, err := convertImage(image, unsquashfsPath)
   412  		if err != nil {
   413  			sylog.Fatalf("while extracting %s: %s", image, err)
   414  		}
   415  		engineConfig.SetImage(dir)
   416  		engineConfig.SetDeleteImage(true)
   417  		generator.AddProcessEnv("SINGULARITY_CONTAINER", dir)
   418  	}
   419  
   420  	cfg := &config.Common{
   421  		EngineName:   singularityConfig.Name,
   422  		ContainerID:  name,
   423  		EngineConfig: engineConfig,
   424  	}
   425  
   426  	configData, err := json.Marshal(cfg)
   427  	if err != nil {
   428  		sylog.Fatalf("CLI Failed to marshal CommonEngineConfig: %s\n", err)
   429  	}
   430  
   431  	if engineConfig.GetInstance() {
   432  		stdout, stderr, err := instance.SetLogFile(name, int(uid), instance.SingSubDir)
   433  		if err != nil {
   434  			sylog.Fatalf("failed to create instance log files: %s", err)
   435  		}
   436  
   437  		start, err := stderr.Seek(0, os.SEEK_END)
   438  		if err != nil {
   439  			sylog.Warningf("failed to get standard error stream offset: %s", err)
   440  		}
   441  
   442  		cmd, err := exec.PipeCommand(starter, []string{procname}, Env, configData)
   443  		cmd.Stdout = stdout
   444  		cmd.Stderr = stderr
   445  
   446  		cmdErr := cmd.Run()
   447  
   448  		if sylog.GetLevel() != 0 {
   449  			// starter can exit a bit before all errors has been reported
   450  			// by instance process, wait a bit to catch all errors
   451  			time.Sleep(100 * time.Millisecond)
   452  
   453  			end, err := stderr.Seek(0, os.SEEK_END)
   454  			if err != nil {
   455  				sylog.Warningf("failed to get standard error stream offset: %s", err)
   456  			}
   457  			if end-start > 0 {
   458  				output := make([]byte, end-start)
   459  				stderr.ReadAt(output, start)
   460  				fmt.Println(string(output))
   461  			}
   462  		}
   463  
   464  		if cmdErr != nil {
   465  			sylog.Fatalf("failed to start instance: %s", cmdErr)
   466  		} else {
   467  			sylog.Verbosef("you will find instance output here: %s", stdout.Name())
   468  			sylog.Verbosef("you will find instance error here: %s", stderr.Name())
   469  			sylog.Infof("instance started successfully")
   470  		}
   471  	} else {
   472  		if err := exec.Pipe(starter, []string{procname}, Env, configData); err != nil {
   473  			sylog.Fatalf("%s", err)
   474  		}
   475  	}
   476  }