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