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 }