main
Raw Download raw file
  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.
 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)
 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
 65func 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.
 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}