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