Permalink
Fetching contributors…
Cannot retrieve contributors at this time
501 lines (426 sloc) 14.5 KB
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2014-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 <http://www.gnu.org/licenses/>.
*
*/
package main
import (
"fmt"
"io"
"os"
"os/user"
"path/filepath"
"regexp"
"strconv"
"strings"
"syscall"
"github.com/jessevdk/go-flags"
"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/i18n"
"github.com/snapcore/snapd/logger"
"github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/snap/snapenv"
"github.com/snapcore/snapd/x11"
)
var (
syscallExec = syscall.Exec
userCurrent = user.Current
osGetenv = os.Getenv
)
type cmdRun struct {
Command string `long:"command" hidden:"yes"`
Hook string `long:"hook" hidden:"yes"`
Revision string `short:"r" default:"unset" hidden:"yes"`
Shell bool `long:"shell" `
}
func init() {
addCommand("run",
i18n.G("Run the given snap command"),
i18n.G("Run the given snap command with the right confinement and environment"),
func() flags.Commander {
return &cmdRun{}
}, map[string]string{
"command": i18n.G("Alternative command to run"),
"hook": i18n.G("Hook to run"),
"r": i18n.G("Use a specific snap revision when running hook"),
"shell": i18n.G("Run a shell instead of the command (useful for debugging)"),
}, nil)
}
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
if x.Hook != "" && x.Command != "" {
return fmt.Errorf(i18n.G("cannot use --hook and --command together"))
}
if x.Revision != "unset" && x.Revision != "" && x.Hook == "" {
return fmt.Errorf(i18n.G("-r can only be used with --hook"))
}
if x.Hook != "" && 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.Hook, strings.Join(args, " "))
}
// Now actually handle the dispatching
if x.Hook != "" {
return snapRunHook(snapApp, x.Revision, x.Hook)
}
// pass shell as a special command to snap-exec
if x.Shell {
x.Command = "shell"
}
if x.Command == "complete" {
snapApp, args = antialias(snapApp, args)
}
return snapRunApp(snapApp, x.Command, 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) (*snap.Info, error) {
if revision.Unset() {
curFn := filepath.Join(dirs.SnapMountDir, snapName, "current")
realFn, err := os.Readlink(curFn)
if err != nil {
return nil, fmt.Errorf("cannot find current revision for snap %s: %s", snapName, err)
}
rev := filepath.Base(realFn)
revision, err = snap.ParseRevision(rev)
if err != nil {
return nil, fmt.Errorf("cannot read revision %s: %s", rev, err)
}
}
info, err := snap.ReadInfo(snapName, &snap.SideInfo{
Revision: revision,
})
if err != nil {
return nil, err
}
return info, nil
}
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 {
usr, err := userCurrent()
if err != nil {
return fmt.Errorf(i18n.G("cannot get the current user: %v"), err)
}
// see snapenv.User
userData := info.UserDataDir(usr.HomeDir)
commonUserData := info.UserCommonDataDir(usr.HomeDir)
for _, d := range []string{userData, commonUserData} {
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)
}
}
return createOrUpdateUserDataSymlink(info, usr)
}
func snapRunApp(snapApp, command 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 runSnapConfine(info, app.SecurityTag(), snapApp, command, "", args)
}
func snapRunHook(snapName, snapRevision, hookName string) error {
revision, err := snap.ParseRevision(snapRevision)
if err != nil {
return err
}
info, err := getSnapInfo(snapName, revision)
if err != nil {
return err
}
hook := info.Hooks[hookName]
if hook == nil {
return fmt.Errorf(i18n.G("cannot find hook %q in %q"), hookName, snapName)
}
return runSnapConfine(info, hook.SecurityTag(), snapName, "", hook.Name, nil)
}
var osReadlink = os.Readlink
func isReexeced() bool {
exe, err := osReadlink("/proc/self/exe")
if err != nil {
logger.Noticef("cannot read /proc/self/exe: %v", err)
return false
}
return strings.HasPrefix(exe, dirs.SnapMountDir)
}
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 runSnapConfine(info *snap.Info, securityTag, snapApp, command, hook string, args []string) error {
snapConfine := filepath.Join(dirs.DistroLibExecDir, "snap-confine")
// if we re-exec, we must run the snap-confine from the core snap
// as well, if they get out of sync, havoc will happen
if isReexeced() {
// run snap-confine from the core snap. that will work because
// snap-confine on the core snap is mostly statically linked
// (except libudev and libc)
snapConfine = filepath.Join(dirs.SnapMountDir, "core/current", dirs.CoreLibExecDir, "snap-confine")
}
if !osutil.FileExists(snapConfine) {
if hook != "" {
logger.Noticef("WARNING: skipping running hook %q of snap %q: missing snap-confine", hook, info.Name())
return nil
}
return fmt.Errorf(i18n.G("missing snap-confine: try updating your 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)
}
cmd := []string{snapConfine}
if info.NeedsClassic() {
cmd = append(cmd, "--classic")
}
if info.Base != "" {
cmd = append(cmd, "--base", info.Base)
}
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
if isReexeced() {
// same rule as when choosing the location of snap-confine
snapExecPath = filepath.Join(dirs.SnapMountDir, "core/current",
dirs.CoreLibExecDir, "snap-exec")
} else {
// there is no mount namespace where 'core' is the
// rootfs, hence we need to use distro's snap-exec
snapExecPath = filepath.Join(dirs.DistroLibExecDir, "snap-exec")
}
}
cmd = append(cmd, snapExecPath)
if command != "" {
cmd = append(cmd, "--command="+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...)
extraEnv := make(map[string]string)
if len(xauthPath) > 0 {
extraEnv["XAUTHORITY"] = xauthPath
}
env := snapenv.ExecEnv(info, extraEnv)
return syscallExec(cmd[0], cmd, env)
}