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