diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index daf5ace..50e4ae7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,7 +26,7 @@ jobs: run: | # This corresponds with the list in Makefile:1, but omits the "userns" # and "capability" modules, which require go1.21 as minimum. - echo 'PACKAGES=mountinfo mount sequential signal symlink user' >> $GITHUB_ENV + echo 'PACKAGES=mountinfo mount reexec sequential signal symlink user' >> $GITHUB_ENV - name: go mod tidy run: | make foreach CMD="go mod tidy" diff --git a/Makefile b/Makefile index 152b1dc..22ad5d9 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -PACKAGES ?= capability mountinfo mount sequential signal symlink user userns # IMPORTANT: when updating this list, also update the conditional one in .github/workflows/test.yml +PACKAGES ?= capability mountinfo mount reexec sequential signal symlink user userns # IMPORTANT: when updating this list, also update the conditional one in .github/workflows/test.yml BINDIR ?= _build/bin CROSS ?= linux/arm linux/arm64 linux/ppc64le linux/s390x \ freebsd/amd64 openbsd/amd64 darwin/amd64 darwin/arm64 windows/amd64 diff --git a/reexec/go.mod b/reexec/go.mod new file mode 100644 index 0000000..86f1160 --- /dev/null +++ b/reexec/go.mod @@ -0,0 +1,3 @@ +module github.com/moby/sys/reexec + +go 1.18 diff --git a/reexec/reexec.go b/reexec/reexec.go new file mode 100644 index 0000000..c3a0c92 --- /dev/null +++ b/reexec/reexec.go @@ -0,0 +1,83 @@ +// Package reexec facilitates the busybox style reexec of a binary. +// +// Handlers can be registered with a name and the argv 0 of the exec of +// the binary will be used to find and execute custom init paths. +// +// It is used to work around forking limitations when using Go. +package reexec + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" +) + +var registeredInitializers = make(map[string]func()) + +// Register adds an initialization func under the specified name. It panics +// if the given name is already registered. +func Register(name string, initializer func()) { + if _, exists := registeredInitializers[name]; exists { + panic(fmt.Sprintf("reexec func already registered under name %q", name)) + } + + registeredInitializers[name] = initializer +} + +// Init is called as the first part of the exec process and returns true if an +// initialization function was called. +func Init() bool { + if initializer, ok := registeredInitializers[os.Args[0]]; ok { + initializer() + return true + } + return false +} + +// Command returns an [*exec.Cmd] with its Path set to the path of the current +// binary using the result of [Self]. +// +// On Linux, the Pdeathsig of [*exec.Cmd.SysProcAttr] is set to SIGTERM. +// This signal is sent to the process when the OS thread that created +// the process dies. +// +// It is the caller's responsibility to ensure that the creating thread is +// not terminated prematurely. See https://go.dev/issue/27505 for more details. +func Command(args ...string) *exec.Cmd { + return command(args...) +} + +// Self returns the path to the current process's binary. +// +// On Linux, it returns "/proc/self/exe", which provides the in-memory version +// of the current binary. This makes it safe to delete or replace the on-disk +// binary (os.Args[0]). +// +// On Other platforms, it attempts to look up the absolute path for os.Args[0], +// or otherwise returns os.Args[0] as-is. For example if current binary is +// "my-binary" at "/usr/bin/" (or "my-binary.exe" at "C:\" on Windows), +// then it returns "/usr/bin/my-binary" and "C:\my-binary.exe" respectively. +func Self() string { + if runtime.GOOS == "linux" { + return "/proc/self/exe" + } + return naiveSelf() +} + +func naiveSelf() string { + name := os.Args[0] + if filepath.Base(name) == name { + if lp, err := exec.LookPath(name); err == nil { + return lp + } + } + // handle conversion of relative paths to absolute + if absName, err := filepath.Abs(name); err == nil { + return absName + } + // if we couldn't get absolute name, return original + // (NOTE: Go only errors on Abs() if os.Getwd fails) + return name +} diff --git a/reexec/reexec_linux.go b/reexec/reexec_linux.go new file mode 100644 index 0000000..03f600e --- /dev/null +++ b/reexec/reexec_linux.go @@ -0,0 +1,16 @@ +package reexec + +import ( + "os/exec" + "syscall" +) + +func command(args ...string) *exec.Cmd { + return &exec.Cmd{ + Path: Self(), + Args: args, + SysProcAttr: &syscall.SysProcAttr{ + Pdeathsig: syscall.SIGTERM, + }, + } +} diff --git a/reexec/reexec_other.go b/reexec/reexec_other.go new file mode 100644 index 0000000..498d28b --- /dev/null +++ b/reexec/reexec_other.go @@ -0,0 +1,14 @@ +//go:build !linux + +package reexec + +import ( + "os/exec" +) + +func command(args ...string) *exec.Cmd { + return &exec.Cmd{ + Path: Self(), + Args: args, + } +} diff --git a/reexec/reexec_test.go b/reexec/reexec_test.go new file mode 100644 index 0000000..0230cad --- /dev/null +++ b/reexec/reexec_test.go @@ -0,0 +1,69 @@ +package reexec + +import ( + "os" + "os/exec" + "testing" +) + +const testReExec = "test-reexec" + +func init() { + Register(testReExec, func() { + panic("Return Error") + }) + Init() +} + +func TestRegister(t *testing.T) { + defer func() { + if r := recover(); r != nil { + const expected = `reexec func already registered under name "test-reexec"` + if r != expected { + t.Errorf("got %q, want %q", r, expected) + } + } + }() + Register(testReExec, func() {}) +} + +func TestCommand(t *testing.T) { + cmd := Command(testReExec) + w, err := cmd.StdinPipe() + if err != nil { + t.Fatalf("Error on pipe creation: %v", err) + } + defer w.Close() + + err = cmd.Start() + if err != nil { + t.Fatalf("Error on re-exec cmd: %v", err) + } + err = cmd.Wait() + const expected = "exit status 2" + if err == nil || err.Error() != expected { + t.Fatalf("got %v, want %v", err, expected) + } +} + +func TestNaiveSelf(t *testing.T) { + if os.Getenv("TEST_CHECK") == "1" { + os.Exit(2) + } + cmd := exec.Command(naiveSelf(), "-test.run=TestNaiveSelf") + cmd.Env = append(os.Environ(), "TEST_CHECK=1") + err := cmd.Start() + if err != nil { + t.Fatalf("Unable to start command: %v", err) + } + err = cmd.Wait() + const expected = "exit status 2" + if err == nil || err.Error() != expected { + t.Fatalf("got %v, want %v", err, expected) + } + + os.Args[0] = "mkdir" + if naiveSelf() == os.Args[0] { + t.Fatalf("Expected naiveSelf to resolve the location of mkdir") + } +}