Skip to content

Commit

Permalink
Merge pull request #179 from thaJeztah/integrate_reexec
Browse files Browse the repository at this point in the history
integrate pkg/reexec
  • Loading branch information
thaJeztah authored Dec 18, 2024
2 parents 50e999a + fdac605 commit 8698290
Show file tree
Hide file tree
Showing 7 changed files with 187 additions and 2 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -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
Expand Down
3 changes: 3 additions & 0 deletions reexec/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/moby/sys/reexec

go 1.18
83 changes: 83 additions & 0 deletions reexec/reexec.go
Original file line number Diff line number Diff line change
@@ -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
}
16 changes: 16 additions & 0 deletions reexec/reexec_linux.go
Original file line number Diff line number Diff line change
@@ -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,
},
}
}
14 changes: 14 additions & 0 deletions reexec/reexec_other.go
Original file line number Diff line number Diff line change
@@ -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,
}
}
69 changes: 69 additions & 0 deletions reexec/reexec_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
}

0 comments on commit 8698290

Please sign in to comment.