github.com/kubiko/snapd@v0.0.0-20201013125620-d4f3094d9ddf/cmd/snap/cmd_run.go (about)

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