execabs.go (3673B)
1 // 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. 14 package execabs 15 16 import ( 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. 27 var ErrNotFound = exec.ErrNotFound 28 29 // Cmd represents an external command being prepared or run. 30 // It is an alias for exec.Cmd. 31 type 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. 35 type Error = exec.Error 36 37 // An ExitError reports an unsuccessful exit by a command. 38 // It is an alias for exec.ExitError. 39 type ExitError = exec.ExitError 40 41 func 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. 54 func LookPath(file string) (string, error) { 55 path, err := exec.LookPath(file) 56 if err != nil && !isGo119ErrDot(err) { 57 return "", err 58 } 59 if filepath.Base(file) == file && !filepath.IsAbs(path) { 60 return "", relError(file, path) 61 } 62 return path, nil 63 } 64 65 func fixCmd(name string, cmd *exec.Cmd) { 66 if filepath.Base(name) == name && !filepath.IsAbs(cmd.Path) && !isGo119ErrFieldSet(cmd) { 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. 83 func 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. 98 func Command(name string, arg ...string) *exec.Cmd { 99 cmd := exec.Command(name, arg...) 100 fixCmd(name, cmd) 101 return cmd 102 }