// -*- Mode: Go; indent-tabs-mode: t -*-
/*
 * Copyright (C) 2014-2024 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 <http://www.gnu.org/licenses/>.
 *
 */

package main

import (
	"bufio"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log/syslog"
	"net"
	"os"
	"os/exec"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"
	"syscall"
	"time"

	"github.com/godbus/dbus/v5"
	"github.com/jessevdk/go-flags"

	"github.com/snapcore/snapd/client"
	"github.com/snapcore/snapd/cmd/snaplock/runinhibit"
	"github.com/snapcore/snapd/desktop/portal"
	"github.com/snapcore/snapd/dirs"
	"github.com/snapcore/snapd/features"
	"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/osutil/user"
	"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/snapdtool"
	"github.com/snapcore/snapd/strutil"
	"github.com/snapcore/snapd/strutil/shlex"
	"github.com/snapcore/snapd/systemd"
	"github.com/snapcore/snapd/timeutil"
	"github.com/snapcore/snapd/x11"

	// sets up the snap.NewContainerFromDir hook from snapdir
	_ "github.com/snapcore/snapd/snap/snapdir"
)

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 {
	mustWaitMixin
	Command     string `long:"command" hidden:"yes"`
	HookName    string `long:"hook" hidden:"yes"`
	Revision    string `short:"r" default:"unset" hidden:"yes"`
	Shell       bool   `long:"shell" `
	DebugLog    bool   `long:"debug-log"`
	Positionals struct {
		SnapName SnapAndApp `hidden:"yes" required:"yes" positional-arg-name:"<NAME-OF-SNAP>.<NAME-OF-APP> [<SNAP-APP-ARG>...]"`
	} `positional-args:"yes" required:"yes" hidden:"yes"`

	// 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{
				mustWaitMixin: mustWaitMixin{
					noProgress: true,
					skipAbort:  true,
				},
			}
		}, 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"),
			// TRANSLATORS: This should not start with a lowercase letter.
			"debug-log":  i18n.G("Enable debug logging during early snap startup phases"),
			"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
}

const defaultSystemKeyRetryCount = 12

var getSystemKeyRetryCount = func() int {
	if timeoutEnv := os.Getenv("SNAPD_DEBUG_SYSTEM_KEY_RETRY"); timeoutEnv != "" {
		if i, err := strconv.Atoi(timeoutEnv); err == nil {
			return i
		}
	}

	return defaultSystemKeyRetryCount
}

func maybeCheckSystemKeyMismatch(cli *client.Client) (changeID string, err error) {
	extraData := interfaces.SystemKeyExtraData{
		AppArmorPrompting: features.AppArmorPrompting.IsEnabled(),
	}

	// check if the security profiles key has changed, if so, we need
	// to wait for snapd to re-generate all profiles
	mismatch, my, err := interfaces.SystemKeyMismatch(extraData)
	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, default retry count is 12,
	// gives 60s of timeout
	retryCount := getSystemKeyRetryCount()

	logger.Debugf("system key mismatch detected, waiting for snapd to start responding...")
	logger.Debugf("my system key: %v", my)

	for i := 0; i < retryCount; i++ {
		// refreshes the state of snapd maintenance internally
		act, err := cli.SystemKeyMismatchAdvice(my)
		if err == nil {
			// we have an explicit response from snapd
			switch act.SuggestedAction {
			case client.MismatchActionProceed:
				// we're good to go
				logger.Debugf("continue execution")
				return "", nil
			case client.MismatchActionWaitForChange:
				// snapd sent us an ID of a change to wait for
				logger.Debugf("snapd started a regenerate profiles change with ID: %v", act.ChangeID)
				return act.ChangeID, nil
			default:
				return "", fmt.Errorf("internal error: unexpected advice: %q", act.SuggestedAction)
			}
		}

		// an error, which could be anything from socket being down, to
		// relevant method, or our system-key version not being supported by the
		// API if we're talking to an older snapd, or snapd is simply updating
		restarting := false
		if maintErr, ok := cli.Maintenance().(*client.Error); ok && maintErr.Kind == client.ErrorKindDaemonRestart {
			restarting = true
		}

		var rspError *client.Error
		if !restarting && errors.Is(err, client.ErrMismatchAdviceUnsupported) {
			// snapd does not support system-key mismatch resolution, but it is
			// responding, so we either woke it up or it was already running. In
			// the context of home directories on NFS-like mounts, if we woke up
			// snapd, the profiles would have regenerated and thus would account
			// for our network mounted home. If snapd was already running when
			// we poked it, and the security profiles may or may not be up to
			// date, but since we're misisng an API there is no way for us to
			// tell snapd to do the update
			logger.Debugf("system-key mismatch advice not supported by snapd")
			return "", nil
		} else if errors.As(err, &rspError) {
			// actual response error
			if !restarting || rspError.Kind != client.ErrorKindSystemKeyVersionUnsupported {
				// we may reach here if snapd is still seeding, in which case we
				// let the app execution continue, but the app may not function
				// properly if the sandbox hasn't been set up completely
				logger.Debugf("ignoring system-key mismatch error: %v", err)
				return "", nil
			}

			// edge case when the system-key version we sent is not
			// supported by snapd, but the daemon appears to be restarting,
			// hopefully as part of a refresh, and so it makes sense to try again
			logger.Debugf("system-key version unsupported by snapd, but daemon is restarting")
		} else {
			logger.Debugf("cannot obtain system-key mismatch action: %v", err)
		}
		// TODO: use ticker
		// sleep a little bit for good measure
		<-timeAfter(1 * time.Second)
	}

	return "", fmt.Errorf("timeout waiting for snap system profiles to get updated")
}

func (x *cmdRun) Usage() string {
	return "[run-OPTIONS]"
}

func (x *cmdRun) Execute(args []string) error {
	snapApp := x.Positionals.SnapName.FullName()
	if len(snapApp) == 0 {
		if len(args) == 0 {
			return errors.New(i18n.G("need the application to run as argument 1"))
		}
		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 errors.New(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 x.Gdb {
		return errors.New("--gdb is no longer supported: use --gdbserver option instead")
	}

	logger.StartupStageTimestamp("start")

	regenProfilesChangeID, err := maybeCheckSystemKeyMismatch(x.client)
	if err != nil {
		return err
	}

	if regenProfilesChangeID != "" {
		logger.Debugf("waiting for security profiles regeneration change: %v", regenProfilesChangeID)
		if _, err := x.wait(regenProfilesChangeID); err != nil {
			logger.Debugf("profile regeneration change failed: %v", 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, opts *dirs.SnapDirOptions) error {
	// 'current' symlink for user data (SNAP_USER_DATA)
	userData := info.UserDataDir(usr.HomeDir, opts)
	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, opts *dirs.SnapDirOptions) error {
	if opts == nil {
		opts = &dirs.SnapDirOptions{}
	}

	// 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)
	}

	snapDir := snap.SnapDir(usr.HomeDir, opts)
	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, opts)
	instanceCommonUserData := info.UserCommonDataDir(usr.HomeDir, opts)
	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(), opts)
		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, opts); err != nil {
		return err
	}

	return maybeRestoreSecurityContext(usr, opts)
}

// maybeRestoreSecurityContext attempts to restore security context of ~/snap on
// systems where it's applicable
func maybeRestoreSecurityContext(usr *user.User, opts *dirs.SnapDirOptions) error {
	snapUserHome := snap.SnapDir(usr.HomeDir, opts)
	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 {
		switch {
		case opt == "--raw":
			raw = true
			continue

		case opt == "--output" || opt == "-o" ||
			strings.HasPrefix(opt, "--output=") ||
			strings.HasPrefix(opt, "-o="):
			// the user may have redirected strace output to a file,
			// in which case we cannot filter out
			// strace-confine/strace-exec call chain
			raw = true
		}

		opts = append(opts, opt)
	}
	return opts, raw, nil
}

// checkSnapRunInhibitionConflict detects if snap refreshed/removed was started
// while not holding the inhibition hint file lock.
//
// For context, on snap first install, the inhibition hint lock file is not created
// so we cannot hold it. It is created after the first refresh/remove. This allows
// for a window where we don't hold the lock before the tracking cgroup is created
// where a snap refresh/removal could start.
func checkSnapRunInhibitionConflict(app *snap.AppInfo) error {
	// Remove hint check takes precedence because we want to exit early
	snapName := app.Snap.InstanceName()
	hint, _, err := runinhibit.IsLocked(snapName, nil)
	if err != nil {
		return err
	}
	if hint == runinhibit.HintInhibitedForRemove {
		return fmt.Errorf(i18n.G("cannot run %q, snap is being removed"), snapName)
	}

	if !features.RefreshAppAwareness.IsEnabled() || app.IsService() {
		// Skip check
		return nil
	}

	// We started without a hint lock file, if it exists now this means that:
	// - There is an ongoing refresh
	// - Or, A refresh was started and finished
	// Let's retry to avoid either existing with an error due to missing current
	// symlink or worse starting with the wrong revision.
	if osutil.FileExists(runinhibit.HintFile(snapName)) {
		// errSnapRefreshConflict should trigger a retry
		return errSnapRefreshConflict
	}

	return nil
}

func (x *cmdRun) snapRunApp(snapApp string, args []string) error {
	if x.DebugLog {
		os.Setenv("SNAPD_DEBUG", "1")
		logger.Debugf("enabled debug logging of early snap startup")
	}
	snapName, appName := snap.SplitSnapApp(snapApp)

	var retryCnt int
	for {
		if retryCnt > 1 {
			// This should never happen, but it is better to fail instead
			// of retrying forever.
			return fmt.Errorf("race condition detected, snap-run can only retry once")
		}

		info, app, hintFlock, err := waitWhileInhibited(context.Background(), x.client, snapName, appName)
		if errors.Is(err, errOngoingSnapRefresh) {
			return fmt.Errorf(i18n.G("cannot run %q, snap is being refreshed"), snapName)
		}
		if errors.Is(err, errInhibitedForRemove) {
			return fmt.Errorf(i18n.G("cannot run %q, snap is being removed"), snapName)
		}
		if errors.Is(err, errSnapRefreshConflict) {
			// Possible race condition detected, let's retry.

			// This will not retry infinitely because this can only be caused by
			// a missing inhibition hint initially which is now created due to a
			// refresh.
			retryCnt++
			logger.Debugf("retry due to possible snap refresh conflict detected")
			continue
		}
		if err != nil {
			return err
		}

		closeFlockOrCheckConflict := func() error {
			// Unlocking the hint file needs to run inside the transient cgroup
			// created such that:
			// - For refresh, snap refresh will get blocked after we release the lock.
			// - For removal, created transient cgroup can be detected by the remove change
			//   and process will be killed after we release the lock.
			if hintFlock != nil {
				// It is okay to release the lock here (beforeExec) because snapd unless forced
				// will not inhibit the snap and do a refresh anymore because it detects app
				// processes are running for via the established transient cgroup cgroup.

				// Note: We cannot rely on O_CLOEXEC to unlock because might run in
				// fork + exec mode like when running under gdb or strace.
				hintFlock.Close()
				return nil
			}

			return checkSnapRunInhibitionConflict(app)
		}

		runner := newAppRunnable(info, app)

		err = x.runSnapConfine(info, runner, closeFlockOrCheckConflict, args)
		if errors.Is(err, errSnapRefreshConflict) {
			// Possible race condition detected, let's retry.
			//
			// This will not retry infinitely because this can only be caused by
			// a missing inhibition hint initially which is now created due to a
			// refresh.
			retryCnt++
			logger.Debugf("retry due to possible snap refresh conflict detected")
			continue
		}
		if err != nil {
			// Make sure we release the lock in case runSnapConfine fails before
			// closing hint lock file, it is fine if we double close.
			if hintFlock != nil {
				hintFlock.Close()
			}
			return err
		}

		return nil
	}
}

func (x *cmdRun) snapRunHook(snapTarget string) error {
	snapInstance, componentName := snap.SplitSnapComponentInstanceName(snapTarget)

	revision, err := snap.ParseRevision(x.Revision)
	if err != nil {
		return err
	}

	info, err := getSnapInfo(snapInstance, revision)
	if err != nil {
		return err
	}

	var (
		hook      *snap.HookInfo
		component *snap.ComponentInfo
	)
	if componentName == "" {
		hook = info.Hooks[x.HookName]
	} else {
		component, err = snap.ReadCurrentComponentInfo(componentName, info)
		if err != nil {
			return err
		}
		hook = component.Hooks[x.HookName]
	}

	if hook == nil {
		return fmt.Errorf(i18n.G("cannot find hook %q in %q"), x.HookName, snapTarget)
	}

	// compoment may be nil here, meaning that this is a hook for the snap
	// itself, not a component hook
	runner := newHookRunnable(info, hook, component)

	return x.runSnapConfine(info, runner, nil, 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
}

/* Kerberos tickets live in /tmp, or in the place specified by KRB5CCNAME
 * environment variable. However, in snaps /tmp is private.
 * So rewire the environment variable to point to /var/lib/snapd/hostfs/tmp,
 * in case the variable is unset or resolves tickets to /tmp/krb5cc*.
 *
 * Snaps that want to read Kerberos tickets must then connect to the
 * kerberos-tickets interface to have access to the tickets.
 */
func exposeKerberosTickets(info *snap.Info) (string, error) {
	krb5EnvVar := osGetenv("KRB5CCNAME")
	if len(krb5EnvVar) == 0 {
		return "", nil
	}
	if strings.HasPrefix(krb5EnvVar, "FILE:") {
		path := strings.TrimPrefix(krb5EnvVar, "FILE:")
		path = filepath.Clean(path)
		if filepath.Dir(path) == "/tmp" && strings.HasPrefix(path, "/tmp/krb5cc") {
			// with vanilla config this ends up returning:
			// FILE:/var/lib/snapd/hostfs/tmp/krb5cc_test
			return "FILE:" + filepath.Join("/var/lib/snapd/hostfs/", path), nil
		}
	}
	return "", fmt.Errorf("Unsupported KRB5CCNAME: %s", krb5EnvVar)
}

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(runner runnable) 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.
	plugs := runner.Plugs()

	plugsDesktop := false
	for _, plug := range plugs {
		if plug.Interface == "desktop" {
			plugsDesktop = true
			break
		}
	}
	if !plugsDesktop {
		return nil
	}

	documentPortal := &portal.Document{}
	expectedMountPoint, err := documentPortal.GetDefaultMountPoint()
	if err != nil {
		return err
	}

	// If $XDG_RUNTIME_DIR/doc appears to be a mount point, assume
	// that the document portal is up and running.
	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.
	xdgRuntimeDir, err := documentPortal.GetUserXdgRuntimeDir()
	if err != nil {
		return err
	}

	portalsUnavailableFile := filepath.Join(xdgRuntimeDir, ".portals-unavailable")
	if osutil.FileExists(portalsUnavailableFile) {
		return nil
	}

	actualMountPoint, err := documentPortal.GetMountPoint()
	if 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 = os.WriteFile(portalsUnavailableFile, []byte(""), 0644); err != nil {
				logger.Noticef("WARNING: cannot write file at %s: %s", portalsUnavailableFile, err)
			}
			return nil
		}
		return err
	}

	// Quick check to make sure the document portal is exposed
	// where we think it is.
	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.WUNTRACED, nil)
	if err != nil {
		return err
	}
	if status.StopSignal() != syscall.SIGSTOP {
		return fmt.Errorf("child terminated prematurely with unexpected status: %v", status)
	}

	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, gdbServerWelcomeFmt, addr)
	// note that only gdbserver needs to run as root, the application
	// keeps running as the user
	gdbSrvCmd := exec.Command("sudo", "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) runCmdWithTraceExec(origCmd []string, envForExec envForExecFunc) error {
	straceShim, err := snapdtool.InternalToolPath("snap-strace-shim")
	if err != nil {
		return fmt.Errorf("cannot locate snap-strace-shim: %w", err)
	}

	// setup private tmp dir with strace fifo
	straceTmp, err := os.MkdirTemp("", "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()

	appCmd := exec.Command(straceShim, origCmd...)
	appCmd.Stdin = os.Stdin
	appCmd.Stdout = os.Stdout
	appCmd.Stderr = os.Stderr
	appCmd.Env = envForExec(nil)
	if err := appCmd.Start(); err != nil {
		return err
	}

	if err := awaitTraceHelperCheckpoint(appCmd.Process.Pid); err != nil {
		return err
	}

	logger.Debugf("child stopped, ready to be traced")
	childStopped := true

	// read strace data from fifo async
	var slg *strace.ExecveTiming
	var traceErr error
	doneCh := make(chan bool, 1)
	go func() {
		// FIXME: make this configurable?
		nSlowest := 10
		slg, traceErr = strace.TraceExecveTimings(straceLog, nSlowest, func() {
			logger.Debug("strace attached to child process")
			if childStopped {
				childStopped = false
				if err := appCmd.Process.Signal(syscall.SIGCONT); err != nil {
					fmt.Fprintf(Stderr, "cannot signal child to continue: %v\n", err)
				}
			}
		})
		close(doneCh)
	}()

	straceCmd, err := strace.TraceExecCommandForPid(appCmd.Process.Pid, straceLog)
	if err != nil {
		return err
	}
	logger.Debugf("trace exec command: %v", straceCmd.Args)
	straceCmd.Stdin = Stdin
	straceCmd.Stdout = Stdout
	straceCmd.Stderr = Stderr

	// before starting strace in blocking mode await app to exit, so that we do
	// not end up with zombies
	appCmdErrC := make(chan error, 1)
	go func() {
		defer close(appCmdErrC)
		appCmdErrC <- appCmd.Wait()
	}()

	straceCmdErr := straceCmd.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 traceErr == nil {
		slg.Display(Stderr)
	} else {
		logger.Noticef("cannot extract runtime data: %v", traceErr)
	}

	// see comment at a similar place in runCmdUnderStrace()
	appKillSent, killErr := maybeKillTracedApp(appCmd)
	if killErr != nil {
		fmt.Fprintf(Stderr, "error: cannot kill child application: %v\n", killErr)
	}
	appCmdErr := <-appCmdErrC

	return strutil.JoinErrors(straceCmdErr, maybeIgnoreTracedAppKillError(appCmdErr, appKillSent))
}

// maybeIgnoreTracedAppKillError processes the error from a snap application that may
// have been forcefully killed during trace as a result of strace process
// finishing prematurely.
func maybeIgnoreTracedAppKillError(err error, killed bool) error {
	if !killed {
		return err
	}

	var exitErr *exec.ExitError
	if !errors.As(err, &exitErr) {
		return err
	}

	ws, ok := exitErr.ProcessState.Sys().(syscall.WaitStatus)
	if !ok {
		return err
	}

	if ws.Signal() != syscall.SIGKILL {
		return err
	}

	return nil
}

func maybeKillTracedApp(appCmd *exec.Cmd) (killSent bool, err error) {
	alreadyDead := func(killErr error) bool {
		return errors.Is(killErr, syscall.ESRCH) || errors.Is(killErr, os.ErrProcessDone)
	}

	err = appCmd.Process.Kill()
	if err != nil && !alreadyDead(err) {
		return false, fmt.Errorf("cannot kill child application: %w", err)
	}

	// kill was sent if the target process was found
	return !alreadyDead(err), nil
}

func awaitTraceHelperCheckpoint(pid int) error {
	// wait for the child process executing tracing helper to raise SIGSTOP
	// signalling readiness to attach strace
	var status syscall.WaitStatus
	_, err := syscall.Wait4(pid, &status, syscall.WUNTRACED, nil)
	if err != nil {
		return err
	}
	if status.StopSignal() != syscall.SIGSTOP {
		return fmt.Errorf("child terminated prematurely with unexpected status: %v", status)
	}

	return nil
}

func (x *cmdRun) runCmdUnderStrace(origCmd []string, envForExec envForExecFunc) error {
	extraStraceOpts, raw, err := x.straceOpts()
	if err != nil {
		return err
	}

	straceShim, err := snapdtool.InternalToolPath("snap-strace-shim")
	if err != nil {
		return fmt.Errorf("cannot locate snap-strace-shim: %w", err)
	}

	appCmd := exec.Command(straceShim, origCmd...)
	appCmd.Stdin = os.Stdin
	appCmd.Stdout = os.Stdout
	appCmd.Stderr = os.Stderr
	appCmd.Env = envForExec(nil)
	if err := appCmd.Start(); err != nil {
		return err
	}

	if err := awaitTraceHelperCheckpoint(appCmd.Process.Pid); err != nil {
		return err
	}

	childContinue := func() error {
		if err := appCmd.Process.Signal(syscall.SIGCONT); err != nil {
			return fmt.Errorf("cannot signal child to continue: %w", err)
		}
		return nil
	}

	logger.Debugf("child stopped, ready to be traced")

	straceCmd, err := strace.CommandWithTraceePid(appCmd.Process.Pid, extraStraceOpts)
	if err != nil {
		return err
	}
	logger.Debugf("strace command: %v", straceCmd.Args)

	straceCmd.Stdin = Stdin
	stderr, err := straceCmd.StderrPipe()
	if err != nil {
		return err
	}

	// note hijacking stdout, means it is no longer a tty and programs
	// expecting stdout to be on a terminal (eg. bash) may misbehave at this
	// point
	stdout, err := straceCmd.StdoutPipe()
	if err != nil {
		return err
	}

	errWrapIf := func(msg string, err error) error {
		if err != nil {
			return fmt.Errorf(msg, err)
		}
		return err
	}

	filterDone := make(chan error, 1)
	stdoutProxyDone := make(chan error, 1)
	go func() {
		defer close(filterDone)

		r := bufio.NewReader(stderr)

		// The first thing we want to see is strace attaching to the child
		for {
			s, err := r.ReadString('\n')
			if err != nil {
				filterDone <- errWrapIf("cannot read while waiting for attach: %w", err)
				return
			}

			fmt.Fprint(Stderr, s)

			if strace.StraceAttachedStart(s) {
				logger.Debug("strace attached to child process")
				if err := childContinue(); err != nil {
					fmt.Fprintf(Stderr, "cannot signal child to continue: %v\n", err)
				}
				break
			}
		}

		if !raw {
			// 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 {
					filterDone <- errWrapIf("cannot read while waiting for exec(): %w", err)
					return
				}

				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 interesting 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 {
					filterDone <- errWrapIf("cannot read while waiting for exec() to snap-confine: %w", err)
					return
				}
				// 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
				}
			}
		}
		_, err := io.Copy(Stderr, r)
		// strace runs independently, so depending on when the goroutine happens
		// to observe that we may get some errors which are expected
		if errors.Is(err, io.EOF) || errors.Is(err, os.ErrClosed) {
			err = nil
		}

		filterDone <- errWrapIf("cannot read while forwarding strace stderr: %w", err)
	}()

	go func() {
		defer close(stdoutProxyDone)
		_, err := io.Copy(Stdout, stdout)
		// strace runs independently, so depending on when the goroutine happens
		// to observe that we may get some errors which are expected
		if errors.Is(err, io.EOF) || errors.Is(err, os.ErrClosed) {
			err = nil
		}

		stdoutProxyDone <- errWrapIf("cannot read while forwarding strace stdout: %w", err)
	}()

	if err := straceCmd.Start(); err != nil {
		return err
	}

	appCmdErrC := make(chan error, 1)
	go func() {
		defer close(appCmdErrC)
		appCmdErrC <- appCmd.Wait()
	}()

	// pipes before calling Wait() on strace process will close stderr/stdout
	// pipes, so all the reads must be done before that
	filterErr := <-filterDone
	proxyErr := <-stdoutProxyDone

	// strace exits when the app finishes, however it may exit early if
	// arguments weren't right or it failed, in which case we need to clean up
	// the snap application process by sending KILL
	straceCmdErr := straceCmd.Wait()

	appKillSent, killErr := maybeKillTracedApp(appCmd)
	if killErr != nil {
		fmt.Fprintf(Stderr, "error: cannot kill child application: %v\n", killErr)
	}

	appCmdErr := <-appCmdErrC

	return strutil.JoinErrors(straceCmdErr, maybeIgnoreTracedAppKillError(appCmdErr, appKillSent), filterErr, proxyErr)
}

func newHookRunnable(info *snap.Info, hook *snap.HookInfo, component *snap.ComponentInfo) runnable {
	return runnable{
		info:      info,
		component: component,
		hook:      hook,
	}
}

func newAppRunnable(info *snap.Info, app *snap.AppInfo) runnable {
	return runnable{
		info: info,
		app:  app,
	}
}

// runnable bundles together the potential things that we could be running. A
// few accessor methods are provided that delegate the request to the
// appropriate field, depending on what we are running.
type runnable struct {
	hook      *snap.HookInfo
	component *snap.ComponentInfo
	app       *snap.AppInfo
	info      *snap.Info
}

// SecurityTag returns the security tag for the thing being run. The tag could
// come from a snap hook, a component hook, or a snap app.
func (r *runnable) SecurityTag() string {
	if r.hook != nil {
		return r.hook.SecurityTag()
	}
	return r.app.SecurityTag()
}

// Target returns the string identifier of the thing that should be run. This
// could either be a component ref, a snap ref, or a snap ref with a specific
// app.
func (r *runnable) Target() string {
	if r.component != nil {
		return snap.SnapComponentName(r.info.InstanceName(), r.component.Component.ComponentName)
	}

	if r.hook != nil {
		return r.info.InstanceName()
	}

	return fmt.Sprintf("%s.%s", r.info.InstanceName(), r.app.Name)
}

// Plugs returns the plugs for the thing being run. The plugs could come from a
// snap hook, a component hook, or a snap app.
func (r *runnable) Plugs() map[string]*snap.PlugInfo {
	if r.hook != nil {
		return r.hook.Plugs
	}
	return r.app.Plugs
}

// IsHook returns true if the runnable is a hook. r.Hook() will not return nil
// if this is true.
func (r *runnable) IsHook() bool {
	return r.hook != nil
}

// Hook returns the hook that is going to be run, if there is one. Will be nil
// if running an app.
func (r *runnable) Hook() *snap.HookInfo {
	return r.hook
}

// Hook returns the hook that contains the thing to be run, if there is one.
// Currently, this will only be present when running a component hook.
func (r *runnable) Component() *snap.ComponentInfo {
	return r.component
}

// App returns the app that is going to be run, if there is one. Will be nil if
// running a hook or component hook.
func (r *runnable) App() *snap.AppInfo {
	return r.app
}

// Validate checks that the runnable is in a valid state. This is used to catch
// programmer errors.
func (r *runnable) Validate() error {
	if r.hook != nil && r.app != nil {
		return fmt.Errorf("internal error: hook and app cannot coexist in a runnable")
	}

	if r.component != nil && r.app != nil {
		return fmt.Errorf("internal error: component and app cannot coexist in a runnable")
	}

	return nil
}

func makeStdStreamsForJournal(app *snap.AppInfo, namespace string) (stdout, stderr *os.File) {
	stdout, err := systemd.NewJournalStreamFile(systemd.JournalStreamFileParams{
		Namespace:   namespace,
		Identifier:  app.Name,
		UnitName:    app.ServiceName(),
		Priority:    syslog.LOG_DAEMON | syslog.LOG_INFO,
		LevelPrefix: true,
	})
	if err != nil {
		logger.Noticef("cannot connect to journal for stdout: %s", err)
	}
	stderr, err = systemd.NewJournalStreamFile(systemd.JournalStreamFileParams{
		Namespace:   namespace,
		Identifier:  app.Name,
		UnitName:    app.ServiceName(),
		Priority:    syslog.LOG_DAEMON | syslog.LOG_WARNING,
		LevelPrefix: true,
	})
	if err != nil {
		logger.Noticef("cannot connect to journal for stderr: %s", err)
	}
	return stdout, stderr
}

func (x *cmdRun) runSnapConfine(info *snap.Info, runner runnable, beforeExec func() error, args []string) error {
	needsClassic := info.NeedsClassic()

	// check for programmer error, should never happen
	if err := runner.Validate(); err != nil {
		return err
	}

	snapConfine, err := snapdHelperPath("snap-confine")
	if err != nil {
		return err
	}
	if !osutil.FileExists(snapConfine) {
		if runner.IsHook() {
			logger.Noticef("WARNING: skipping running hook %q of %q: missing snap-confine", runner.Hook().Name, runner.Target())
			return nil
		}
		return errors.New(i18n.G("missing snap-confine: try updating your core/snapd package"))
	}

	logger.Debugf("executing snap-confine from %s", snapConfine)

	opts, err := getSnapDirOptions(info.InstanceName())
	if err != nil {
		return fmt.Errorf("cannot get snap dir options: %w", err)
	}

	if err := createUserDataDirs(info, opts); 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)
	}

	// For strictly confined snaps, Kerberos tickets stored in /tmp are exposed
	// through /var/lib/snapd/hostfs. Snaps running under classic confinement
	// can access /tmp without any additional help.
	var krb5ccnamePath string
	if !needsClassic {
		krb5ccnamePath, err = exposeKerberosTickets(info)
		if err != nil {
			logger.Noticef("WARNING: will not expose Kerberos tickets' path: %s", err)
		}
	}

	if err := activateXdgDocumentPortal(runner); err != nil {
		logger.Noticef("WARNING: cannot start document portal: %s", err)
	}

	cmd := []string{snapConfine}
	if 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 runner.IsHook() {
					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)
			}
		}
	}

	securityTag := runner.SecurityTag()
	cmd = append(cmd, securityTag)

	// when under confinement, snap-exec is run from 'core' snap rootfs
	snapExecPath := filepath.Join(dirs.CoreLibExecDir, "snap-exec")

	if 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.useGdbserver() {
		cmd = append(cmd, "--command=gdbserver")
	}
	if x.Command != "" {
		cmd = append(cmd, "--command="+x.Command)
	}

	if runner.IsHook() {
		cmd = append(cmd, "--hook="+runner.Hook().Name)
	}

	// snap-exec is POSIXly-- options must come before positionals.
	cmd = append(cmd, runner.Target())
	cmd = append(cmd, args...)

	env, err := osutil.OSEnvironment()
	if err != nil {
		return err
	}

	snapenv.ExtendEnvForRun(env, info, runner.Component(), opts)

	if len(xauthPath) > 0 {
		// Environment is not nil here because it comes from
		// osutil.OSEnvironment and that guarantees this
		// property.
		env["XAUTHORITY"] = xauthPath
	}

	// We have a new location for the ticket, update the environment variable.
	if len(krb5ccnamePath) > 0 {
		env["KRB5CCNAME"] = krb5ccnamePath
	}

	// Guarantee that XDG_RUNTIME_DIR does exist before launching the snap.
	// XDG_RUNTIME_DIR has already been updated to point to a snap specific directory
	// under the original $XDG_RUNTIME_DIR.
	if xdg_runtime_dir, ok := env["XDG_RUNTIME_DIR"]; ok {
		if err = os.Mkdir(xdg_runtime_dir, 0700); err != nil {
			// The directory may already exist if snap was ran before, or one of the
			// parent path components may not exist if /run/user/<uid>, which we
			// expect as the path prefix, was not created during session setup.
			if !errors.Is(err, os.ErrExist) && !errors.Is(err, os.ErrNotExist) {
				return fmt.Errorf("cannot create XDG_RUNTIME_DIR folder %q: %w", xdg_runtime_dir, err)
			}
		}
	}

	// 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 !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/
	needsTracking := true

	if app := runner.App(); 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
		// however it is still possible that the app (which is a
		// service) was invoked by the user, so it may be running inside
		// a user's scope cgroup, in which case separate tracking group
		// needs to be established
		if err := cgroupConfirmSystemdServiceTracking(securityTag); err != nil {
			if err == cgroup.ErrCannotTrackProcess {
				// we are not being tracked in a service cgroup
				// after all, go ahead and create a transient
				// scope
				needsTracking = true
				logger.Debugf("service app not tracked by systemd")
			} else {
				return err
			}
		}

		// If a journal namespace is supplied for the service, then we reopen stdout/stderr
		// connected to that journal namespace instead of the main journal. Since we are not
		// using systemd's LogNamespace= directly, we must do this ourselves.
		if lns := os.Getenv("SNAPD_LOG_NAMESPACE"); lns != "" {
			stdout, stderr := makeStdStreamsForJournal(app, lns)
			if stdout != nil {
				defer stdout.Close()
				if err := osutil.DupFD(stdout.Fd(), uintptr(syscall.Stdout)); err != nil {
					logger.Noticef("cannot duplicate stdout for connection: %v", err)
				}
			}
			if stderr != nil {
				defer stderr.Close()
				if err := osutil.DupFD(stderr.Fd(), uintptr(syscall.Stderr)); err != nil {
					logger.Noticef("cannot duplicate stderr for connection: %v", err)
				}
			}

			// Clear out the LOG_NAMESPACE variable, no reason to leak this to the process
			// itself.
			os.Unsetenv("SNAPD_LOG_NAMESPACE")
		}
	}
	// Allow using the session bus for all apps but not for hooks.
	allowSessionBus := !runner.IsHook()
	// Track, or confirm existing tracking from systemd.
	if err := cgroupConfirmSystemdAppTracking(securityTag); err != nil {
		if err != cgroup.ErrCannotTrackProcess {
			return err
		}
	} else {
		// A transient scope was already created in a previous attempt. Skip creating
		// another transient scope to avoid leaking cgroups.
		//
		// Note: This could happen if beforeExec fails and triggers a retry.
		needsTracking = false
	}
	if needsTracking {
		opts := &cgroup.TrackingOptions{AllowSessionBus: allowSessionBus}
		if err = cgroupCreateTransientScopeForTracking(securityTag, opts); err != nil {
			if err != cgroup.ErrCannotTrackProcess {
				return err
			}
			// 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 beforeExec != nil {
		if err := beforeExec(); err != nil {
			return err
		}
	}

	logger.StartupStageTimestamp("snap to snap-confine")
	if x.TraceExec {
		return x.runCmdWithTraceExec(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))
	}
}

func getSnapDirOptions(snap string) (*dirs.SnapDirOptions, error) {
	var opts dirs.SnapDirOptions

	data, err := os.ReadFile(filepath.Join(dirs.SnapSeqDir, snap+".json"))
	if errors.Is(err, os.ErrNotExist) {
		return &opts, nil
	} else if err != nil {
		return nil, err
	}

	var seq struct {
		MigratedToHiddenDir   bool `json:"migrated-hidden"`
		MigratedToExposedHome bool `json:"migrated-exposed-home"`
	}
	if err := json.Unmarshal(data, &seq); err != nil {
		return nil, err
	}

	opts.HiddenSnapDataDir = seq.MigratedToHiddenDir
	opts.MigratedToExposedHome = seq.MigratedToExposedHome

	return &opts, nil
}

var cgroupCreateTransientScopeForTracking = cgroup.CreateTransientScopeForTracking
var cgroupConfirmSystemdServiceTracking = cgroup.ConfirmSystemdServiceTracking
var cgroupConfirmSystemdAppTracking = cgroup.ConfirmSystemdAppTracking
