github.com/freetocompute/snapd@v0.0.0-20210618182524-2fb355d72fd9/cmd/snap-exec/main.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2014-2015 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package main
    21  
    22  import (
    23  	"fmt"
    24  	"os"
    25  	"path/filepath"
    26  	"strings"
    27  	"syscall"
    28  
    29  	"github.com/jessevdk/go-flags"
    30  
    31  	"github.com/snapcore/snapd/dirs"
    32  	"github.com/snapcore/snapd/osutil"
    33  	"github.com/snapcore/snapd/snap"
    34  	"github.com/snapcore/snapd/snap/snapenv"
    35  )
    36  
    37  // for the tests
    38  var syscallExec = syscall.Exec
    39  var osReadlink = os.Readlink
    40  
    41  // commandline args
    42  var opts struct {
    43  	Command string `long:"command" description:"use a different command like {stop,post-stop} from the app"`
    44  	Hook    string `long:"hook" description:"hook to run" hidden:"yes"`
    45  }
    46  
    47  func init() {
    48  	// plug/slot sanitization not used nor possible from snap-exec, make it no-op
    49  	snap.SanitizePlugsSlots = func(snapInfo *snap.Info) {}
    50  }
    51  
    52  func main() {
    53  	if err := run(); err != nil {
    54  		fmt.Fprintf(os.Stderr, "cannot snap-exec: %s\n", err)
    55  		os.Exit(1)
    56  	}
    57  }
    58  
    59  func parseArgs(args []string) (app string, appArgs []string, err error) {
    60  	parser := flags.NewParser(&opts, flags.HelpFlag|flags.PassDoubleDash|flags.PassAfterNonOption)
    61  	rest, err := parser.ParseArgs(args)
    62  	if err != nil {
    63  		return "", nil, err
    64  	}
    65  	if len(rest) == 0 {
    66  		return "", nil, fmt.Errorf("need the application to run as argument")
    67  	}
    68  
    69  	// Catch some invalid parameter combinations, provide helpful errors
    70  	if opts.Hook != "" && opts.Command != "" {
    71  		return "", nil, fmt.Errorf("cannot use --hook and --command together")
    72  	}
    73  	if opts.Hook != "" && len(rest) > 1 {
    74  		return "", nil, fmt.Errorf("too many arguments for hook %q: %s", opts.Hook, strings.Join(rest, " "))
    75  	}
    76  
    77  	return rest[0], rest[1:], nil
    78  }
    79  
    80  func run() error {
    81  	snapApp, extraArgs, err := parseArgs(os.Args[1:])
    82  	if err != nil {
    83  		return err
    84  	}
    85  
    86  	// the SNAP_REVISION is set by `snap run` - we can not (easily)
    87  	// find it in `snap-exec` because `snap-exec` is run inside the
    88  	// confinement and (generally) can not talk to snapd
    89  	revision := os.Getenv("SNAP_REVISION")
    90  
    91  	// Now actually handle the dispatching
    92  	if opts.Hook != "" {
    93  		return execHook(snapApp, revision, opts.Hook)
    94  	}
    95  
    96  	return execApp(snapApp, revision, opts.Command, extraArgs)
    97  }
    98  
    99  const defaultShell = "/bin/bash"
   100  
   101  func findCommand(app *snap.AppInfo, command string) (string, error) {
   102  	var cmd string
   103  	switch command {
   104  	case "shell":
   105  		cmd = defaultShell
   106  	case "complete":
   107  		if app.Completer != "" {
   108  			cmd = defaultShell
   109  		}
   110  	case "stop":
   111  		cmd = app.StopCommand
   112  	case "reload":
   113  		cmd = app.ReloadCommand
   114  	case "post-stop":
   115  		cmd = app.PostStopCommand
   116  	case "", "gdb", "gdbserver":
   117  		cmd = app.Command
   118  	default:
   119  		return "", fmt.Errorf("cannot use %q command", command)
   120  	}
   121  
   122  	if cmd == "" {
   123  		return "", fmt.Errorf("no %q command found for %q", command, app.Name)
   124  	}
   125  	return cmd, nil
   126  }
   127  
   128  func absoluteCommandChain(snapInfo *snap.Info, commandChain []string) []string {
   129  	chain := make([]string, 0, len(commandChain))
   130  	snapMountDir := snapInfo.MountDir()
   131  
   132  	for _, element := range commandChain {
   133  		chain = append(chain, filepath.Join(snapMountDir, element))
   134  	}
   135  
   136  	return chain
   137  }
   138  
   139  // expandEnvCmdArgs takes the string list of commandline arguments
   140  // and expands any $VAR with the given var from the env argument.
   141  func expandEnvCmdArgs(args []string, env osutil.Environment) []string {
   142  	cmdArgs := make([]string, 0, len(args))
   143  	for _, arg := range args {
   144  		maybeExpanded := os.Expand(arg, func(varName string) string {
   145  			return env[varName]
   146  		})
   147  		if maybeExpanded != "" {
   148  			cmdArgs = append(cmdArgs, maybeExpanded)
   149  		}
   150  	}
   151  	return cmdArgs
   152  }
   153  
   154  func completionHelper() (string, error) {
   155  	exe, err := osReadlink("/proc/self/exe")
   156  	if err != nil {
   157  		return "", err
   158  	}
   159  	return filepath.Join(filepath.Dir(exe), "etelpmoc.sh"), nil
   160  }
   161  
   162  func execApp(snapApp, revision, command string, args []string) error {
   163  	rev, err := snap.ParseRevision(revision)
   164  	if err != nil {
   165  		return fmt.Errorf("cannot parse revision %q: %s", revision, err)
   166  	}
   167  
   168  	snapName, appName := snap.SplitSnapApp(snapApp)
   169  	info, err := snap.ReadInfo(snapName, &snap.SideInfo{
   170  		Revision: rev,
   171  	})
   172  	if err != nil {
   173  		return fmt.Errorf("cannot read info for %q: %s", snapName, err)
   174  	}
   175  
   176  	app := info.Apps[appName]
   177  	if app == nil {
   178  		return fmt.Errorf("cannot find app %q in %q", appName, snapName)
   179  	}
   180  
   181  	cmdAndArgs, err := findCommand(app, command)
   182  	if err != nil {
   183  		return err
   184  	}
   185  
   186  	// build the environment from the yaml, translating TMPDIR and
   187  	// similar variables back from where they were hidden when
   188  	// invoking the setuid snap-confine.
   189  	env, err := osutil.OSEnvironmentUnescapeUnsafe(snapenv.PreservedUnsafePrefix)
   190  	if err != nil {
   191  		return err
   192  	}
   193  	for _, eenv := range app.EnvChain() {
   194  		env.ExtendWithExpanded(eenv)
   195  	}
   196  
   197  	// strings.Split() is ok here because we validate all app fields and the
   198  	// whitelist is pretty strict (see snap/validate.go:appContentWhitelist)
   199  	// (see also overlord/snapstate/check_snap.go's normPath)
   200  	tmpArgv := strings.Split(cmdAndArgs, " ")
   201  	cmd := tmpArgv[0]
   202  	cmdArgs := expandEnvCmdArgs(tmpArgv[1:], env)
   203  
   204  	// run the command
   205  	fullCmd := []string{filepath.Join(app.Snap.MountDir(), cmd)}
   206  	switch command {
   207  	case "shell":
   208  		fullCmd[0] = defaultShell
   209  		cmdArgs = nil
   210  	case "complete":
   211  		fullCmd[0] = defaultShell
   212  		helper, err := completionHelper()
   213  		if err != nil {
   214  			return fmt.Errorf("cannot find completion helper: %v", err)
   215  		}
   216  		cmdArgs = []string{
   217  			helper,
   218  			filepath.Join(app.Snap.MountDir(), app.Completer),
   219  		}
   220  	case "gdb":
   221  		fullCmd = append(fullCmd, fullCmd[0])
   222  		fullCmd[0] = filepath.Join(dirs.CoreLibExecDir, "snap-gdb-shim")
   223  	case "gdbserver":
   224  		fullCmd = append(fullCmd, fullCmd[0])
   225  		fullCmd[0] = filepath.Join(dirs.CoreLibExecDir, "snap-gdbserver-shim")
   226  	}
   227  	fullCmd = append(fullCmd, cmdArgs...)
   228  	fullCmd = append(fullCmd, args...)
   229  
   230  	fullCmd = append(absoluteCommandChain(app.Snap, app.CommandChain), fullCmd...)
   231  
   232  	if err := syscallExec(fullCmd[0], fullCmd, env.ForExec()); err != nil {
   233  		return fmt.Errorf("cannot exec %q: %s", fullCmd[0], err)
   234  	}
   235  	// this is never reached except in tests
   236  	return nil
   237  }
   238  
   239  func execHook(snapName, revision, hookName string) error {
   240  	rev, err := snap.ParseRevision(revision)
   241  	if err != nil {
   242  		return err
   243  	}
   244  
   245  	info, err := snap.ReadInfo(snapName, &snap.SideInfo{
   246  		Revision: rev,
   247  	})
   248  	if err != nil {
   249  		return err
   250  	}
   251  
   252  	hook := info.Hooks[hookName]
   253  	if hook == nil {
   254  		return fmt.Errorf("cannot find hook %q in %q", hookName, snapName)
   255  	}
   256  
   257  	// build the environment
   258  	// NOTE: we do not use OSEnvironmentUnescapeUnsafe, we do not
   259  	// particurly want to transmit snapd exec environment details
   260  	// to the hooks
   261  	env, err := osutil.OSEnvironment()
   262  	if err != nil {
   263  		return err
   264  	}
   265  	for _, eenv := range hook.EnvChain() {
   266  		env.ExtendWithExpanded(eenv)
   267  	}
   268  
   269  	// run the hook
   270  	cmd := append(absoluteCommandChain(hook.Snap, hook.CommandChain), filepath.Join(hook.Snap.HooksDir(), hook.Name))
   271  	return syscallExec(cmd[0], cmd, env.ForExec())
   272  }