blob: b981cfbb4ae3f84ec26a821f156b1a1c72ae592d [file] [log] [blame]
Roland Shoemakerb64e53b2021-01-11 10:06:18 -08001// Copyright 2020 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5// Package execabs is a drop-in replacement for os/exec
6// that requires PATH lookups to find absolute paths.
7// That is, execabs.Command("cmd") runs the same PATH lookup
8// as exec.Command("cmd"), but if the result is a path
9// which is relative, the Run and Start methods will report
10// an error instead of running the executable.
11//
12// See https://blog.golang.org/path-security for more information
13// about when it may be necessary or appropriate to use this package.
14package execabs
15
16import (
17 "context"
18 "fmt"
19 "os/exec"
20 "path/filepath"
21 "reflect"
22 "unsafe"
23)
24
25// ErrNotFound is the error resulting if a path search failed to find an executable file.
26// It is an alias for exec.ErrNotFound.
27var ErrNotFound = exec.ErrNotFound
28
29// Cmd represents an external command being prepared or run.
30// It is an alias for exec.Cmd.
31type Cmd = exec.Cmd
32
33// Error is returned by LookPath when it fails to classify a file as an executable.
34// It is an alias for exec.Error.
35type Error = exec.Error
36
37// An ExitError reports an unsuccessful exit by a command.
38// It is an alias for exec.ExitError.
39type ExitError = exec.ExitError
40
41func relError(file, path string) error {
42 return fmt.Errorf("%s resolves to executable in current directory (.%c%s)", file, filepath.Separator, path)
43}
44
45// LookPath searches for an executable named file in the directories
46// named by the PATH environment variable. If file contains a slash,
47// it is tried directly and the PATH is not consulted. The result will be
48// an absolute path.
49//
50// LookPath differs from exec.LookPath in its handling of PATH lookups,
51// which are used for file names without slashes. If exec.LookPath's
52// PATH lookup would have returned an executable from the current directory,
53// LookPath instead returns an error.
54func LookPath(file string) (string, error) {
55 path, err := exec.LookPath(file)
Russ Coxb6088cc2022-04-29 20:33:09 -040056 if err != nil && !isGo119ErrDot(err) {
Roland Shoemakerb64e53b2021-01-11 10:06:18 -080057 return "", err
58 }
59 if filepath.Base(file) == file && !filepath.IsAbs(path) {
60 return "", relError(file, path)
61 }
62 return path, nil
63}
64
65func fixCmd(name string, cmd *exec.Cmd) {
66 if filepath.Base(name) == name && !filepath.IsAbs(cmd.Path) {
67 // exec.Command was called with a bare binary name and
68 // exec.LookPath returned a path which is not absolute.
69 // Set cmd.lookPathErr and clear cmd.Path so that it
70 // cannot be run.
71 lookPathErr := (*error)(unsafe.Pointer(reflect.ValueOf(cmd).Elem().FieldByName("lookPathErr").Addr().Pointer()))
72 if *lookPathErr == nil {
73 *lookPathErr = relError(name, cmd.Path)
74 }
75 cmd.Path = ""
76 }
77}
78
79// CommandContext is like Command but includes a context.
80//
81// The provided context is used to kill the process (by calling os.Process.Kill)
82// if the context becomes done before the command completes on its own.
83func CommandContext(ctx context.Context, name string, arg ...string) *exec.Cmd {
84 cmd := exec.CommandContext(ctx, name, arg...)
85 fixCmd(name, cmd)
86 return cmd
87
88}
89
90// Command returns the Cmd struct to execute the named program with the given arguments.
91// See exec.Command for most details.
92//
93// Command differs from exec.Command in its handling of PATH lookups,
94// which are used when the program name contains no slashes.
95// If exec.Command would have returned an exec.Cmd configured to run an
96// executable from the current directory, Command instead
97// returns an exec.Cmd that will return an error from Start or Run.
98func Command(name string, arg ...string) *exec.Cmd {
99 cmd := exec.Command(name, arg...)
100 fixCmd(name, cmd)
101 return cmd
102}