diff --git a/cmd/snap/cmd_run.go b/cmd/snap/cmd_run.go
index da16c4a4387..29d3b1dd387 100644
--- a/cmd/snap/cmd_run.go
+++ b/cmd/snap/cmd_run.go
@@ -372,6 +372,10 @@ func createUserDataDirs(info *snap.Info) error {
return fmt.Errorf(i18n.G("cannot get the current user: %v"), err)
}
+ snapDir := filepath.Join(usr.HomeDir, dirs.UserHomeSnapDir)
+ if err := os.MkdirAll(snapDir, 0700); err != nil {
+ return fmt.Errorf(i18n.G("cannot create snap home dir: %w"), err)
+ }
// see snapenv.User
instanceUserData := info.UserDataDir(usr.HomeDir)
instanceCommonUserData := info.UserCommonDataDir(usr.HomeDir)
diff --git a/cmd/snap/cmd_run.go.orig b/cmd/snap/cmd_run.go.orig
new file mode 100644
index 00000000000..da16c4a4387
--- /dev/null
+++ b/cmd/snap/cmd_run.go.orig
@@ -0,0 +1,1197 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2018 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package main
+
+import (
+ "bufio"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "net"
+ "os"
+ "os/exec"
+ "os/user"
+ "path/filepath"
+ "regexp"
+ "strconv"
+ "strings"
+ "syscall"
+ "time"
+
+ "github.com/godbus/dbus"
+ "github.com/jessevdk/go-flags"
+
+ "github.com/snapcore/snapd/client"
+ "github.com/snapcore/snapd/dbusutil"
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/i18n"
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/logger"
+ "github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/osutil/strace"
+ "github.com/snapcore/snapd/sandbox/cgroup"
+ "github.com/snapcore/snapd/sandbox/selinux"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/snapenv"
+ "github.com/snapcore/snapd/strutil/shlex"
+ "github.com/snapcore/snapd/timeutil"
+ "github.com/snapcore/snapd/x11"
+)
+
+var (
+ syscallExec = syscall.Exec
+ userCurrent = user.Current
+ osGetenv = os.Getenv
+ timeNow = time.Now
+ selinuxIsEnabled = selinux.IsEnabled
+ selinuxVerifyPathContext = selinux.VerifyPathContext
+ selinuxRestoreContext = selinux.RestoreContext
+)
+
+type cmdRun struct {
+ clientMixin
+ Command string `long:"command" hidden:"yes"`
+ HookName string `long:"hook" hidden:"yes"`
+ Revision string `short:"r" default:"unset" hidden:"yes"`
+ Shell bool `long:"shell" `
+
+ // This options is both a selector (use or don't use strace) and it
+ // can also carry extra options for strace. This is why there is
+ // "default" and "optional-value" to distinguish this.
+ Strace string `long:"strace" optional:"true" optional-value:"with-strace" default:"no-strace" default-mask:"-"`
+ // deprecated in favor of Gdbserver
+ Gdb bool `long:"gdb" hidden:"yes"`
+ Gdbserver string `long:"gdbserver" default:"no-gdbserver" optional-value:":0" optional:"true"`
+ ExperimentalGdbserver string `long:"experimental-gdbserver" default:"no-gdbserver" optional-value:":0" optional:"true" hidden:"yes"`
+ TraceExec bool `long:"trace-exec"`
+
+ // not a real option, used to check if cmdRun is initialized by
+ // the parser
+ ParserRan int `long:"parser-ran" default:"1" hidden:"yes"`
+ Timer string `long:"timer" hidden:"yes"`
+}
+
+func init() {
+ addCommand("run",
+ i18n.G("Run the given snap command"),
+ i18n.G(`
+The run command executes the given snap command with the right confinement
+and environment.
+`),
+ func() flags.Commander {
+ return &cmdRun{}
+ }, map[string]string{
+ // TRANSLATORS: This should not start with a lowercase letter.
+ "command": i18n.G("Alternative command to run"),
+ // TRANSLATORS: This should not start with a lowercase letter.
+ "hook": i18n.G("Hook to run"),
+ // TRANSLATORS: This should not start with a lowercase letter.
+ "r": i18n.G("Use a specific snap revision when running hook"),
+ // TRANSLATORS: This should not start with a lowercase letter.
+ "shell": i18n.G("Run a shell instead of the command (useful for debugging)"),
+ // TRANSLATORS: This should not start with a lowercase letter.
+ "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."),
+ // TRANSLATORS: This should not start with a lowercase letter.
+ "gdb": i18n.G("Run the command with gdb (deprecated, use --gdbserver instead)"),
+ // TRANSLATORS: This should not start with a lowercase letter.
+ "gdbserver": i18n.G("Run the command with gdbserver"),
+ "experimental-gdbserver": "",
+ // TRANSLATORS: This should not start with a lowercase letter.
+ "timer": i18n.G("Run as a timer service with given schedule"),
+ // TRANSLATORS: This should not start with a lowercase letter.
+ "trace-exec": i18n.G("Display exec calls timing data"),
+ "parser-ran": "",
+ }, nil)
+}
+
+// isStopping returns true if the system is shutting down.
+func isStopping() (bool, error) {
+ // Make sure, just in case, that systemd doesn't localize the output string.
+ env, err := osutil.OSEnvironment()
+ if err != nil {
+ return false, err
+ }
+ env["LC_MESSAGES"] = "C"
+ // Check if systemd is stopping (shutting down or rebooting).
+ cmd := exec.Command("systemctl", "is-system-running")
+ cmd.Env = env.ForExec()
+ stdout, err := cmd.Output()
+ // systemctl is-system-running returns non-zero for outcomes other than "running"
+ // As such, ignore any ExitError and just process the stdout buffer.
+ if _, ok := err.(*exec.ExitError); ok {
+ return string(stdout) == "stopping\n", nil
+ }
+ return false, err
+}
+
+func maybeWaitForSecurityProfileRegeneration(cli *client.Client) error {
+ // check if the security profiles key has changed, if so, we need
+ // to wait for snapd to re-generate all profiles
+ mismatch, err := interfaces.SystemKeyMismatch()
+ if err == nil && !mismatch {
+ return nil
+ }
+ // something went wrong with the system-key compare, try to
+ // reach snapd before continuing
+ if err != nil {
+ logger.Debugf("SystemKeyMismatch returned an error: %v", err)
+ }
+
+ // We have a mismatch but maybe it is only because systemd is shutting down
+ // and core or snapd were already unmounted and we failed to re-execute.
+ // For context see: https://bugs.launchpad.net/snapd/+bug/1871652
+ stopping, err := isStopping()
+ if err != nil {
+ logger.Debugf("cannot check if system is stopping: %s", err)
+ }
+ if stopping {
+ logger.Debugf("ignoring system key mismatch during system shutdown/reboot")
+ return nil
+ }
+
+ // We have a mismatch, try to connect to snapd, once we can
+ // connect we just continue because that usually means that
+ // a new snapd is ready and has generated profiles.
+ //
+ // There is a corner case if an upgrade leaves the old snapd
+ // running and we connect to the old snapd. Handling this
+ // correctly is tricky because our "snap run" pipeline may
+ // depend on profiles written by the new snapd. So for now we
+ // just continue and hope for the best. The real fix for this
+ // is to fix the packaging so that snapd is stopped, upgraded
+ // and started.
+ //
+ // connect timeout for client is 5s on each try, so 12*5s = 60s
+ timeout := 12
+ if timeoutEnv := os.Getenv("SNAPD_DEBUG_SYSTEM_KEY_RETRY"); timeoutEnv != "" {
+ if i, err := strconv.Atoi(timeoutEnv); err == nil {
+ timeout = i
+ }
+ }
+
+ logger.Debugf("system key mismatch detected, waiting for snapd to start responding...")
+
+ for i := 0; i < timeout; i++ {
+ // TODO: we could also check cli.Maintenance() here too in case snapd is
+ // down semi-permanently for a refresh, but what message do we show to
+ // the user or what do we do if we know snapd is down for maintenance?
+ if _, err := cli.SysInfo(); err == nil {
+ return nil
+ }
+ // sleep a little bit for good measure
+ time.Sleep(1 * time.Second)
+ }
+
+ return fmt.Errorf("timeout waiting for snap system profiles to get updated")
+}
+
+func (x *cmdRun) Execute(args []string) error {
+ if len(args) == 0 {
+ return fmt.Errorf(i18n.G("need the application to run as argument"))
+ }
+ snapApp := args[0]
+ args = args[1:]
+
+ // Catch some invalid parameter combinations, provide helpful errors
+ optionsSet := 0
+ for _, param := range []string{x.HookName, x.Command, x.Timer} {
+ if param != "" {
+ optionsSet++
+ }
+ }
+ if optionsSet > 1 {
+ return fmt.Errorf("you can only use one of --hook, --command, and --timer")
+ }
+
+ if x.Revision != "unset" && x.Revision != "" && x.HookName == "" {
+ return fmt.Errorf(i18n.G("-r can only be used with --hook"))
+ }
+ if x.HookName != "" && len(args) > 0 {
+ // TRANSLATORS: %q is the hook name; %s a space-separated list of extra arguments
+ return fmt.Errorf(i18n.G("too many arguments for hook %q: %s"), x.HookName, strings.Join(args, " "))
+ }
+
+ if err := maybeWaitForSecurityProfileRegeneration(x.client); err != nil {
+ return err
+ }
+
+ // Now actually handle the dispatching
+ if x.HookName != "" {
+ return x.snapRunHook(snapApp)
+ }
+
+ if x.Command == "complete" {
+ snapApp, args = antialias(snapApp, args)
+ }
+
+ if x.Timer != "" {
+ return x.snapRunTimer(snapApp, x.Timer, args)
+ }
+
+ return x.snapRunApp(snapApp, args)
+}
+
+// antialias changes snapApp and args if snapApp is actually an alias
+// for something else. If not, or if the args aren't what's expected
+// for completion, it returns them unchanged.
+func antialias(snapApp string, args []string) (string, []string) {
+ if len(args) < 7 {
+ // NOTE if len(args) < 7, Something is Wrong (at least WRT complete.sh and etelpmoc.sh)
+ return snapApp, args
+ }
+
+ actualApp, err := resolveApp(snapApp)
+ if err != nil || actualApp == snapApp {
+ // no alias! woop.
+ return snapApp, args
+ }
+
+ compPoint, err := strconv.Atoi(args[2])
+ if err != nil {
+ // args[2] is not COMP_POINT
+ return snapApp, args
+ }
+
+ if compPoint <= len(snapApp) {
+ // COMP_POINT is inside $0
+ return snapApp, args
+ }
+
+ if compPoint > len(args[5]) {
+ // COMP_POINT is bigger than $#
+ return snapApp, args
+ }
+
+ if args[6] != snapApp {
+ // args[6] is not COMP_WORDS[0]
+ return snapApp, args
+ }
+
+ // it _should_ be COMP_LINE followed by one of
+ // COMP_WORDBREAKS, but that's hard to do
+ re, err := regexp.Compile(`^` + regexp.QuoteMeta(snapApp) + `\b`)
+ if err != nil || !re.MatchString(args[5]) {
+ // (weird regexp error, or) args[5] is not COMP_LINE
+ return snapApp, args
+ }
+
+ argsOut := make([]string, len(args))
+ copy(argsOut, args)
+
+ argsOut[2] = strconv.Itoa(compPoint - len(snapApp) + len(actualApp))
+ argsOut[5] = re.ReplaceAllLiteralString(args[5], actualApp)
+ argsOut[6] = actualApp
+
+ return actualApp, argsOut
+}
+
+func getSnapInfo(snapName string, revision snap.Revision) (info *snap.Info, err error) {
+ if revision.Unset() {
+ info, err = snap.ReadCurrentInfo(snapName)
+ } else {
+ info, err = snap.ReadInfo(snapName, &snap.SideInfo{
+ Revision: revision,
+ })
+ }
+
+ return info, err
+}
+
+func createOrUpdateUserDataSymlink(info *snap.Info, usr *user.User) error {
+ // 'current' symlink for user data (SNAP_USER_DATA)
+ userData := info.UserDataDir(usr.HomeDir)
+ wantedSymlinkValue := filepath.Base(userData)
+ currentActiveSymlink := filepath.Join(userData, "..", "current")
+
+ var err error
+ var currentSymlinkValue string
+ for i := 0; i < 5; i++ {
+ currentSymlinkValue, err = os.Readlink(currentActiveSymlink)
+ // Failure other than non-existing symlink is fatal
+ if err != nil && !os.IsNotExist(err) {
+ // TRANSLATORS: %v the error message
+ return fmt.Errorf(i18n.G("cannot read symlink: %v"), err)
+ }
+
+ if currentSymlinkValue == wantedSymlinkValue {
+ break
+ }
+
+ if err == nil {
+ // We may be racing with other instances of snap-run that try to do the same thing
+ // If the symlink is already removed then we can ignore this error.
+ err = os.Remove(currentActiveSymlink)
+ if err != nil && !os.IsNotExist(err) {
+ // abort with error
+ break
+ }
+ }
+
+ err = os.Symlink(wantedSymlinkValue, currentActiveSymlink)
+ // Error other than symlink already exists will abort and be propagated
+ if err == nil || !os.IsExist(err) {
+ break
+ }
+ // If we arrived here it means the symlink couldn't be created because it got created
+ // in the meantime by another instance, so we will try again.
+ }
+ if err != nil {
+ return fmt.Errorf(i18n.G("cannot update the 'current' symlink of %q: %v"), currentActiveSymlink, err)
+ }
+ return nil
+}
+
+func createUserDataDirs(info *snap.Info) error {
+ // Adjust umask so that the created directories have the permissions we
+ // expect and are unaffected by the initial umask. While go runtime creates
+ // threads at will behind the scenes, the setting of umask applies to the
+ // entire process so it doesn't need any special handling to lock the
+ // executing goroutine to a single thread.
+ oldUmask := syscall.Umask(0)
+ defer syscall.Umask(oldUmask)
+
+ usr, err := userCurrent()
+ if err != nil {
+ return fmt.Errorf(i18n.G("cannot get the current user: %v"), err)
+ }
+
+ // see snapenv.User
+ instanceUserData := info.UserDataDir(usr.HomeDir)
+ instanceCommonUserData := info.UserCommonDataDir(usr.HomeDir)
+ createDirs := []string{instanceUserData, instanceCommonUserData}
+ if info.InstanceKey != "" {
+ // parallel instance snaps get additional mapping in their mount
+ // namespace, namely /home/joe/snap/foo_bar ->
+ // /home/joe/snap/foo, make sure that the mount point exists and
+ // is owned by the user
+ snapUserDir := snap.UserSnapDir(usr.HomeDir, info.SnapName())
+ createDirs = append(createDirs, snapUserDir)
+ }
+ for _, d := range createDirs {
+ if err := os.MkdirAll(d, 0755); err != nil {
+ // TRANSLATORS: %q is the directory whose creation failed, %v the error message
+ return fmt.Errorf(i18n.G("cannot create %q: %v"), d, err)
+ }
+ }
+
+ if err := createOrUpdateUserDataSymlink(info, usr); err != nil {
+ return err
+ }
+
+ return maybeRestoreSecurityContext(usr)
+}
+
+// maybeRestoreSecurityContext attempts to restore security context of ~/snap on
+// systems where it's applicable
+func maybeRestoreSecurityContext(usr *user.User) error {
+ snapUserHome := filepath.Join(usr.HomeDir, dirs.UserHomeSnapDir)
+ enabled, err := selinuxIsEnabled()
+ if err != nil {
+ return fmt.Errorf("cannot determine SELinux status: %v", err)
+ }
+ if !enabled {
+ logger.Debugf("SELinux not enabled")
+ return nil
+ }
+
+ match, err := selinuxVerifyPathContext(snapUserHome)
+ if err != nil {
+ return fmt.Errorf("failed to verify SELinux context of %v: %v", snapUserHome, err)
+ }
+ if match {
+ return nil
+ }
+ logger.Noticef("restoring default SELinux context of %v", snapUserHome)
+
+ if err := selinuxRestoreContext(snapUserHome, selinux.RestoreMode{Recursive: true}); err != nil {
+ return fmt.Errorf("cannot restore SELinux context of %v: %v", snapUserHome, err)
+ }
+ return nil
+}
+
+func (x *cmdRun) useStrace() bool {
+ // make sure the go-flag parser ran and assigned default values
+ return x.ParserRan == 1 && x.Strace != "no-strace"
+}
+
+func (x *cmdRun) straceOpts() (opts []string, raw bool, err error) {
+ if x.Strace == "with-strace" {
+ return nil, false, nil
+ }
+
+ split, err := shlex.Split(x.Strace)
+ if err != nil {
+ return nil, false, err
+ }
+
+ opts = make([]string, 0, len(split))
+ for _, opt := range split {
+ if opt == "--raw" {
+ raw = true
+ continue
+ }
+ opts = append(opts, opt)
+ }
+ return opts, raw, nil
+}
+
+func (x *cmdRun) snapRunApp(snapApp string, args []string) error {
+ snapName, appName := snap.SplitSnapApp(snapApp)
+ info, err := getSnapInfo(snapName, snap.R(0))
+ if err != nil {
+ return err
+ }
+
+ app := info.Apps[appName]
+ if app == nil {
+ return fmt.Errorf(i18n.G("cannot find app %q in %q"), appName, snapName)
+ }
+
+ return x.runSnapConfine(info, app.SecurityTag(), snapApp, "", args)
+}
+
+func (x *cmdRun) snapRunHook(snapName string) error {
+ revision, err := snap.ParseRevision(x.Revision)
+ if err != nil {
+ return err
+ }
+
+ info, err := getSnapInfo(snapName, revision)
+ if err != nil {
+ return err
+ }
+
+ hook := info.Hooks[x.HookName]
+ if hook == nil {
+ return fmt.Errorf(i18n.G("cannot find hook %q in %q"), x.HookName, snapName)
+ }
+
+ return x.runSnapConfine(info, hook.SecurityTag(), snapName, hook.Name, nil)
+}
+
+func (x *cmdRun) snapRunTimer(snapApp, timer string, args []string) error {
+ schedule, err := timeutil.ParseSchedule(timer)
+ if err != nil {
+ return fmt.Errorf("invalid timer format: %v", err)
+ }
+
+ now := timeNow()
+ if !timeutil.Includes(schedule, now) {
+ fmt.Fprintf(Stderr, "%s: attempted to run %q timer outside of scheduled time %q\n", now.Format(time.RFC3339), snapApp, timer)
+ return nil
+ }
+
+ return x.snapRunApp(snapApp, args)
+}
+
+var osReadlink = os.Readlink
+
+// snapdHelperPath return the path of a helper like "snap-confine" or
+// "snap-exec" based on if snapd is re-execed or not
+func snapdHelperPath(toolName string) (string, error) {
+ exe, err := osReadlink("/proc/self/exe")
+ if err != nil {
+ return "", fmt.Errorf("cannot read /proc/self/exe: %v", err)
+ }
+ // no re-exec
+ if !strings.HasPrefix(exe, dirs.SnapMountDir) {
+ return filepath.Join(dirs.DistroLibExecDir, toolName), nil
+ }
+ // The logic below only works if the last two path components
+ // are /usr/bin
+ // FIXME: use a snap warning?
+ if !strings.HasSuffix(exe, "/usr/bin/"+filepath.Base(exe)) {
+ logger.Noticef("(internal error): unexpected exe input in snapdHelperPath: %v", exe)
+ return filepath.Join(dirs.DistroLibExecDir, toolName), nil
+ }
+ // snapBase will be "/snap/{core,snapd}/$rev/" because
+ // the snap binary is always at $root/usr/bin/snap
+ snapBase := filepath.Clean(filepath.Join(filepath.Dir(exe), "..", ".."))
+ // Run snap-confine from the core/snapd snap. The tools in
+ // core/snapd snap are statically linked, or mostly
+ // statically, with the exception of libraries such as libudev
+ // and libc.
+ return filepath.Join(snapBase, dirs.CoreLibExecDir, toolName), nil
+}
+
+func migrateXauthority(info *snap.Info) (string, error) {
+ u, err := userCurrent()
+ if err != nil {
+ return "", fmt.Errorf(i18n.G("cannot get the current user: %s"), err)
+ }
+
+ // If our target directory (XDG_RUNTIME_DIR) doesn't exist we
+ // don't attempt to create it.
+ baseTargetDir := filepath.Join(dirs.XdgRuntimeDirBase, u.Uid)
+ if !osutil.FileExists(baseTargetDir) {
+ return "", nil
+ }
+
+ xauthPath := osGetenv("XAUTHORITY")
+ if len(xauthPath) == 0 || !osutil.FileExists(xauthPath) {
+ // Nothing to do for us. Most likely running outside of any
+ // graphical X11 session.
+ return "", nil
+ }
+
+ fin, err := os.Open(xauthPath)
+ if err != nil {
+ return "", err
+ }
+ defer fin.Close()
+
+ // Abs() also calls Clean(); see https://golang.org/pkg/path/filepath/#Abs
+ xauthPathAbs, err := filepath.Abs(fin.Name())
+ if err != nil {
+ return "", nil
+ }
+
+ // Remove all symlinks from path
+ xauthPathCan, err := filepath.EvalSymlinks(xauthPathAbs)
+ if err != nil {
+ return "", nil
+ }
+
+ // Ensure the XAUTHORITY env is not abused by checking that
+ // it point to exactly the file we just opened (no symlinks,
+ // no funny "../.." etc)
+ if fin.Name() != xauthPathCan {
+ logger.Noticef("WARNING: XAUTHORITY environment value is not a clean path: %q", xauthPathCan)
+ return "", nil
+ }
+
+ // Only do the migration from /tmp since the real /tmp is not visible for snaps
+ if !strings.HasPrefix(fin.Name(), "/tmp/") {
+ return "", nil
+ }
+
+ // We are performing a Stat() here to make sure that the user can't
+ // steal another user's Xauthority file. Note that while Stat() uses
+ // fstat() on the file descriptor created during Open(), the file might
+ // have changed ownership between the Open() and the Stat(). That's ok
+ // because we aren't trying to block access that the user already has:
+ // if the user has the privileges to chown another user's Xauthority
+ // file, we won't block that since the user can just steal it without
+ // having to use snap run. This code is just to ensure that a user who
+ // doesn't have those privileges can't steal the file via snap run
+ // (also note that the (potentially untrusted) snap isn't running yet).
+ fi, err := fin.Stat()
+ if err != nil {
+ return "", err
+ }
+ sys := fi.Sys()
+ if sys == nil {
+ return "", fmt.Errorf(i18n.G("cannot validate owner of file %s"), fin.Name())
+ }
+ // cheap comparison as the current uid is only available as a string
+ // but it is better to convert the uid from the stat result to a
+ // string than a string into a number.
+ if fmt.Sprintf("%d", sys.(*syscall.Stat_t).Uid) != u.Uid {
+ return "", fmt.Errorf(i18n.G("Xauthority file isn't owned by the current user %s"), u.Uid)
+ }
+
+ targetPath := filepath.Join(baseTargetDir, ".Xauthority")
+
+ // Only validate Xauthority file again when both files don't match
+ // otherwise we can continue using the existing Xauthority file.
+ // This is ok to do here because we aren't trying to protect against
+ // the user changing the Xauthority file in XDG_RUNTIME_DIR outside
+ // of snapd.
+ if osutil.FileExists(targetPath) {
+ var fout *os.File
+ if fout, err = os.Open(targetPath); err != nil {
+ return "", err
+ }
+ if osutil.StreamsEqual(fin, fout) {
+ fout.Close()
+ return targetPath, nil
+ }
+
+ fout.Close()
+ if err := os.Remove(targetPath); err != nil {
+ return "", err
+ }
+
+ // Ensure we're validating the Xauthority file from the beginning
+ if _, err := fin.Seek(int64(os.SEEK_SET), 0); err != nil {
+ return "", err
+ }
+ }
+
+ // To guard against setting XAUTHORITY to non-xauth files, check
+ // that we have a valid Xauthority. Specifically, the file must be
+ // parseable as an Xauthority file and not be empty.
+ if err := x11.ValidateXauthority(fin); err != nil {
+ return "", err
+ }
+
+ // Read data from the beginning of the file
+ if _, err = fin.Seek(int64(os.SEEK_SET), 0); err != nil {
+ return "", err
+ }
+
+ fout, err := os.OpenFile(targetPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
+ if err != nil {
+ return "", err
+ }
+ defer fout.Close()
+
+ // Read and write validated Xauthority file to its right location
+ if _, err = io.Copy(fout, fin); err != nil {
+ if err := os.Remove(targetPath); err != nil {
+ logger.Noticef("WARNING: cannot remove file at %s: %s", targetPath, err)
+ }
+ return "", fmt.Errorf(i18n.G("cannot write new Xauthority file at %s: %s"), targetPath, err)
+ }
+
+ return targetPath, nil
+}
+
+func activateXdgDocumentPortal(info *snap.Info, snapApp, hook string) error {
+ // Don't do anything for apps or hooks that don't plug the
+ // desktop interface
+ //
+ // NOTE: This check is imperfect because we don't really know
+ // if the interface is connected or not but this is an
+ // acceptable compromise for not having to communicate with
+ // snapd in snap run. In a typical desktop session the
+ // document portal can be in use by many applications, not
+ // just by snaps, so this is at most, pre-emptively using some
+ // extra memory.
+ var plugs map[string]*snap.PlugInfo
+ if hook != "" {
+ plugs = info.Hooks[hook].Plugs
+ } else {
+ _, appName := snap.SplitSnapApp(snapApp)
+ plugs = info.Apps[appName].Plugs
+ }
+ plugsDesktop := false
+ for _, plug := range plugs {
+ if plug.Interface == "desktop" {
+ plugsDesktop = true
+ break
+ }
+ }
+ if !plugsDesktop {
+ return nil
+ }
+
+ u, err := userCurrent()
+ if err != nil {
+ return fmt.Errorf(i18n.G("cannot get the current user: %s"), err)
+ }
+ xdgRuntimeDir := filepath.Join(dirs.XdgRuntimeDirBase, u.Uid)
+
+ // If $XDG_RUNTIME_DIR/doc appears to be a mount point, assume
+ // that the document portal is up and running.
+ expectedMountPoint := filepath.Join(xdgRuntimeDir, "doc")
+ if mounted, err := osutil.IsMounted(expectedMountPoint); err != nil {
+ logger.Noticef("Could not check document portal mount state: %s", err)
+ } else if mounted {
+ return nil
+ }
+
+ // If there is no session bus, our job is done. We check this
+ // manually to avoid dbus.SessionBus() auto-launching a new
+ // bus.
+ busAddress := osGetenv("DBUS_SESSION_BUS_ADDRESS")
+ if len(busAddress) == 0 {
+ return nil
+ }
+
+ // We've previously tried to start the document portal and
+ // were told the service is unknown: don't bother connecting
+ // to the session bus again.
+ //
+ // As the file is in $XDG_RUNTIME_DIR, it will be cleared over
+ // full logout/login or reboot cycles.
+ portalsUnavailableFile := filepath.Join(xdgRuntimeDir, ".portals-unavailable")
+ if osutil.FileExists(portalsUnavailableFile) {
+ return nil
+ }
+
+ conn, err := dbusutil.SessionBus()
+ if err != nil {
+ return err
+ }
+
+ portal := conn.Object("org.freedesktop.portal.Documents",
+ "/org/freedesktop/portal/documents")
+ var mountPoint []byte
+ if err := portal.Call("org.freedesktop.portal.Documents.GetMountPoint", 0).Store(&mountPoint); err != nil {
+ // It is not considered an error if
+ // xdg-document-portal is not available on the system.
+ if dbusErr, ok := err.(dbus.Error); ok && dbusErr.Name == "org.freedesktop.DBus.Error.ServiceUnknown" {
+ // We ignore errors here: if writing the file
+ // fails, we'll just try connecting to D-Bus
+ // again next time.
+ if err = ioutil.WriteFile(portalsUnavailableFile, []byte(""), 0644); err != nil {
+ logger.Noticef("WARNING: cannot write file at %s: %s", portalsUnavailableFile, err)
+ }
+ return nil
+ }
+ return err
+ }
+
+ // Sanity check to make sure the document portal is exposed
+ // where we think it is.
+ actualMountPoint := strings.TrimRight(string(mountPoint), "\x00")
+ if actualMountPoint != expectedMountPoint {
+ return fmt.Errorf(i18n.G("Expected portal at %#v, got %#v"), expectedMountPoint, actualMountPoint)
+ }
+ return nil
+}
+
+type envForExecFunc func(extra map[string]string) []string
+
+var gdbServerWelcomeFmt = `
+Welcome to "snap run --gdbserver".
+You are right before your application is run.
+Please open a different terminal and run:
+
+gdb -ex="target remote %[1]s" -ex=continue -ex="signal SIGCONT"
+(gdb) continue
+
+or use your favorite gdb frontend and connect to %[1]s
+`
+
+func racyFindFreePort() (int, error) {
+ l, err := net.Listen("tcp", ":0")
+ if err != nil {
+ return 0, err
+ }
+ defer l.Close()
+ return l.Addr().(*net.TCPAddr).Port, nil
+}
+
+func (x *cmdRun) useGdbserver() bool {
+ // compatibility, can be removed after 2021
+ if x.ExperimentalGdbserver != "no-gdbserver" {
+ x.Gdbserver = x.ExperimentalGdbserver
+ }
+
+ // make sure the go-flag parser ran and assigned default values
+ return x.ParserRan == 1 && x.Gdbserver != "no-gdbserver"
+}
+
+func (x *cmdRun) runCmdUnderGdbserver(origCmd []string, envForExec envForExecFunc) error {
+ gcmd := exec.Command(origCmd[0], origCmd[1:]...)
+ gcmd.Stdin = os.Stdin
+ gcmd.Stdout = os.Stdout
+ gcmd.Stderr = os.Stderr
+ gcmd.Env = envForExec(map[string]string{"SNAP_CONFINE_RUN_UNDER_GDBSERVER": "1"})
+ if err := gcmd.Start(); err != nil {
+ return err
+ }
+ // wait for the child process executing gdb helper to raise SIGSTOP
+ // signalling readiness to attach a gdbserver process
+ var status syscall.WaitStatus
+ _, err := syscall.Wait4(gcmd.Process.Pid, &status, syscall.WSTOPPED, nil)
+ if err != nil {
+ return err
+ }
+
+ addr := x.Gdbserver
+ if addr == ":0" {
+ // XXX: run "gdbserver :0" instead and parse "Listening on port 45971"
+ // on stderr instead?
+ port, err := racyFindFreePort()
+ if err != nil {
+ return fmt.Errorf("cannot find free port: %v", err)
+ }
+ addr = fmt.Sprintf(":%v", port)
+ }
+ // XXX: should we provide a helper here instead? something like
+ // `snap run --attach-debugger` or similar? The downside
+ // is that attaching a gdb frontend is harder?
+ fmt.Fprintf(Stdout, fmt.Sprintf(gdbServerWelcomeFmt, addr))
+ // note that only gdbserver needs to run as root, the application
+ // keeps running as the user
+ gdbSrvCmd := exec.Command("sudo", "-E", "gdbserver", "--attach", addr, strconv.Itoa(gcmd.Process.Pid))
+ if output, err := gdbSrvCmd.CombinedOutput(); err != nil {
+ return osutil.OutputErr(output, err)
+ }
+ return nil
+}
+
+func (x *cmdRun) runCmdUnderGdb(origCmd []string, envForExec envForExecFunc) error {
+ // the resulting application process runs as root
+ cmd := []string{"sudo", "-E", "gdb", "-ex=run", "-ex=catch exec", "-ex=continue", "--args"}
+ cmd = append(cmd, origCmd...)
+
+ gcmd := exec.Command(cmd[0], cmd[1:]...)
+ gcmd.Stdin = os.Stdin
+ gcmd.Stdout = os.Stdout
+ gcmd.Stderr = os.Stderr
+ gcmd.Env = envForExec(map[string]string{"SNAP_CONFINE_RUN_UNDER_GDB": "1"})
+ return gcmd.Run()
+}
+
+func (x *cmdRun) runCmdWithTraceExec(origCmd []string, envForExec envForExecFunc) error {
+ // setup private tmp dir with strace fifo
+ straceTmp, err := ioutil.TempDir("", "exec-trace")
+ if err != nil {
+ return err
+ }
+ defer os.RemoveAll(straceTmp)
+ straceLog := filepath.Join(straceTmp, "strace.fifo")
+ if err := syscall.Mkfifo(straceLog, 0640); err != nil {
+ return err
+ }
+ // ensure we have one writer on the fifo so that if strace fails
+ // nothing blocks
+ fw, err := os.OpenFile(straceLog, os.O_RDWR, 0640)
+ if err != nil {
+ return err
+ }
+ defer fw.Close()
+
+ // read strace data from fifo async
+ var slg *strace.ExecveTiming
+ var straceErr error
+ doneCh := make(chan bool, 1)
+ go func() {
+ // FIXME: make this configurable?
+ nSlowest := 10
+ slg, straceErr = strace.TraceExecveTimings(straceLog, nSlowest)
+ close(doneCh)
+ }()
+
+ cmd, err := strace.TraceExecCommand(straceLog, origCmd...)
+ if err != nil {
+ return err
+ }
+ // run
+ cmd.Env = envForExec(nil)
+ cmd.Stdin = Stdin
+ cmd.Stdout = Stdout
+ cmd.Stderr = Stderr
+ err = cmd.Run()
+ // ensure we close the fifo here so that the strace.TraceExecCommand()
+ // helper gets a EOF from the fifo (i.e. all writers must be closed
+ // for this)
+ fw.Close()
+
+ // wait for strace reader
+ <-doneCh
+ if straceErr == nil {
+ slg.Display(Stderr)
+ } else {
+ logger.Noticef("cannot extract runtime data: %v", straceErr)
+ }
+ return err
+}
+
+func (x *cmdRun) runCmdUnderStrace(origCmd []string, envForExec envForExecFunc) error {
+ extraStraceOpts, raw, err := x.straceOpts()
+ if err != nil {
+ return err
+ }
+ cmd, err := strace.Command(extraStraceOpts, origCmd...)
+ if err != nil {
+ return err
+ }
+
+ // run with filter
+ cmd.Env = envForExec(nil)
+ cmd.Stdin = Stdin
+ cmd.Stdout = Stdout
+ stderr, err := cmd.StderrPipe()
+ if err != nil {
+ return err
+ }
+ filterDone := make(chan bool, 1)
+ go func() {
+ defer func() { filterDone <- true }()
+
+ if raw {
+ // Passing --strace='--raw' disables the filtering of
+ // early strace output. This is useful when tracking
+ // down issues with snap helpers such as snap-confine,
+ // snap-exec ...
+ io.Copy(Stderr, stderr)
+ return
+ }
+
+ r := bufio.NewReader(stderr)
+
+ // The first thing from strace if things work is
+ // "exeve(" - show everything until we see this to
+ // not swallow real strace errors.
+ for {
+ s, err := r.ReadString('\n')
+ if err != nil {
+ break
+ }
+ if strings.Contains(s, "execve(") {
+ break
+ }
+ fmt.Fprint(Stderr, s)
+ }
+
+ // The last thing that snap-exec does is to
+ // execve() something inside the snap dir so
+ // we know that from that point on the output
+ // will be interessting to the user.
+ //
+ // We need check both /snap (which is where snaps
+ // are located inside the mount namespace) and the
+ // distro snap mount dir (which is different on e.g.
+ // fedora/arch) to fully work with classic snaps.
+ needle1 := fmt.Sprintf(`execve("%s`, dirs.SnapMountDir)
+ needle2 := `execve("/snap`
+ for {
+ s, err := r.ReadString('\n')
+ if err != nil {
+ if err != io.EOF {
+ fmt.Fprintf(Stderr, "cannot read strace output: %s\n", err)
+ }
+ break
+ }
+ // Ensure we catch the execve but *not* the
+ // exec into
+ // /snap/core/current/usr/lib/snapd/snap-confine
+ // which is just `snap run` using the core version
+ // snap-confine.
+ if (strings.Contains(s, needle1) || strings.Contains(s, needle2)) && !strings.Contains(s, "usr/lib/snapd/snap-confine") {
+ fmt.Fprint(Stderr, s)
+ break
+ }
+ }
+ io.Copy(Stderr, r)
+ }()
+ if err := cmd.Start(); err != nil {
+ return err
+ }
+ <-filterDone
+ err = cmd.Wait()
+ return err
+}
+
+func (x *cmdRun) runSnapConfine(info *snap.Info, securityTag, snapApp, hook string, args []string) error {
+ snapConfine, err := snapdHelperPath("snap-confine")
+ if err != nil {
+ return err
+ }
+ if !osutil.FileExists(snapConfine) {
+ if hook != "" {
+ logger.Noticef("WARNING: skipping running hook %q of snap %q: missing snap-confine", hook, info.InstanceName())
+ return nil
+ }
+ return fmt.Errorf(i18n.G("missing snap-confine: try updating your core/snapd package"))
+ }
+
+ if err := createUserDataDirs(info); err != nil {
+ logger.Noticef("WARNING: cannot create user data directory: %s", err)
+ }
+
+ xauthPath, err := migrateXauthority(info)
+ if err != nil {
+ logger.Noticef("WARNING: cannot copy user Xauthority file: %s", err)
+ }
+
+ if err := activateXdgDocumentPortal(info, snapApp, hook); err != nil {
+ logger.Noticef("WARNING: cannot start document portal: %s", err)
+ }
+
+ cmd := []string{snapConfine}
+ if info.NeedsClassic() {
+ cmd = append(cmd, "--classic")
+ }
+
+ // this should never happen since we validate snaps with "base: none" and do not allow hooks/apps
+ if info.Base == "none" {
+ return fmt.Errorf(`cannot run hooks / applications with base "none"`)
+ }
+ if info.Base != "" {
+ cmd = append(cmd, "--base", info.Base)
+ } else {
+ if info.Type() == snap.TypeKernel {
+ // kernels have no explicit base, we use the boot base
+ modelAssertion, err := x.client.CurrentModelAssertion()
+ if err != nil {
+ if hook != "" {
+ return fmt.Errorf("cannot get model assertion to setup kernel hook run: %v", err)
+ } else {
+ return fmt.Errorf("cannot get model assertion to setup kernel app run: %v", err)
+ }
+ }
+ modelBase := modelAssertion.Base()
+ if modelBase != "" {
+ cmd = append(cmd, "--base", modelBase)
+ }
+ }
+ }
+ cmd = append(cmd, securityTag)
+
+ // when under confinement, snap-exec is run from 'core' snap rootfs
+ snapExecPath := filepath.Join(dirs.CoreLibExecDir, "snap-exec")
+
+ if info.NeedsClassic() {
+ // running with classic confinement, carefully pick snap-exec we
+ // are going to use
+ snapExecPath, err = snapdHelperPath("snap-exec")
+ if err != nil {
+ return err
+ }
+ }
+ cmd = append(cmd, snapExecPath)
+
+ if x.Shell {
+ cmd = append(cmd, "--command=shell")
+ }
+ if x.Gdb {
+ cmd = append(cmd, "--command=gdb")
+ }
+ if x.useGdbserver() {
+ cmd = append(cmd, "--command=gdbserver")
+ }
+ if x.Command != "" {
+ cmd = append(cmd, "--command="+x.Command)
+ }
+
+ if hook != "" {
+ cmd = append(cmd, "--hook="+hook)
+ }
+
+ // snap-exec is POSIXly-- options must come before positionals.
+ cmd = append(cmd, snapApp)
+ cmd = append(cmd, args...)
+
+ env, err := osutil.OSEnvironment()
+ if err != nil {
+ return err
+ }
+ snapenv.ExtendEnvForRun(env, info)
+
+ if len(xauthPath) > 0 {
+ // Environment is not nil here because it comes from
+ // osutil.OSEnvironment and that guarantees this
+ // property.
+ env["XAUTHORITY"] = xauthPath
+ }
+
+ // on each run variant path this will be used once to get
+ // the environment plus additions in the right form
+ envForExec := func(extra map[string]string) []string {
+ for varName, value := range extra {
+ env[varName] = value
+ }
+ if !info.NeedsClassic() {
+ return env.ForExec()
+ }
+ // For a classic snap, environment variables that are
+ // usually stripped out by ld.so when starting a
+ // setuid process are presevered by being renamed by
+ // prepending PreservedUnsafePrefix -- which snap-exec
+ // will remove, restoring the variables to their
+ // original names.
+ return env.ForExecEscapeUnsafe(snapenv.PreservedUnsafePrefix)
+ }
+
+ // Systemd automatically places services under a unique cgroup encoding the
+ // security tag, but for apps and hooks we need to create a transient scope
+ // with similar purpose ourselves.
+ //
+ // The way this happens is as follows:
+ //
+ // 1) Services are implemented using systemd service units. Starting a
+ // unit automatically places it in a cgroup named after the service unit
+ // name. Snapd controls the name of the service units thus indirectly
+ // controls the cgroup name.
+ //
+ // 2) Non-services, including hooks, are started inside systemd
+ // transient scopes. Scopes are a systemd unit type that are defined
+ // programmatically and are meant for groups of processes started and
+ // stopped by an _arbitrary process_ (ie, not systemd). Systemd
+ // requires that each scope is given a unique name. We employ a scheme
+ // where random UUID is combined with the name of the security tag
+ // derived from snap application or hook name. Multiple concurrent
+ // invocations of "snap run" will use distinct UUIDs.
+ //
+ // Transient scopes allow launched snaps to integrate into
+ // the systemd design. See:
+ // https://www.freedesktop.org/wiki/Software/systemd/ControlGroupInterface/
+ //
+ // Programs running as root, like system-wide services and programs invoked
+ // using tools like sudo are placed under system.slice. Programs running as
+ // a non-root user are placed under user.slice, specifically in a scope
+ // specific to a logind session.
+ //
+ // This arrangement allows for proper accounting and control of resources
+ // used by snap application processes of each type.
+ //
+ // For more information about systemd cgroups, including unit types, see:
+ // https://www.freedesktop.org/wiki/Software/systemd/ControlGroupInterface/
+ _, appName := snap.SplitSnapApp(snapApp)
+ needsTracking := true
+ if app := info.Apps[appName]; hook == "" && app != nil && app.IsService() {
+ // If we are running a service app then we do not need to use
+ // application tracking. Services, both in the system and user scope,
+ // do not need tracking because systemd already places them in a
+ // tracking cgroup, named after the systemd unit name, and those are
+ // sufficient to identify both the snap name and the app name.
+ needsTracking = false
+ }
+ // Allow using the session bus for all apps but not for hooks.
+ allowSessionBus := hook == ""
+ // Track, or confirm existing tracking from systemd.
+ var trackingErr error
+ if needsTracking {
+ opts := &cgroup.TrackingOptions{AllowSessionBus: allowSessionBus}
+ trackingErr = cgroupCreateTransientScopeForTracking(securityTag, opts)
+ } else {
+ trackingErr = cgroupConfirmSystemdServiceTracking(securityTag)
+ }
+ if trackingErr != nil {
+ if trackingErr != cgroup.ErrCannotTrackProcess {
+ return trackingErr
+ }
+ // If we cannot track the process then log a debug message.
+ // TODO: if we could, create a warning. Currently this is not possible
+ // because only snapd can create warnings, internally.
+ logger.Debugf("snapd cannot track the started application")
+ logger.Debugf("snap refreshes will not be postponed by this process")
+ }
+ if x.TraceExec {
+ return x.runCmdWithTraceExec(cmd, envForExec)
+ } else if x.Gdb {
+ return x.runCmdUnderGdb(cmd, envForExec)
+ } else if x.useGdbserver() {
+ if _, err := exec.LookPath("gdbserver"); err != nil {
+ // TODO: use xerrors.Is(err, exec.ErrNotFound) once
+ // we moved off from go-1.9
+ if execErr, ok := err.(*exec.Error); ok {
+ if execErr.Err == exec.ErrNotFound {
+ return fmt.Errorf("please install gdbserver on your system")
+ }
+ }
+ return err
+ }
+ return x.runCmdUnderGdbserver(cmd, envForExec)
+ } else if x.useStrace() {
+ return x.runCmdUnderStrace(cmd, envForExec)
+ } else {
+ return syscallExec(cmd[0], cmd, envForExec(nil))
+ }
+}
+
+var cgroupCreateTransientScopeForTracking = cgroup.CreateTransientScopeForTracking
+var cgroupConfirmSystemdServiceTracking = cgroup.ConfirmSystemdServiceTracking
diff --git a/cmd/snap/cmd_run_test.go b/cmd/snap/cmd_run_test.go
index d37119dafa3..7f402a7fcbd 100644
--- a/cmd/snap/cmd_run_test.go
+++ b/cmd/snap/cmd_run_test.go
@@ -1583,3 +1583,20 @@ func (s *RunSuite) TestRunGdbserverNoGdbserver(c *check.C) {
_, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--gdbserver", "snapname.app"})
c.Assert(err, check.ErrorMatches, "please install gdbserver on your system")
}
+
+func (s *RunSuite) TestCreateSnapDirPermissions(c *check.C) {
+ usr, err := user.Current()
+ c.Assert(err, check.IsNil)
+
+ usr.HomeDir = s.fakeHome
+ snaprun.MockUserCurrent(func() (*user.User, error) {
+ return usr, nil
+ })
+
+ info := &snap.Info{SuggestedName: "some-snap"}
+ c.Assert(snaprun.CreateUserDataDirs(info), check.IsNil)
+
+ fi, err := os.Stat(filepath.Join(s.fakeHome, dirs.UserHomeSnapDir))
+ c.Assert(err, check.IsNil)
+ c.Assert(fi.Mode()&os.ModePerm, check.Equals, os.FileMode(0700))
+}
diff --git a/cmd/snap/cmd_run_test.go.orig b/cmd/snap/cmd_run_test.go.orig
new file mode 100644
index 00000000000..d37119dafa3
--- /dev/null
+++ b/cmd/snap/cmd_run_test.go.orig
@@ -0,0 +1,1585 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package main_test
+
+import (
+ "errors"
+ "fmt"
+ "net/http"
+ "os"
+ "os/user"
+ "path/filepath"
+ "strings"
+ "time"
+
+ "gopkg.in/check.v1"
+
+ snaprun "github.com/snapcore/snapd/cmd/snap"
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/logger"
+ "github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/sandbox/cgroup"
+ "github.com/snapcore/snapd/sandbox/selinux"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/snaptest"
+ "github.com/snapcore/snapd/testutil"
+ "github.com/snapcore/snapd/x11"
+)
+
+var mockYaml = []byte(`name: snapname
+version: 1.0
+apps:
+ app:
+ command: run-app
+ svc:
+ command: run-svc
+ daemon: simple
+hooks:
+ configure:
+`)
+
+var mockYamlBaseNone1 = []byte(`name: snapname1
+version: 1.0
+base: none
+apps:
+ app:
+ command: run-app
+`)
+
+var mockYamlBaseNone2 = []byte(`name: snapname2
+version: 1.0
+base: none
+hooks:
+ configure:
+`)
+
+type RunSuite struct {
+ fakeHome string
+ BaseSnapSuite
+}
+
+var _ = check.Suite(&RunSuite{})
+
+func (s *RunSuite) SetUpTest(c *check.C) {
+ s.BaseSnapSuite.SetUpTest(c)
+ s.fakeHome = c.MkDir()
+
+ u, err := user.Current()
+ c.Assert(err, check.IsNil)
+ s.AddCleanup(snaprun.MockUserCurrent(func() (*user.User, error) {
+ return &user.User{Uid: u.Uid, HomeDir: s.fakeHome}, nil
+ }))
+ s.AddCleanup(snaprun.MockCreateTransientScopeForTracking(func(string, *cgroup.TrackingOptions) error {
+ return nil
+ }))
+}
+
+func (s *RunSuite) TestInvalidParameters(c *check.C) {
+ invalidParameters := []string{"run", "--hook=configure", "--command=command-name", "--", "snap-name"}
+ _, err := snaprun.Parser(snaprun.Client()).ParseArgs(invalidParameters)
+ c.Check(err, check.ErrorMatches, ".*you can only use one of --hook, --command, and --timer.*")
+
+ invalidParameters = []string{"run", "--hook=configure", "--timer=10:00-12:00", "--", "snap-name"}
+ _, err = snaprun.Parser(snaprun.Client()).ParseArgs(invalidParameters)
+ c.Check(err, check.ErrorMatches, ".*you can only use one of --hook, --command, and --timer.*")
+
+ invalidParameters = []string{"run", "--command=command-name", "--timer=10:00-12:00", "--", "snap-name"}
+ _, err = snaprun.Parser(snaprun.Client()).ParseArgs(invalidParameters)
+ c.Check(err, check.ErrorMatches, ".*you can only use one of --hook, --command, and --timer.*")
+
+ invalidParameters = []string{"run", "-r=1", "--command=command-name", "--", "snap-name"}
+ _, err = snaprun.Parser(snaprun.Client()).ParseArgs(invalidParameters)
+ c.Check(err, check.ErrorMatches, ".*-r can only be used with --hook.*")
+
+ invalidParameters = []string{"run", "-r=1", "--", "snap-name"}
+ _, err = snaprun.Parser(snaprun.Client()).ParseArgs(invalidParameters)
+ c.Check(err, check.ErrorMatches, ".*-r can only be used with --hook.*")
+
+ invalidParameters = []string{"run", "--hook=configure", "--", "foo", "bar", "snap-name"}
+ _, err = snaprun.Parser(snaprun.Client()).ParseArgs(invalidParameters)
+ c.Check(err, check.ErrorMatches, ".*too many arguments for hook \"configure\": bar.*")
+}
+
+func (s *RunSuite) TestRunCmdWithBaseNone(c *check.C) {
+ defer mockSnapConfine(dirs.DistroLibExecDir)()
+
+ // mock installed snap
+ snaptest.MockSnapCurrent(c, string(mockYamlBaseNone1), &snap.SideInfo{
+ Revision: snap.R("1"),
+ })
+ snaptest.MockSnapCurrent(c, string(mockYamlBaseNone2), &snap.SideInfo{
+ Revision: snap.R("1"),
+ })
+
+ _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname1.app", "--arg1", "arg2"})
+ c.Assert(err, check.ErrorMatches, `cannot run hooks / applications with base \"none\"`)
+
+ _, err = snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--hook=configure", "--", "snapname2"})
+ c.Assert(err, check.ErrorMatches, `cannot run hooks / applications with base \"none\"`)
+}
+
+func (s *RunSuite) TestSnapRunWhenMissingConfine(c *check.C) {
+ _, r := logger.MockLogger()
+ defer r()
+
+ // mock installed snap
+ snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{
+ Revision: snap.R("x2"),
+ })
+
+ // redirect exec
+ var execs [][]string
+ restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error {
+ execs = append(execs, args)
+ return nil
+ })
+ defer restorer()
+
+ // and run it!
+ // a regular run will fail
+ _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.app", "--arg1", "arg2"})
+ c.Assert(err, check.ErrorMatches, `.* your core/snapd package`)
+ // a hook run will not fail
+ _, err = snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--hook=configure", "--", "snapname"})
+ c.Assert(err, check.IsNil)
+
+ // but nothing is run ever
+ c.Check(execs, check.IsNil)
+}
+
+func (s *RunSuite) TestSnapRunAppIntegration(c *check.C) {
+ defer mockSnapConfine(dirs.DistroLibExecDir)()
+
+ tmpdir := os.Getenv("TMPDIR")
+ if tmpdir == "" {
+ tmpdir = "/var/tmp"
+ os.Setenv("TMPDIR", tmpdir)
+ defer os.Unsetenv("TMPDIR")
+ }
+
+ // mock installed snap
+ snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{
+ Revision: snap.R("x2"),
+ })
+
+ // redirect exec
+ execArg0 := ""
+ execArgs := []string{}
+ execEnv := []string{}
+ restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error {
+ execArg0 = arg0
+ execArgs = args
+ execEnv = envv
+ return nil
+ })
+ defer restorer()
+
+ // and run it!
+ rest, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.app", "--arg1", "arg2"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{"snapname.app", "--arg1", "arg2"})
+ c.Check(execArg0, check.Equals, filepath.Join(dirs.DistroLibExecDir, "snap-confine"))
+ c.Check(execArgs, check.DeepEquals, []string{
+ filepath.Join(dirs.DistroLibExecDir, "snap-confine"),
+ "snap.snapname.app",
+ filepath.Join(dirs.CoreLibExecDir, "snap-exec"),
+ "snapname.app", "--arg1", "arg2"})
+ c.Check(execEnv, testutil.Contains, "SNAP_REVISION=x2")
+ c.Check(execEnv, testutil.Contains, fmt.Sprintf("TMPDIR=%s", tmpdir))
+}
+
+func (s *RunSuite) TestSnapRunClassicAppIntegration(c *check.C) {
+ defer mockSnapConfine(dirs.DistroLibExecDir)()
+
+ tmpdir := os.Getenv("TMPDIR")
+ if tmpdir == "" {
+ tmpdir = "/var/tmp"
+ os.Setenv("TMPDIR", tmpdir)
+ defer os.Unsetenv("TMPDIR")
+ }
+
+ // mock installed snap
+ snaptest.MockSnapCurrent(c, string(mockYaml)+"confinement: classic\n", &snap.SideInfo{
+ Revision: snap.R("x2"),
+ })
+
+ // redirect exec
+ execArg0 := ""
+ execArgs := []string{}
+ execEnv := []string{}
+ restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error {
+ execArg0 = arg0
+ execArgs = args
+ execEnv = envv
+ return nil
+ })
+ defer restorer()
+
+ // and run it!
+ rest, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.app", "--arg1", "arg2"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{"snapname.app", "--arg1", "arg2"})
+ c.Check(execArg0, check.Equals, filepath.Join(dirs.DistroLibExecDir, "snap-confine"))
+ c.Check(execArgs, check.DeepEquals, []string{
+ filepath.Join(dirs.DistroLibExecDir, "snap-confine"), "--classic",
+ "snap.snapname.app",
+ filepath.Join(dirs.DistroLibExecDir, "snap-exec"),
+ "snapname.app", "--arg1", "arg2"})
+ c.Check(execEnv, testutil.Contains, "SNAP_REVISION=x2")
+ c.Check(execEnv, testutil.Contains, fmt.Sprintf("SNAP_SAVED_TMPDIR=%s", tmpdir))
+}
+
+func (s *RunSuite) TestSnapRunClassicAppIntegrationReexecedFromCore(c *check.C) {
+ mountedCorePath := filepath.Join(dirs.SnapMountDir, "core/current")
+ mountedCoreLibExecPath := filepath.Join(mountedCorePath, dirs.CoreLibExecDir)
+
+ defer mockSnapConfine(mountedCoreLibExecPath)()
+
+ // mock installed snap
+ snaptest.MockSnapCurrent(c, string(mockYaml)+"confinement: classic\n", &snap.SideInfo{
+ Revision: snap.R("x2"),
+ })
+
+ restore := snaprun.MockOsReadlink(func(name string) (string, error) {
+ // pretend 'snap' is reexeced from 'core'
+ return filepath.Join(mountedCorePath, "usr/bin/snap"), nil
+ })
+ defer restore()
+
+ execArgs := []string{}
+ restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error {
+ execArgs = args
+ return nil
+ })
+ defer restorer()
+ rest, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.app", "--arg1", "arg2"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{"snapname.app", "--arg1", "arg2"})
+ c.Check(execArgs, check.DeepEquals, []string{
+ filepath.Join(mountedCoreLibExecPath, "snap-confine"), "--classic",
+ "snap.snapname.app",
+ filepath.Join(mountedCoreLibExecPath, "snap-exec"),
+ "snapname.app", "--arg1", "arg2"})
+}
+
+func (s *RunSuite) TestSnapRunClassicAppIntegrationReexecedFromSnapd(c *check.C) {
+ mountedSnapdPath := filepath.Join(dirs.SnapMountDir, "snapd/current")
+ mountedSnapdLibExecPath := filepath.Join(mountedSnapdPath, dirs.CoreLibExecDir)
+
+ defer mockSnapConfine(mountedSnapdLibExecPath)()
+
+ // mock installed snap
+ snaptest.MockSnapCurrent(c, string(mockYaml)+"confinement: classic\n", &snap.SideInfo{
+ Revision: snap.R("x2"),
+ })
+
+ restore := snaprun.MockOsReadlink(func(name string) (string, error) {
+ // pretend 'snap' is reexeced from 'core'
+ return filepath.Join(mountedSnapdPath, "usr/bin/snap"), nil
+ })
+ defer restore()
+
+ execArgs := []string{}
+ restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error {
+ execArgs = args
+ return nil
+ })
+ defer restorer()
+ rest, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.app", "--arg1", "arg2"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{"snapname.app", "--arg1", "arg2"})
+ c.Check(execArgs, check.DeepEquals, []string{
+ filepath.Join(mountedSnapdLibExecPath, "snap-confine"), "--classic",
+ "snap.snapname.app",
+ filepath.Join(mountedSnapdLibExecPath, "snap-exec"),
+ "snapname.app", "--arg1", "arg2"})
+}
+
+func (s *RunSuite) TestSnapRunAppWithCommandIntegration(c *check.C) {
+ defer mockSnapConfine(dirs.DistroLibExecDir)()
+
+ // mock installed snap
+ snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{
+ Revision: snap.R(42),
+ })
+
+ // redirect exec
+ execArg0 := ""
+ execArgs := []string{}
+ execEnv := []string{}
+ restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error {
+ execArg0 = arg0
+ execArgs = args
+ execEnv = envv
+ return nil
+ })
+ defer restorer()
+
+ // and run it!
+ _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--command=my-command", "--", "snapname.app", "arg1", "arg2"})
+ c.Assert(err, check.IsNil)
+ c.Check(execArg0, check.Equals, filepath.Join(dirs.DistroLibExecDir, "snap-confine"))
+ c.Check(execArgs, check.DeepEquals, []string{
+ filepath.Join(dirs.DistroLibExecDir, "snap-confine"),
+ "snap.snapname.app",
+ filepath.Join(dirs.CoreLibExecDir, "snap-exec"),
+ "--command=my-command", "snapname.app", "arg1", "arg2"})
+ c.Check(execEnv, testutil.Contains, "SNAP_REVISION=42")
+}
+
+func (s *RunSuite) TestSnapRunCreateDataDirs(c *check.C) {
+ info, err := snap.InfoFromSnapYaml(mockYaml)
+ c.Assert(err, check.IsNil)
+ info.SideInfo.Revision = snap.R(42)
+
+ err = snaprun.CreateUserDataDirs(info)
+ c.Assert(err, check.IsNil)
+ c.Check(osutil.FileExists(filepath.Join(s.fakeHome, "/snap/snapname/42")), check.Equals, true)
+ c.Check(osutil.FileExists(filepath.Join(s.fakeHome, "/snap/snapname/common")), check.Equals, true)
+}
+
+func (s *RunSuite) TestParallelInstanceSnapRunCreateDataDirs(c *check.C) {
+ info, err := snap.InfoFromSnapYaml(mockYaml)
+ c.Assert(err, check.IsNil)
+ info.SideInfo.Revision = snap.R(42)
+ info.InstanceKey = "foo"
+
+ err = snaprun.CreateUserDataDirs(info)
+ c.Assert(err, check.IsNil)
+ c.Check(osutil.FileExists(filepath.Join(s.fakeHome, "/snap/snapname_foo/42")), check.Equals, true)
+ c.Check(osutil.FileExists(filepath.Join(s.fakeHome, "/snap/snapname_foo/common")), check.Equals, true)
+ // mount point for snap instance mapping has been created
+ c.Check(osutil.FileExists(filepath.Join(s.fakeHome, "/snap/snapname")), check.Equals, true)
+ // and it's empty inside
+ m, err := filepath.Glob(filepath.Join(s.fakeHome, "/snap/snapname/*"))
+ c.Assert(err, check.IsNil)
+ c.Assert(m, check.HasLen, 0)
+}
+
+func (s *RunSuite) TestSnapRunHookIntegration(c *check.C) {
+ defer mockSnapConfine(dirs.DistroLibExecDir)()
+
+ // mock installed snap
+ snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{
+ Revision: snap.R(42),
+ })
+
+ // redirect exec
+ execArg0 := ""
+ execArgs := []string{}
+ execEnv := []string{}
+ restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error {
+ execArg0 = arg0
+ execArgs = args
+ execEnv = envv
+ return nil
+ })
+ defer restorer()
+
+ // Run a hook from the active revision
+ _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--hook=configure", "--", "snapname"})
+ c.Assert(err, check.IsNil)
+ c.Check(execArg0, check.Equals, filepath.Join(dirs.DistroLibExecDir, "snap-confine"))
+ c.Check(execArgs, check.DeepEquals, []string{
+ filepath.Join(dirs.DistroLibExecDir, "snap-confine"),
+ "snap.snapname.hook.configure",
+ filepath.Join(dirs.CoreLibExecDir, "snap-exec"),
+ "--hook=configure", "snapname"})
+ c.Check(execEnv, testutil.Contains, "SNAP_REVISION=42")
+}
+
+func (s *RunSuite) TestSnapRunHookUnsetRevisionIntegration(c *check.C) {
+ defer mockSnapConfine(dirs.DistroLibExecDir)()
+
+ // mock installed snap
+ snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{
+ Revision: snap.R(42),
+ })
+
+ // redirect exec
+ execArg0 := ""
+ execArgs := []string{}
+ execEnv := []string{}
+ restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error {
+ execArg0 = arg0
+ execArgs = args
+ execEnv = envv
+ return nil
+ })
+ defer restorer()
+
+ // Specifically pass "unset" which would use the active version.
+ _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--hook=configure", "-r=unset", "--", "snapname"})
+ c.Assert(err, check.IsNil)
+ c.Check(execArg0, check.Equals, filepath.Join(dirs.DistroLibExecDir, "snap-confine"))
+ c.Check(execArgs, check.DeepEquals, []string{
+ filepath.Join(dirs.DistroLibExecDir, "snap-confine"),
+ "snap.snapname.hook.configure",
+ filepath.Join(dirs.CoreLibExecDir, "snap-exec"),
+ "--hook=configure", "snapname"})
+ c.Check(execEnv, testutil.Contains, "SNAP_REVISION=42")
+}
+
+func (s *RunSuite) TestSnapRunHookSpecificRevisionIntegration(c *check.C) {
+ defer mockSnapConfine(dirs.DistroLibExecDir)()
+
+ // mock installed snap
+ // Create both revisions 41 and 42
+ snaptest.MockSnap(c, string(mockYaml), &snap.SideInfo{
+ Revision: snap.R(41),
+ })
+ snaptest.MockSnap(c, string(mockYaml), &snap.SideInfo{
+ Revision: snap.R(42),
+ })
+
+ // redirect exec
+ execArg0 := ""
+ execArgs := []string{}
+ execEnv := []string{}
+ restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error {
+ execArg0 = arg0
+ execArgs = args
+ execEnv = envv
+ return nil
+ })
+ defer restorer()
+
+ // Run a hook on revision 41
+ _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--hook=configure", "-r=41", "--", "snapname"})
+ c.Assert(err, check.IsNil)
+ c.Check(execArg0, check.Equals, filepath.Join(dirs.DistroLibExecDir, "snap-confine"))
+ c.Check(execArgs, check.DeepEquals, []string{
+ filepath.Join(dirs.DistroLibExecDir, "snap-confine"),
+ "snap.snapname.hook.configure",
+ filepath.Join(dirs.CoreLibExecDir, "snap-exec"),
+ "--hook=configure", "snapname"})
+ c.Check(execEnv, testutil.Contains, "SNAP_REVISION=41")
+}
+
+func (s *RunSuite) TestSnapRunHookMissingRevisionIntegration(c *check.C) {
+ // Only create revision 42
+ snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{
+ Revision: snap.R(42),
+ })
+
+ // redirect exec
+ restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error {
+ return nil
+ })
+ defer restorer()
+
+ // Attempt to run a hook on revision 41, which doesn't exist
+ _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--hook=configure", "-r=41", "--", "snapname"})
+ c.Assert(err, check.NotNil)
+ c.Check(err, check.ErrorMatches, "cannot find .*")
+}
+
+func (s *RunSuite) TestSnapRunHookInvalidRevisionIntegration(c *check.C) {
+ _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--hook=configure", "-r=invalid", "--", "snapname"})
+ c.Assert(err, check.NotNil)
+ c.Check(err, check.ErrorMatches, "invalid snap revision: \"invalid\"")
+}
+
+func (s *RunSuite) TestSnapRunHookMissingHookIntegration(c *check.C) {
+ // Only create revision 42
+ snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{
+ Revision: snap.R(42),
+ })
+
+ // redirect exec
+ called := false
+ restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error {
+ called = true
+ return nil
+ })
+ defer restorer()
+
+ _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--hook=missing-hook", "--", "snapname"})
+ c.Assert(err, check.ErrorMatches, `cannot find hook "missing-hook" in "snapname"`)
+ c.Check(called, check.Equals, false)
+}
+
+func (s *RunSuite) TestSnapRunErorsForUnknownRunArg(c *check.C) {
+ _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--unknown", "--", "snapname.app", "--arg1", "arg2"})
+ c.Assert(err, check.ErrorMatches, "unknown flag `unknown'")
+}
+
+func (s *RunSuite) TestSnapRunErorsForMissingApp(c *check.C) {
+ _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--command=shell"})
+ c.Assert(err, check.ErrorMatches, "need the application to run as argument")
+}
+
+func (s *RunSuite) TestSnapRunErorrForUnavailableApp(c *check.C) {
+ _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "not-there"})
+ c.Assert(err, check.ErrorMatches, fmt.Sprintf("cannot find current revision for snap not-there: readlink %s/not-there/current: no such file or directory", dirs.SnapMountDir))
+}
+
+func (s *RunSuite) TestSnapRunSaneEnvironmentHandling(c *check.C) {
+ defer mockSnapConfine(dirs.DistroLibExecDir)()
+
+ // mock installed snap
+ snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{
+ Revision: snap.R(42),
+ })
+
+ // redirect exec
+ execEnv := []string{}
+ restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error {
+ execEnv = envv
+ return nil
+ })
+ defer restorer()
+
+ // set a SNAP{,_*} variable in the environment
+ os.Setenv("SNAP_NAME", "something-else")
+ os.Setenv("SNAP_ARCH", "PDP-7")
+ defer os.Unsetenv("SNAP_NAME")
+ defer os.Unsetenv("SNAP_ARCH")
+ // but unrelated stuff is ok
+ os.Setenv("SNAP_THE_WORLD", "YES")
+ defer os.Unsetenv("SNAP_THE_WORLD")
+
+ // and ensure those SNAP_ vars get overridden
+ rest, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.app", "--arg1", "arg2"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{"snapname.app", "--arg1", "arg2"})
+ c.Check(execEnv, testutil.Contains, "SNAP_REVISION=42")
+ c.Check(execEnv, check.Not(testutil.Contains), "SNAP_NAME=something-else")
+ c.Check(execEnv, check.Not(testutil.Contains), "SNAP_ARCH=PDP-7")
+ c.Check(execEnv, testutil.Contains, "SNAP_THE_WORLD=YES")
+}
+
+func (s *RunSuite) TestSnapRunSnapdHelperPath(c *check.C) {
+ _, r := logger.MockLogger()
+ defer r()
+
+ var osReadlinkResult string
+ restore := snaprun.MockOsReadlink(func(name string) (string, error) {
+ return osReadlinkResult, nil
+ })
+ defer restore()
+
+ tool := "snap-confine"
+ for _, t := range []struct {
+ readlink string
+ expected string
+ }{
+ {
+ filepath.Join(dirs.SnapMountDir, "core/current/usr/bin/snap"),
+ filepath.Join(dirs.SnapMountDir, "core/current", dirs.CoreLibExecDir, tool),
+ },
+ {
+ filepath.Join(dirs.SnapMountDir, "snapd/current/usr/bin/snap"),
+ filepath.Join(dirs.SnapMountDir, "snapd/current", dirs.CoreLibExecDir, tool),
+ },
+ {
+ filepath.Join("/usr/bin/snap"),
+ filepath.Join(dirs.DistroLibExecDir, tool),
+ },
+ {
+ filepath.Join("/home/foo/ws/snapd/snap"),
+ filepath.Join(dirs.DistroLibExecDir, tool),
+ },
+ // unexpected case
+ {
+ filepath.Join(dirs.SnapMountDir, "snapd2/current/bin/snap"),
+ filepath.Join(dirs.DistroLibExecDir, tool),
+ },
+ } {
+ osReadlinkResult = t.readlink
+ toolPath, err := snaprun.SnapdHelperPath(tool)
+ c.Assert(err, check.IsNil)
+ c.Check(toolPath, check.Equals, t.expected)
+ }
+}
+
+func (s *RunSuite) TestSnapRunAppIntegrationFromCore(c *check.C) {
+ defer mockSnapConfine(filepath.Join(dirs.SnapMountDir, "core", "111", dirs.CoreLibExecDir))()
+
+ // mock installed snap
+ snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{
+ Revision: snap.R("x2"),
+ })
+
+ // pretend to be running from core
+ restorer := snaprun.MockOsReadlink(func(string) (string, error) {
+ return filepath.Join(dirs.SnapMountDir, "core/111/usr/bin/snap"), nil
+ })
+ defer restorer()
+
+ // redirect exec
+ execArg0 := ""
+ execArgs := []string{}
+ execEnv := []string{}
+ restorer = snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error {
+ execArg0 = arg0
+ execArgs = args
+ execEnv = envv
+ return nil
+ })
+ defer restorer()
+
+ // and run it!
+ rest, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.app", "--arg1", "arg2"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{"snapname.app", "--arg1", "arg2"})
+ c.Check(execArg0, check.Equals, filepath.Join(dirs.SnapMountDir, "/core/111", dirs.CoreLibExecDir, "snap-confine"))
+ c.Check(execArgs, check.DeepEquals, []string{
+ filepath.Join(dirs.SnapMountDir, "/core/111", dirs.CoreLibExecDir, "snap-confine"),
+ "snap.snapname.app",
+ filepath.Join(dirs.CoreLibExecDir, "snap-exec"),
+ "snapname.app", "--arg1", "arg2"})
+ c.Check(execEnv, testutil.Contains, "SNAP_REVISION=x2")
+}
+
+func (s *RunSuite) TestSnapRunAppIntegrationFromSnapd(c *check.C) {
+ defer mockSnapConfine(filepath.Join(dirs.SnapMountDir, "snapd", "222", dirs.CoreLibExecDir))()
+
+ // mock installed snap
+ snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{
+ Revision: snap.R("x2"),
+ })
+
+ // pretend to be running from snapd
+ restorer := snaprun.MockOsReadlink(func(string) (string, error) {
+ return filepath.Join(dirs.SnapMountDir, "snapd/222/usr/bin/snap"), nil
+ })
+ defer restorer()
+
+ // redirect exec
+ execArg0 := ""
+ execArgs := []string{}
+ execEnv := []string{}
+ restorer = snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error {
+ execArg0 = arg0
+ execArgs = args
+ execEnv = envv
+ return nil
+ })
+ defer restorer()
+
+ // and run it!
+ rest, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.app", "--arg1", "arg2"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{"snapname.app", "--arg1", "arg2"})
+ c.Check(execArg0, check.Equals, filepath.Join(dirs.SnapMountDir, "/snapd/222", dirs.CoreLibExecDir, "snap-confine"))
+ c.Check(execArgs, check.DeepEquals, []string{
+ filepath.Join(dirs.SnapMountDir, "/snapd/222", dirs.CoreLibExecDir, "snap-confine"),
+ "snap.snapname.app",
+ filepath.Join(dirs.CoreLibExecDir, "snap-exec"),
+ "snapname.app", "--arg1", "arg2"})
+ c.Check(execEnv, testutil.Contains, "SNAP_REVISION=x2")
+}
+
+func (s *RunSuite) TestSnapRunXauthorityMigration(c *check.C) {
+ defer mockSnapConfine(dirs.DistroLibExecDir)()
+
+ u, err := user.Current()
+ c.Assert(err, check.IsNil)
+
+ // Ensure XDG_RUNTIME_DIR exists for the user we're testing with
+ err = os.MkdirAll(filepath.Join(dirs.XdgRuntimeDirBase, u.Uid), 0700)
+ c.Assert(err, check.IsNil)
+
+ // mock installed snap; happily this also gives us a directory
+ // below /tmp which the Xauthority migration expects.
+ snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{
+ Revision: snap.R("x2"),
+ })
+
+ // redirect exec
+ execArg0 := ""
+ execArgs := []string{}
+ execEnv := []string{}
+ restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error {
+ execArg0 = arg0
+ execArgs = args
+ execEnv = envv
+ return nil
+ })
+ defer restorer()
+
+ xauthPath, err := x11.MockXauthority(2)
+ c.Assert(err, check.IsNil)
+ defer os.Remove(xauthPath)
+
+ defer snaprun.MockGetEnv(func(name string) string {
+ if name == "XAUTHORITY" {
+ return xauthPath
+ }
+ return ""
+ })()
+
+ // and run it!
+ rest, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.app"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{"snapname.app"})
+ c.Check(execArg0, check.Equals, filepath.Join(dirs.DistroLibExecDir, "snap-confine"))
+ c.Check(execArgs, check.DeepEquals, []string{
+ filepath.Join(dirs.DistroLibExecDir, "snap-confine"),
+ "snap.snapname.app",
+ filepath.Join(dirs.CoreLibExecDir, "snap-exec"),
+ "snapname.app"})
+
+ expectedXauthPath := filepath.Join(dirs.XdgRuntimeDirBase, u.Uid, ".Xauthority")
+ c.Check(execEnv, testutil.Contains, fmt.Sprintf("XAUTHORITY=%s", expectedXauthPath))
+
+ info, err := os.Stat(expectedXauthPath)
+ c.Assert(err, check.IsNil)
+ c.Assert(info.Mode().Perm(), check.Equals, os.FileMode(0600))
+
+ err = x11.ValidateXauthorityFile(expectedXauthPath)
+ c.Assert(err, check.IsNil)
+}
+
+// build the args for a hypothetical completer
+func mkCompArgs(compPoint string, argv ...string) []string {
+ out := []string{
+ "99", // COMP_TYPE
+ "99", // COMP_KEY
+ "", // COMP_POINT
+ "2", // COMP_CWORD
+ " ", // COMP_WORDBREAKS
+ }
+ out[2] = compPoint
+ out = append(out, strings.Join(argv, " "))
+ out = append(out, argv...)
+ return out
+}
+
+func (s *RunSuite) TestAntialiasHappy(c *check.C) {
+ c.Assert(os.MkdirAll(dirs.SnapBinariesDir, 0755), check.IsNil)
+
+ inArgs := mkCompArgs("10", "alias", "alias", "bo-alias")
+
+ // first not so happy because no alias symlink
+ app, outArgs := snaprun.Antialias("alias", inArgs)
+ c.Check(app, check.Equals, "alias")
+ c.Check(outArgs, check.DeepEquals, inArgs)
+
+ c.Assert(os.Symlink("an-app", filepath.Join(dirs.SnapBinariesDir, "alias")), check.IsNil)
+
+ // now really happy
+ app, outArgs = snaprun.Antialias("alias", inArgs)
+ c.Check(app, check.Equals, "an-app")
+ c.Check(outArgs, check.DeepEquals, []string{
+ "99", // COMP_TYPE (no change)
+ "99", // COMP_KEY (no change)
+ "11", // COMP_POINT (+1 because "an-app" is one longer than "alias")
+ "2", // COMP_CWORD (no change)
+ " ", // COMP_WORDBREAKS (no change)
+ "an-app alias bo-alias", // COMP_LINE (argv[0] changed)
+ "an-app", // argv (arv[0] changed)
+ "alias",
+ "bo-alias",
+ })
+}
+
+func (s *RunSuite) TestAntialiasBailsIfUnhappy(c *check.C) {
+ // alias exists but args are somehow wonky
+ c.Assert(os.MkdirAll(dirs.SnapBinariesDir, 0755), check.IsNil)
+ c.Assert(os.Symlink("an-app", filepath.Join(dirs.SnapBinariesDir, "alias")), check.IsNil)
+
+ // weird1 has COMP_LINE not start with COMP_WORDS[0], argv[0] equal to COMP_WORDS[0]
+ weird1 := mkCompArgs("6", "alias", "")
+ weird1[5] = "xxxxx "
+ // weird2 has COMP_LINE not start with COMP_WORDS[0], argv[0] equal to the first word in COMP_LINE
+ weird2 := mkCompArgs("6", "xxxxx", "")
+ weird2[5] = "alias "
+
+ for desc, inArgs := range map[string][]string{
+ "nil args": nil,
+ "too-short args": {"alias"},
+ "COMP_POINT not a number": mkCompArgs("hello", "alias"),
+ "COMP_POINT is inside argv[0]": mkCompArgs("2", "alias", ""),
+ "COMP_POINT is outside argv": mkCompArgs("99", "alias", ""),
+ "COMP_WORDS[0] is not argv[0]": mkCompArgs("10", "not-alias", ""),
+ "mismatch between argv[0], COMP_LINE and COMP_WORDS, #1": weird1,
+ "mismatch between argv[0], COMP_LINE and COMP_WORDS, #2": weird2,
+ } {
+ // antialias leaves args alone if it's too short
+ app, outArgs := snaprun.Antialias("alias", inArgs)
+ c.Check(app, check.Equals, "alias", check.Commentf(desc))
+ c.Check(outArgs, check.DeepEquals, inArgs, check.Commentf(desc))
+ }
+}
+
+func (s *RunSuite) TestSnapRunAppWithStraceIntegration(c *check.C) {
+ defer mockSnapConfine(dirs.DistroLibExecDir)()
+
+ // mock installed snap
+ snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{
+ Revision: snap.R("x2"),
+ })
+
+ // pretend we have sudo and simulate some useful output that would
+ // normally come from strace
+ sudoCmd := testutil.MockCommand(c, "sudo", fmt.Sprintf(`
+echo "stdout output 1"
+>&2 echo 'execve("/path/to/snap-confine")'
+>&2 echo "snap-confine/snap-exec strace stuff"
+>&2 echo "getuid() = 1000"
+>&2 echo 'execve("%s/snapName/x2/bin/foo")'
+>&2 echo "interessting strace output"
+>&2 echo "and more"
+echo "stdout output 2"
+`, dirs.SnapMountDir))
+ defer sudoCmd.Restore()
+
+ // pretend we have strace
+ straceCmd := testutil.MockCommand(c, "strace", "")
+ defer straceCmd.Restore()
+
+ user, err := user.Current()
+ c.Assert(err, check.IsNil)
+
+ // and run it under strace
+ rest, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--strace", "--", "snapname.app", "--arg1", "arg2"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{"snapname.app", "--arg1", "arg2"})
+ c.Check(sudoCmd.Calls(), check.DeepEquals, [][]string{
+ {
+ "sudo", "-E",
+ filepath.Join(straceCmd.BinDir(), "strace"),
+ "-u", user.Username,
+ "-f",
+ "-e", "!select,pselect6,_newselect,clock_gettime,sigaltstack,gettid,gettimeofday,nanosleep",
+ filepath.Join(dirs.DistroLibExecDir, "snap-confine"),
+ "snap.snapname.app",
+ filepath.Join(dirs.CoreLibExecDir, "snap-exec"),
+ "snapname.app", "--arg1", "arg2",
+ },
+ })
+ c.Check(s.Stdout(), check.Equals, "stdout output 1\nstdout output 2\n")
+ c.Check(s.Stderr(), check.Equals, fmt.Sprintf("execve(%q)\ninteressting strace output\nand more\n", filepath.Join(dirs.SnapMountDir, "snapName/x2/bin/foo")))
+
+ s.ResetStdStreams()
+ sudoCmd.ForgetCalls()
+
+ // try again without filtering
+ rest, err = snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--strace=--raw", "--", "snapname.app", "--arg1", "arg2"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{"snapname.app", "--arg1", "arg2"})
+ c.Check(sudoCmd.Calls(), check.DeepEquals, [][]string{
+ {
+ "sudo", "-E",
+ filepath.Join(straceCmd.BinDir(), "strace"),
+ "-u", user.Username,
+ "-f",
+ "-e", "!select,pselect6,_newselect,clock_gettime,sigaltstack,gettid,gettimeofday,nanosleep",
+ filepath.Join(dirs.DistroLibExecDir, "snap-confine"),
+ "snap.snapname.app",
+ filepath.Join(dirs.CoreLibExecDir, "snap-exec"),
+ "snapname.app", "--arg1", "arg2",
+ },
+ })
+ c.Check(s.Stdout(), check.Equals, "stdout output 1\nstdout output 2\n")
+ expectedFullFmt := `execve("/path/to/snap-confine")
+snap-confine/snap-exec strace stuff
+getuid() = 1000
+execve("%s/snapName/x2/bin/foo")
+interessting strace output
+and more
+`
+ c.Check(s.Stderr(), check.Equals, fmt.Sprintf(expectedFullFmt, dirs.SnapMountDir))
+}
+
+func (s *RunSuite) TestSnapRunAppWithStraceOptions(c *check.C) {
+ defer mockSnapConfine(dirs.DistroLibExecDir)()
+
+ // mock installed snap
+ snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{
+ Revision: snap.R("x2"),
+ })
+
+ // pretend we have sudo
+ sudoCmd := testutil.MockCommand(c, "sudo", "")
+ defer sudoCmd.Restore()
+
+ // pretend we have strace
+ straceCmd := testutil.MockCommand(c, "strace", "")
+ defer straceCmd.Restore()
+
+ user, err := user.Current()
+ c.Assert(err, check.IsNil)
+
+ // and run it under strace
+ rest, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", `--strace=-tt --raw -o "file with spaces"`, "--", "snapname.app", "--arg1", "arg2"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{"snapname.app", "--arg1", "arg2"})
+ c.Check(sudoCmd.Calls(), check.DeepEquals, [][]string{
+ {
+ "sudo", "-E",
+ filepath.Join(straceCmd.BinDir(), "strace"),
+ "-u", user.Username,
+ "-f",
+ "-e", "!select,pselect6,_newselect,clock_gettime,sigaltstack,gettid,gettimeofday,nanosleep",
+ "-tt",
+ "-o",
+ "file with spaces",
+ filepath.Join(dirs.DistroLibExecDir, "snap-confine"),
+ "snap.snapname.app",
+ filepath.Join(dirs.CoreLibExecDir, "snap-exec"),
+ "snapname.app", "--arg1", "arg2",
+ },
+ })
+}
+
+func (s *RunSuite) TestSnapRunShellIntegration(c *check.C) {
+ defer mockSnapConfine(dirs.DistroLibExecDir)()
+
+ // mock installed snap
+ snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{
+ Revision: snap.R("x2"),
+ })
+
+ // redirect exec
+ execArg0 := ""
+ execArgs := []string{}
+ execEnv := []string{}
+ restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error {
+ execArg0 = arg0
+ execArgs = args
+ execEnv = envv
+ return nil
+ })
+ defer restorer()
+
+ // and run it!
+ rest, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--shell", "--", "snapname.app", "--arg1", "arg2"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{"snapname.app", "--arg1", "arg2"})
+ c.Check(execArg0, check.Equals, filepath.Join(dirs.DistroLibExecDir, "snap-confine"))
+ c.Check(execArgs, check.DeepEquals, []string{
+ filepath.Join(dirs.DistroLibExecDir, "snap-confine"),
+ "snap.snapname.app",
+ filepath.Join(dirs.CoreLibExecDir, "snap-exec"),
+ "--command=shell", "snapname.app", "--arg1", "arg2"})
+ c.Check(execEnv, testutil.Contains, "SNAP_REVISION=x2")
+}
+
+func (s *RunSuite) TestSnapRunAppTimer(c *check.C) {
+ defer mockSnapConfine(dirs.DistroLibExecDir)()
+
+ // mock installed snap
+ snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{
+ Revision: snap.R("x2"),
+ })
+
+ // redirect exec
+ execArg0 := ""
+ execArgs := []string{}
+ execCalled := false
+ restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error {
+ execArg0 = arg0
+ execArgs = args
+ execCalled = true
+ return nil
+ })
+ defer restorer()
+
+ fakeNow := time.Date(2018, 02, 12, 9, 55, 0, 0, time.Local)
+ restorer = snaprun.MockTimeNow(func() time.Time {
+ // Monday Feb 12, 9:55
+ return fakeNow
+ })
+ defer restorer()
+
+ // pretend we are outside of timer range
+ rest, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", `--timer="mon,10:00~12:00,,fri,13:00"`, "--", "snapname.app", "--arg1", "arg2"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{"snapname.app", "--arg1", "arg2"})
+ c.Assert(execCalled, check.Equals, false)
+
+ c.Check(s.Stderr(), check.Equals, fmt.Sprintf(`%s: attempted to run "snapname.app" timer outside of scheduled time "mon,10:00~12:00,,fri,13:00"
+`, fakeNow.Format(time.RFC3339)))
+ s.ResetStdStreams()
+
+ restorer = snaprun.MockTimeNow(func() time.Time {
+ // Monday Feb 12, 10:20
+ return time.Date(2018, 02, 12, 10, 20, 0, 0, time.Local)
+ })
+ defer restorer()
+
+ // and run it under strace
+ _, err = snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", `--timer="mon,10:00~12:00,,fri,13:00"`, "--", "snapname.app", "--arg1", "arg2"})
+ c.Assert(err, check.IsNil)
+ c.Assert(execCalled, check.Equals, true)
+ c.Check(execArg0, check.Equals, filepath.Join(dirs.DistroLibExecDir, "snap-confine"))
+ c.Check(execArgs, check.DeepEquals, []string{
+ filepath.Join(dirs.DistroLibExecDir, "snap-confine"),
+ "snap.snapname.app",
+ filepath.Join(dirs.CoreLibExecDir, "snap-exec"),
+ "snapname.app", "--arg1", "arg2"})
+}
+
+func (s *RunSuite) TestRunCmdWithTraceExecUnhappy(c *check.C) {
+ _, r := logger.MockLogger()
+ defer r()
+
+ defer mockSnapConfine(dirs.DistroLibExecDir)()
+
+ // mock installed snap
+ snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{
+ Revision: snap.R("1"),
+ })
+
+ // pretend we have sudo
+ sudoCmd := testutil.MockCommand(c, "sudo", "echo unhappy; exit 12")
+ defer sudoCmd.Restore()
+
+ // pretend we have strace
+ straceCmd := testutil.MockCommand(c, "strace", "")
+ defer straceCmd.Restore()
+
+ rest, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--trace-exec", "--", "snapname.app", "--arg1", "arg2"})
+ c.Assert(err, check.ErrorMatches, "exit status 12")
+ c.Assert(rest, check.DeepEquals, []string{"--", "snapname.app", "--arg1", "arg2"})
+ c.Check(s.Stdout(), check.Equals, "unhappy\n")
+ c.Check(s.Stderr(), check.Equals, "")
+}
+
+func (s *RunSuite) TestSnapRunRestoreSecurityContextHappy(c *check.C) {
+ logbuf, restorer := logger.MockLogger()
+ defer restorer()
+
+ defer mockSnapConfine(dirs.DistroLibExecDir)()
+
+ // mock installed snap
+ snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{
+ Revision: snap.R("x2"),
+ })
+
+ // redirect exec
+ execCalled := 0
+ restorer = snaprun.MockSyscallExec(func(_ string, args []string, envv []string) error {
+ execCalled++
+ return nil
+ })
+ defer restorer()
+
+ verifyCalls := 0
+ restoreCalls := 0
+ isEnabledCalls := 0
+ enabled := false
+ verify := true
+
+ snapUserDir := filepath.Join(s.fakeHome, dirs.UserHomeSnapDir)
+
+ restorer = snaprun.MockSELinuxVerifyPathContext(func(what string) (bool, error) {
+ c.Check(what, check.Equals, snapUserDir)
+ verifyCalls++
+ return verify, nil
+ })
+ defer restorer()
+
+ restorer = snaprun.MockSELinuxRestoreContext(func(what string, mode selinux.RestoreMode) error {
+ c.Check(mode, check.Equals, selinux.RestoreMode{Recursive: true})
+ c.Check(what, check.Equals, snapUserDir)
+ restoreCalls++
+ return nil
+ })
+ defer restorer()
+
+ restorer = snaprun.MockSELinuxIsEnabled(func() (bool, error) {
+ isEnabledCalls++
+ return enabled, nil
+ })
+ defer restorer()
+
+ _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.app"})
+ c.Assert(err, check.IsNil)
+ c.Check(execCalled, check.Equals, 1)
+ c.Check(isEnabledCalls, check.Equals, 1)
+ c.Check(verifyCalls, check.Equals, 0)
+ c.Check(restoreCalls, check.Equals, 0)
+
+ // pretend SELinux is on
+ enabled = true
+
+ _, err = snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.app"})
+ c.Assert(err, check.IsNil)
+ c.Check(execCalled, check.Equals, 2)
+ c.Check(isEnabledCalls, check.Equals, 2)
+ c.Check(verifyCalls, check.Equals, 1)
+ c.Check(restoreCalls, check.Equals, 0)
+
+ // pretend the context does not match
+ verify = false
+
+ logbuf.Reset()
+
+ _, err = snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.app"})
+ c.Assert(err, check.IsNil)
+ c.Check(execCalled, check.Equals, 3)
+ c.Check(isEnabledCalls, check.Equals, 3)
+ c.Check(verifyCalls, check.Equals, 2)
+ c.Check(restoreCalls, check.Equals, 1)
+
+ // and we let the user know what we're doing
+ c.Check(logbuf.String(), testutil.Contains, fmt.Sprintf("restoring default SELinux context of %s", snapUserDir))
+}
+
+func (s *RunSuite) TestSnapRunRestoreSecurityContextFail(c *check.C) {
+ logbuf, restorer := logger.MockLogger()
+ defer restorer()
+
+ defer mockSnapConfine(dirs.DistroLibExecDir)()
+
+ // mock installed snap
+ snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{
+ Revision: snap.R("x2"),
+ })
+
+ // redirect exec
+ execCalled := 0
+ restorer = snaprun.MockSyscallExec(func(_ string, args []string, envv []string) error {
+ execCalled++
+ return nil
+ })
+ defer restorer()
+
+ verifyCalls := 0
+ restoreCalls := 0
+ isEnabledCalls := 0
+ enabledErr := errors.New("enabled failed")
+ verifyErr := errors.New("verify failed")
+ restoreErr := errors.New("restore failed")
+
+ snapUserDir := filepath.Join(s.fakeHome, dirs.UserHomeSnapDir)
+
+ restorer = snaprun.MockSELinuxVerifyPathContext(func(what string) (bool, error) {
+ c.Check(what, check.Equals, snapUserDir)
+ verifyCalls++
+ return false, verifyErr
+ })
+ defer restorer()
+
+ restorer = snaprun.MockSELinuxRestoreContext(func(what string, mode selinux.RestoreMode) error {
+ c.Check(mode, check.Equals, selinux.RestoreMode{Recursive: true})
+ c.Check(what, check.Equals, snapUserDir)
+ restoreCalls++
+ return restoreErr
+ })
+ defer restorer()
+
+ restorer = snaprun.MockSELinuxIsEnabled(func() (bool, error) {
+ isEnabledCalls++
+ return enabledErr == nil, enabledErr
+ })
+ defer restorer()
+
+ _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.app"})
+ // these errors are only logged, but we still run the snap
+ c.Assert(err, check.IsNil)
+ c.Check(execCalled, check.Equals, 1)
+ c.Check(logbuf.String(), testutil.Contains, "cannot determine SELinux status: enabled failed")
+ c.Check(isEnabledCalls, check.Equals, 1)
+ c.Check(verifyCalls, check.Equals, 0)
+ c.Check(restoreCalls, check.Equals, 0)
+ // pretend selinux is on
+ enabledErr = nil
+
+ logbuf.Reset()
+
+ _, err = snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.app"})
+ c.Assert(err, check.IsNil)
+ c.Check(execCalled, check.Equals, 2)
+ c.Check(logbuf.String(), testutil.Contains, fmt.Sprintf("failed to verify SELinux context of %s: verify failed", snapUserDir))
+ c.Check(isEnabledCalls, check.Equals, 2)
+ c.Check(verifyCalls, check.Equals, 1)
+ c.Check(restoreCalls, check.Equals, 0)
+
+ // pretend the context does not match
+ verifyErr = nil
+
+ logbuf.Reset()
+
+ _, err = snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.app"})
+ c.Assert(err, check.IsNil)
+ c.Check(execCalled, check.Equals, 3)
+ c.Check(logbuf.String(), testutil.Contains, fmt.Sprintf("cannot restore SELinux context of %s: restore failed", snapUserDir))
+ c.Check(isEnabledCalls, check.Equals, 3)
+ c.Check(verifyCalls, check.Equals, 2)
+ c.Check(restoreCalls, check.Equals, 1)
+}
+
+// systemctl is-system-running returns "running" in normal situations.
+func (s *RunSuite) TestIsStoppingRunning(c *check.C) {
+ systemctl := testutil.MockCommand(c, "systemctl", `
+case "$1" in
+ is-system-running)
+ echo "running"
+ exit 0
+ ;;
+esac
+`)
+ defer systemctl.Restore()
+ stop, err := snaprun.IsStopping()
+ c.Check(err, check.IsNil)
+ c.Check(stop, check.Equals, false)
+ c.Check(systemctl.Calls(), check.DeepEquals, [][]string{
+ {"systemctl", "is-system-running"},
+ })
+}
+
+// systemctl is-system-running returns "stopping" when the system is
+// shutting down or rebooting. At the same time it returns a non-zero
+// exit status.
+func (s *RunSuite) TestIsStoppingStopping(c *check.C) {
+ systemctl := testutil.MockCommand(c, "systemctl", `
+case "$1" in
+ is-system-running)
+ echo "stopping"
+ exit 1
+ ;;
+esac
+`)
+ defer systemctl.Restore()
+ stop, err := snaprun.IsStopping()
+ c.Check(err, check.IsNil)
+ c.Check(stop, check.Equals, true)
+ c.Check(systemctl.Calls(), check.DeepEquals, [][]string{
+ {"systemctl", "is-system-running"},
+ })
+}
+
+// systemctl is-system-running can often return "degraded"
+// Let's make sure that is not confusing us.
+func (s *RunSuite) TestIsStoppingDegraded(c *check.C) {
+ systemctl := testutil.MockCommand(c, "systemctl", `
+case "$1" in
+ is-system-running)
+ echo "degraded"
+ exit 1
+ ;;
+esac
+`)
+ defer systemctl.Restore()
+ stop, err := snaprun.IsStopping()
+ c.Check(err, check.IsNil)
+ c.Check(stop, check.Equals, false)
+ c.Check(systemctl.Calls(), check.DeepEquals, [][]string{
+ {"systemctl", "is-system-running"},
+ })
+}
+
+func (s *RunSuite) TestSnapRunTrackingApps(c *check.C) {
+ restore := mockSnapConfine(filepath.Join(dirs.SnapMountDir, "core", "111", dirs.CoreLibExecDir))
+ defer restore()
+
+ // mock installed snap
+ snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{
+ Revision: snap.R("x2"),
+ })
+
+ // pretend to be running from core
+ restore = snaprun.MockOsReadlink(func(string) (string, error) {
+ return filepath.Join(dirs.SnapMountDir, "core/111/usr/bin/snap"), nil
+ })
+ defer restore()
+
+ created := false
+ restore = snaprun.MockCreateTransientScopeForTracking(func(securityTag string, opts *cgroup.TrackingOptions) error {
+ c.Assert(securityTag, check.Equals, "snap.snapname.app")
+ c.Assert(opts, check.NotNil)
+ c.Assert(opts.AllowSessionBus, check.Equals, true)
+ created = true
+ return nil
+ })
+ defer restore()
+
+ restore = snaprun.MockConfirmSystemdServiceTracking(func(securityTag string) error {
+ panic("apps need to create a scope and do not use systemd service tracking")
+ })
+ defer restore()
+
+ // redirect exec
+ execArg0 := ""
+ execArgs := []string{}
+ execEnv := []string{}
+ restore = snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error {
+ execArg0 = arg0
+ execArgs = args
+ execEnv = envv
+ return nil
+ })
+ defer restore()
+
+ // and run it!
+ rest, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.app", "--arg1", "arg2"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{"snapname.app", "--arg1", "arg2"})
+ c.Check(execArg0, check.Equals, filepath.Join(dirs.SnapMountDir, "/core/111", dirs.CoreLibExecDir, "snap-confine"))
+ c.Check(execArgs, check.DeepEquals, []string{
+ filepath.Join(dirs.SnapMountDir, "/core/111", dirs.CoreLibExecDir, "snap-confine"),
+ "snap.snapname.app",
+ filepath.Join(dirs.CoreLibExecDir, "snap-exec"),
+ "snapname.app", "--arg1", "arg2"})
+ c.Check(execEnv, testutil.Contains, "SNAP_REVISION=x2")
+ c.Assert(created, check.Equals, true)
+}
+
+func (s *RunSuite) TestSnapRunTrackingHooks(c *check.C) {
+ restore := mockSnapConfine(filepath.Join(dirs.SnapMountDir, "core", "111", dirs.CoreLibExecDir))
+ defer restore()
+
+ // mock installed snap
+ snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{
+ Revision: snap.R("x2"),
+ })
+
+ // pretend to be running from core
+ restore = snaprun.MockOsReadlink(func(string) (string, error) {
+ return filepath.Join(dirs.SnapMountDir, "core/111/usr/bin/snap"), nil
+ })
+ defer restore()
+
+ created := false
+ restore = snaprun.MockCreateTransientScopeForTracking(func(securityTag string, opts *cgroup.TrackingOptions) error {
+ c.Assert(securityTag, check.Equals, "snap.snapname.hook.configure")
+ c.Assert(opts, check.NotNil)
+ c.Assert(opts.AllowSessionBus, check.Equals, false)
+ created = true
+ return nil
+ })
+ defer restore()
+
+ restore = snaprun.MockConfirmSystemdServiceTracking(func(securityTag string) error {
+ panic("hooks need to create a scope and do not use systemd service tracking")
+ })
+ defer restore()
+
+ // redirect exec
+ execArg0 := ""
+ execArgs := []string{}
+ execEnv := []string{}
+ restore = snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error {
+ execArg0 = arg0
+ execArgs = args
+ execEnv = envv
+ return nil
+ })
+ defer restore()
+
+ // and run it!
+ rest, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--hook", "configure", "-r", "x2", "snapname"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{"snapname"})
+ c.Check(execArg0, check.Equals, filepath.Join(dirs.SnapMountDir, "/core/111", dirs.CoreLibExecDir, "snap-confine"))
+ c.Check(execArgs, check.DeepEquals, []string{
+ filepath.Join(dirs.SnapMountDir, "/core/111", dirs.CoreLibExecDir, "snap-confine"),
+ "snap.snapname.hook.configure",
+ filepath.Join(dirs.CoreLibExecDir, "snap-exec"),
+ "--hook=configure", "snapname"})
+ c.Check(execEnv, testutil.Contains, "SNAP_REVISION=x2")
+ c.Assert(created, check.Equals, true)
+}
+
+func (s *RunSuite) TestSnapRunTrackingServices(c *check.C) {
+ restore := mockSnapConfine(filepath.Join(dirs.SnapMountDir, "core", "111", dirs.CoreLibExecDir))
+ defer restore()
+
+ // mock installed snap
+ snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{
+ Revision: snap.R("x2"),
+ })
+
+ // pretend to be running from core
+ restore = snaprun.MockOsReadlink(func(string) (string, error) {
+ return filepath.Join(dirs.SnapMountDir, "core/111/usr/bin/snap"), nil
+ })
+ defer restore()
+
+ restore = snaprun.MockCreateTransientScopeForTracking(func(securityTag string, opts *cgroup.TrackingOptions) error {
+ panic("services rely on systemd tracking, should not have created a transient scope")
+ })
+ defer restore()
+
+ confirmed := false
+ restore = snaprun.MockConfirmSystemdServiceTracking(func(securityTag string) error {
+ confirmed = true
+ c.Assert(securityTag, check.Equals, "snap.snapname.svc")
+ return nil
+ })
+ defer restore()
+
+ // redirect exec
+ execArg0 := ""
+ execArgs := []string{}
+ execEnv := []string{}
+ restore = snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error {
+ execArg0 = arg0
+ execArgs = args
+ execEnv = envv
+ return nil
+ })
+ defer restore()
+
+ // and run it!
+ rest, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.svc", "--arg1", "arg2"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{"snapname.svc", "--arg1", "arg2"})
+ c.Check(execArg0, check.Equals, filepath.Join(dirs.SnapMountDir, "/core/111", dirs.CoreLibExecDir, "snap-confine"))
+ c.Check(execArgs, check.DeepEquals, []string{
+ filepath.Join(dirs.SnapMountDir, "/core/111", dirs.CoreLibExecDir, "snap-confine"),
+ "snap.snapname.svc",
+ filepath.Join(dirs.CoreLibExecDir, "snap-exec"),
+ "snapname.svc", "--arg1", "arg2"})
+ c.Check(execEnv, testutil.Contains, "SNAP_REVISION=x2")
+ c.Assert(confirmed, check.Equals, true)
+}
+
+func (s *RunSuite) TestSnapRunTrackingFailure(c *check.C) {
+ restore := mockSnapConfine(filepath.Join(dirs.SnapMountDir, "core", "111", dirs.CoreLibExecDir))
+ defer restore()
+
+ // mock installed snap
+ snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{
+ Revision: snap.R("x2"),
+ })
+
+ // pretend to be running from core
+ restore = snaprun.MockOsReadlink(func(string) (string, error) {
+ return filepath.Join(dirs.SnapMountDir, "core/111/usr/bin/snap"), nil
+ })
+ defer restore()
+
+ created := false
+ restore = snaprun.MockCreateTransientScopeForTracking(func(securityTag string, opts *cgroup.TrackingOptions) error {
+ c.Assert(securityTag, check.Equals, "snap.snapname.app")
+ c.Assert(opts, check.NotNil)
+ c.Assert(opts.AllowSessionBus, check.Equals, true)
+ created = true
+ // Pretend that the tracking system was unable to track this application.
+ return cgroup.ErrCannotTrackProcess
+ })
+ defer restore()
+
+ restore = snaprun.MockConfirmSystemdServiceTracking(func(securityTag string) error {
+ panic("apps need to create a scope and do not use systemd service tracking")
+ })
+ defer restore()
+
+ // redirect exec
+ execArg0 := ""
+ execArgs := []string{}
+ execEnv := []string{}
+ restore = snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error {
+ execArg0 = arg0
+ execArgs = args
+ execEnv = envv
+ return nil
+ })
+ defer restore()
+
+ // Capture the debug log that is printed by this test.
+ os.Setenv("SNAPD_DEBUG", "1")
+ defer os.Unsetenv("SNAPD_DEBUG")
+ logbuf, restore := logger.MockLogger()
+ defer restore()
+
+ // and run it!
+ rest, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.app", "--arg1", "arg2"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{"snapname.app", "--arg1", "arg2"})
+ c.Check(execArg0, check.Equals, filepath.Join(dirs.SnapMountDir, "/core/111", dirs.CoreLibExecDir, "snap-confine"))
+ c.Check(execArgs, check.DeepEquals, []string{
+ filepath.Join(dirs.SnapMountDir, "/core/111", dirs.CoreLibExecDir, "snap-confine"),
+ "snap.snapname.app",
+ filepath.Join(dirs.CoreLibExecDir, "snap-exec"),
+ "snapname.app", "--arg1", "arg2"})
+ c.Check(execEnv, testutil.Contains, "SNAP_REVISION=x2")
+ c.Assert(created, check.Equals, true)
+
+ // Ensure that the debug message is printed.
+ c.Assert(logbuf.String(), testutil.Contains, "snapd cannot track the started application\n")
+}
+
+var mockKernelYaml = []byte(`name: pc-kernel
+type: kernel
+version: 1.0
+hooks:
+ fde-setup:
+`)
+
+func (s *RunSuite) TestSnapRunHookKernelImplicitBase(c *check.C) {
+ defer mockSnapConfine(dirs.DistroLibExecDir)()
+
+ nModel := 0
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case "/v2/model":
+ switch nModel {
+ case 0:
+ c.Check(r.Method, check.Equals, "GET")
+ c.Check(r.URL.RawQuery, check.Equals, "")
+ fmt.Fprintln(w, happyUC20ModelAssertionResponse)
+ default:
+ c.Fatalf("expected to get 1 request for /v2/model, now on %d", nModel+1)
+ }
+ nModel++
+ }
+ })
+
+ // mock installed kernel
+ snaptest.MockSnapCurrent(c, string(mockKernelYaml), &snap.SideInfo{
+ Revision: snap.R(42),
+ })
+
+ // redirect exec
+ execArg0 := ""
+ execArgs := []string{}
+ execEnv := []string{}
+ restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error {
+ execArg0 = arg0
+ execArgs = args
+ execEnv = envv
+ return nil
+ })
+ defer restorer()
+
+ // Run a hook from the active revision
+ _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--hook=fde-setup", "--", "pc-kernel"})
+ c.Assert(err, check.IsNil)
+ c.Check(execArg0, check.Equals, filepath.Join(dirs.DistroLibExecDir, "snap-confine"))
+ c.Check(execArgs, check.DeepEquals, []string{
+ filepath.Join(dirs.DistroLibExecDir, "snap-confine"),
+ "--base", "core20",
+ "snap.pc-kernel.hook.fde-setup",
+ filepath.Join(dirs.CoreLibExecDir, "snap-exec"),
+ "--hook=fde-setup", "pc-kernel"})
+ c.Check(execEnv, testutil.Contains, "SNAP_REVISION=42")
+ c.Check(nModel, check.Equals, 1)
+}
+
+func (s *RunSuite) TestRunGdbserverNoGdbserver(c *check.C) {
+ oldPath := os.Getenv("PATH")
+ os.Setenv("PATH", "/no-path:/really-not")
+ defer os.Setenv("PATH", oldPath)
+
+ defer mockSnapConfine(dirs.DistroLibExecDir)()
+ snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{
+ Revision: snap.R("x2"),
+ })
+
+ _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--gdbserver", "snapname.app"})
+ c.Assert(err, check.ErrorMatches, "please install gdbserver on your system")
+}