or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

cpu.mdexecabs.mdindex.mdplan9.mdunix.mdwindows.md
tile.json

execabs.mddocs/

golang.org/x/sys/execabs

Package execabs is a drop-in replacement for os/exec that requires PATH lookups to find absolute paths.

Package Information

  • Import Path: golang.org/x/sys/execabs
  • Version: v0.38.0
  • Purpose: Secure command execution with absolute path requirements

Overview

Package execabs provides a more secure alternative to os/exec by preventing the execution of programs from the current directory through PATH lookups. When a relative path is returned from a PATH search, execabs will return an error instead of executing the program.

This addresses a security vulnerability where malicious executables in the current directory could be executed unintentionally. See https://blog.golang.org/path-security for more information about when it may be necessary or appropriate to use this package.

Core Imports

import "golang.org/x/sys/execabs"

Key Differences from os/exec

The execabs package differs from os/exec in the following ways:

  1. PATH Lookups: If exec.Command would have returned an exec.Cmd configured to run an executable from the current directory, execabs.Command instead returns an exec.Cmd that will return an error from Start or Run.

  2. LookPath Behavior: If exec.LookPath's PATH lookup would have returned an executable from the current directory, execabs.LookPath instead returns an error.

  3. Absolute Paths Required: The result of PATH lookups must be absolute paths.

Variables

Error Variables

var ErrNotFound = exec.ErrNotFound

ErrNotFound is the error resulting if a path search failed to find an executable file. It is an alias for exec.ErrNotFound.

Functions

Command

func Command(name string, arg ...string) *exec.Cmd

Command returns the Cmd struct to execute the named program with the given arguments. See exec.Command for most details.

Command differs from exec.Command in its handling of PATH lookups, which are used when the program name contains no slashes. If exec.Command would have returned an exec.Cmd configured to run an executable from the current directory, Command instead returns an exec.Cmd that will return an error from Start or Run.

Parameters:

  • name: The name of the program to execute
  • arg: Variable number of command-line arguments

Returns:

  • *exec.Cmd: A Cmd structure that can be used to execute the command

CommandContext

func CommandContext(ctx context.Context, name string, arg ...string) *exec.Cmd

CommandContext is like Command but includes a context.

The provided context is used to kill the process (by calling os.Process.Kill) if the context becomes done before the command completes on its own.

Parameters:

  • ctx: Context for cancellation and timeout control
  • name: The name of the program to execute
  • arg: Variable number of command-line arguments

Returns:

  • *exec.Cmd: A Cmd structure that can be used to execute the command

LookPath

func LookPath(file string) (string, error)

LookPath searches for an executable named file in the directories named by the PATH environment variable. If file contains a slash, it is tried directly and the PATH is not consulted. The result will be an absolute path.

LookPath differs from exec.LookPath in its handling of PATH lookups, which are used for file names without slashes. If exec.LookPath's PATH lookup would have returned an executable from the current directory, LookPath instead returns an error.

Parameters:

  • file: The name of the executable to search for

Returns:

  • string: The absolute path to the executable
  • error: An error if the executable was not found or if it would have resolved to the current directory

Types

Cmd

type Cmd = exec.Cmd

Cmd represents an external command being prepared or run. It is an alias for exec.Cmd.

Error

type Error = exec.Error

Error is returned by LookPath when it fails to classify a file as an executable. It is an alias for exec.Error.

ExitError

type ExitError = exec.ExitError

An ExitError reports an unsuccessful exit by a command. It is an alias for exec.ExitError.

Usage Examples

Basic Command Execution

package main

import (
    "fmt"
    "log"

    "golang.org/x/sys/execabs"
)

func main() {
    // Execute a command safely - will not run executables from current directory
    cmd := execabs.Command("ls", "-la")

    output, err := cmd.Output()
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(string(output))
}

Using CommandContext with Timeout

package main

import (
    "context"
    "fmt"
    "log"
    "time"

    "golang.org/x/sys/execabs"
)

func main() {
    // Create a context with 5 second timeout
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    // Execute command with timeout
    cmd := execabs.CommandContext(ctx, "sleep", "10")

    err := cmd.Run()
    if err != nil {
        // Will error with "signal: killed" after 5 seconds
        log.Printf("Command failed: %v", err)
    }
}

Safe Path Lookup

package main

import (
    "fmt"
    "log"

    "golang.org/x/sys/execabs"
)

func main() {
    // Look up the absolute path to an executable
    path, err := execabs.LookPath("git")
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("git found at: %s\n", path)
    // Output: git found at: /usr/bin/git

    // This will fail if there's a "git" executable in current directory
    // but not in PATH
}

Capturing Standard Output and Error

package main

import (
    "bytes"
    "fmt"
    "log"

    "golang.org/x/sys/execabs"
)

func main() {
    cmd := execabs.Command("git", "status")

    var stdout, stderr bytes.Buffer
    cmd.Stdout = &stdout
    cmd.Stderr = &stderr

    err := cmd.Run()
    if err != nil {
        fmt.Printf("Error: %v\n", err)
        fmt.Printf("Stderr: %s\n", stderr.String())
        return
    }

    fmt.Printf("Output:\n%s\n", stdout.String())
}

Checking for Command Availability

package main

import (
    "fmt"

    "golang.org/x/sys/execabs"
)

func isCommandAvailable(name string) bool {
    _, err := execabs.LookPath(name)
    return err == nil
}

func main() {
    if isCommandAvailable("docker") {
        fmt.Println("Docker is installed")
    } else {
        fmt.Println("Docker is not available")
    }

    if isCommandAvailable("kubectl") {
        fmt.Println("kubectl is installed")
    } else {
        fmt.Println("kubectl is not available")
    }
}

Piping Commands Together

package main

import (
    "fmt"
    "log"

    "golang.org/x/sys/execabs"
)

func main() {
    // First command: generate data
    cmd1 := execabs.Command("echo", "hello world")

    // Second command: convert to uppercase
    cmd2 := execabs.Command("tr", "a-z", "A-Z")

    // Connect cmd1's stdout to cmd2's stdin
    cmd2.Stdin, _ = cmd1.StdoutPipe()

    // Start both commands
    cmd2.Start()
    cmd1.Run()

    // Get output from cmd2
    output, err := cmd2.Output()
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(string(output))
    // Output: HELLO WORLD
}

Working Directory and Environment

package main

import (
    "fmt"
    "log"
    "os"

    "golang.org/x/sys/execabs"
)

func main() {
    cmd := execabs.Command("pwd")

    // Set working directory
    cmd.Dir = "/tmp"

    // Set environment variables
    cmd.Env = append(os.Environ(),
        "CUSTOM_VAR=value",
        "ANOTHER_VAR=another_value",
    )

    output, err := cmd.Output()
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("Working directory: %s", string(output))
}

Handling Exit Codes

package main

import (
    "fmt"
    "os/exec"

    "golang.org/x/sys/execabs"
)

func main() {
    cmd := execabs.Command("false") // 'false' command always exits with code 1

    err := cmd.Run()
    if err != nil {
        // Check if it's an ExitError to get the exit code
        if exitErr, ok := err.(*exec.ExitError); ok {
            fmt.Printf("Command exited with code: %d\n", exitErr.ExitCode())
        } else {
            fmt.Printf("Command failed: %v\n", err)
        }
    }
}

Streaming Output in Real-Time

package main

import (
    "bufio"
    "fmt"
    "log"

    "golang.org/x/sys/execabs"
)

func main() {
    cmd := execabs.Command("ping", "-c", "5", "golang.org")

    // Get a pipe to read from stdout
    stdout, err := cmd.StdoutPipe()
    if err != nil {
        log.Fatal(err)
    }

    // Start the command
    if err := cmd.Start(); err != nil {
        log.Fatal(err)
    }

    // Read output line by line in real-time
    scanner := bufio.NewScanner(stdout)
    for scanner.Scan() {
        fmt.Println(scanner.Text())
    }

    // Wait for command to complete
    if err := cmd.Wait(); err != nil {
        log.Fatal(err)
    }
}

Security Considerations

Why Use execabs?

The execabs package addresses a security vulnerability in the standard os/exec package. Consider this scenario:

// Using os/exec (POTENTIALLY UNSAFE)
import "os/exec"

cmd := exec.Command("git", "status")
cmd.Run()

If there's a malicious executable named git in the current directory, and the current directory is in your PATH (or appears before the real git in PATH), the malicious version will be executed instead.

// Using execabs (SAFE)
import "golang.org/x/sys/execabs"

cmd := execabs.Command("git", "status")
cmd.Run()

With execabs, even if there's a git executable in the current directory, it will not be executed. Instead, only executables found via absolute paths from PATH lookups are allowed.

When to Use execabs

Use execabs when:

  1. Executing commands based on user input - If command names come from user input or configuration files
  2. Running in untrusted directories - When your program might run in directories controlled by other users
  3. Security is critical - For any security-sensitive applications
  4. Building tools or scripts - For build systems, deployment tools, or automation scripts

Migration from os/exec

Migration is straightforward - just change the import:

// Before
import "os/exec"

// After
import "golang.org/x/sys/execabs"

The API is identical, so no code changes are needed beyond the import statement.

Best Practices

  1. Prefer execabs over os/exec - Unless you have a specific need for the os/exec behavior, use execabs for better security.

  2. Use absolute paths when possible - If you know the exact path to an executable, specify it directly:

    cmd := execabs.Command("/usr/bin/git", "status")
  3. Validate command names - If accepting command names from external sources, validate them against an allowlist.

  4. Use CommandContext for timeouts - Always use context for long-running commands to prevent hanging:

    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    cmd := execabs.CommandContext(ctx, "slow-command")
  5. Check errors from LookPath - Before executing a command, use LookPath to verify it exists:

    if _, err := execabs.LookPath("docker"); err != nil {
        return fmt.Errorf("docker not found: %w", err)
    }
  6. Handle exit codes properly - Check for ExitError to distinguish between different failure modes:

    if err := cmd.Run(); err != nil {
        if exitErr, ok := err.(*exec.ExitError); ok {
            // Command ran but exited with non-zero status
            fmt.Printf("Exit code: %d\n", exitErr.ExitCode())
        } else {
            // Command failed to start or other error
            fmt.Printf("Failed to run: %v\n", err)
        }
    }

Related Packages

  • os/exec - Standard library command execution (less secure)
  • context - Context for cancellation and timeouts
  • golang.org/x/sys/unix - Low-level Unix system calls
  • golang.org/x/sys/windows - Low-level Windows system calls