Why Go’s Cmd.Run fails without /dev/null
Go’s Cmd.Run
can fail in environments where /dev/null
isn’t present (or isn’t accessible).
To explore this behaviour, let’s start with a quick test program:
package main
import (
"fmt"
"os"
"os/exec"
)
func main() {
cmd := exec.Command("/usr/local/bin/docker-explorer", "echo", "hey")
err := cmd.Run()
if err != nil {
fmt.Println("Error:", err)
os.Exit(1)
}
fmt.Println("Executed command")
}
This program runs the following command:
$ /usr/local/bin/docker-explorer echo hey
Note: docker-explorer is an executable program that exposes the
echo
command. I have it locally installed at /usr/local/bin/docker-explorer
.
Running this locally works fine, since /dev/null
is available.
$ go run main.go
Executed command
To simulate an environment where /dev/null
isn’t available, let’s setup a chroot jail.
# Build the program to /tmp/test-program
$ CGO_ENABLED=0 go build -o /tmp/test-program main.go
# Create a directory to use as the chroot jail
$ mkdir chroot_jail
# Copy docker-explorer into the chroot jail
$ mkdir -p chroot_jail/usr/local/bin && cp /usr/local/bin/docker-explorer chroot_jail/usr/local/bin
# Copy the test program into the chroot jail directory
$ cp /tmp/test-program chroot_jail/
Note: The setup above might not work on macOS because of this change.
Now, let’s execute the test program within the chroot jail:
$ sudo chroot chroot_jail /test-program
Error: open /dev/null: no such file or directory
That fails citing that /dev/null
isn’t available.
Since our small Go program just executes /usr/local/bin/docker-explorer
, let’s try running that directly:
$ sudo chroot chroot_jail /usr/local/bin/docker-explorer echo hey
hey
That didn’t fail, so the /dev/null
error definitely has something to do with our Go script, and not docker-explorer
itself.
A look at the source for
Cmd.Run reveals why /dev/null
is used:
Let’s validate this by rewriting our program to use dummy values for Stdout, Stderr and Stdin, so that Go doesn’t use /dev/null
:
package main import ( "fmt" "os" "os/exec" ) + type nullReader struct{}+ type nullWriter struct{}+ + func (nullReader) Read(p []byte) (n int, err error) { return len(p), nil }+ func (nullWriter) Write(p []byte) (n int, err error) { return len(p), nil }+ func main() { cmd := exec.Command("/usr/local/bin/docker-explorer", "echo", "hey") + cmd.Stdin = nullReader{}+ cmd.Stdout = nullWriter{}+ cmd.Stderr = nullWriter{}+ err := cmd.Run() if err != nil { fmt.Println("Error:", err) os.Exit(1) } fmt.Println("Executed command") }
When run with the new code, the program executes successfully:
$ CGO_ENABLED=0 go build -o /tmp/test-program-2 /vagrant_data/main2.go
$ cp /tmp/test-program-2 chroot_jail/test-program-2
$ sudo chroot chroot_jail /test-program-2
Executed command
TLDR: If either one of Cmd.Stdout
, Cmd.Stderr
, Cmd.Stdin
are not explicitly set, Go sets them to /dev/null
. This can
cause failures when using Cmd.Run
in Go programs that execute in environments where /dev/null
isn’t accessible. To
fix this, either assign dummy values to Cmd.Stdout
, Cmd.Stderr
and Cmd.Stdin
, or ensure that /dev/null
is
accessible.
Note: I stumbled upon this behaviour when working on the “Build your own Docker” challenge.