github.com/kubiko/snapd@v0.0.0-20201013125620-d4f3094d9ddf/cmd/snap/cmd_run.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2014-2018 Canonical Ltd 5 * 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License version 3 as 8 * published by the Free Software Foundation. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 package main 21 22 import ( 23 "bufio" 24 "fmt" 25 "io" 26 "io/ioutil" 27 "net" 28 "os" 29 "os/exec" 30 "os/user" 31 "path/filepath" 32 "regexp" 33 "strconv" 34 "strings" 35 "syscall" 36 "time" 37 38 "github.com/godbus/dbus" 39 "github.com/jessevdk/go-flags" 40 41 "github.com/snapcore/snapd/client" 42 "github.com/snapcore/snapd/dbusutil" 43 "github.com/snapcore/snapd/dirs" 44 "github.com/snapcore/snapd/i18n" 45 "github.com/snapcore/snapd/interfaces" 46 "github.com/snapcore/snapd/logger" 47 "github.com/snapcore/snapd/osutil" 48 "github.com/snapcore/snapd/osutil/strace" 49 "github.com/snapcore/snapd/sandbox/cgroup" 50 "github.com/snapcore/snapd/sandbox/selinux" 51 "github.com/snapcore/snapd/snap" 52 "github.com/snapcore/snapd/snap/snapenv" 53 "github.com/snapcore/snapd/strutil/shlex" 54 "github.com/snapcore/snapd/timeutil" 55 "github.com/snapcore/snapd/x11" 56 ) 57 58 var ( 59 syscallExec = syscall.Exec 60 userCurrent = user.Current 61 osGetenv = os.Getenv 62 timeNow = time.Now 63 selinuxIsEnabled = selinux.IsEnabled 64 selinuxVerifyPathContext = selinux.VerifyPathContext 65 selinuxRestoreContext = selinux.RestoreContext 66 ) 67 68 type cmdRun struct { 69 clientMixin 70 Command string `long:"command" hidden:"yes"` 71 HookName string `long:"hook" hidden:"yes"` 72 Revision string `short:"r" default:"unset" hidden:"yes"` 73 Shell bool `long:"shell" ` 74 75 // This options is both a selector (use or don't use strace) and it 76 // can also carry extra options for strace. This is why there is 77 // "default" and "optional-value" to distinguish this. 78 Strace string `long:"strace" optional:"true" optional-value:"with-strace" default:"no-strace" default-mask:"-"` 79 Gdb bool `long:"gdb"` 80 Gdbserver string `long:"experimental-gdbserver" default:"no-gdbserver" optional-value:":0" optional:"true"` 81 TraceExec bool `long:"trace-exec"` 82 83 // not a real option, used to check if cmdRun is initialized by 84 // the parser 85 ParserRan int `long:"parser-ran" default:"1" hidden:"yes"` 86 Timer string `long:"timer" hidden:"yes"` 87 } 88 89 func init() { 90 addCommand("run", 91 i18n.G("Run the given snap command"), 92 i18n.G(` 93 The run command executes the given snap command with the right confinement 94 and environment. 95 `), 96 func() flags.Commander { 97 return &cmdRun{} 98 }, map[string]string{ 99 // TRANSLATORS: This should not start with a lowercase letter. 100 "command": i18n.G("Alternative command to run"), 101 // TRANSLATORS: This should not start with a lowercase letter. 102 "hook": i18n.G("Hook to run"), 103 // TRANSLATORS: This should not start with a lowercase letter. 104 "r": i18n.G("Use a specific snap revision when running hook"), 105 // TRANSLATORS: This should not start with a lowercase letter. 106 "shell": i18n.G("Run a shell instead of the command (useful for debugging)"), 107 // TRANSLATORS: This should not start with a lowercase letter. 108 "strace": i18n.G("Run the command under strace (useful for debugging). Extra strace options can be specified as well here. Pass --raw to strace early snap helpers."), 109 // TRANSLATORS: This should not start with a lowercase letter. 110 "gdb": i18n.G("Run the command with gdb"), 111 // TRANSLATORS: This should not start with a lowercase letter. 112 "experimental-gdbserver": i18n.G("Run the command with gdbserver (experimental)"), 113 // TRANSLATORS: This should not start with a lowercase letter. 114 "timer": i18n.G("Run as a timer service with given schedule"), 115 // TRANSLATORS: This should not start with a lowercase letter. 116 "trace-exec": i18n.G("Display exec calls timing data"), 117 "parser-ran": "", 118 }, nil) 119 } 120 121 // isStopping returns true if the system is shutting down. 122 func isStopping() (bool, error) { 123 // Make sure, just in case, that systemd doesn't localize the output string. 124 env, err := osutil.OSEnvironment() 125 if err != nil { 126 return false, err 127 } 128 env["LC_MESSAGES"] = "C" 129 // Check if systemd is stopping (shutting down or rebooting). 130 cmd := exec.Command("systemctl", "is-system-running") 131 cmd.Env = env.ForExec() 132 stdout, err := cmd.Output() 133 // systemctl is-system-running returns non-zero for outcomes other than "running" 134 // As such, ignore any ExitError and just process the stdout buffer. 135 if _, ok := err.(*exec.ExitError); ok { 136 return string(stdout) == "stopping\n", nil 137 } 138 return false, err 139 } 140 141 func maybeWaitForSecurityProfileRegeneration(cli *client.Client) error { 142 // check if the security profiles key has changed, if so, we need 143 // to wait for snapd to re-generate all profiles 144 mismatch, err := interfaces.SystemKeyMismatch() 145 if err == nil && !mismatch { 146 return nil 147 } 148 // something went wrong with the system-key compare, try to 149 // reach snapd before continuing 150 if err != nil { 151 logger.Debugf("SystemKeyMismatch returned an error: %v", err) 152 } 153 154 // We have a mismatch but maybe it is only because systemd is shutting down 155 // and core or snapd were already unmounted and we failed to re-execute. 156 // For context see: https://bugs.launchpad.net/snapd/+bug/1871652 157 stopping, err := isStopping() 158 if err != nil { 159 logger.Debugf("cannot check if system is stopping: %s", err) 160 } 161 if stopping { 162 logger.Debugf("ignoring system key mismatch during system shutdown/reboot") 163 return nil 164 } 165 166 // We have a mismatch, try to connect to snapd, once we can 167 // connect we just continue because that usually means that 168 // a new snapd is ready and has generated profiles. 169 // 170 // There is a corner case if an upgrade leaves the old snapd 171 // running and we connect to the old snapd. Handling this 172 // correctly is tricky because our "snap run" pipeline may 173 // depend on profiles written by the new snapd. So for now we 174 // just continue and hope for the best. The real fix for this 175 // is to fix the packaging so that snapd is stopped, upgraded 176 // and started. 177 // 178 // connect timeout for client is 5s on each try, so 12*5s = 60s 179 timeout := 12 180 if timeoutEnv := os.Getenv("SNAPD_DEBUG_SYSTEM_KEY_RETRY"); timeoutEnv != "" { 181 if i, err := strconv.Atoi(timeoutEnv); err == nil { 182 timeout = i 183 } 184 } 185 186 logger.Debugf("system key mismatch detected, waiting for snapd to start responding...") 187 188 for i := 0; i < timeout; i++ { 189 if _, err := cli.SysInfo(); err == nil { 190 return nil 191 } 192 // sleep a little bit for good measure 193 time.Sleep(1 * time.Second) 194 } 195 196 return fmt.Errorf("timeout waiting for snap system profiles to get updated") 197 } 198 199 func (x *cmdRun) Execute(args []string) error { 200 if len(args) == 0 { 201 return fmt.Errorf(i18n.G("need the application to run as argument")) 202 } 203 snapApp := args[0] 204 args = args[1:] 205 206 // Catch some invalid parameter combinations, provide helpful errors 207 optionsSet := 0 208 for _, param := range []string{x.HookName, x.Command, x.Timer} { 209 if param != "" { 210 optionsSet++ 211 } 212 } 213 if optionsSet > 1 { 214 return fmt.Errorf("you can only use one of --hook, --command, and --timer") 215 } 216 217 if x.Revision != "unset" && x.Revision != "" && x.HookName == "" { 218 return fmt.Errorf(i18n.G("-r can only be used with --hook")) 219 } 220 if x.HookName != "" && len(args) > 0 { 221 // TRANSLATORS: %q is the hook name; %s a space-separated list of extra arguments 222 return fmt.Errorf(i18n.G("too many arguments for hook %q: %s"), x.HookName, strings.Join(args, " ")) 223 } 224 225 if err := maybeWaitForSecurityProfileRegeneration(x.client); err != nil { 226 return err 227 } 228 229 // Now actually handle the dispatching 230 if x.HookName != "" { 231 return x.snapRunHook(snapApp) 232 } 233 234 if x.Command == "complete" { 235 snapApp, args = antialias(snapApp, args) 236 } 237 238 if x.Timer != "" { 239 return x.snapRunTimer(snapApp, x.Timer, args) 240 } 241 242 return x.snapRunApp(snapApp, args) 243 } 244 245 // antialias changes snapApp and args if snapApp is actually an alias 246 // for something else. If not, or if the args aren't what's expected 247 // for completion, it returns them unchanged. 248 func antialias(snapApp string, args []string) (string, []string) { 249 if len(args) < 7 { 250 // NOTE if len(args) < 7, Something is Wrong (at least WRT complete.sh and etelpmoc.sh) 251 return snapApp, args 252 } 253 254 actualApp, err := resolveApp(snapApp) 255 if err != nil || actualApp == snapApp { 256 // no alias! woop. 257 return snapApp, args 258 } 259 260 compPoint, err := strconv.Atoi(args[2]) 261 if err != nil { 262 // args[2] is not COMP_POINT 263 return snapApp, args 264 } 265 266 if compPoint <= len(snapApp) { 267 // COMP_POINT is inside $0 268 return snapApp, args 269 } 270 271 if compPoint > len(args[5]) { 272 // COMP_POINT is bigger than $# 273 return snapApp, args 274 } 275 276 if args[6] != snapApp { 277 // args[6] is not COMP_WORDS[0] 278 return snapApp, args 279 } 280 281 // it _should_ be COMP_LINE followed by one of 282 // COMP_WORDBREAKS, but that's hard to do 283 re, err := regexp.Compile(`^` + regexp.QuoteMeta(snapApp) + `\b`) 284 if err != nil || !re.MatchString(args[5]) { 285 // (weird regexp error, or) args[5] is not COMP_LINE 286 return snapApp, args 287 } 288 289 argsOut := make([]string, len(args)) 290 copy(argsOut, args) 291 292 argsOut[2] = strconv.Itoa(compPoint - len(snapApp) + len(actualApp)) 293 argsOut[5] = re.ReplaceAllLiteralString(args[5], actualApp) 294 argsOut[6] = actualApp 295 296 return actualApp, argsOut 297 } 298 299 func getSnapInfo(snapName string, revision snap.Revision) (info *snap.Info, err error) { 300 if revision.Unset() { 301 info, err = snap.ReadCurrentInfo(snapName) 302 } else { 303 info, err = snap.ReadInfo(snapName, &snap.SideInfo{ 304 Revision: revision, 305 }) 306 } 307 308 return info, err 309 } 310 311 func createOrUpdateUserDataSymlink(info *snap.Info, usr *user.User) error { 312 // 'current' symlink for user data (SNAP_USER_DATA) 313 userData := info.UserDataDir(usr.HomeDir) 314 wantedSymlinkValue := filepath.Base(userData) 315 currentActiveSymlink := filepath.Join(userData, "..", "current") 316 317 var err error 318 var currentSymlinkValue string 319 for i := 0; i < 5; i++ { 320 currentSymlinkValue, err = os.Readlink(currentActiveSymlink) 321 // Failure other than non-existing symlink is fatal 322 if err != nil && !os.IsNotExist(err) { 323 // TRANSLATORS: %v the error message 324 return fmt.Errorf(i18n.G("cannot read symlink: %v"), err) 325 } 326 327 if currentSymlinkValue == wantedSymlinkValue { 328 break 329 } 330 331 if err == nil { 332 // We may be racing with other instances of snap-run that try to do the same thing 333 // If the symlink is already removed then we can ignore this error. 334 err = os.Remove(currentActiveSymlink) 335 if err != nil && !os.IsNotExist(err) { 336 // abort with error 337 break 338 } 339 } 340 341 err = os.Symlink(wantedSymlinkValue, currentActiveSymlink) 342 // Error other than symlink already exists will abort and be propagated 343 if err == nil || !os.IsExist(err) { 344 break 345 } 346 // If we arrived here it means the symlink couldn't be created because it got created 347 // in the meantime by another instance, so we will try again. 348 } 349 if err != nil { 350 return fmt.Errorf(i18n.G("cannot update the 'current' symlink of %q: %v"), currentActiveSymlink, err) 351 } 352 return nil 353 } 354 355 func createUserDataDirs(info *snap.Info) error { 356 // Adjust umask so that the created directories have the permissions we 357 // expect and are unaffected by the initial umask. While go runtime creates 358 // threads at will behind the scenes, the setting of umask applies to the 359 // entire process so it doesn't need any special handling to lock the 360 // executing goroutine to a single thread. 361 oldUmask := syscall.Umask(0) 362 defer syscall.Umask(oldUmask) 363 364 usr, err := userCurrent() 365 if err != nil { 366 return fmt.Errorf(i18n.G("cannot get the current user: %v"), err) 367 } 368 369 // see snapenv.User 370 instanceUserData := info.UserDataDir(usr.HomeDir) 371 instanceCommonUserData := info.UserCommonDataDir(usr.HomeDir) 372 createDirs := []string{instanceUserData, instanceCommonUserData} 373 if info.InstanceKey != "" { 374 // parallel instance snaps get additional mapping in their mount 375 // namespace, namely /home/joe/snap/foo_bar -> 376 // /home/joe/snap/foo, make sure that the mount point exists and 377 // is owned by the user 378 snapUserDir := snap.UserSnapDir(usr.HomeDir, info.SnapName()) 379 createDirs = append(createDirs, snapUserDir) 380 } 381 for _, d := range createDirs { 382 if err := os.MkdirAll(d, 0755); err != nil { 383 // TRANSLATORS: %q is the directory whose creation failed, %v the error message 384 return fmt.Errorf(i18n.G("cannot create %q: %v"), d, err) 385 } 386 } 387 388 if err := createOrUpdateUserDataSymlink(info, usr); err != nil { 389 return err 390 } 391 392 return maybeRestoreSecurityContext(usr) 393 } 394 395 // maybeRestoreSecurityContext attempts to restore security context of ~/snap on 396 // systems where it's applicable 397 func maybeRestoreSecurityContext(usr *user.User) error { 398 snapUserHome := filepath.Join(usr.HomeDir, dirs.UserHomeSnapDir) 399 enabled, err := selinuxIsEnabled() 400 if err != nil { 401 return fmt.Errorf("cannot determine SELinux status: %v", err) 402 } 403 if !enabled { 404 logger.Debugf("SELinux not enabled") 405 return nil 406 } 407 408 match, err := selinuxVerifyPathContext(snapUserHome) 409 if err != nil { 410 return fmt.Errorf("failed to verify SELinux context of %v: %v", snapUserHome, err) 411 } 412 if match { 413 return nil 414 } 415 logger.Noticef("restoring default SELinux context of %v", snapUserHome) 416 417 if err := selinuxRestoreContext(snapUserHome, selinux.RestoreMode{Recursive: true}); err != nil { 418 return fmt.Errorf("cannot restore SELinux context of %v: %v", snapUserHome, err) 419 } 420 return nil 421 } 422 423 func (x *cmdRun) useStrace() bool { 424 // make sure the go-flag parser ran and assigned default values 425 return x.ParserRan == 1 && x.Strace != "no-strace" 426 } 427 428 func (x *cmdRun) straceOpts() (opts []string, raw bool, err error) { 429 if x.Strace == "with-strace" { 430 return nil, false, nil 431 } 432 433 split, err := shlex.Split(x.Strace) 434 if err != nil { 435 return nil, false, err 436 } 437 438 opts = make([]string, 0, len(split)) 439 for _, opt := range split { 440 if opt == "--raw" { 441 raw = true 442 continue 443 } 444 opts = append(opts, opt) 445 } 446 return opts, raw, nil 447 } 448 449 func (x *cmdRun) snapRunApp(snapApp string, args []string) error { 450 snapName, appName := snap.SplitSnapApp(snapApp) 451 info, err := getSnapInfo(snapName, snap.R(0)) 452 if err != nil { 453 return err 454 } 455 456 app := info.Apps[appName] 457 if app == nil { 458 return fmt.Errorf(i18n.G("cannot find app %q in %q"), appName, snapName) 459 } 460 461 return x.runSnapConfine(info, app.SecurityTag(), snapApp, "", args) 462 } 463 464 func (x *cmdRun) snapRunHook(snapName string) error { 465 revision, err := snap.ParseRevision(x.Revision) 466 if err != nil { 467 return err 468 } 469 470 info, err := getSnapInfo(snapName, revision) 471 if err != nil { 472 return err 473 } 474 475 hook := info.Hooks[x.HookName] 476 if hook == nil { 477 return fmt.Errorf(i18n.G("cannot find hook %q in %q"), x.HookName, snapName) 478 } 479 480 return x.runSnapConfine(info, hook.SecurityTag(), snapName, hook.Name, nil) 481 } 482 483 func (x *cmdRun) snapRunTimer(snapApp, timer string, args []string) error { 484 schedule, err := timeutil.ParseSchedule(timer) 485 if err != nil { 486 return fmt.Errorf("invalid timer format: %v", err) 487 } 488 489 now := timeNow() 490 if !timeutil.Includes(schedule, now) { 491 fmt.Fprintf(Stderr, "%s: attempted to run %q timer outside of scheduled time %q\n", now.Format(time.RFC3339), snapApp, timer) 492 return nil 493 } 494 495 return x.snapRunApp(snapApp, args) 496 } 497 498 var osReadlink = os.Readlink 499 500 // snapdHelperPath return the path of a helper like "snap-confine" or 501 // "snap-exec" based on if snapd is re-execed or not 502 func snapdHelperPath(toolName string) (string, error) { 503 exe, err := osReadlink("/proc/self/exe") 504 if err != nil { 505 return "", fmt.Errorf("cannot read /proc/self/exe: %v", err) 506 } 507 // no re-exec 508 if !strings.HasPrefix(exe, dirs.SnapMountDir) { 509 return filepath.Join(dirs.DistroLibExecDir, toolName), nil 510 } 511 // The logic below only works if the last two path components 512 // are /usr/bin 513 // FIXME: use a snap warning? 514 if !strings.HasSuffix(exe, "/usr/bin/"+filepath.Base(exe)) { 515 logger.Noticef("(internal error): unexpected exe input in snapdHelperPath: %v", exe) 516 return filepath.Join(dirs.DistroLibExecDir, toolName), nil 517 } 518 // snapBase will be "/snap/{core,snapd}/$rev/" because 519 // the snap binary is always at $root/usr/bin/snap 520 snapBase := filepath.Clean(filepath.Join(filepath.Dir(exe), "..", "..")) 521 // Run snap-confine from the core/snapd snap. The tools in 522 // core/snapd snap are statically linked, or mostly 523 // statically, with the exception of libraries such as libudev 524 // and libc. 525 return filepath.Join(snapBase, dirs.CoreLibExecDir, toolName), nil 526 } 527 528 func migrateXauthority(info *snap.Info) (string, error) { 529 u, err := userCurrent() 530 if err != nil { 531 return "", fmt.Errorf(i18n.G("cannot get the current user: %s"), err) 532 } 533 534 // If our target directory (XDG_RUNTIME_DIR) doesn't exist we 535 // don't attempt to create it. 536 baseTargetDir := filepath.Join(dirs.XdgRuntimeDirBase, u.Uid) 537 if !osutil.FileExists(baseTargetDir) { 538 return "", nil 539 } 540 541 xauthPath := osGetenv("XAUTHORITY") 542 if len(xauthPath) == 0 || !osutil.FileExists(xauthPath) { 543 // Nothing to do for us. Most likely running outside of any 544 // graphical X11 session. 545 return "", nil 546 } 547 548 fin, err := os.Open(xauthPath) 549 if err != nil { 550 return "", err 551 } 552 defer fin.Close() 553 554 // Abs() also calls Clean(); see https://golang.org/pkg/path/filepath/#Abs 555 xauthPathAbs, err := filepath.Abs(fin.Name()) 556 if err != nil { 557 return "", nil 558 } 559 560 // Remove all symlinks from path 561 xauthPathCan, err := filepath.EvalSymlinks(xauthPathAbs) 562 if err != nil { 563 return "", nil 564 } 565 566 // Ensure the XAUTHORITY env is not abused by checking that 567 // it point to exactly the file we just opened (no symlinks, 568 // no funny "../.." etc) 569 if fin.Name() != xauthPathCan { 570 logger.Noticef("WARNING: XAUTHORITY environment value is not a clean path: %q", xauthPathCan) 571 return "", nil 572 } 573 574 // Only do the migration from /tmp since the real /tmp is not visible for snaps 575 if !strings.HasPrefix(fin.Name(), "/tmp/") { 576 return "", nil 577 } 578 579 // We are performing a Stat() here to make sure that the user can't 580 // steal another user's Xauthority file. Note that while Stat() uses 581 // fstat() on the file descriptor created during Open(), the file might 582 // have changed ownership between the Open() and the Stat(). That's ok 583 // because we aren't trying to block access that the user already has: 584 // if the user has the privileges to chown another user's Xauthority 585 // file, we won't block that since the user can just steal it without 586 // having to use snap run. This code is just to ensure that a user who 587 // doesn't have those privileges can't steal the file via snap run 588 // (also note that the (potentially untrusted) snap isn't running yet). 589 fi, err := fin.Stat() 590 if err != nil { 591 return "", err 592 } 593 sys := fi.Sys() 594 if sys == nil { 595 return "", fmt.Errorf(i18n.G("cannot validate owner of file %s"), fin.Name()) 596 } 597 // cheap comparison as the current uid is only available as a string 598 // but it is better to convert the uid from the stat result to a 599 // string than a string into a number. 600 if fmt.Sprintf("%d", sys.(*syscall.Stat_t).Uid) != u.Uid { 601 return "", fmt.Errorf(i18n.G("Xauthority file isn't owned by the current user %s"), u.Uid) 602 } 603 604 targetPath := filepath.Join(baseTargetDir, ".Xauthority") 605 606 // Only validate Xauthority file again when both files don't match 607 // otherwise we can continue using the existing Xauthority file. 608 // This is ok to do here because we aren't trying to protect against 609 // the user changing the Xauthority file in XDG_RUNTIME_DIR outside 610 // of snapd. 611 if osutil.FileExists(targetPath) { 612 var fout *os.File 613 if fout, err = os.Open(targetPath); err != nil { 614 return "", err 615 } 616 if osutil.StreamsEqual(fin, fout) { 617 fout.Close() 618 return targetPath, nil 619 } 620 621 fout.Close() 622 if err := os.Remove(targetPath); err != nil { 623 return "", err 624 } 625 626 // Ensure we're validating the Xauthority file from the beginning 627 if _, err := fin.Seek(int64(os.SEEK_SET), 0); err != nil { 628 return "", err 629 } 630 } 631 632 // To guard against setting XAUTHORITY to non-xauth files, check 633 // that we have a valid Xauthority. Specifically, the file must be 634 // parseable as an Xauthority file and not be empty. 635 if err := x11.ValidateXauthority(fin); err != nil { 636 return "", err 637 } 638 639 // Read data from the beginning of the file 640 if _, err = fin.Seek(int64(os.SEEK_SET), 0); err != nil { 641 return "", err 642 } 643 644 fout, err := os.OpenFile(targetPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600) 645 if err != nil { 646 return "", err 647 } 648 defer fout.Close() 649 650 // Read and write validated Xauthority file to its right location 651 if _, err = io.Copy(fout, fin); err != nil { 652 if err := os.Remove(targetPath); err != nil { 653 logger.Noticef("WARNING: cannot remove file at %s: %s", targetPath, err) 654 } 655 return "", fmt.Errorf(i18n.G("cannot write new Xauthority file at %s: %s"), targetPath, err) 656 } 657 658 return targetPath, nil 659 } 660 661 func activateXdgDocumentPortal(info *snap.Info, snapApp, hook string) error { 662 // Don't do anything for apps or hooks that don't plug the 663 // desktop interface 664 // 665 // NOTE: This check is imperfect because we don't really know 666 // if the interface is connected or not but this is an 667 // acceptable compromise for not having to communicate with 668 // snapd in snap run. In a typical desktop session the 669 // document portal can be in use by many applications, not 670 // just by snaps, so this is at most, pre-emptively using some 671 // extra memory. 672 var plugs map[string]*snap.PlugInfo 673 if hook != "" { 674 plugs = info.Hooks[hook].Plugs 675 } else { 676 _, appName := snap.SplitSnapApp(snapApp) 677 plugs = info.Apps[appName].Plugs 678 } 679 plugsDesktop := false 680 for _, plug := range plugs { 681 if plug.Interface == "desktop" { 682 plugsDesktop = true 683 break 684 } 685 } 686 if !plugsDesktop { 687 return nil 688 } 689 690 u, err := userCurrent() 691 if err != nil { 692 return fmt.Errorf(i18n.G("cannot get the current user: %s"), err) 693 } 694 xdgRuntimeDir := filepath.Join(dirs.XdgRuntimeDirBase, u.Uid) 695 696 // If $XDG_RUNTIME_DIR/doc appears to be a mount point, assume 697 // that the document portal is up and running. 698 expectedMountPoint := filepath.Join(xdgRuntimeDir, "doc") 699 if mounted, err := osutil.IsMounted(expectedMountPoint); err != nil { 700 logger.Noticef("Could not check document portal mount state: %s", err) 701 } else if mounted { 702 return nil 703 } 704 705 // If there is no session bus, our job is done. We check this 706 // manually to avoid dbus.SessionBus() auto-launching a new 707 // bus. 708 busAddress := osGetenv("DBUS_SESSION_BUS_ADDRESS") 709 if len(busAddress) == 0 { 710 return nil 711 } 712 713 // We've previously tried to start the document portal and 714 // were told the service is unknown: don't bother connecting 715 // to the session bus again. 716 // 717 // As the file is in $XDG_RUNTIME_DIR, it will be cleared over 718 // full logout/login or reboot cycles. 719 portalsUnavailableFile := filepath.Join(xdgRuntimeDir, ".portals-unavailable") 720 if osutil.FileExists(portalsUnavailableFile) { 721 return nil 722 } 723 724 conn, err := dbusutil.SessionBus() 725 if err != nil { 726 return err 727 } 728 729 portal := conn.Object("org.freedesktop.portal.Documents", 730 "/org/freedesktop/portal/documents") 731 var mountPoint []byte 732 if err := portal.Call("org.freedesktop.portal.Documents.GetMountPoint", 0).Store(&mountPoint); err != nil { 733 // It is not considered an error if 734 // xdg-document-portal is not available on the system. 735 if dbusErr, ok := err.(dbus.Error); ok && dbusErr.Name == "org.freedesktop.DBus.Error.ServiceUnknown" { 736 // We ignore errors here: if writing the file 737 // fails, we'll just try connecting to D-Bus 738 // again next time. 739 if err = ioutil.WriteFile(portalsUnavailableFile, []byte(""), 0644); err != nil { 740 logger.Noticef("WARNING: cannot write file at %s: %s", portalsUnavailableFile, err) 741 } 742 return nil 743 } 744 return err 745 } 746 747 // Sanity check to make sure the document portal is exposed 748 // where we think it is. 749 actualMountPoint := strings.TrimRight(string(mountPoint), "\x00") 750 if actualMountPoint != expectedMountPoint { 751 return fmt.Errorf(i18n.G("Expected portal at %#v, got %#v"), expectedMountPoint, actualMountPoint) 752 } 753 return nil 754 } 755 756 type envForExecFunc func(extra map[string]string) []string 757 758 var gdbServerWelcomeFmt = ` 759 Welcome to "snap run --gdbserver". 760 You are right before your application is run. 761 Please open a different terminal and run: 762 763 gdb -ex="target remote %[1]s" -ex=continue -ex="signal SIGCONT" 764 (gdb) continue 765 766 or use your favorite gdb frontend and connect to %[1]s 767 ` 768 769 func racyFindFreePort() (int, error) { 770 l, err := net.Listen("tcp", ":0") 771 if err != nil { 772 return 0, err 773 } 774 defer l.Close() 775 return l.Addr().(*net.TCPAddr).Port, nil 776 } 777 778 func (x *cmdRun) useGdbserver() bool { 779 // make sure the go-flag parser ran and assigned default values 780 return x.ParserRan == 1 && x.Gdbserver != "no-gdbserver" 781 } 782 783 func (x *cmdRun) runCmdUnderGdbserver(origCmd []string, envForExec envForExecFunc) error { 784 gcmd := exec.Command(origCmd[0], origCmd[1:]...) 785 gcmd.Stdin = os.Stdin 786 gcmd.Stdout = os.Stdout 787 gcmd.Stderr = os.Stderr 788 gcmd.Env = envForExec(map[string]string{"SNAP_CONFINE_RUN_UNDER_GDBSERVER": "1"}) 789 if err := gcmd.Start(); err != nil { 790 return err 791 } 792 // wait for the child process executing gdb helper to raise SIGSTOP 793 // signalling readiness to attach a gdbserver process 794 var status syscall.WaitStatus 795 _, err := syscall.Wait4(gcmd.Process.Pid, &status, syscall.WSTOPPED, nil) 796 if err != nil { 797 return err 798 } 799 800 addr := x.Gdbserver 801 if addr == ":0" { 802 // XXX: run "gdbserver :0" instead and parse "Listening on port 45971" 803 // on stderr instead? 804 port, err := racyFindFreePort() 805 if err != nil { 806 return fmt.Errorf("cannot find free port: %v", err) 807 } 808 addr = fmt.Sprintf(":%v", port) 809 } 810 // XXX: should we provide a helper here instead? something like 811 // `snap run --attach-debugger` or similar? The downside 812 // is that attaching a gdb frontend is harder? 813 fmt.Fprintf(Stdout, fmt.Sprintf(gdbServerWelcomeFmt, addr)) 814 // note that only gdbserver needs to run as root, the application 815 // keeps running as the user 816 gdbSrvCmd := exec.Command("sudo", "-E", "gdbserver", "--attach", addr, strconv.Itoa(gcmd.Process.Pid)) 817 if output, err := gdbSrvCmd.CombinedOutput(); err != nil { 818 return osutil.OutputErr(output, err) 819 } 820 return nil 821 } 822 823 func (x *cmdRun) runCmdUnderGdb(origCmd []string, envForExec envForExecFunc) error { 824 // the resulting application process runs as root 825 cmd := []string{"sudo", "-E", "gdb", "-ex=run", "-ex=catch exec", "-ex=continue", "--args"} 826 cmd = append(cmd, origCmd...) 827 828 gcmd := exec.Command(cmd[0], cmd[1:]...) 829 gcmd.Stdin = os.Stdin 830 gcmd.Stdout = os.Stdout 831 gcmd.Stderr = os.Stderr 832 gcmd.Env = envForExec(map[string]string{"SNAP_CONFINE_RUN_UNDER_GDB": "1"}) 833 return gcmd.Run() 834 } 835 836 func (x *cmdRun) runCmdWithTraceExec(origCmd []string, envForExec envForExecFunc) error { 837 // setup private tmp dir with strace fifo 838 straceTmp, err := ioutil.TempDir("", "exec-trace") 839 if err != nil { 840 return err 841 } 842 defer os.RemoveAll(straceTmp) 843 straceLog := filepath.Join(straceTmp, "strace.fifo") 844 if err := syscall.Mkfifo(straceLog, 0640); err != nil { 845 return err 846 } 847 // ensure we have one writer on the fifo so that if strace fails 848 // nothing blocks 849 fw, err := os.OpenFile(straceLog, os.O_RDWR, 0640) 850 if err != nil { 851 return err 852 } 853 defer fw.Close() 854 855 // read strace data from fifo async 856 var slg *strace.ExecveTiming 857 var straceErr error 858 doneCh := make(chan bool, 1) 859 go func() { 860 // FIXME: make this configurable? 861 nSlowest := 10 862 slg, straceErr = strace.TraceExecveTimings(straceLog, nSlowest) 863 close(doneCh) 864 }() 865 866 cmd, err := strace.TraceExecCommand(straceLog, origCmd...) 867 if err != nil { 868 return err 869 } 870 // run 871 cmd.Env = envForExec(nil) 872 cmd.Stdin = Stdin 873 cmd.Stdout = Stdout 874 cmd.Stderr = Stderr 875 err = cmd.Run() 876 // ensure we close the fifo here so that the strace.TraceExecCommand() 877 // helper gets a EOF from the fifo (i.e. all writers must be closed 878 // for this) 879 fw.Close() 880 881 // wait for strace reader 882 <-doneCh 883 if straceErr == nil { 884 slg.Display(Stderr) 885 } else { 886 logger.Noticef("cannot extract runtime data: %v", straceErr) 887 } 888 return err 889 } 890 891 func (x *cmdRun) runCmdUnderStrace(origCmd []string, envForExec envForExecFunc) error { 892 extraStraceOpts, raw, err := x.straceOpts() 893 if err != nil { 894 return err 895 } 896 cmd, err := strace.Command(extraStraceOpts, origCmd...) 897 if err != nil { 898 return err 899 } 900 901 // run with filter 902 cmd.Env = envForExec(nil) 903 cmd.Stdin = Stdin 904 cmd.Stdout = Stdout 905 stderr, err := cmd.StderrPipe() 906 if err != nil { 907 return err 908 } 909 filterDone := make(chan bool, 1) 910 go func() { 911 defer func() { filterDone <- true }() 912 913 if raw { 914 // Passing --strace='--raw' disables the filtering of 915 // early strace output. This is useful when tracking 916 // down issues with snap helpers such as snap-confine, 917 // snap-exec ... 918 io.Copy(Stderr, stderr) 919 return 920 } 921 922 r := bufio.NewReader(stderr) 923 924 // The first thing from strace if things work is 925 // "exeve(" - show everything until we see this to 926 // not swallow real strace errors. 927 for { 928 s, err := r.ReadString('\n') 929 if err != nil { 930 break 931 } 932 if strings.Contains(s, "execve(") { 933 break 934 } 935 fmt.Fprint(Stderr, s) 936 } 937 938 // The last thing that snap-exec does is to 939 // execve() something inside the snap dir so 940 // we know that from that point on the output 941 // will be interessting to the user. 942 // 943 // We need check both /snap (which is where snaps 944 // are located inside the mount namespace) and the 945 // distro snap mount dir (which is different on e.g. 946 // fedora/arch) to fully work with classic snaps. 947 needle1 := fmt.Sprintf(`execve("%s`, dirs.SnapMountDir) 948 needle2 := `execve("/snap` 949 for { 950 s, err := r.ReadString('\n') 951 if err != nil { 952 if err != io.EOF { 953 fmt.Fprintf(Stderr, "cannot read strace output: %s\n", err) 954 } 955 break 956 } 957 // Ensure we catch the execve but *not* the 958 // exec into 959 // /snap/core/current/usr/lib/snapd/snap-confine 960 // which is just `snap run` using the core version 961 // snap-confine. 962 if (strings.Contains(s, needle1) || strings.Contains(s, needle2)) && !strings.Contains(s, "usr/lib/snapd/snap-confine") { 963 fmt.Fprint(Stderr, s) 964 break 965 } 966 } 967 io.Copy(Stderr, r) 968 }() 969 if err := cmd.Start(); err != nil { 970 return err 971 } 972 <-filterDone 973 err = cmd.Wait() 974 return err 975 } 976 977 func (x *cmdRun) runSnapConfine(info *snap.Info, securityTag, snapApp, hook string, args []string) error { 978 snapConfine, err := snapdHelperPath("snap-confine") 979 if err != nil { 980 return err 981 } 982 if !osutil.FileExists(snapConfine) { 983 if hook != "" { 984 logger.Noticef("WARNING: skipping running hook %q of snap %q: missing snap-confine", hook, info.InstanceName()) 985 return nil 986 } 987 return fmt.Errorf(i18n.G("missing snap-confine: try updating your core/snapd package")) 988 } 989 990 if err := createUserDataDirs(info); err != nil { 991 logger.Noticef("WARNING: cannot create user data directory: %s", err) 992 } 993 994 xauthPath, err := migrateXauthority(info) 995 if err != nil { 996 logger.Noticef("WARNING: cannot copy user Xauthority file: %s", err) 997 } 998 999 if err := activateXdgDocumentPortal(info, snapApp, hook); err != nil { 1000 logger.Noticef("WARNING: cannot start document portal: %s", err) 1001 } 1002 1003 cmd := []string{snapConfine} 1004 if info.NeedsClassic() { 1005 cmd = append(cmd, "--classic") 1006 } 1007 1008 // this should never happen since we validate snaps with "base: none" and do not allow hooks/apps 1009 if info.Base == "none" { 1010 return fmt.Errorf(`cannot run hooks / applications with base "none"`) 1011 } 1012 if info.Base != "" { 1013 cmd = append(cmd, "--base", info.Base) 1014 } 1015 cmd = append(cmd, securityTag) 1016 1017 // when under confinement, snap-exec is run from 'core' snap rootfs 1018 snapExecPath := filepath.Join(dirs.CoreLibExecDir, "snap-exec") 1019 1020 if info.NeedsClassic() { 1021 // running with classic confinement, carefully pick snap-exec we 1022 // are going to use 1023 snapExecPath, err = snapdHelperPath("snap-exec") 1024 if err != nil { 1025 return err 1026 } 1027 } 1028 cmd = append(cmd, snapExecPath) 1029 1030 if x.Shell { 1031 cmd = append(cmd, "--command=shell") 1032 } 1033 if x.Gdb { 1034 cmd = append(cmd, "--command=gdb") 1035 } 1036 if x.useGdbserver() { 1037 cmd = append(cmd, "--command=gdbserver") 1038 } 1039 if x.Command != "" { 1040 cmd = append(cmd, "--command="+x.Command) 1041 } 1042 1043 if hook != "" { 1044 cmd = append(cmd, "--hook="+hook) 1045 } 1046 1047 // snap-exec is POSIXly-- options must come before positionals. 1048 cmd = append(cmd, snapApp) 1049 cmd = append(cmd, args...) 1050 1051 env, err := osutil.OSEnvironment() 1052 if err != nil { 1053 return err 1054 } 1055 snapenv.ExtendEnvForRun(env, info) 1056 1057 if len(xauthPath) > 0 { 1058 // Environment is not nil here because it comes from 1059 // osutil.OSEnvironment and that guarantees this 1060 // property. 1061 env["XAUTHORITY"] = xauthPath 1062 } 1063 1064 // on each run variant path this will be used once to get 1065 // the environment plus additions in the right form 1066 envForExec := func(extra map[string]string) []string { 1067 for varName, value := range extra { 1068 env[varName] = value 1069 } 1070 if !info.NeedsClassic() { 1071 return env.ForExec() 1072 } 1073 // For a classic snap, environment variables that are 1074 // usually stripped out by ld.so when starting a 1075 // setuid process are presevered by being renamed by 1076 // prepending PreservedUnsafePrefix -- which snap-exec 1077 // will remove, restoring the variables to their 1078 // original names. 1079 return env.ForExecEscapeUnsafe(snapenv.PreservedUnsafePrefix) 1080 } 1081 1082 // Systemd automatically places services under a unique cgroup encoding the 1083 // security tag, but for apps and hooks we need to create a transient scope 1084 // with similar purpose ourselves. 1085 // 1086 // The way this happens is as follows: 1087 // 1088 // 1) Services are implemented using systemd service units. Starting a 1089 // unit automatically places it in a cgroup named after the service unit 1090 // name. Snapd controls the name of the service units thus indirectly 1091 // controls the cgroup name. 1092 // 1093 // 2) Non-services, including hooks, are started inside systemd 1094 // transient scopes. Scopes are a systemd unit type that are defined 1095 // programmatically and are meant for groups of processes started and 1096 // stopped by an _arbitrary process_ (ie, not systemd). Systemd 1097 // requires that each scope is given a unique name. We employ a scheme 1098 // where random UUID is combined with the name of the security tag 1099 // derived from snap application or hook name. Multiple concurrent 1100 // invocations of "snap run" will use distinct UUIDs. 1101 // 1102 // Transient scopes allow launched snaps to integrate into 1103 // the systemd design. See: 1104 // https://www.freedesktop.org/wiki/Software/systemd/ControlGroupInterface/ 1105 // 1106 // Programs running as root, like system-wide services and programs invoked 1107 // using tools like sudo are placed under system.slice. Programs running as 1108 // a non-root user are placed under user.slice, specifically in a scope 1109 // specific to a logind session. 1110 // 1111 // This arrangement allows for proper accounting and control of resources 1112 // used by snap application processes of each type. 1113 // 1114 // For more information about systemd cgroups, including unit types, see: 1115 // https://www.freedesktop.org/wiki/Software/systemd/ControlGroupInterface/ 1116 _, appName := snap.SplitSnapApp(snapApp) 1117 needsTracking := true 1118 if app := info.Apps[appName]; hook == "" && app != nil && app.IsService() { 1119 // If we are running a service app then we do not need to use 1120 // application tracking. Services, both in the system and user scope, 1121 // do not need tracking because systemd already places them in a 1122 // tracking cgroup, named after the systemd unit name, and those are 1123 // sufficient to identify both the snap name and the app name. 1124 needsTracking = false 1125 } 1126 // Allow using the session bus for all apps but not for hooks. 1127 allowSessionBus := hook == "" 1128 // Track, or confirm existing tracking from systemd. 1129 var trackingErr error 1130 if needsTracking { 1131 opts := &cgroup.TrackingOptions{AllowSessionBus: allowSessionBus} 1132 trackingErr = cgroupCreateTransientScopeForTracking(securityTag, opts) 1133 } else { 1134 trackingErr = cgroupConfirmSystemdServiceTracking(securityTag) 1135 } 1136 if trackingErr != nil { 1137 if trackingErr != cgroup.ErrCannotTrackProcess { 1138 return trackingErr 1139 } 1140 // If we cannot track the process then log a debug message. 1141 // TODO: if we could, create a warning. Currently this is not possible 1142 // because only snapd can create warnings, internally. 1143 logger.Debugf("snapd cannot track the started application") 1144 logger.Debugf("snap refreshes will not be postponed by this process") 1145 } 1146 if x.TraceExec { 1147 return x.runCmdWithTraceExec(cmd, envForExec) 1148 } else if x.Gdb { 1149 return x.runCmdUnderGdb(cmd, envForExec) 1150 } else if x.useGdbserver() { 1151 return x.runCmdUnderGdbserver(cmd, envForExec) 1152 } else if x.useStrace() { 1153 return x.runCmdUnderStrace(cmd, envForExec) 1154 } else { 1155 return syscallExec(cmd[0], cmd, envForExec(nil)) 1156 } 1157 } 1158 1159 var cgroupCreateTransientScopeForTracking = cgroup.CreateTransientScopeForTracking 1160 var cgroupConfirmSystemdServiceTracking = cgroup.ConfirmSystemdServiceTracking