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 }