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") +}